Compare commits
8 Commits
b84ee586bf
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| d4238e3086 | |||
| f552a4e9e1 | |||
| 5b34da6d6c | |||
| 1f1e21905e | |||
| acd8e60ffb | |||
| c6e58b057c | |||
| 34dc6fc8c9 | |||
| d9343a94dc |
@@ -32,22 +32,30 @@ public sealed class CustomAssetHandlerAttribute : Attribute
|
|||||||
} = true;
|
} = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAsset : IDisposable
|
public abstract class IAsset : GhostObject
|
||||||
{
|
{
|
||||||
Guid ID
|
public Guid ID
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid TypeID
|
public Guid TypeID
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
IAssetSettings? Settings
|
public IAssetSettings? Settings
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected IAsset(Guid id, Guid typeId, IAssetSettings? settings)
|
||||||
|
:base(id)
|
||||||
|
{
|
||||||
|
ID = id;
|
||||||
|
TypeID = typeId;
|
||||||
|
Settings = settings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAssetExportOptions;
|
public interface IAssetExportOptions;
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ internal unsafe class MeshParsingJob
|
|||||||
|
|
||||||
private readonly string _filePath;
|
private readonly string _filePath;
|
||||||
private readonly AllocationHandle _allocationHandle;
|
private readonly AllocationHandle _allocationHandle;
|
||||||
private readonly MeshAssetSettings _settings;
|
private readonly ModelAssetSettings _settings;
|
||||||
|
|
||||||
private readonly TaskCompletionSource<Result> _taskCompletionSource;
|
private readonly TaskCompletionSource<Result> _taskCompletionSource;
|
||||||
|
|
||||||
public Task<Result> Task => _taskCompletionSource.Task;
|
public Task<Result> Task => _taskCompletionSource.Task;
|
||||||
|
|
||||||
public MeshParsingJob(MeshNode rootNode, string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
|
public MeshParsingJob(MeshNode rootNode, string filePath, AllocationHandle allocationHandle, ModelAssetSettings settings)
|
||||||
{
|
{
|
||||||
_rootNode = rootNode;
|
_rootNode = rootNode;
|
||||||
_filePath = filePath;
|
_filePath = filePath;
|
||||||
@@ -368,7 +368,7 @@ internal unsafe class MeshParsingJob
|
|||||||
|
|
||||||
internal static partial class MeshProcessor
|
internal static partial class MeshProcessor
|
||||||
{
|
{
|
||||||
public static Task<Result> ParseMeshAsync(MeshNode root, string sourcePath, AllocationHandle allocationHandle, MeshAssetSettings meshSettings, CancellationToken token = default)
|
public static Task<Result> ParseMeshAsync(MeshNode root, string sourcePath, AllocationHandle allocationHandle, ModelAssetSettings meshSettings, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var parseJob = new MeshParsingJob(root, sourcePath, allocationHandle, meshSettings);
|
var parseJob = new MeshParsingJob(root, sourcePath, allocationHandle, meshSettings);
|
||||||
return Task.Run(parseJob.Execute, token);
|
return Task.Run(parseJob.Execute, token);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Core.Utilities;
|
using Ghost.Core.Utilities;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
using Ghost.Engine;
|
|
||||||
using Ghost.Engine.Streaming;
|
using Ghost.Engine.Streaming;
|
||||||
using Ghost.Graphics.Core;
|
using Ghost.Graphics.Core;
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.Jobs;
|
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using Misaki.HighPerformance.Mathematics;
|
using Misaki.HighPerformance.Mathematics;
|
||||||
@@ -71,54 +69,25 @@ public sealed class ModelManifestMetadata
|
|||||||
|
|
||||||
internal sealed class ImportedModelAsset : IAsset
|
internal sealed class ImportedModelAsset : IAsset
|
||||||
{
|
{
|
||||||
public Guid ID
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid TypeID => typeof(MeshAsset).GUID;
|
|
||||||
|
|
||||||
public IAssetSettings? Settings
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ModelManifest Manifest
|
public ModelManifest Manifest
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImportedModelAsset(Guid id, IAssetSettings? settings, ModelManifest manifest)
|
public ImportedModelAsset(Guid id, IAssetSettings? settings, ModelManifest manifest)
|
||||||
|
: base(id, typeof(ModelAsset).GUID, settings)
|
||||||
{
|
{
|
||||||
ID = id;
|
|
||||||
Settings = settings;
|
|
||||||
Manifest = manifest;
|
Manifest = manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Guid(GUID)]
|
[Guid(GUID)]
|
||||||
public abstract class MeshAsset : IAsset
|
public abstract class ModelAsset : IAsset
|
||||||
{
|
{
|
||||||
public const string GUID = "B99CA68E-EE7A-4822-BF1C-AA0A5120C36A";
|
public const string GUID = "B99CA68E-EE7A-4822-BF1C-AA0A5120C36A";
|
||||||
|
|
||||||
private MeshNode _root;
|
private MeshNode _root;
|
||||||
|
|
||||||
public Guid ID
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAssetSettings Settings
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid TypeID => typeof(MeshAsset).GUID;
|
|
||||||
|
|
||||||
public MeshNode Root
|
public MeshNode Root
|
||||||
{
|
{
|
||||||
get => _root;
|
get => _root;
|
||||||
@@ -129,17 +98,18 @@ public abstract class MeshAsset : IAsset
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal MeshAsset(MeshNode root, Guid id, MeshAssetSettings settings)
|
internal ModelAsset(MeshNode root, Guid id, ModelAssetSettings settings)
|
||||||
|
: base(id, typeof(ModelAsset).GUID, settings)
|
||||||
{
|
{
|
||||||
_root = root;
|
_root = root;
|
||||||
|
|
||||||
ID = id;
|
|
||||||
Settings = settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
_root?.Dispose();
|
if (disposing)
|
||||||
|
{
|
||||||
|
_root?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +130,7 @@ public enum VertexDataSource
|
|||||||
ComputedIfMissing
|
ComputedIfMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MeshAssetSettings : IAssetSettings
|
public class ModelAssetSettings : IAssetSettings
|
||||||
{
|
{
|
||||||
public VertexDataSource NormalDataSource
|
public VertexDataSource NormalDataSource
|
||||||
{
|
{
|
||||||
@@ -173,7 +143,7 @@ public class MeshAssetSettings : IAssetSettings
|
|||||||
} = VertexDataSource.ComputedIfMissing;
|
} = VertexDataSource.ComputedIfMissing;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ObjAssetSettings : MeshAssetSettings
|
internal class ObjAssetSettings : ModelAssetSettings
|
||||||
{
|
{
|
||||||
public CoordinateAxis ObjectUpAxis
|
public CoordinateAxis ObjectUpAxis
|
||||||
{
|
{
|
||||||
@@ -196,12 +166,12 @@ internal class ObjAssetSettings : MeshAssetSettings
|
|||||||
} = 1.0f;
|
} = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class FbxAssetSettings : MeshAssetSettings
|
internal class FbxAssetSettings : ModelAssetSettings
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[CustomAssetHandler(AssetTypeId = MeshAsset.GUID, RuntimeAssetType = AssetType.Mesh, Extensions = new[] { ".fbx", ".obj" })]
|
[CustomAssetHandler(AssetTypeId = ModelAsset.GUID, RuntimeAssetType = AssetType.Mesh, Extensions = new[] { ".fbx", ".obj" })]
|
||||||
internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
internal class ModelAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
|
private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
@@ -295,9 +265,9 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
|||||||
return ValueTask.FromResult(Result.Failure("Packing model assets is not supported yet."));
|
return ValueTask.FromResult(Result.Failure("Packing model assets is not supported yet."));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MeshAssetSettings ResolveSettings(string sourcePath, IAssetSettings? settings)
|
private static ModelAssetSettings ResolveSettings(string sourcePath, IAssetSettings? settings)
|
||||||
{
|
{
|
||||||
if (settings is MeshAssetSettings meshSettings)
|
if (settings is ModelAssetSettings meshSettings)
|
||||||
{
|
{
|
||||||
return meshSettings;
|
return meshSettings;
|
||||||
}
|
}
|
||||||
@@ -354,7 +324,7 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
|||||||
node.Name,
|
node.Name,
|
||||||
stablePath,
|
stablePath,
|
||||||
$"{sourcePath}#Mesh/{stablePath}",
|
$"{sourcePath}#Mesh/{stablePath}",
|
||||||
typeof(MeshAsset).GUID));
|
typeof(ModelAsset).GUID));
|
||||||
}
|
}
|
||||||
else if (node is LightMeshNode)
|
else if (node is LightMeshNode)
|
||||||
{
|
{
|
||||||
@@ -7,18 +7,9 @@ public sealed class SceneAsset : IAsset
|
|||||||
{
|
{
|
||||||
public const string GUID = "1B5E3F2A-8D91-4C67-BE32-A0F9C6D4E781";
|
public const string GUID = "1B5E3F2A-8D91-4C67-BE32-A0F9C6D4E781";
|
||||||
|
|
||||||
private static readonly Guid s_typeID = Guid.Parse(GUID);
|
public ushort RuntimeSceneID
|
||||||
|
|
||||||
public Guid ID
|
|
||||||
{
|
{
|
||||||
get;
|
get; set;
|
||||||
}
|
|
||||||
|
|
||||||
public Guid TypeID => s_typeID;
|
|
||||||
|
|
||||||
public IAssetSettings? Settings
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SceneName
|
public string SceneName
|
||||||
@@ -32,16 +23,11 @@ public sealed class SceneAsset : IAsset
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SceneAsset(Guid id, IAssetSettings? settings)
|
public SceneAsset(Guid id, IAssetSettings? settings)
|
||||||
|
: base(id, typeof(SceneAsset).GUID, settings)
|
||||||
{
|
{
|
||||||
ID = id;
|
|
||||||
Settings = settings;
|
|
||||||
SceneName = string.Empty;
|
SceneName = string.Empty;
|
||||||
EntityCount = 0;
|
EntityCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SceneAssetSettings : IAssetSettings
|
public sealed class SceneAssetSettings : IAssetSettings
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Ghost.Engine;
|
||||||
using Ghost.Engine.Streaming;
|
using Ghost.Engine.Streaming;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Assets;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
@@ -10,6 +12,12 @@ internal class SceneAssetHandler : IImportableAssetHandler, IPackableAssetHandle
|
|||||||
[AssetOpenHandler(".gscene")]
|
[AssetOpenHandler(".gscene")]
|
||||||
private static async Task<Result> OpenAsync(string path)
|
private static async Task<Result> OpenAsync(string path)
|
||||||
{
|
{
|
||||||
|
// Actually double clicking the asset in content browser will just open it.
|
||||||
|
// We probably shouldn't do the actual loading in OpenAsync, but let's keep it simple for now.
|
||||||
|
// OpenAsync usually returns immediately if there's no UI, or we should use AssetRegistry.LoadAssetAsync
|
||||||
|
var assetRegistry = EditorApplication.GetService<IAssetRegistry>();
|
||||||
|
var id = Guid.NewGuid(); // Wait, how do we know the ID?
|
||||||
|
// AssetMeta handles this. This method is just a quick hack for double clicking.
|
||||||
var data = await SceneSerializationService.DeserializeSceneFileAsync(path);
|
var data = await SceneSerializationService.DeserializeSceneFileAsync(path);
|
||||||
if (data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
@@ -17,7 +25,7 @@ internal class SceneAssetHandler : IImportableAssetHandler, IPackableAssetHandle
|
|||||||
}
|
}
|
||||||
|
|
||||||
var service = EditorApplication.GetService<SceneSerializationService>();
|
var service = EditorApplication.GetService<SceneSerializationService>();
|
||||||
service.LoadSceneIntoEditorWorld(data);
|
service.LoadSceneIntoEditorWorld(data, SceneLoadingType.Single, null);
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +48,22 @@ internal class SceneAssetHandler : IImportableAssetHandler, IPackableAssetHandle
|
|||||||
{
|
{
|
||||||
SceneName = Path.GetFileNameWithoutExtension(assetPath),
|
SceneName = Path.GetFileNameWithoutExtension(assetPath),
|
||||||
EntityCount = data?.Entities?.Count ?? 0,
|
EntityCount = data?.Entities?.Count ?? 0,
|
||||||
|
RuntimeSceneID = Ghost.Engine.Core.Scene.INVALID_ID // Default
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<IAsset>();
|
||||||
|
var service = EditorApplication.GetService<SceneSerializationService>();
|
||||||
|
service.LoadSceneIntoEditorWorld(data, SceneLoadingType.Single, (scene) =>
|
||||||
|
{
|
||||||
|
asset.RuntimeSceneID = scene.ID;
|
||||||
|
EditorApplication.GetService<IEditorWorldService>().RegisterSceneAsset(scene.ID, asset);
|
||||||
|
tcs.TrySetResult(asset);
|
||||||
|
});
|
||||||
|
return Result.Success(await tcs.Task);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.Success<IAsset>(asset);
|
return Result.Success<IAsset>(asset);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -50,14 +72,41 @@ internal class SceneAssetHandler : IImportableAssetHandler, IPackableAssetHandle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
public async ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (asset is not SceneAsset sceneAsset)
|
if (asset is not SceneAsset sceneAsset)
|
||||||
{
|
{
|
||||||
return ValueTask.FromResult(Result.Failure("Asset type is not SceneAsset"));
|
return Result.Failure("Asset type is not SceneAsset");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ValueTask.FromResult(Result.Failure("Scene saving is handled by SceneSerializationService directly."));
|
var worldService = EditorApplication.GetService<IEditorWorldService>();
|
||||||
|
var tcs = new TaskCompletionSource<byte[]>();
|
||||||
|
|
||||||
|
worldService.Defer(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scene = Ghost.Engine.Core.Scene.FromID(sceneAsset.RuntimeSceneID);
|
||||||
|
var service = EditorApplication.GetService<SceneSerializationService>();
|
||||||
|
var bytes = service.SerializeSceneToMemory(scene);
|
||||||
|
tcs.TrySetResult(bytes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
tcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = await tcs.Task;
|
||||||
|
await File.WriteAllBytesAsync(targetPath, bytes, token);
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Failure($"Failed to save scene: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<ImportedSubAsset[]>> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
public async ValueTask<Result<ImportedSubAsset[]>> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
|
|||||||
@@ -11,32 +11,16 @@ public sealed partial class GraphicsShaderAsset : IAsset
|
|||||||
{
|
{
|
||||||
public const string GUID = "7BD4591C-B017-4814-AA0B-3F30EB3E727E";
|
public const string GUID = "7BD4591C-B017-4814-AA0B-3F30EB3E727E";
|
||||||
|
|
||||||
public Guid ID
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAssetSettings? Settings
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid TypeID => typeof(GraphicsShaderAsset).GUID;
|
|
||||||
|
|
||||||
public GraphicsShaderDescriptor Descriptor
|
public GraphicsShaderDescriptor Descriptor
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal GraphicsShaderAsset(GraphicsShaderDescriptor descriptor, Guid id)
|
internal GraphicsShaderAsset(GraphicsShaderDescriptor descriptor, Guid id)
|
||||||
|
: base(id, typeof(GraphicsShaderAsset).GUID, null)
|
||||||
{
|
{
|
||||||
ID = id;
|
|
||||||
Descriptor = descriptor;
|
Descriptor = descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Guid(GUID)]
|
[Guid(GUID)]
|
||||||
@@ -44,32 +28,16 @@ public sealed partial class ComputeShaderAsset : IAsset
|
|||||||
{
|
{
|
||||||
public const string GUID = "EA881979-CD8D-4088-B568-D42645F18C2A";
|
public const string GUID = "EA881979-CD8D-4088-B568-D42645F18C2A";
|
||||||
|
|
||||||
public Guid ID
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAssetSettings? Settings
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid TypeID => typeof(ComputeShaderAsset).GUID;
|
|
||||||
|
|
||||||
public ComputeShaderDescriptor Descriptor
|
public ComputeShaderDescriptor Descriptor
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ComputeShaderAsset(ComputeShaderDescriptor descriptor, Guid id)
|
internal ComputeShaderAsset(ComputeShaderDescriptor descriptor, Guid id)
|
||||||
|
: base(id, typeof(ComputeShaderAsset).GUID, null)
|
||||||
{
|
{
|
||||||
ID = id;
|
|
||||||
Descriptor = descriptor;
|
Descriptor = descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shader does not handle import/export via asset registry, it will handled by the hot reload system.
|
// Shader does not handle import/export via asset registry, it will handled by the hot reload system.
|
||||||
|
|||||||
@@ -54,11 +54,6 @@ public unsafe class TextureAsset : IAsset
|
|||||||
{
|
{
|
||||||
public const string GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC";
|
public const string GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC";
|
||||||
|
|
||||||
private static readonly Guid s_typeID = Guid.Parse(GUID);
|
|
||||||
|
|
||||||
private readonly Guid _id;
|
|
||||||
private readonly IAssetSettings _settings;
|
|
||||||
|
|
||||||
private readonly IntPtr _textureData;
|
private readonly IntPtr _textureData;
|
||||||
private readonly uint _width;
|
private readonly uint _width;
|
||||||
private readonly uint _height;
|
private readonly uint _height;
|
||||||
@@ -66,10 +61,6 @@ public unsafe class TextureAsset : IAsset
|
|||||||
private readonly uint _colorComponents;
|
private readonly uint _colorComponents;
|
||||||
private readonly uint _dimension;
|
private readonly uint _dimension;
|
||||||
|
|
||||||
public Guid ID => _id;
|
|
||||||
public Guid TypeID => typeof(TextureAsset).GUID;
|
|
||||||
public IAssetSettings Settings => _settings;
|
|
||||||
|
|
||||||
public IntPtr TextureData => _textureData;
|
public IntPtr TextureData => _textureData;
|
||||||
public uint Width => _width;
|
public uint Width => _width;
|
||||||
public uint Height => _height;
|
public uint Height => _height;
|
||||||
@@ -78,10 +69,8 @@ public unsafe class TextureAsset : IAsset
|
|||||||
public uint ColorComponents => _colorComponents;
|
public uint ColorComponents => _colorComponents;
|
||||||
|
|
||||||
internal TextureAsset([OwnershipTransfer] IntPtr data, TextureContentHeader header, Guid id, IAssetSettings settings)
|
internal TextureAsset([OwnershipTransfer] IntPtr data, TextureContentHeader header, Guid id, IAssetSettings settings)
|
||||||
|
: base(id, typeof(TextureAsset).GUID, settings)
|
||||||
{
|
{
|
||||||
_id = id;
|
|
||||||
_settings = settings;
|
|
||||||
|
|
||||||
_textureData = data;
|
_textureData = data;
|
||||||
_width = header.width;
|
_width = header.width;
|
||||||
_height = header.height;
|
_height = header.height;
|
||||||
@@ -90,15 +79,9 @@ public unsafe class TextureAsset : IAsset
|
|||||||
_colorComponents = header.colorComponents;
|
_colorComponents = header.colorComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
~TextureAsset()
|
protected override void Dispose(bool disposing)
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
StbIApi.ImageFree((void*)_textureData);
|
StbIApi.ImageFree((void*)_textureData);
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Windows.System;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core;
|
namespace Ghost.Editor.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -50,10 +52,35 @@ public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContextMenuItemAttribute(string tag, string name, int group = 0)
|
public int Priority
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContextMenuItemAttribute(string tag, string name, int group = 0, int priority = 0)
|
||||||
{
|
{
|
||||||
Tag = tag;
|
Tag = tag;
|
||||||
Name = name;
|
Name = name;
|
||||||
Group = group;
|
Group = group;
|
||||||
|
Priority = priority;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ShortcutAttribute : DiscoverableAttributeBase
|
||||||
|
{
|
||||||
|
public VirtualKey Key
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualKeyModifiers Modifiers
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShortcutAttribute(VirtualKey key, VirtualKeyModifiers modifiers = VirtualKeyModifiers.None)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Modifiers = modifiers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Contracts;
|
||||||
|
|
||||||
|
public interface IDirtyTrackerService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the specified object as dirty.
|
||||||
|
/// </summary>
|
||||||
|
void MarkDirty(GhostObject obj);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the specified object is dirty compared to its clean state.
|
||||||
|
/// </summary>
|
||||||
|
bool IsDirty(GhostObject obj);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the specified object as clean (e.g., after a successful save).
|
||||||
|
/// </summary>
|
||||||
|
void MarkClean(GhostObject obj);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of all currently dirty objects.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<GhostObject> GetDirtyObjects();
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ public enum ShaderStage
|
|||||||
Library // For ray tracing shaders or work graph shaders that don't fit into the traditional shader stages
|
Library // For ray tracing shaders or work graph shaders that don't fit into the traditional shader stages
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IShaderCompiler : IDisposable
|
internal interface IShaderCompiler : IDisposable
|
||||||
{
|
{
|
||||||
Result<UnsafeArray<byte>> Compile(ref readonly ShaderCompilationConfig config, AllocationHandle handle);
|
Result<UnsafeArray<byte>> Compile(ref readonly ShaderCompilationConfig config, AllocationHandle handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ public sealed partial class Float3Field : ValueControl<float3>
|
|||||||
_yComponent = GetTemplateChild("YComponent") as NumberBox;
|
_yComponent = GetTemplateChild("YComponent") as NumberBox;
|
||||||
_zComponent = GetTemplateChild("ZComponent") as NumberBox;
|
_zComponent = GetTemplateChild("ZComponent") as NumberBox;
|
||||||
|
|
||||||
|
SuppressChangedEvent = true;
|
||||||
SyncFromValue();
|
SyncFromValue();
|
||||||
|
SuppressChangedEvent = false;
|
||||||
|
|
||||||
_xComponent?.ValueChanged += OnComponentChanged;
|
_xComponent?.ValueChanged += OnComponentChanged;
|
||||||
_yComponent?.ValueChanged += OnComponentChanged;
|
_yComponent?.ValueChanged += OnComponentChanged;
|
||||||
@@ -44,11 +46,9 @@ public sealed partial class Float3Field : ValueControl<float3>
|
|||||||
|
|
||||||
private void SyncFromValue()
|
private void SyncFromValue()
|
||||||
{
|
{
|
||||||
SuppressChangedEvent = true;
|
|
||||||
_xComponent?.Value = Value.x;
|
_xComponent?.Value = Value.x;
|
||||||
_yComponent?.Value = Value.y;
|
_yComponent?.Value = Value.y;
|
||||||
_zComponent?.Value = Value.z;
|
_zComponent?.Value = Value.z;
|
||||||
SuppressChangedEvent = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnComponentChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
|
private void OnComponentChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
|
||||||
@@ -63,7 +63,6 @@ public sealed partial class Float3Field : ValueControl<float3>
|
|||||||
(float)(_yComponent?.Value ?? 0),
|
(float)(_yComponent?.Value ?? 0),
|
||||||
(float)(_zComponent?.Value ?? 0));
|
(float)(_zComponent?.Value ?? 0));
|
||||||
|
|
||||||
RiseChangedEvent(Value, newValue);
|
|
||||||
Value = newValue;
|
Value = newValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,12 @@
|
|||||||
using Ghost.Editor.Core.Utilities;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Controls;
|
namespace Ghost.Editor.Core.Controls;
|
||||||
|
|
||||||
public sealed partial class ContextFlyout : MenuFlyout
|
public sealed partial class ContextFlyout : MenuFlyout
|
||||||
{
|
{
|
||||||
private class MenuNode
|
|
||||||
{
|
|
||||||
public required string Name
|
|
||||||
{
|
|
||||||
get; init;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MethodInfo? Method
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MenuNode> Children
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
} = new();
|
|
||||||
|
|
||||||
public int RawGroup
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = int.MaxValue;
|
|
||||||
|
|
||||||
// The calculated group used for sorting (min of children for folders)
|
|
||||||
public int EffectiveGroup
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _isPopulated;
|
private bool _isPopulated;
|
||||||
|
|
||||||
public string Tag
|
public string ContextMenuTag
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = string.Empty;
|
} = string.Empty;
|
||||||
@@ -48,160 +16,13 @@ public sealed partial class ContextFlyout : MenuFlyout
|
|||||||
Opening += ContextFlyout_Opening;
|
Opening += ContextFlyout_Opening;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively sorts nodes and calculates folder pGroups
|
|
||||||
private static void PrepareNodes(List<MenuNode> nodes)
|
|
||||||
{
|
|
||||||
if (nodes.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var node in nodes)
|
|
||||||
{
|
|
||||||
if (node.Children.Count > 0)
|
|
||||||
{
|
|
||||||
// Go deep first
|
|
||||||
PrepareNodes(node.Children);
|
|
||||||
|
|
||||||
// A folder's group is determined by its highest priority child (lowest group number).
|
|
||||||
// This ensures a "File" folder (containing Group 0 items) sits at the top
|
|
||||||
// alongside other Group 0 leaf items.
|
|
||||||
node.EffectiveGroup = node.Children.Min(c => c.EffectiveGroup);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
node.EffectiveGroup = node.RawGroup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by Group, then by Name
|
|
||||||
nodes.Sort((a, b) =>
|
|
||||||
{
|
|
||||||
var groupCompare = a.EffectiveGroup.CompareTo(b.EffectiveGroup);
|
|
||||||
return groupCompare != 0
|
|
||||||
? groupCompare
|
|
||||||
: string.CompareOrdinal(a.Name, b.Name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively builds the UI elements
|
|
||||||
private static void BuildNodes(List<MenuNode> nodes, IList<MenuFlyoutItemBase> targetCollection)
|
|
||||||
{
|
|
||||||
if (nodes.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentGroup = nodes[0].EffectiveGroup;
|
|
||||||
|
|
||||||
foreach (var node in nodes)
|
|
||||||
{
|
|
||||||
if (node.EffectiveGroup != currentGroup)
|
|
||||||
{
|
|
||||||
targetCollection.Add(new MenuFlyoutSeparator());
|
|
||||||
currentGroup = node.EffectiveGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.Children.Count > 0)
|
|
||||||
{
|
|
||||||
var subItem = new MenuFlyoutSubItem
|
|
||||||
{
|
|
||||||
Text = node.Name
|
|
||||||
};
|
|
||||||
|
|
||||||
// Recursively render children into the subitem
|
|
||||||
BuildNodes(node.Children, subItem.Items);
|
|
||||||
targetCollection.Add(subItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var menuItem = new MenuFlyoutItem
|
|
||||||
{
|
|
||||||
Text = node.Name
|
|
||||||
};
|
|
||||||
|
|
||||||
var methodToInvoke = node.Method;
|
|
||||||
menuItem.Click += (_, _) =>
|
|
||||||
{
|
|
||||||
methodToInvoke?.Invoke(null, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
targetCollection.Add(menuItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PopulateContextMenu()
|
private void PopulateContextMenu()
|
||||||
{
|
{
|
||||||
var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>();
|
var rootNodes = MenuUtility.BuildTree(ContextMenuTag);
|
||||||
if (methods == null)
|
MenuUtility.BuildNodes(rootNodes, Items);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Build the Tree
|
|
||||||
var rootNodes = new List<MenuNode>();
|
|
||||||
|
|
||||||
foreach (var method in methods)
|
|
||||||
{
|
|
||||||
var attr = method.GetCustomAttribute<ContextMenuItemAttribute>();
|
|
||||||
if (attr == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter tags
|
|
||||||
if (!string.Equals(attr.Tag, Tag, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nameSpan = attr.Name.AsSpan();
|
|
||||||
var pathParts = nameSpan.Split('/');
|
|
||||||
|
|
||||||
var currentLevel = rootNodes;
|
|
||||||
MenuNode? currentNode = null;
|
|
||||||
|
|
||||||
foreach (var range in pathParts)
|
|
||||||
{
|
|
||||||
var part = nameSpan[range.Start..range.End];
|
|
||||||
|
|
||||||
MenuNode? foundNode = null;
|
|
||||||
|
|
||||||
// Try to find existing node in the current level
|
|
||||||
foreach (var node in currentLevel)
|
|
||||||
{
|
|
||||||
if (part.Equals(node.Name.AsSpan(), StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
foundNode = node;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundNode == null)
|
|
||||||
{
|
|
||||||
foundNode = new MenuNode { Name = part.ToString() };
|
|
||||||
currentLevel.Add(foundNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentNode = foundNode;
|
|
||||||
|
|
||||||
// If this is the last part, it's the executable item
|
|
||||||
if (range.End.Value == nameSpan.Length)
|
|
||||||
{
|
|
||||||
currentNode.Method = method;
|
|
||||||
currentNode.RawGroup = attr.Group;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentLevel = currentNode.Children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PrepareNodes(rootNodes);
|
|
||||||
BuildNodes(rootNodes, Items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ContextFlyout_Opening(object? sender, object e)
|
private void ContextFlyout_Opening(object? sender, object e)
|
||||||
{
|
{
|
||||||
if (_isPopulated)
|
if (_isPopulated)
|
||||||
{
|
{
|
||||||
@@ -211,4 +32,4 @@ public sealed partial class ContextFlyout : MenuFlyout
|
|||||||
PopulateContextMenu();
|
PopulateContextMenu();
|
||||||
_isPopulated = true;
|
_isPopulated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/Editor/Ghost.Editor.Core/Controls/Menu/MenuContextBar.cs
Normal file
53
src/Editor/Ghost.Editor.Core/Controls/Menu/MenuContextBar.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Controls;
|
||||||
|
|
||||||
|
public sealed partial class MenuContextBar : MenuBar
|
||||||
|
{
|
||||||
|
private bool _isPopulated;
|
||||||
|
|
||||||
|
public string ContextMenuTag
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = string.Empty;
|
||||||
|
|
||||||
|
public MenuContextBar()
|
||||||
|
{
|
||||||
|
Loaded += MenuContextBar_Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MenuContextBar_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isPopulated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopulateMenu();
|
||||||
|
_isPopulated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateMenu()
|
||||||
|
{
|
||||||
|
var rootNodes = MenuUtility.BuildTree(ContextMenuTag);
|
||||||
|
|
||||||
|
foreach (var node in rootNodes)
|
||||||
|
{
|
||||||
|
if (node.Children.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.Warning($"Menu item '{node.Name}' cannot be placed at the root of a MenuContextBar because it lacks a parent group.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var menuBarItem = new MenuBarItem
|
||||||
|
{
|
||||||
|
Title = node.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
MenuUtility.BuildNodes(node.Children, menuBarItem.Items);
|
||||||
|
Items.Add(menuBarItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
236
src/Editor/Ghost.Editor.Core/Controls/Menu/MenuUtility.cs
Normal file
236
src/Editor/Ghost.Editor.Core/Controls/Menu/MenuUtility.cs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
using Ghost.Editor.Core.Utilities;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System.Reflection;
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Controls;
|
||||||
|
|
||||||
|
internal class MenuNode
|
||||||
|
{
|
||||||
|
public required string Name
|
||||||
|
{
|
||||||
|
get; init;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodInfo? Method
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MenuNode> Children
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
} = new();
|
||||||
|
|
||||||
|
public int RawGroup
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = int.MaxValue;
|
||||||
|
|
||||||
|
// The calculated group used for sorting (min of children for folders)
|
||||||
|
public int EffectiveGroup
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RawPriority
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = 0;
|
||||||
|
|
||||||
|
public int EffectivePriority
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualKey ShortCut
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = VirtualKey.None;
|
||||||
|
|
||||||
|
public VirtualKeyModifiers ShortCutModifiers
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = VirtualKeyModifiers.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MenuUtility
|
||||||
|
{
|
||||||
|
// Recursively sorts nodes and calculates folder pGroups
|
||||||
|
public static void PrepareNodes(List<MenuNode> nodes)
|
||||||
|
{
|
||||||
|
if (nodes.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
if (node.Children.Count > 0)
|
||||||
|
{
|
||||||
|
// Go deep first
|
||||||
|
PrepareNodes(node.Children);
|
||||||
|
|
||||||
|
// A folder's group is determined by its highest priority child (lowest group number).
|
||||||
|
// This ensures a "File" folder (containing Group 0 items) sits at the top
|
||||||
|
// alongside other Group 0 leaf items.
|
||||||
|
node.EffectiveGroup = node.Children.Min(c => c.EffectiveGroup);
|
||||||
|
node.EffectivePriority = node.Children.Max(c => c.EffectivePriority);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node.EffectiveGroup = node.RawGroup;
|
||||||
|
node.EffectivePriority = node.RawPriority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by Group, then by Priority (higher first), then by Name
|
||||||
|
nodes.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
var groupCompare = a.EffectiveGroup.CompareTo(b.EffectiveGroup);
|
||||||
|
if (groupCompare != 0)
|
||||||
|
{
|
||||||
|
return groupCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
var priorityCompare = b.EffectivePriority.CompareTo(a.EffectivePriority);
|
||||||
|
return priorityCompare != 0
|
||||||
|
? priorityCompare
|
||||||
|
: string.CompareOrdinal(a.Name, b.Name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively builds the UI elements
|
||||||
|
public static void BuildNodes(List<MenuNode> nodes, IList<MenuFlyoutItemBase> targetCollection)
|
||||||
|
{
|
||||||
|
if (nodes.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentGroup = nodes[0].EffectiveGroup;
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
if (node.EffectiveGroup != currentGroup)
|
||||||
|
{
|
||||||
|
targetCollection.Add(new MenuFlyoutSeparator());
|
||||||
|
currentGroup = node.EffectiveGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.Children.Count > 0)
|
||||||
|
{
|
||||||
|
var subItem = new MenuFlyoutSubItem
|
||||||
|
{
|
||||||
|
Text = node.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursively render children into the subitem
|
||||||
|
BuildNodes(node.Children, subItem.Items);
|
||||||
|
targetCollection.Add(subItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var menuItem = new MenuFlyoutItem
|
||||||
|
{
|
||||||
|
Text = node.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
var methodToInvoke = node.Method;
|
||||||
|
menuItem.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
methodToInvoke?.Invoke(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (node.ShortCut != VirtualKey.None)
|
||||||
|
{
|
||||||
|
menuItem.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator
|
||||||
|
{
|
||||||
|
Key = node.ShortCut,
|
||||||
|
Modifiers = node.ShortCutModifiers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
targetCollection.Add(menuItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MenuNode> BuildTree(string tag)
|
||||||
|
{
|
||||||
|
var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>();
|
||||||
|
if (methods == null)
|
||||||
|
{
|
||||||
|
return new List<MenuNode>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Build the Tree
|
||||||
|
var rootNodes = new List<MenuNode>();
|
||||||
|
|
||||||
|
foreach (var method in methods)
|
||||||
|
{
|
||||||
|
var attr = method.GetCustomAttribute<ContextMenuItemAttribute>();
|
||||||
|
if (attr == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tags
|
||||||
|
if (!string.Equals(attr.Tag, tag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameSpan = attr.Name.AsSpan();
|
||||||
|
var pathParts = nameSpan.Split('/');
|
||||||
|
|
||||||
|
var currentLevel = rootNodes;
|
||||||
|
MenuNode? currentNode = null;
|
||||||
|
|
||||||
|
foreach (var range in pathParts)
|
||||||
|
{
|
||||||
|
var part = nameSpan[range.Start..range.End];
|
||||||
|
|
||||||
|
MenuNode? foundNode = null;
|
||||||
|
|
||||||
|
// Try to find existing node in the current level
|
||||||
|
foreach (var node in currentLevel)
|
||||||
|
{
|
||||||
|
if (part.Equals(node.Name.AsSpan(), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
foundNode = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundNode == null)
|
||||||
|
{
|
||||||
|
foundNode = new MenuNode { Name = part.ToString() };
|
||||||
|
currentLevel.Add(foundNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = foundNode;
|
||||||
|
|
||||||
|
// If this is the last part, it's the executable item
|
||||||
|
if (range.End.Value == nameSpan.Length)
|
||||||
|
{
|
||||||
|
currentNode.Method = method;
|
||||||
|
currentNode.RawGroup = attr.Group;
|
||||||
|
currentNode.RawPriority = attr.Priority;
|
||||||
|
|
||||||
|
var shortCutAttr = method.GetCustomAttribute<ShortcutAttribute>();
|
||||||
|
if (shortCutAttr != null)
|
||||||
|
{
|
||||||
|
currentNode.ShortCut = shortCutAttr.Key;
|
||||||
|
currentNode.ShortCutModifiers = shortCutAttr.Modifiers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = currentNode.Children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareNodes(rootNodes);
|
||||||
|
return rootNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
using Ghost.Editor.Core.Event;
|
using Ghost.Editor.Core.Event;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Controls;
|
namespace Ghost.Editor.Core.Controls;
|
||||||
|
|
||||||
public partial class ValueControl<T> : Control
|
public interface INotifyValueChanged<T>
|
||||||
|
{
|
||||||
|
T Value { get; set; }
|
||||||
|
|
||||||
|
event ValueChangedEventHandler<T>? OnValueChanged;
|
||||||
|
|
||||||
|
void SetValueWithoutNotify(T value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ValueControl<T> : Control, INotifyValueChanged<T>
|
||||||
{
|
{
|
||||||
private bool _suppressChangedEvent;
|
private bool _suppressChangedEvent;
|
||||||
|
|
||||||
@@ -39,7 +49,7 @@ public partial class ValueControl<T> : Control
|
|||||||
{
|
{
|
||||||
valueControl.ValueChanged((T)e.OldValue, (T)e.NewValue);
|
valueControl.ValueChanged((T)e.OldValue, (T)e.NewValue);
|
||||||
|
|
||||||
if (!valueControl._suppressChangedEvent)
|
if (!valueControl.SuppressChangedEvent)
|
||||||
{
|
{
|
||||||
valueControl.OnValueChanged?.Invoke(valueControl, new((T)e.OldValue, (T)e.NewValue));
|
valueControl.OnValueChanged?.Invoke(valueControl, new((T)e.OldValue, (T)e.NewValue));
|
||||||
}
|
}
|
||||||
@@ -59,6 +69,7 @@ public partial class ValueControl<T> : Control
|
|||||||
/// Sets the value of the control.
|
/// Sets the value of the control.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The new value to set.</param>
|
/// <param name="value">The new value to set.</param>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void SetValue(T value)
|
public void SetValue(T value)
|
||||||
{
|
{
|
||||||
Value = value;
|
Value = value;
|
||||||
@@ -70,10 +81,10 @@ public partial class ValueControl<T> : Control
|
|||||||
/// <param name="value">The new _value to set.</param>
|
/// <param name="value">The new _value to set.</param>
|
||||||
/// <remarks>This method only suppresses the change event notification, not the <see cref="ValueChanged(T, T)"/> method.
|
/// <remarks>This method only suppresses the change event notification, not the <see cref="ValueChanged(T, T)"/> method.
|
||||||
/// Useful when you need to change the _value programmatically without triggering the change event.</remarks>
|
/// Useful when you need to change the _value programmatically without triggering the change event.</remarks>
|
||||||
public void SetValueWithoutNotifying(T value)
|
public void SetValueWithoutNotify(T value)
|
||||||
{
|
{
|
||||||
_suppressChangedEvent = true;
|
SuppressChangedEvent = true;
|
||||||
SetValue(ValueProperty, value);
|
SetValue(value);
|
||||||
_suppressChangedEvent = false;
|
SuppressChangedEvent = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,14 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
|
|
||||||
namespace Ghost.Editor.Core;
|
namespace Ghost.Editor.Core;
|
||||||
|
|
||||||
|
public enum EditorState
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Playing,
|
||||||
|
Paused,
|
||||||
|
Compiling,
|
||||||
|
}
|
||||||
|
|
||||||
public static class EditorApplication
|
public static class EditorApplication
|
||||||
{
|
{
|
||||||
public const string ASSETS_FOLDER_NAME = "Assets";
|
public const string ASSETS_FOLDER_NAME = "Assets";
|
||||||
@@ -55,6 +63,11 @@ public static class EditorApplication
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static EditorState State
|
||||||
|
{
|
||||||
|
get; internal set;
|
||||||
|
} = EditorState.Idle;
|
||||||
|
|
||||||
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
|
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
|
||||||
{
|
{
|
||||||
projectPath = PathUtility.Normalize(projectPath);
|
projectPath = PathUtility.Normalize(projectPath);
|
||||||
|
|||||||
@@ -10,16 +10,33 @@
|
|||||||
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
|
<NoWarn>$(NoWarn);MVVMTK0050</NoWarn>
|
||||||
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
|
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" />
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" />
|
<DebugType>embedded</DebugType>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" />
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|x64'" />
|
<DebugType>embedded</DebugType>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|ARM64'" />
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_Editor|x64'" />
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_Editor|ARM64'" />
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|x64'">
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|ARM64'">
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_Editor|x64'">
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_Editor|ARM64'">
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Remove="Assets\MeshNode.cs" />
|
<Content Remove="Assets\MeshNode.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
100
src/Editor/Ghost.Editor.Core/GhostObject.cs
Normal file
100
src/Editor/Ghost.Editor.Core/GhostObject.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using Ghost.Editor.Core.Contracts;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base class for all objects that can be tracked and recorded by the Undo system.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class GhostObject : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A persistent unique identifier used to track this object across Undo/Redo operations,
|
||||||
|
/// even if the underlying object is destroyed and resurrected.
|
||||||
|
/// </summary>
|
||||||
|
public Guid InstanceID { get; protected set; }
|
||||||
|
|
||||||
|
// Use WeakReference so we don't prevent Garbage Collection of dead objects
|
||||||
|
private static readonly Dictionary<Guid, WeakReference<GhostObject>> s_objectRegistry = new();
|
||||||
|
|
||||||
|
public static event Action<GhostObject>? OnObjectModified;
|
||||||
|
|
||||||
|
protected GhostObject()
|
||||||
|
{
|
||||||
|
InstanceID = Guid.NewGuid();
|
||||||
|
s_objectRegistry[InstanceID] = new WeakReference<GhostObject>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GhostObject(Guid instanceID)
|
||||||
|
{
|
||||||
|
InstanceID = instanceID;
|
||||||
|
s_objectRegistry[InstanceID] = new WeakReference<GhostObject>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves a GhostObject by its InstanceID in O(1) time.
|
||||||
|
/// </summary>
|
||||||
|
public static GhostObject? Find(Guid id)
|
||||||
|
{
|
||||||
|
if (s_objectRegistry.TryGetValue(id, out var weakRef))
|
||||||
|
{
|
||||||
|
if (weakRef.TryGetTarget(out var obj))
|
||||||
|
{
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Dead object, GC has collected it
|
||||||
|
s_objectRegistry.Remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called before mutating state.
|
||||||
|
/// Hooks into the Undo and Dirty Tracking systems.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Modify()
|
||||||
|
{
|
||||||
|
OnObjectModified?.Invoke(this);
|
||||||
|
|
||||||
|
// TODO: Unify RecordObject in future sessions. For now, we skip IUndoService.RecordObject here
|
||||||
|
// since specialized methods are still required in UndoService.
|
||||||
|
|
||||||
|
// Mark dirty for persistence directly
|
||||||
|
EditorApplication.GetService<IDirtyTrackerService>().MarkDirty(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the state of this object into a binary format.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SerializeState(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes the state of this object from a binary format.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void DeserializeState(BinaryReader reader)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
s_objectRegistry.Remove(InstanceID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~GhostObject()
|
||||||
|
{
|
||||||
|
Dispose(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ using Ghost.Entities;
|
|||||||
|
|
||||||
namespace Ghost.Editor.Core.Inspector;
|
namespace Ghost.Editor.Core.Inspector;
|
||||||
|
|
||||||
|
// TODO: We can use source generator to directly generate ComponentDescriptor on each component type and avoid reflection and caching altogether. This is just a quick solution for now.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Thread-safe cache of ComponentDescriptor per component type.
|
/// Thread-safe cache of ComponentDescriptor per component type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -30,12 +32,6 @@ public static class ComponentDescriptorRegistry
|
|||||||
|
|
||||||
public static ComponentDescriptor GetOrCreate(Identifier<IComponent> componentId)
|
public static ComponentDescriptor GetOrCreate(Identifier<IComponent> componentId)
|
||||||
{
|
{
|
||||||
#if DEBUG || GHOST_EDITOR
|
return GetOrCreate(ComponentRegistry.s_runtimeIDToType[componentId.Value]);
|
||||||
if (ComponentRegistry.s_runtimeIDToType.TryGetValue(componentId.Value, out var type))
|
|
||||||
{
|
|
||||||
return GetOrCreate(type);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
throw new InvalidOperationException($"Cannot resolve ComponentDescriptor for component ID {componentId.Value}. Type mapping not available.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,16 @@
|
|||||||
using Ghost.Editor.Core.Controls;
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Inspector;
|
namespace Ghost.Editor.Core.Inspector;
|
||||||
|
|
||||||
public abstract class ComponentEditor
|
public abstract class ComponentEditor
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Represents the underlying component object used by this class to manage its functionality.
|
|
||||||
/// </summary>
|
|
||||||
private readonly List<IPropertyBinding> _bindings = new();
|
|
||||||
|
|
||||||
protected ComponentObject ComponentObject { get; private set; }
|
|
||||||
|
|
||||||
internal void Initialize(ComponentObject componentObject)
|
|
||||||
{
|
|
||||||
ComponentObject = componentObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Declarative two-way binding. Replaces manual Update().
|
|
||||||
/// </summary>
|
|
||||||
protected void Bind<T>(
|
|
||||||
ValueControl<T> control,
|
|
||||||
Func<ComponentObject, T> getter,
|
|
||||||
Action<ComponentObject, T> setter)
|
|
||||||
{
|
|
||||||
var binding = new PropertyBinding<T>(control, ComponentObject, getter, setter);
|
|
||||||
_bindings.Add(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when the component editor is created.
|
/// Called when the component editor is created.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="container">The container to add the editor controls to.</param>
|
/// <param name="root">The root panel to which the editor should add its UI elements.</param>
|
||||||
public abstract void Create(Panel container);
|
/// <param name="componentNode">The component node being edited.</param>
|
||||||
|
public abstract void Create(Panel root, ComponentNode componentNode);
|
||||||
|
|
||||||
public virtual void Destroy() { }
|
public virtual void Destroy() { }
|
||||||
|
}
|
||||||
internal void SyncBindings()
|
|
||||||
{
|
|
||||||
foreach (var binding in _bindings)
|
|
||||||
{
|
|
||||||
binding.Sync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Ghost.Entities;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Inspector;
|
|
||||||
|
|
||||||
public readonly struct ComponentObject
|
|
||||||
{
|
|
||||||
private readonly World _world;
|
|
||||||
private readonly Entity _entity;
|
|
||||||
|
|
||||||
internal ComponentObject(World world, Entity entity)
|
|
||||||
{
|
|
||||||
_world = world;
|
|
||||||
_entity = entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ref T GetData<T>()
|
|
||||||
where T : unmanaged, IComponentData
|
|
||||||
{
|
|
||||||
return ref _world.EntityManager.GetComponent<T>(_entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetData<T>(in T data)
|
|
||||||
where T : unmanaged, IComponentData
|
|
||||||
{
|
|
||||||
_world.EntityManager.SetComponent(_entity, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ref T GetSharedData<T>()
|
|
||||||
where T : unmanaged, ISharedComponent
|
|
||||||
{
|
|
||||||
return ref _world.EntityManager.GetSharedComponent<T>(_entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetSharedData<T>(in T data)
|
|
||||||
where T : unmanaged, ISharedComponent
|
|
||||||
{
|
|
||||||
_world.EntityManager.SetSharedComponent(_entity, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,20 +9,13 @@ internal class EntityDrawer : PropertyDrawer<Entity>
|
|||||||
{
|
{
|
||||||
public override FrameworkElement CreateControlT(PropertyNode<Entity> model)
|
public override FrameworkElement CreateControlT(PropertyNode<Entity> model)
|
||||||
{
|
{
|
||||||
var field = new ReferenceField
|
static void UpdateUI(Entity val, ReferenceField field)
|
||||||
{
|
|
||||||
TypeLabel = "Entity",
|
|
||||||
IconGlyph = "\uF158",
|
|
||||||
Margin = new Thickness(0, 2, 0, 2)
|
|
||||||
};
|
|
||||||
|
|
||||||
Action<Entity> updateUI = (val) =>
|
|
||||||
{
|
{
|
||||||
if (val.IsValid)
|
if (val.IsValid)
|
||||||
{
|
{
|
||||||
field.HasValue = true;
|
field.HasValue = true;
|
||||||
|
|
||||||
// For now, just display the Entity ID. We could resolve its SceneGraph Node name in the future.
|
// TODO: For now, just display the Entity ID. We could resolve its SceneGraph Node name in the future.
|
||||||
field.DisplayText = $"Entity {val.ID}:{val.Generation}";
|
field.DisplayText = $"Entity {val.ID}:{val.Generation}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -30,28 +23,33 @@ internal class EntityDrawer : PropertyDrawer<Entity>
|
|||||||
field.HasValue = false;
|
field.HasValue = false;
|
||||||
field.DisplayText = "None (Entity)";
|
field.DisplayText = "None (Entity)";
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
field.ValidateDrop = (args) =>
|
var field = new ReferenceField
|
||||||
{
|
{
|
||||||
// TODO: Implement drag and drop for entities from the hierarchy
|
TypeLabel = "Entity",
|
||||||
return false;
|
IconGlyph = "\uF158",
|
||||||
|
Margin = new Thickness(0, 2, 0, 2),
|
||||||
|
ValidateDrop = (args) =>
|
||||||
|
{
|
||||||
|
// TODO: Implement drag and drop for entities from the hierarchy
|
||||||
|
return false;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
field.OnClearClicked = () =>
|
field.OnClearClicked = () =>
|
||||||
{
|
{
|
||||||
model.SetValueFromUI(Entity.Invalid);
|
model.SetValueFromUI(Entity.Invalid);
|
||||||
model.FlushToECS();
|
UpdateUI(Entity.Invalid, field);
|
||||||
updateUI(Entity.Invalid);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateUI(model.Value);
|
UpdateUI(model.Value, field);
|
||||||
|
|
||||||
model.OnValueChanged += (val) =>
|
model.OnValueChanged += (val) =>
|
||||||
{
|
{
|
||||||
field.DispatcherQueue.TryEnqueue(() =>
|
field.DispatcherQueue.TryEnqueue(() =>
|
||||||
{
|
{
|
||||||
updateUI(val);
|
UpdateUI(val, field);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Ghost.Editor.Core.Controls;
|
using Ghost.Editor.Core.Controls;
|
||||||
|
using Ghost.Editor.Core.Utilities;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
using Misaki.HighPerformance.Mathematics;
|
using Misaki.HighPerformance.Mathematics;
|
||||||
@@ -7,23 +8,15 @@ namespace Ghost.Editor.Core.Inspector.Drawers;
|
|||||||
|
|
||||||
public sealed class Float3Drawer : PropertyDrawer<float3>
|
public sealed class Float3Drawer : PropertyDrawer<float3>
|
||||||
{
|
{
|
||||||
public override FrameworkElement CreateControlT(Ghost.Editor.Core.SceneGraph.PropertyNode<float3> model)
|
public override FrameworkElement CreateControlT(SceneGraph.PropertyNode<float3> node)
|
||||||
{
|
{
|
||||||
var field = new Float3Field
|
var field = new Float3Field
|
||||||
{
|
{
|
||||||
IsEnabled = !model.Descriptor.IsReadOnly,
|
IsEnabled = !node.Descriptor.IsReadOnly,
|
||||||
Value = model.Value
|
Value = node.Value
|
||||||
};
|
};
|
||||||
|
|
||||||
field.OnValueChanged += (s, e) =>
|
field.BindTwoWay(node);
|
||||||
{
|
|
||||||
model.SetValueFromUI(e.NewValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
model.OnValueChanged += (newVal) =>
|
|
||||||
{
|
|
||||||
field.Value = newVal;
|
|
||||||
};
|
|
||||||
|
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public sealed class NumberBoxDrawer<T> : PropertyDrawer<T>
|
|||||||
return new NumberBoxDrawer<T>(0, double.CreateTruncating(T.MinValue), double.CreateTruncating(T.MaxValue));
|
return new NumberBoxDrawer<T>(0, double.CreateTruncating(T.MinValue), double.CreateTruncating(T.MaxValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override FrameworkElement CreateControlT(Ghost.Editor.Core.SceneGraph.PropertyNode<T> model)
|
public override FrameworkElement CreateControlT(SceneGraph.PropertyNode<T> model)
|
||||||
{
|
{
|
||||||
var box = new NumberBox
|
var box = new NumberBox
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Ghost.Editor.Core.Inspector.Drawers;
|
|||||||
|
|
||||||
public sealed class ToggleSwitchDrawer : PropertyDrawer<bool>
|
public sealed class ToggleSwitchDrawer : PropertyDrawer<bool>
|
||||||
{
|
{
|
||||||
public override FrameworkElement CreateControlT(Ghost.Editor.Core.SceneGraph.PropertyNode<bool> model)
|
public override FrameworkElement CreateControlT(SceneGraph.PropertyNode<bool> model)
|
||||||
{
|
{
|
||||||
var toggle = new ToggleSwitch
|
var toggle = new ToggleSwitch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class EntityInspectorModel : ISyncableInspectorModel
|
|||||||
private readonly Entity _entity;
|
private readonly Entity _entity;
|
||||||
private EntityNode? _entityNode;
|
private EntityNode? _entityNode;
|
||||||
private readonly List<ComponentNode> _components = new();
|
private readonly List<ComponentNode> _components = new();
|
||||||
|
private readonly List<ComponentEditor> _activeCustomEditors = new();
|
||||||
private int _lastArchetypeId = -1;
|
private int _lastArchetypeId = -1;
|
||||||
|
|
||||||
public World World => _world;
|
public World World => _world;
|
||||||
@@ -28,67 +29,6 @@ public sealed class EntityInspectorModel : ISyncableInspectorModel
|
|||||||
_entity = entity;
|
_entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when entity archetype may have changed.
|
|
||||||
/// Returns true if structure was rebuilt (components added/removed).
|
|
||||||
/// </summary>
|
|
||||||
public bool RefreshStructure()
|
|
||||||
{
|
|
||||||
var locationResult = _world.EntityManager.GetEntityLocation(_entity);
|
|
||||||
if (locationResult.IsFailure)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var location = locationResult.Value;
|
|
||||||
if (location.archetypeID == _lastArchetypeId)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastArchetypeId = location.archetypeID;
|
|
||||||
RebuildComponentList();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read all component values from ECS -> model.
|
|
||||||
/// </summary>
|
|
||||||
public void SyncFromECS()
|
|
||||||
{
|
|
||||||
if (!_world.EntityManager.Exists(_entity))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var comp in _components)
|
|
||||||
{
|
|
||||||
foreach (var prop in comp.Properties)
|
|
||||||
{
|
|
||||||
prop.SyncFromECS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Write dirty model values -> ECS.
|
|
||||||
/// </summary>
|
|
||||||
public void FlushToECS()
|
|
||||||
{
|
|
||||||
if (!_world.EntityManager.Exists(_entity))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var comp in _components)
|
|
||||||
{
|
|
||||||
foreach (var prop in comp.Properties)
|
|
||||||
{
|
|
||||||
prop.FlushToECS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RebuildComponentList()
|
private void RebuildComponentList()
|
||||||
{
|
{
|
||||||
_components.Clear();
|
_components.Clear();
|
||||||
@@ -119,68 +59,32 @@ public sealed class EntityInspectorModel : ISyncableInspectorModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<ComponentEditor> _activeCustomEditors = new();
|
/// <summary>
|
||||||
|
/// Called when entity archetype may have changed.
|
||||||
public void Sync()
|
/// Returns true if structure was rebuilt (components added/removed).
|
||||||
|
/// </summary>
|
||||||
|
public bool RefreshStructure()
|
||||||
{
|
{
|
||||||
if (!_world.EntityManager.Exists(_entity)) return;
|
var locationResult = _world.EntityManager.GetEntityLocation(_entity);
|
||||||
RefreshStructure();
|
if (locationResult.IsFailure)
|
||||||
SyncFromECS();
|
|
||||||
foreach (var editor in _activeCustomEditors)
|
|
||||||
{
|
{
|
||||||
editor.SyncBindings();
|
return false;
|
||||||
}
|
|
||||||
FlushToECS();
|
|
||||||
}
|
|
||||||
|
|
||||||
public UIElement BuildUI()
|
|
||||||
{
|
|
||||||
RefreshStructure();
|
|
||||||
|
|
||||||
var container = new StackPanel { Spacing = 4 };
|
|
||||||
|
|
||||||
foreach (var compNode in _components)
|
|
||||||
{
|
|
||||||
var expander = new Expander
|
|
||||||
{
|
|
||||||
Header = compNode.Descriptor.DisplayName,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
||||||
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
|
||||||
IsExpanded = true,
|
|
||||||
Margin = new Thickness(4, 2, 4, 2)
|
|
||||||
};
|
|
||||||
|
|
||||||
var propertiesPanel = new StackPanel { Spacing = 8 };
|
|
||||||
|
|
||||||
if (ComponentEditorRegistry.HasCustomEditor(compNode.ComponentType))
|
|
||||||
{
|
|
||||||
var editor = ComponentEditorRegistry.CreateCustomEditor(compNode.ComponentType);
|
|
||||||
if (editor != null)
|
|
||||||
{
|
|
||||||
var compObject = new ComponentObject(_world, _entity);
|
|
||||||
editor.Initialize(compObject);
|
|
||||||
editor.Create(propertiesPanel);
|
|
||||||
_activeCustomEditors.Add(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var propNode in compNode.Properties)
|
|
||||||
{
|
|
||||||
BuildPropertyUI(propNode, propertiesPanel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expander.Content = propertiesPanel;
|
|
||||||
container.Children.Add(expander);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
var location = locationResult.Value;
|
||||||
|
if (location.archetypeID == _lastArchetypeId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastArchetypeId = location.archetypeID;
|
||||||
|
RebuildComponentList();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BuildPropertyUI(PropertyNode propNode, Panel container)
|
private static void BuildPropertyUI(PropertyNode propNode, Panel container)
|
||||||
{
|
{
|
||||||
var drawer = PropertyDrawerRegistry.GetDrawer(propNode.Descriptor.FieldType);
|
var drawer = PropertyDrawerRegistry.GetDrawer(propNode.Descriptor.ValueType);
|
||||||
var control = drawer.CreateControl(propNode);
|
var control = drawer.CreateControl(propNode);
|
||||||
|
|
||||||
var propertyField = new Controls.PropertyField
|
var propertyField = new Controls.PropertyField
|
||||||
@@ -203,6 +107,82 @@ public sealed class EntityInspectorModel : ISyncableInspectorModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read all component values from ECS -> model.
|
||||||
|
/// </summary>
|
||||||
|
public void SyncFromECS()
|
||||||
|
{
|
||||||
|
if (!_world.EntityManager.Exists(_entity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var comp in _components)
|
||||||
|
{
|
||||||
|
foreach (var prop in comp.Properties)
|
||||||
|
{
|
||||||
|
prop.Sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sync()
|
||||||
|
{
|
||||||
|
if (!_world.EntityManager.Exists(_entity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshStructure();
|
||||||
|
SyncFromECS();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Deselect is not supported yet.
|
||||||
|
|
||||||
|
public UIElement BuildUI()
|
||||||
|
{
|
||||||
|
RefreshStructure();
|
||||||
|
|
||||||
|
var container = new StackPanel { Spacing = 4 };
|
||||||
|
|
||||||
|
foreach (var compNode in _components)
|
||||||
|
{
|
||||||
|
// TODO: Use a more compact UI for components
|
||||||
|
var expander = new Expander
|
||||||
|
{
|
||||||
|
Header = compNode.Descriptor.DisplayName,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||||
|
IsExpanded = true,
|
||||||
|
Margin = new Thickness(4, 2, 4, 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
var propertiesPanel = new StackPanel { Spacing = 8 };
|
||||||
|
|
||||||
|
if (ComponentEditorRegistry.HasCustomEditor(compNode.ComponentType))
|
||||||
|
{
|
||||||
|
var editor = ComponentEditorRegistry.CreateCustomEditor(compNode.ComponentType);
|
||||||
|
if (editor != null)
|
||||||
|
{
|
||||||
|
editor.Create(propertiesPanel, compNode);
|
||||||
|
_activeCustomEditors.Add(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var propNode in compNode.Properties)
|
||||||
|
{
|
||||||
|
BuildPropertyUI(propNode, propertiesPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expander.Content = propertiesPanel;
|
||||||
|
container.Children.Add(expander);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_components.Clear();
|
_components.Clear();
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
using Ghost.Editor.Core.Controls;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Inspector;
|
|
||||||
|
|
||||||
internal interface IPropertyBinding
|
|
||||||
{
|
|
||||||
void Sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class PropertyBinding<T> : IPropertyBinding
|
|
||||||
{
|
|
||||||
private readonly ValueControl<T> _control;
|
|
||||||
private readonly ComponentObject _componentObject;
|
|
||||||
private readonly Func<ComponentObject, T> _getter;
|
|
||||||
private readonly Action<ComponentObject, T> _setter;
|
|
||||||
|
|
||||||
public PropertyBinding(
|
|
||||||
ValueControl<T> control,
|
|
||||||
ComponentObject componentObject,
|
|
||||||
Func<ComponentObject, T> getter,
|
|
||||||
Action<ComponentObject, T> setter)
|
|
||||||
{
|
|
||||||
_control = control;
|
|
||||||
_componentObject = componentObject;
|
|
||||||
_getter = getter;
|
|
||||||
_setter = setter;
|
|
||||||
|
|
||||||
// Wire user edits -> ECS write
|
|
||||||
_control.OnValueChanged += (_, args) =>
|
|
||||||
{
|
|
||||||
_setter(_componentObject, args.NewValue!);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Sync()
|
|
||||||
{
|
|
||||||
var current = _getter(_componentObject);
|
|
||||||
_control.SetValueWithoutNotifying(current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ public sealed class PropertyDescriptor
|
|||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public string DisplayName { get; }
|
public string DisplayName { get; }
|
||||||
public Type FieldType { get; }
|
public Type ValueType { get; }
|
||||||
public int OffsetInComponent { get; }
|
public int OffsetInComponent { get; }
|
||||||
public bool IsReadOnly { get; }
|
public bool IsReadOnly { get; }
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ public sealed class PropertyDescriptor
|
|||||||
internal PropertyDescriptor(FieldInfo fieldInfo, int parentOffset)
|
internal PropertyDescriptor(FieldInfo fieldInfo, int parentOffset)
|
||||||
{
|
{
|
||||||
Name = fieldInfo.Name;
|
Name = fieldInfo.Name;
|
||||||
FieldType = fieldInfo.FieldType;
|
ValueType = fieldInfo.FieldType;
|
||||||
OffsetInComponent = parentOffset + (int)Marshal.OffsetOf(fieldInfo.DeclaringType!, fieldInfo.Name);
|
OffsetInComponent = parentOffset + (int)Marshal.OffsetOf(fieldInfo.DeclaringType!, fieldInfo.Name);
|
||||||
|
|
||||||
IsReadOnly = fieldInfo.GetCustomAttribute<ReadOnlyInInspectorAttribute>() != null;
|
IsReadOnly = fieldInfo.GetCustomAttribute<ReadOnlyInInspectorAttribute>() != null;
|
||||||
@@ -32,12 +32,12 @@ public sealed class PropertyDescriptor
|
|||||||
DisplayName = nameAttr?.Name ?? FormatName(Name);
|
DisplayName = nameAttr?.Name ?? FormatName(Name);
|
||||||
|
|
||||||
// Handle nested structs if this is an unmanaged struct that is not a primitive or common vector type we have custom drawers for.
|
// Handle nested structs if this is an unmanaged struct that is not a primitive or common vector type we have custom drawers for.
|
||||||
if (FieldType.IsValueType && !FieldType.IsPrimitive && !FieldType.IsEnum)
|
if (ValueType.IsValueType && !ValueType.IsPrimitive && !ValueType.IsEnum)
|
||||||
{
|
{
|
||||||
if (!PropertyDrawerRegistry.HasCustomDrawer(FieldType))
|
if (!PropertyDrawerRegistry.HasCustomDrawer(ValueType))
|
||||||
{
|
{
|
||||||
var children = new List<PropertyDescriptor>();
|
var children = new List<PropertyDescriptor>();
|
||||||
var fields = FieldType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
var fields = ValueType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
foreach (var nestedField in fields)
|
foreach (var nestedField in fields)
|
||||||
{
|
{
|
||||||
if (!nestedField.IsPublic &&
|
if (!nestedField.IsPublic &&
|
||||||
@@ -60,7 +60,7 @@ public sealed class PropertyDescriptor
|
|||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
DisplayName = FormatName(name);
|
DisplayName = FormatName(name);
|
||||||
FieldType = type;
|
ValueType = type;
|
||||||
OffsetInComponent = offset;
|
OffsetInComponent = offset;
|
||||||
IsReadOnly = isReadOnly;
|
IsReadOnly = isReadOnly;
|
||||||
Children = children;
|
Children = children;
|
||||||
@@ -89,7 +89,7 @@ public sealed class PropertyDescriptor
|
|||||||
public unsafe object ReadBoxed(void* pComponent)
|
public unsafe object ReadBoxed(void* pComponent)
|
||||||
{
|
{
|
||||||
var src = (byte*)pComponent + OffsetInComponent;
|
var src = (byte*)pComponent + OffsetInComponent;
|
||||||
return Marshal.PtrToStructure((nint)src, FieldType)!;
|
return Marshal.PtrToStructure((nint)src, ValueType)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void WriteBoxed(void* pComponent, object value)
|
public unsafe void WriteBoxed(void* pComponent, object value)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Ghost.Editor.Core.Inspector;
|
using Ghost.Editor.Core.Inspector;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
namespace Ghost.Editor.Core.SceneGraph;
|
||||||
@@ -8,155 +10,218 @@ namespace Ghost.Editor.Core.SceneGraph;
|
|||||||
/// Represents a single component on an entity within the Editor's scene graph.
|
/// Represents a single component on an entity within the Editor's scene graph.
|
||||||
/// Acts as the middleware between the Inspector's PropertyModels and the actual ECS memory.
|
/// Acts as the middleware between the Inspector's PropertyModels and the actual ECS memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ComponentNode
|
public unsafe class ComponentNode
|
||||||
{
|
{
|
||||||
|
private readonly IUndoService _undoService;
|
||||||
|
private readonly IEditorWorldService _worldService;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, int> _propertyIndices;
|
||||||
protected readonly World _world;
|
protected readonly World _world;
|
||||||
protected readonly Entity _entity;
|
|
||||||
|
public EntityNode EntityNode { get; }
|
||||||
|
|
||||||
public Type ComponentType { get; }
|
public Type ComponentType { get; }
|
||||||
public ComponentDescriptor Descriptor { get; }
|
public ComponentDescriptor Descriptor { get; }
|
||||||
public PropertyNode[] Properties { get; }
|
public PropertyNode[] Properties { get; }
|
||||||
public string Name => Descriptor.DisplayName;
|
public string Name => Descriptor.DisplayName;
|
||||||
|
|
||||||
public ComponentNode(World world, Entity entity, Type componentType, ComponentDescriptor descriptor)
|
internal ComponentNode(World world, EntityNode entityNode, Type componentType, ComponentDescriptor descriptor)
|
||||||
{
|
{
|
||||||
|
_undoService = EditorApplication.GetService<IUndoService>();
|
||||||
|
_worldService = EditorApplication.GetService<IEditorWorldService>();
|
||||||
|
|
||||||
|
_propertyIndices = new Dictionary<string, int>(descriptor.Properties.Length);
|
||||||
_world = world;
|
_world = world;
|
||||||
_entity = entity;
|
|
||||||
|
EntityNode = entityNode;
|
||||||
|
|
||||||
ComponentType = componentType;
|
ComponentType = componentType;
|
||||||
Descriptor = descriptor;
|
Descriptor = descriptor;
|
||||||
|
|
||||||
Properties = new PropertyNode[descriptor.Properties.Length];
|
Properties = new PropertyNode[descriptor.Properties.Length];
|
||||||
for (var i = 0; i < descriptor.Properties.Length; i++)
|
for (var i = 0; i < descriptor.Properties.Length; i++)
|
||||||
{
|
{
|
||||||
|
_propertyIndices[descriptor.Properties[i].Name] = i;
|
||||||
|
|
||||||
|
// TODO: We should use a registry/factory for different PropertyNode types instead of hardcoding HandlePropertyNode here. This is just a quick solution for handles for now.
|
||||||
var prop = descriptor.Properties[i];
|
var prop = descriptor.Properties[i];
|
||||||
if (prop.FieldType.IsGenericType && prop.FieldType.GetGenericTypeDefinition() == typeof(Ghost.Core.Handle<>))
|
if (prop.ValueType.IsGenericType && prop.ValueType.GetGenericTypeDefinition() == typeof(Ghost.Core.Handle<>))
|
||||||
{
|
{
|
||||||
var nodeType = typeof(HandlePropertyNode<>).MakeGenericType(prop.FieldType.GetGenericArguments()[0]);
|
var nodeType = typeof(HandlePropertyNode<>).MakeGenericType(prop.ValueType.GetGenericArguments()[0]);
|
||||||
Properties[i] = (PropertyNode)Activator.CreateInstance(nodeType, prop, this)!;
|
Properties[i] = (PropertyNode)Activator.CreateInstance(nodeType, prop, this)!;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Create a standard PropertyNode<T> for non-handle types
|
// Create a standard PropertyNode<T> for non-handle types
|
||||||
// We use MakeGenericType to create the correct PropertyNode<T> based on FieldType
|
// We use MakeGenericType to create the correct PropertyNode<T> based on FieldType
|
||||||
var nodeType = typeof(PropertyNode<>).MakeGenericType(prop.FieldType);
|
var nodeType = typeof(PropertyNode<>).MakeGenericType(prop.ValueType);
|
||||||
Properties[i] = (PropertyNode)Activator.CreateInstance(nodeType, prop, this, null)!;
|
Properties[i] = (PropertyNode)Activator.CreateInstance(nodeType, prop, this, null)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Data Access ---
|
public void SetPropertyValue<T>(PropertyDescriptor property, T value)
|
||||||
|
where T : unmanaged
|
||||||
public object ReadBoxedValue(PropertyDescriptor field)
|
|
||||||
{
|
{
|
||||||
unsafe
|
if (property.ValueType != typeof(T))
|
||||||
{
|
{
|
||||||
var pComponent = GetComponentPointer();
|
throw new ArgumentException("Property type does not match value type");
|
||||||
return field.ReadBoxed(pComponent);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public T GetFieldValue<T>(PropertyDescriptor field) where T : unmanaged
|
_undoService.RecordEntityComponent(this, $"Edit property {property.DisplayName} on {Descriptor.DisplayName}");
|
||||||
{
|
_worldService.Defer(() =>
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
var pComponent = GetComponentPointer();
|
|
||||||
return field.Read<T>(pComponent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetFieldValue<T>(PropertyDescriptor field, T value) where T : unmanaged
|
|
||||||
{
|
|
||||||
unsafe
|
|
||||||
{
|
{
|
||||||
if (Descriptor.IsShared)
|
if (Descriptor.IsShared)
|
||||||
{
|
{
|
||||||
var ptr = _world.EntityManager.GetSharedComponent(_entity, Descriptor.ComponentId);
|
var ptr = _world.EntityManager.GetSharedComponent(EntityNode.Entity, Descriptor.ComponentId);
|
||||||
if (ptr != null)
|
if (ptr != null)
|
||||||
{
|
{
|
||||||
using var scope = Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.CreateStackScope();
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
using var buffer = new Misaki.HighPerformance.LowLevel.Buffer.MemoryBlock((nuint)Descriptor.Size, 16, scope.AllocationHandle);
|
using var buffer = new MemoryBlock((nuint)Descriptor.Size, 16, scope.AllocationHandle);
|
||||||
System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.GetUnsafePtr(), ptr, (uint)Descriptor.Size);
|
System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.GetUnsafePtr(), ptr, (uint)Descriptor.Size);
|
||||||
field.Write<T>(buffer.GetUnsafePtr(), value);
|
property.Write(buffer.GetUnsafePtr(), value);
|
||||||
_world.EntityManager.SetSharedComponent(_entity, Descriptor.ComponentId, buffer.GetUnsafePtr());
|
_world.EntityManager.SetSharedComponent(EntityNode.Entity, Descriptor.ComponentId, buffer.GetUnsafePtr());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var pComponent = GetComponentPointer();
|
var pComponent = GetComponentPointer();
|
||||||
field.Write<T>(pComponent, value);
|
property.Write(pComponent, value);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetComponent<T>(T value)
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
if (typeof(T) != ComponentType)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Value type does not match component type");
|
||||||
|
}
|
||||||
|
|
||||||
|
_undoService.RecordEntityComponent(this, $"Edit component {Descriptor.DisplayName}");
|
||||||
|
_worldService.Defer(() =>
|
||||||
|
{
|
||||||
|
if (Descriptor.IsShared)
|
||||||
|
{
|
||||||
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
|
using var buffer = new MemoryBlock((nuint)Descriptor.Size, 16, scope.AllocationHandle);
|
||||||
|
buffer.GetElementAt<T>(0) = value;
|
||||||
|
_world.EntityManager.SetSharedComponent(EntityNode.Entity, Descriptor.ComponentId, buffer.GetUnsafePtr());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var pComponent = GetComponentPointer();
|
||||||
|
*(T*)pComponent = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyNode GetProperty(string propertyName)
|
||||||
|
{
|
||||||
|
if (_propertyIndices.TryGetValue(propertyName, out var index))
|
||||||
|
{
|
||||||
|
return Properties[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"Property '{propertyName}' not found in component '{Name}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyNode<T> GetProperty<T>(string propertyName)
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
var prop = GetProperty(propertyName);
|
||||||
|
if (prop is PropertyNode<T> typedProp)
|
||||||
|
{
|
||||||
|
return typedProp;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"Property '{propertyName}' is not of type {typeof(T).Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void* GetComponentPointer()
|
||||||
|
{
|
||||||
|
if (Descriptor.IsShared)
|
||||||
|
{
|
||||||
|
return _world.EntityManager.GetSharedComponent(EntityNode.Entity, Descriptor.ComponentId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _world.EntityManager.GetComponent(EntityNode.Entity, Descriptor.ComponentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Serialization ---
|
public T GetComponent<T>()
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
if (typeof(T) != ComponentType)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Field type does not match component type");
|
||||||
|
}
|
||||||
|
|
||||||
|
var pComponent = GetComponentPointer();
|
||||||
|
return *(T*)pComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetPropertyValue<T>(PropertyDescriptor field)
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
var pComponent = GetComponentPointer();
|
||||||
|
return field.Read<T>(pComponent);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Serialize this component to JSON. Base reads from ECS directly.</summary>
|
/// <summary>Serialize this component to JSON. Base reads from ECS directly.</summary>
|
||||||
public virtual void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, Action<object>? preSerialize = null)
|
public virtual void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, Action<object>? preSerialize = null)
|
||||||
{
|
{
|
||||||
unsafe
|
var boxed = System.Runtime.InteropServices.Marshal.PtrToStructure((nint)GetComponentPointer(), ComponentType);
|
||||||
|
if (boxed != null)
|
||||||
{
|
{
|
||||||
var boxed = System.Runtime.InteropServices.Marshal.PtrToStructure((nint)GetComponentPointer(), ComponentType);
|
preSerialize?.Invoke(boxed);
|
||||||
if (boxed != null)
|
|
||||||
|
var jsonString = JsonSerializer.Serialize(boxed, ComponentType, options);
|
||||||
|
using var doc = JsonDocument.Parse(jsonString);
|
||||||
|
var root = System.Text.Json.Nodes.JsonObject.Create(doc.RootElement);
|
||||||
|
if (root != null)
|
||||||
{
|
{
|
||||||
preSerialize?.Invoke(boxed);
|
foreach (var prop in Properties)
|
||||||
|
|
||||||
var jsonString = JsonSerializer.Serialize(boxed, ComponentType, options);
|
|
||||||
using var doc = JsonDocument.Parse(jsonString);
|
|
||||||
var root = System.Text.Json.Nodes.JsonObject.Create(doc.RootElement);
|
|
||||||
if (root != null)
|
|
||||||
{
|
{
|
||||||
foreach (var prop in Properties)
|
prop.SerializeOverride(root, boxed);
|
||||||
{
|
|
||||||
prop.SerializeOverride(root, boxed);
|
|
||||||
}
|
|
||||||
root.WriteTo(writer, options);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
root.WriteTo(writer, options);
|
||||||
JsonSerializer.Serialize(writer, boxed, ComponentType, options);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonSerializer.Serialize(writer, boxed, ComponentType, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Deserialize from JSON and apply to ECS. Base writes to ECS directly.</summary>
|
/// <summary>Deserialize from JSON and apply to ECS. Base writes to ECS directly.</summary>
|
||||||
public virtual void Deserialize(JsonElement element, JsonSerializerOptions options, Action<object>? postDeserialize = null)
|
public virtual void Deserialize(JsonElement element, JsonSerializerOptions options, Action<object>? postDeserialize = null)
|
||||||
{
|
{
|
||||||
unsafe
|
var boxed = element.Deserialize(ComponentType, options);
|
||||||
|
if (boxed != null)
|
||||||
{
|
{
|
||||||
var boxed = element.Deserialize(ComponentType, options);
|
postDeserialize?.Invoke(boxed);
|
||||||
if (boxed != null)
|
|
||||||
|
foreach (var prop in Properties)
|
||||||
{
|
{
|
||||||
postDeserialize?.Invoke(boxed);
|
prop.DeserializeOverride(element, boxed);
|
||||||
|
}
|
||||||
foreach (var prop in Properties)
|
|
||||||
{
|
|
||||||
prop.DeserializeOverride(element, boxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
_worldService.Defer(() =>
|
||||||
|
{
|
||||||
if (Descriptor.IsShared)
|
if (Descriptor.IsShared)
|
||||||
{
|
{
|
||||||
using var scope = Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.CreateStackScope();
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
using var buffer = new Misaki.HighPerformance.LowLevel.Buffer.MemoryBlock((nuint)Descriptor.Size, 16, scope.AllocationHandle);
|
using var buffer = new MemoryBlock((nuint)Descriptor.Size, 16, scope.AllocationHandle);
|
||||||
System.Runtime.InteropServices.Marshal.StructureToPtr(boxed, (nint)buffer.GetUnsafePtr(), false);
|
System.Runtime.InteropServices.Marshal.StructureToPtr(boxed, (nint)buffer.GetUnsafePtr(), false);
|
||||||
_world.EntityManager.SetSharedComponent(_entity, Descriptor.ComponentId, buffer.GetUnsafePtr());
|
_world.EntityManager.SetSharedComponent(EntityNode.Entity, Descriptor.ComponentId, buffer.GetUnsafePtr());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
System.Runtime.InteropServices.Marshal.StructureToPtr(boxed, (nint)GetComponentPointer(), false);
|
System.Runtime.InteropServices.Marshal.StructureToPtr(boxed, (nint)GetComponentPointer(), false);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void* GetComponentPointer()
|
|
||||||
{
|
|
||||||
if (Descriptor.IsShared)
|
|
||||||
{
|
|
||||||
return _world.EntityManager.GetSharedComponent(_entity, Descriptor.ComponentId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return _world.EntityManager.GetComponent(_entity, Descriptor.ComponentId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ public sealed partial class EntityNode : SceneGraphNode
|
|||||||
}
|
}
|
||||||
public List<ComponentNode> Components { get; } = new();
|
public List<ComponentNode> Components { get; } = new();
|
||||||
|
|
||||||
public EntityNode(World world, Entity entity, string name)
|
public SceneNode? SceneNode { get; }
|
||||||
|
|
||||||
|
internal EntityNode(World world, Entity entity, string name, SceneNode? sceneNode)
|
||||||
: base(world, name)
|
: base(world, name)
|
||||||
{
|
{
|
||||||
Entity = entity;
|
Entity = entity;
|
||||||
|
SceneNode = sceneNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override SceneNode? GetOwningSceneNode() => SceneNode;
|
||||||
|
|
||||||
public void BuildComponents()
|
public void BuildComponents()
|
||||||
{
|
{
|
||||||
Components.Clear();
|
Components.Clear();
|
||||||
@@ -43,7 +48,7 @@ public sealed partial class EntityNode : SceneGraphNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
var compDescriptor = Inspector.ComponentDescriptor.Create(type);
|
var compDescriptor = Inspector.ComponentDescriptor.Create(type);
|
||||||
Components.Add(new ComponentNode(World, Entity, type, compDescriptor));
|
Components.Add(new ComponentNode(World, this, type, compDescriptor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,26 +11,19 @@ namespace Ghost.Editor.Core.SceneGraph;
|
|||||||
public abstract class PropertyNode
|
public abstract class PropertyNode
|
||||||
{
|
{
|
||||||
public PropertyDescriptor Descriptor { get; }
|
public PropertyDescriptor Descriptor { get; }
|
||||||
public ComponentNode Parent { get; }
|
public ComponentNode ComponentNode { get; }
|
||||||
public PropertyNode[]? Children { get; protected set; }
|
public PropertyNode[]? Children { get; protected set; }
|
||||||
|
|
||||||
protected PropertyNode(PropertyDescriptor descriptor, ComponentNode parent)
|
protected PropertyNode(PropertyDescriptor descriptor, ComponentNode parent)
|
||||||
{
|
{
|
||||||
Descriptor = descriptor;
|
Descriptor = descriptor;
|
||||||
Parent = parent;
|
ComponentNode = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Synchronize the cached value from the ECS backend.
|
/// Synchronize the cached value from the ECS backend.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract void SyncFromECS();
|
public abstract void Sync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Flush any dirty UI changes back to the ECS backend.
|
|
||||||
/// </summary>
|
|
||||||
public abstract void FlushToECS();
|
|
||||||
|
|
||||||
// --- Serialization Hooks ---
|
|
||||||
|
|
||||||
public virtual void SerializeOverride(JsonObject jsonRoot, object boxedComponent)
|
public virtual void SerializeOverride(JsonObject jsonRoot, object boxedComponent)
|
||||||
{
|
{
|
||||||
@@ -44,3 +37,50 @@ public abstract class PropertyNode
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PropertyNode<T> : PropertyNode
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private T _value;
|
||||||
|
public T Value => _value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when the value is updated from ECS. UI controls bind to this.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<T>? OnValueChanged;
|
||||||
|
|
||||||
|
public PropertyNode(PropertyDescriptor descriptor, ComponentNode parent, PropertyNode[]? children = null)
|
||||||
|
: base(descriptor, parent)
|
||||||
|
{
|
||||||
|
_value = parent.GetPropertyValue<T>(descriptor);
|
||||||
|
Children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Sync()
|
||||||
|
{
|
||||||
|
var newValue = ComponentNode.GetPropertyValue<T>(Descriptor);
|
||||||
|
|
||||||
|
if (!EqualityComparer<T>.Default.Equals(_value, newValue))
|
||||||
|
{
|
||||||
|
_value = newValue;
|
||||||
|
OnValueChanged?.Invoke(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Children != null)
|
||||||
|
{
|
||||||
|
foreach (var child in Children)
|
||||||
|
{
|
||||||
|
child.Sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by the UI when the user edits the value.
|
||||||
|
/// </summary>
|
||||||
|
public void SetValueFromUI(T newValue)
|
||||||
|
{
|
||||||
|
_value = newValue;
|
||||||
|
ComponentNode.SetPropertyValue(Descriptor, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
using Ghost.Editor.Core.Inspector;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
|
||||||
|
|
||||||
public class PropertyNode<T> : PropertyNode where T : unmanaged
|
|
||||||
{
|
|
||||||
private T _value;
|
|
||||||
public bool IsDirty { get; private set; }
|
|
||||||
|
|
||||||
public T Value => _value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event fired when the value is updated from ECS. UI controls bind to this.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<T>? OnValueChanged;
|
|
||||||
|
|
||||||
public PropertyNode(PropertyDescriptor descriptor, ComponentNode parent, PropertyNode[]? children = null)
|
|
||||||
: base(descriptor, parent)
|
|
||||||
{
|
|
||||||
Children = children;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SyncFromECS()
|
|
||||||
{
|
|
||||||
var newValue = Parent.GetFieldValue<T>(Descriptor);
|
|
||||||
|
|
||||||
if (!EqualityComparer<T>.Default.Equals(_value, newValue))
|
|
||||||
{
|
|
||||||
_value = newValue;
|
|
||||||
OnValueChanged?.Invoke(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Children != null)
|
|
||||||
{
|
|
||||||
foreach (var child in Children)
|
|
||||||
{
|
|
||||||
child.SyncFromECS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by the UI when the user edits the value.
|
|
||||||
/// </summary>
|
|
||||||
public void SetValueFromUI(T newValue)
|
|
||||||
{
|
|
||||||
IsDirty = true;
|
|
||||||
_value = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void FlushToECS()
|
|
||||||
{
|
|
||||||
if (IsDirty)
|
|
||||||
{
|
|
||||||
Parent.SetFieldValue(Descriptor, _value);
|
|
||||||
IsDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Children != null)
|
|
||||||
{
|
|
||||||
foreach (var child in Children)
|
|
||||||
{
|
|
||||||
child.FlushToECS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ public static class SceneGraphBuilder
|
|||||||
foreach (var rootEntity in roots)
|
foreach (var rootEntity in roots)
|
||||||
{
|
{
|
||||||
var name = initialNames != null && initialNames.TryGetValue(rootEntity, out var n) ? n : "Entity";
|
var name = initialNames != null && initialNames.TryGetValue(rootEntity, out var n) ? n : "Entity";
|
||||||
var entityNode = new EntityNode(parentNode.World, rootEntity, name);
|
var entityNode = new EntityNode(parentNode.World, rootEntity, name, parentNode.GetOwningSceneNode());
|
||||||
parentNode.Children.Add(entityNode);
|
parentNode.Children.Add(entityNode);
|
||||||
BuildSubtree(entityNode, childrenByParent, initialNames);
|
BuildSubtree(entityNode, childrenByParent, initialNames);
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ public static class SceneGraphBuilder
|
|||||||
foreach (var childEntity in childList)
|
foreach (var childEntity in childList)
|
||||||
{
|
{
|
||||||
var name = initialNames != null && initialNames.TryGetValue(childEntity, out var n) ? n : "Entity";
|
var name = initialNames != null && initialNames.TryGetValue(childEntity, out var n) ? n : "Entity";
|
||||||
var childNode = new EntityNode(parentNode.World, childEntity, name);
|
var childNode = new EntityNode(parentNode.World, childEntity, name, parentNode.SceneNode);
|
||||||
parentNode.Children.Add(childNode);
|
parentNode.Children.Add(childNode);
|
||||||
BuildSubtree(childNode, childrenByParent, initialNames);
|
BuildSubtree(childNode, childrenByParent, initialNames);
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ public static class SceneGraphBuilder
|
|||||||
if (childList.Contains(sibling))
|
if (childList.Contains(sibling))
|
||||||
{
|
{
|
||||||
var name = initialNames != null && initialNames.TryGetValue(sibling, out var n) ? n : "Entity";
|
var name = initialNames != null && initialNames.TryGetValue(sibling, out var n) ? n : "Entity";
|
||||||
var childNode = new EntityNode(parentNode.World, sibling, name);
|
var childNode = new EntityNode(parentNode.World, sibling, name, parentNode.SceneNode);
|
||||||
parentNode.Children.Add(childNode);
|
parentNode.Children.Add(childNode);
|
||||||
BuildSubtree(childNode, childrenByParent, initialNames);
|
BuildSubtree(childNode, childrenByParent, initialNames);
|
||||||
}
|
}
|
||||||
@@ -141,6 +141,10 @@ public static class SceneGraphBuilder
|
|||||||
|
|
||||||
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
|
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
|
||||||
var hierarchyID = ComponentTypeID<Hierarchy>.Value;
|
var hierarchyID = ComponentTypeID<Hierarchy>.Value;
|
||||||
|
if (!archetype.HasComponent(hierarchyID))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var pData = archetype.GetComponentData(location.Value.chunkIndex, location.Value.rowIndex, hierarchyID);
|
var pData = archetype.GetComponentData(location.Value.chunkIndex, location.Value.rowIndex, hierarchyID);
|
||||||
if (pData == null)
|
if (pData == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ using Ghost.Entities;
|
|||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.SceneGraph;
|
namespace Ghost.Editor.Core.SceneGraph;
|
||||||
|
|
||||||
public abstract partial class SceneGraphNode : ObservableObject, IInspectable
|
[ObservableObject]
|
||||||
|
public abstract partial class SceneGraphNode : GhostObject, IInspectable
|
||||||
{
|
{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string Name
|
public partial string Name
|
||||||
@@ -20,6 +22,11 @@ public abstract partial class SceneGraphNode : ObservableObject, IInspectable
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SceneGraphNode? Parent
|
||||||
|
{
|
||||||
|
get; internal set;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<SceneGraphNode> Children
|
public ObservableCollection<SceneGraphNode> Children
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -29,6 +36,59 @@ public abstract partial class SceneGraphNode : ObservableObject, IInspectable
|
|||||||
{
|
{
|
||||||
World = world;
|
World = world;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
Children.CollectionChanged += OnChildrenChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChildrenChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OldItems != null)
|
||||||
|
{
|
||||||
|
foreach (SceneGraphNode oldItem in e.OldItems)
|
||||||
|
{
|
||||||
|
if (oldItem.Parent == this)
|
||||||
|
{
|
||||||
|
oldItem.Parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.NewItems != null)
|
||||||
|
{
|
||||||
|
foreach (SceneGraphNode newItem in e.NewItems)
|
||||||
|
{
|
||||||
|
newItem.Parent = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual SceneNode? GetOwningSceneNode()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Modify()
|
||||||
|
{
|
||||||
|
base.Modify(); // Marks this node dirty via base GhostObject logic
|
||||||
|
|
||||||
|
var sceneNode = GetOwningSceneNode();
|
||||||
|
if (sceneNode != null)
|
||||||
|
{
|
||||||
|
var worldService = EditorApplication.GetService<IEditorWorldService>();
|
||||||
|
var sceneAsset = worldService.GetAssetForScene(sceneNode.Scene.ID);
|
||||||
|
if (sceneAsset != null)
|
||||||
|
{
|
||||||
|
EditorApplication.GetService<IDirtyTrackerService>().MarkDirty(sceneAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SerializeState(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DeserializeState(BinaryReader reader)
|
||||||
|
{
|
||||||
|
Name = reader.ReadString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IconSource? CreateIcon()
|
public virtual IconSource? CreateIcon()
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ public sealed partial class SceneNode : SceneGraphNode
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SceneNode(World world, Scene scene, string name)
|
internal SceneNode(World world, Scene scene, string name)
|
||||||
: base(world, name)
|
: base(world, name)
|
||||||
{
|
{
|
||||||
Scene = scene;
|
Scene = scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override SceneNode? GetOwningSceneNode() => this;
|
||||||
|
|
||||||
public override IconSource? CreateIcon()
|
public override IconSource? CreateIcon()
|
||||||
{
|
{
|
||||||
return new FontIconSource
|
return new FontIconSource
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error(ex);
|
Logger.Warning($"FileSystemEvent exception: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
77
src/Editor/Ghost.Editor.Core/Services/DirtyTrackerService.cs
Normal file
77
src/Editor/Ghost.Editor.Core/Services/DirtyTrackerService.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Editor.Core.Assets;
|
||||||
|
using Ghost.Editor.Core.Contracts;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
|
internal class DirtyTrackerService : IDirtyTrackerService
|
||||||
|
{
|
||||||
|
private readonly IUndoService _undoService;
|
||||||
|
private readonly Dictionary<Guid, int> _cleanVersions = new();
|
||||||
|
private readonly HashSet<GhostObject> _trackedObjects = new();
|
||||||
|
|
||||||
|
public DirtyTrackerService(IUndoService undoService)
|
||||||
|
{
|
||||||
|
_undoService = undoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkDirty(GhostObject obj)
|
||||||
|
{
|
||||||
|
// When marked dirty, we just ensure it is tracked.
|
||||||
|
// Its "clean version" remains whatever it was (or 0 if it was never saved).
|
||||||
|
// If it was never saved and just got modified, its clean version is assumed to be 0 (or something that won't match GlobalVersion).
|
||||||
|
|
||||||
|
if (!_cleanVersions.ContainsKey(obj.InstanceID))
|
||||||
|
{
|
||||||
|
// If we've never seen it, and it's being marked dirty,
|
||||||
|
// its "clean state" is whatever state existed BEFORE this edit (which caused GlobalVersion to increment).
|
||||||
|
// Actually, if it's a brand new edit, UndoService will push an operation and increment GlobalVersion.
|
||||||
|
// If the object was clean at the *current* version before the edit, we should record its clean version as (GlobalVersion - 1),
|
||||||
|
// but since UndoService.RecordObject increments GlobalVersion, the timing matters.
|
||||||
|
// Let's just say its clean version is 0. If GlobalVersion is > 0, it will be dirty.
|
||||||
|
_cleanVersions[obj.InstanceID] = _undoService.GlobalVersion - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackedObjects.Add(obj);
|
||||||
|
|
||||||
|
if (obj is IAsset asset)
|
||||||
|
{
|
||||||
|
EditorApplication.GetService<IAssetRegistry>().SetAssetDirty(asset.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDirty(GhostObject obj)
|
||||||
|
{
|
||||||
|
if (_cleanVersions.TryGetValue(obj.InstanceID, out var cleanVersion))
|
||||||
|
{
|
||||||
|
return cleanVersion != _undoService.GlobalVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not tracked, it's clean.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkClean(GhostObject obj)
|
||||||
|
{
|
||||||
|
_cleanVersions[obj.InstanceID] = _undoService.GlobalVersion;
|
||||||
|
_trackedObjects.Add(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<GhostObject> GetDirtyObjects()
|
||||||
|
{
|
||||||
|
var dirtyObjects = new List<GhostObject>();
|
||||||
|
|
||||||
|
// Remove dead references
|
||||||
|
_trackedObjects.RemoveWhere(obj => GhostObject.Find(obj.InstanceID) == null);
|
||||||
|
|
||||||
|
foreach (var obj in _trackedObjects)
|
||||||
|
{
|
||||||
|
if (IsDirty(obj))
|
||||||
|
{
|
||||||
|
dirtyObjects.Add(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirtyObjects;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ using Ghost.Core.Graphics;
|
|||||||
using Ghost.Editor.Core.Assets;
|
using Ghost.Editor.Core.Assets;
|
||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Editor.Core.Utilities;
|
using Ghost.Editor.Core.Utilities;
|
||||||
using Ghost.Graphics.Core;
|
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -24,11 +23,11 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
|
|||||||
public event ShaderVariantCompiledHandler? OnShaderVariantCompiled;
|
public event ShaderVariantCompiledHandler? OnShaderVariantCompiled;
|
||||||
public event Action<ulong>? OnShaderInvalidated;
|
public event Action<ulong>? OnShaderInvalidated;
|
||||||
|
|
||||||
public EditorShaderCompilerBridge(IAssetRegistry assetRegistry, IServiceProvider serviceProvider)
|
public EditorShaderCompilerBridge(IAssetRegistry assetRegistry, IServiceProvider serviceProvider, IShaderCompiler shaderCompiler)
|
||||||
{
|
{
|
||||||
_assetRegistry = assetRegistry;
|
_assetRegistry = assetRegistry;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_compiler = new DXCShaderCompiler();
|
_compiler = shaderCompiler;
|
||||||
|
|
||||||
_assetRegistry.OnAssetImported += OnAssetImported;
|
_assetRegistry.OnAssetImported += OnAssetImported;
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/Editor/Ghost.Editor.Core/Services/EditorTickEngine.cs
Normal file
89
src/Editor/Ghost.Editor.Core/Services/EditorTickEngine.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Ghost.Entities;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
|
public sealed class EditorTickEngine : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IEditorWorldService _worldService;
|
||||||
|
private readonly DispatcherQueueTimer _timer;
|
||||||
|
private bool _isStarted;
|
||||||
|
|
||||||
|
// Time data
|
||||||
|
private TimeData _timeData;
|
||||||
|
private long _startTimestamp;
|
||||||
|
private long _lastFrameTimestamp;
|
||||||
|
|
||||||
|
public event Action? OnSafeZone;
|
||||||
|
public event Action? OnSystemUpdate;
|
||||||
|
public event Action? OnInspectorSync;
|
||||||
|
public event Action? OnFireEvents;
|
||||||
|
|
||||||
|
public EditorTickEngine(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
_worldService = worldService;
|
||||||
|
|
||||||
|
_timer = EditorApplication.DispatcherQueue.CreateTimer();
|
||||||
|
_timer.Interval = TimeSpan.FromMilliseconds(16); // ~60Hz
|
||||||
|
_timer.Tick += OnTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_isStarted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startTimestamp = Stopwatch.GetTimestamp();
|
||||||
|
_lastFrameTimestamp = _startTimestamp;
|
||||||
|
_timeData = new TimeData();
|
||||||
|
|
||||||
|
_timer.Start();
|
||||||
|
_isStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTick(DispatcherQueueTimer sender, object args)
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
var dt = (float)(now - _lastFrameTimestamp) / Stopwatch.Frequency;
|
||||||
|
var elapsed = (double)(now - _startTimestamp) / Stopwatch.Frequency;
|
||||||
|
|
||||||
|
_timeData = new TimeData
|
||||||
|
{
|
||||||
|
FrameCount = _timeData.FrameCount + 1,
|
||||||
|
DeltaTime = dt,
|
||||||
|
ElapsedTime = elapsed
|
||||||
|
};
|
||||||
|
|
||||||
|
_lastFrameTimestamp = now;
|
||||||
|
|
||||||
|
// Safe Zone (Drain Commands & ECB)
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
OnSafeZone?.Invoke();
|
||||||
|
|
||||||
|
// Editor Systems
|
||||||
|
_worldService.EditorWorld.SystemManager.UpdateAll(_timeData);
|
||||||
|
OnSystemUpdate?.Invoke();
|
||||||
|
|
||||||
|
// Inspector Sync
|
||||||
|
OnInspectorSync?.Invoke();
|
||||||
|
|
||||||
|
// Fire Events
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
OnFireEvents?.Invoke();
|
||||||
|
|
||||||
|
_worldService.EditorWorld.AdvanceVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isStarted && _timer != null)
|
||||||
|
{
|
||||||
|
_timer.Stop();
|
||||||
|
_timer.Tick -= OnTick;
|
||||||
|
_isStarted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,53 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
|
using Ghost.Editor.Core.Assets;
|
||||||
using Ghost.Editor.Core.SceneGraph;
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
using Ghost.Engine.Core;
|
using Ghost.Engine.Core;
|
||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
public class EditorWorldService : IDisposable
|
public interface IEditorWorldService : IDisposable
|
||||||
{
|
{
|
||||||
|
World EditorWorld { get; }
|
||||||
|
ObservableCollection<SceneNode> RootNodes { get; }
|
||||||
|
|
||||||
|
event Action<Entity, string, ushort>? EntityCreated;
|
||||||
|
event Action<Entity>? EntityDestroyed;
|
||||||
|
event Action<Entity, Entity, Entity>? EntityParentChanged;
|
||||||
|
event Action<Entity, string>? EntityNameChanged;
|
||||||
|
event Action? SceneGraphRebuilt;
|
||||||
|
|
||||||
|
void ChangeEntityScene(Entity entity, ushort sceneID);
|
||||||
|
void CreateDefaultScene();
|
||||||
|
void CreateEntity(string name, ushort sceneID, Entity parent = default);
|
||||||
|
void Defer(Action action);
|
||||||
|
void DestroyEntity(Entity entity);
|
||||||
|
void FirePendingEvents();
|
||||||
|
void FlushCommands();
|
||||||
|
ushort GetEntitySceneID(Entity entity);
|
||||||
|
SceneAsset? GetAssetForScene(ushort sceneID);
|
||||||
|
void RegisterSceneAsset(ushort sceneID, SceneAsset asset);
|
||||||
|
void RebuildSceneGraph(Dictionary<Entity, string>? initialNames = null);
|
||||||
|
Error RemoveParent(Entity child);
|
||||||
|
void RenameEntity(Entity entity, string newName);
|
||||||
|
Error SetParent(Entity child, Entity parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EditorWorldService : IEditorWorldService
|
||||||
|
{
|
||||||
|
private readonly ConcurrentQueue<Action> _deferredActions = new();
|
||||||
|
private readonly ConcurrentQueue<Action> _pendingEvents = new();
|
||||||
|
private readonly ConcurrentDictionary<ushort, SceneAsset> _sceneAssetMap = new();
|
||||||
|
|
||||||
public World EditorWorld
|
public World EditorWorld
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<SceneNode> RootNodes
|
public ObservableCollection<SceneNode> RootNodes
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -31,47 +64,68 @@ public class EditorWorldService : IDisposable
|
|||||||
EditorWorld = World.Create(jobScheduler, 1024);
|
EditorWorld = World.Create(jobScheduler, 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entity CreateEntity(string name, ushort sceneID, Entity parent = default)
|
public void Defer(Action action)
|
||||||
{
|
{
|
||||||
var entity = EditorWorld.EntityManager.CreateEntity();
|
_deferredActions.Enqueue(action);
|
||||||
|
}
|
||||||
|
|
||||||
EditorWorld.EntityManager.AddComponent(entity, new Engine.Components.Hierarchy
|
public void FlushCommands()
|
||||||
|
{
|
||||||
|
while (_deferredActions.TryDequeue(out var action))
|
||||||
{
|
{
|
||||||
parent = Entity.Invalid,
|
action();
|
||||||
firstChild = Entity.Invalid,
|
|
||||||
nextSibling = Entity.Invalid
|
|
||||||
});
|
|
||||||
|
|
||||||
EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
|
|
||||||
{
|
|
||||||
value = sceneID
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parent.IsValid)
|
|
||||||
{
|
|
||||||
HierarchyUtility.SetParent(EditorWorld, entity, parent);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EditorWorld.AdvanceVersion();
|
public void FirePendingEvents()
|
||||||
EntityCreated?.Invoke(entity, name, sceneID);
|
{
|
||||||
|
while (_pendingEvents.TryDequeue(out var evt))
|
||||||
if (parent.IsValid)
|
|
||||||
{
|
{
|
||||||
EntityParentChanged?.Invoke(entity, Entity.Invalid, parent);
|
evt();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return entity;
|
public void CreateEntity(string name, ushort sceneID, Entity parent = default)
|
||||||
|
{
|
||||||
|
Defer(() =>
|
||||||
|
{
|
||||||
|
var entity = EditorWorld.EntityManager.CreateEntity();
|
||||||
|
|
||||||
|
EditorWorld.EntityManager.AddComponent(entity, new Engine.Components.Hierarchy
|
||||||
|
{
|
||||||
|
parent = Entity.Invalid,
|
||||||
|
firstChild = Entity.Invalid,
|
||||||
|
nextSibling = Entity.Invalid
|
||||||
|
});
|
||||||
|
|
||||||
|
EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
|
||||||
|
{
|
||||||
|
value = sceneID
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parent.IsValid)
|
||||||
|
{
|
||||||
|
HierarchyUtility.SetParent(EditorWorld, entity, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingEvents.Enqueue(() =>
|
||||||
|
{
|
||||||
|
EntityCreated?.Invoke(entity, name, sceneID);
|
||||||
|
if (parent.IsValid)
|
||||||
|
{
|
||||||
|
EntityParentChanged?.Invoke(entity, Entity.Invalid, parent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DestroyEntity(Entity entity)
|
public void DestroyEntity(Entity entity)
|
||||||
{
|
{
|
||||||
if (!entity.IsValid)
|
Defer(() =>
|
||||||
{
|
{
|
||||||
return;
|
if (!entity.IsValid) return;
|
||||||
}
|
DestroyEntityRecursive(entity);
|
||||||
|
});
|
||||||
DestroyEntityRecursive(entity);
|
|
||||||
EditorWorld.AdvanceVersion();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DestroyEntityRecursive(Entity entity)
|
private void DestroyEntityRecursive(Entity entity)
|
||||||
@@ -91,7 +145,7 @@ public class EditorWorldService : IDisposable
|
|||||||
|
|
||||||
HierarchyUtility.RemoveParent(EditorWorld, entity);
|
HierarchyUtility.RemoveParent(EditorWorld, entity);
|
||||||
EditorWorld.EntityManager.DestroyEntity(entity);
|
EditorWorld.EntityManager.DestroyEntity(entity);
|
||||||
EntityDestroyed?.Invoke(entity);
|
_pendingEvents.Enqueue(() => EntityDestroyed?.Invoke(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSceneIDRecursive(Entity entity, ushort sceneID)
|
private void UpdateSceneIDRecursive(Entity entity, ushort sceneID)
|
||||||
@@ -117,41 +171,54 @@ public class EditorWorldService : IDisposable
|
|||||||
|
|
||||||
public void ChangeEntityScene(Entity entity, ushort sceneID)
|
public void ChangeEntityScene(Entity entity, ushort sceneID)
|
||||||
{
|
{
|
||||||
if (!entity.IsValid)
|
Defer(() =>
|
||||||
{
|
{
|
||||||
return;
|
if (!entity.IsValid) return;
|
||||||
}
|
|
||||||
|
|
||||||
UpdateSceneIDRecursive(entity, sceneID);
|
UpdateSceneIDRecursive(entity, sceneID);
|
||||||
EditorWorld.AdvanceVersion();
|
_pendingEvents.Enqueue(() => EntityParentChanged?.Invoke(entity, Entity.Invalid, Entity.Invalid));
|
||||||
EntityParentChanged?.Invoke(entity, Entity.Invalid, Entity.Invalid);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Error SetParent(Entity child, Entity parent)
|
public Error SetParent(Entity child, Entity parent)
|
||||||
{
|
{
|
||||||
if (!child.IsValid)
|
if (!child.IsValid) return Error.InvalidArgument;
|
||||||
{
|
|
||||||
return Error.InvalidArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldParent = Entity.Invalid;
|
Error err = Error.None;
|
||||||
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(child))
|
|
||||||
{
|
|
||||||
oldParent = EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child).parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
Error err;
|
|
||||||
if (parent.IsValid)
|
if (parent.IsValid)
|
||||||
{
|
{
|
||||||
err = HierarchyUtility.SetParent(EditorWorld, child, parent);
|
err = HierarchyUtility.IsValidParent(EditorWorld, child, parent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
err = HierarchyUtility.RemoveParent(EditorWorld, child);
|
if (!EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(child))
|
||||||
|
{
|
||||||
|
err = Error.NotFound;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err == Error.None)
|
if (err != Error.None)
|
||||||
{
|
{
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
Defer(() =>
|
||||||
|
{
|
||||||
|
var oldParent = Entity.Invalid;
|
||||||
|
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(child))
|
||||||
|
{
|
||||||
|
oldParent = EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child).parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.IsValid)
|
||||||
|
{
|
||||||
|
HierarchyUtility.SetParent(EditorWorld, child, parent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HierarchyUtility.RemoveParent(EditorWorld, child);
|
||||||
|
}
|
||||||
|
|
||||||
if (parent.IsValid && EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(parent))
|
if (parent.IsValid && EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(parent))
|
||||||
{
|
{
|
||||||
var locRes = EditorWorld.EntityManager.GetEntityLocation(parent);
|
var locRes = EditorWorld.EntityManager.GetEntityLocation(parent);
|
||||||
@@ -165,11 +232,10 @@ public class EditorWorldService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorWorld.AdvanceVersion();
|
_pendingEvents.Enqueue(() => EntityParentChanged?.Invoke(child, oldParent, parent));
|
||||||
EntityParentChanged?.Invoke(child, oldParent, parent);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return err;
|
return Error.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Error RemoveParent(Entity child)
|
public Error RemoveParent(Entity child)
|
||||||
@@ -199,14 +265,24 @@ public class EditorWorldService : IDisposable
|
|||||||
return Scene.INVALID_ID;
|
return Scene.INVALID_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SceneAsset? GetAssetForScene(ushort sceneID)
|
||||||
|
{
|
||||||
|
_sceneAssetMap.TryGetValue(sceneID, out var asset);
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSceneAsset(ushort sceneID, SceneAsset asset)
|
||||||
|
{
|
||||||
|
_sceneAssetMap[sceneID] = asset;
|
||||||
|
}
|
||||||
|
|
||||||
public void RenameEntity(Entity entity, string newName)
|
public void RenameEntity(Entity entity, string newName)
|
||||||
{
|
{
|
||||||
if (!entity.IsValid)
|
Defer(() =>
|
||||||
{
|
{
|
||||||
return;
|
if (!entity.IsValid) return;
|
||||||
}
|
_pendingEvents.Enqueue(() => EntityNameChanged?.Invoke(entity, newName));
|
||||||
|
});
|
||||||
EntityNameChanged?.Invoke(entity, newName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateDefaultScene()
|
public void CreateDefaultScene()
|
||||||
@@ -216,13 +292,19 @@ public class EditorWorldService : IDisposable
|
|||||||
}
|
}
|
||||||
public void RebuildSceneGraph(Dictionary<Entity, string>? initialNames = null)
|
public void RebuildSceneGraph(Dictionary<Entity, string>? initialNames = null)
|
||||||
{
|
{
|
||||||
RootNodes.Clear();
|
Defer(() =>
|
||||||
var sceneNodes = SceneGraphBuilder.Build(EditorWorld, initialNames);
|
|
||||||
foreach (var node in sceneNodes)
|
|
||||||
{
|
{
|
||||||
RootNodes.Add(node);
|
var sceneNodes = SceneGraphBuilder.Build(EditorWorld, initialNames);
|
||||||
}
|
_pendingEvents.Enqueue(() =>
|
||||||
SceneGraphRebuilt?.Invoke();
|
{
|
||||||
|
RootNodes.Clear();
|
||||||
|
foreach (var node in sceneNodes)
|
||||||
|
{
|
||||||
|
RootNodes.Add(node);
|
||||||
|
}
|
||||||
|
SceneGraphRebuilt?.Invoke();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Syncs the inspector model from ECS data on every render frame.
|
/// Syncs the inspector model from ECS data on every editor tick (Phase 3).
|
||||||
/// Uses CompositionTarget.Rendering with a 60Hz cap.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class InspectorSyncService : IDisposable
|
public sealed class InspectorSyncService : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly EditorTickEngine _tickEngine;
|
||||||
private ISyncableInspectorModel? _activeModel;
|
private ISyncableInspectorModel? _activeModel;
|
||||||
private long _lastSyncTick;
|
|
||||||
private static readonly long s_minSyncInterval = Stopwatch.Frequency / 60;
|
|
||||||
private bool _isStarted;
|
private bool _isStarted;
|
||||||
|
|
||||||
|
public InspectorSyncService(EditorTickEngine tickEngine)
|
||||||
|
{
|
||||||
|
_tickEngine = tickEngine;
|
||||||
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
if (_isStarted)
|
if (_isStarted)
|
||||||
@@ -22,7 +23,7 @@ public sealed class InspectorSyncService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CompositionTarget.Rendering += OnRendering;
|
_tickEngine.OnInspectorSync += OnInspectorSync;
|
||||||
_isStarted = true;
|
_isStarted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,16 +37,8 @@ public sealed class InspectorSyncService : IDisposable
|
|||||||
_activeModel = null;
|
_activeModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRendering(object? sender, object e)
|
private void OnInspectorSync()
|
||||||
{
|
{
|
||||||
var now = Stopwatch.GetTimestamp();
|
|
||||||
if (now - _lastSyncTick < s_minSyncInterval)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastSyncTick = now;
|
|
||||||
|
|
||||||
if (_activeModel == null)
|
if (_activeModel == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -58,7 +51,7 @@ public sealed class InspectorSyncService : IDisposable
|
|||||||
{
|
{
|
||||||
if (_isStarted)
|
if (_isStarted)
|
||||||
{
|
{
|
||||||
CompositionTarget.Rendering -= OnRendering;
|
_tickEngine.OnInspectorSync -= OnInspectorSync;
|
||||||
_isStarted = false;
|
_isStarted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ using Ghost.Entities;
|
|||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
public class SceneGraphSyncService : IDisposable
|
internal class SceneGraphSyncService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly EditorWorldService _worldService;
|
private readonly IEditorWorldService _worldService;
|
||||||
private readonly Dictionary<Entity, EntityNode> _nodeMap = new();
|
private readonly Dictionary<Entity, EntityNode> _nodeMap = new();
|
||||||
|
|
||||||
public SceneGraphSyncService(EditorWorldService worldService)
|
public SceneGraphSyncService(IEditorWorldService worldService)
|
||||||
{
|
{
|
||||||
_worldService = worldService;
|
_worldService = worldService;
|
||||||
|
|
||||||
@@ -67,11 +67,12 @@ public class SceneGraphSyncService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var node = new EntityNode(_worldService.EditorWorld, entity, name);
|
|
||||||
_nodeMap[entity] = node;
|
|
||||||
|
|
||||||
// By default, add to the scene's root collection
|
// By default, add to the scene's root collection
|
||||||
var sceneNode = FindOrCreateSceneNode(sceneID);
|
var sceneNode = FindOrCreateSceneNode(sceneID);
|
||||||
|
|
||||||
|
var node = new EntityNode(_worldService.EditorWorld, entity, name, sceneNode);
|
||||||
|
_nodeMap[entity] = node;
|
||||||
|
|
||||||
sceneNode.Children.Add(node);
|
sceneNode.Children.Add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ internal class SceneSerializationService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly EditorWorldService _worldService;
|
private readonly IEditorWorldService _worldService;
|
||||||
private readonly IAssetRegistry _assetRegistry;
|
private readonly IAssetRegistry _assetRegistry;
|
||||||
private readonly SceneGraphSyncService _syncService;
|
private readonly SceneGraphSyncService _syncService;
|
||||||
|
|
||||||
public SceneSerializationService(EditorWorldService worldService, IAssetRegistry assetRegistry, SceneGraphSyncService syncService)
|
public SceneSerializationService(IEditorWorldService worldService, IAssetRegistry assetRegistry, SceneGraphSyncService syncService)
|
||||||
{
|
{
|
||||||
_worldService = worldService;
|
_worldService = worldService;
|
||||||
_assetRegistry = assetRegistry;
|
_assetRegistry = assetRegistry;
|
||||||
@@ -288,146 +288,149 @@ internal class SceneSerializationService : IDisposable
|
|||||||
|
|
||||||
#region Load Scene into Editor World
|
#region Load Scene into Editor World
|
||||||
|
|
||||||
public unsafe Result<Scene> LoadSceneIntoEditorWorld(SceneSaveData data, SceneLoadingType loadingType = SceneLoadingType.Single)
|
public unsafe void LoadSceneIntoEditorWorld(SceneSaveData data, SceneLoadingType loadingType = SceneLoadingType.Single, Action<Scene>? onComplete = null)
|
||||||
{
|
{
|
||||||
if (loadingType == SceneLoadingType.Single)
|
_worldService.Defer(() =>
|
||||||
{
|
{
|
||||||
_worldService.EditorWorld.Reset();
|
if (loadingType == SceneLoadingType.Single)
|
||||||
}
|
|
||||||
|
|
||||||
var world = _worldService.EditorWorld;
|
|
||||||
var activeScene = SceneManager.CreateScene();
|
|
||||||
|
|
||||||
var entityCount = data.Entities.Count;
|
|
||||||
var forwardMap = new Dictionary<int, Entity>(entityCount);
|
|
||||||
if (entityCount == 0)
|
|
||||||
{
|
|
||||||
goto RebuildAndReturn;
|
|
||||||
}
|
|
||||||
|
|
||||||
var scope = AllocationManager.CreateStackScope();
|
|
||||||
var typeIds = new UnsafeArray<UnsafeList<Identifier<IComponent>>>(entityCount, scope.AllocationHandle);
|
|
||||||
for (var i = 0; i < typeIds.Length; i++)
|
|
||||||
{
|
|
||||||
typeIds[i] = new UnsafeList<Identifier<IComponent>>(16, scope.AllocationHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
|
||||||
{
|
{
|
||||||
var entityData = data.Entities[fileIndex];
|
_worldService.EditorWorld.Reset();
|
||||||
ref var list = ref typeIds[fileIndex];
|
|
||||||
|
|
||||||
list.Add(ComponentRegistry.GetOrRegisterComponentID<SceneID>());
|
|
||||||
|
|
||||||
foreach (var (typeName, _) in entityData.Components)
|
|
||||||
{
|
|
||||||
var compId = ComponentRegistry.GetComponentIDByName(typeName);
|
|
||||||
if (compId.IsInvalid)
|
|
||||||
{
|
|
||||||
var type = TypeCache.GetTypes().FirstOrDefault(t => t.FullName == typeName);
|
|
||||||
if (type == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
compId = RegisterComponentByType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(compId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var componentSet = new ComponentSetView(list);
|
|
||||||
var entity = world.EntityManager.CreateEntity(componentSet);
|
|
||||||
forwardMap[fileIndex] = entity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using var buffer = new MemoryBlock(1024, 16, scope.AllocationHandle);
|
var world = _worldService.EditorWorld;
|
||||||
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
var activeScene = SceneManager.CreateScene();
|
||||||
|
|
||||||
|
var entityCount = data.Entities.Count;
|
||||||
|
var forwardMap = new Dictionary<int, Entity>(entityCount);
|
||||||
|
if (entityCount == 0)
|
||||||
{
|
{
|
||||||
if (!forwardMap.TryGetValue(fileIndex, out var entity))
|
goto RebuildAndReturn;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
world.EntityManager.SetSharedComponent(entity, new SceneID { value = activeScene.ID });
|
|
||||||
|
|
||||||
var entityData = data.Entities[fileIndex];
|
|
||||||
|
|
||||||
foreach (var (typeName, componentElement) in entityData.Components)
|
|
||||||
{
|
|
||||||
var compId = ComponentRegistry.GetComponentIDByName(typeName);
|
|
||||||
if (compId.IsInvalid)
|
|
||||||
{
|
|
||||||
var type = TypeCache.GetTypes().FirstOrDefault(t => t.FullName == typeName);
|
|
||||||
if (type == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
compId = ComponentRegistry.GetComponentIDByName(typeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compId.IsInvalid)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var componentType = ComponentRegistry.s_runtimeIDToType[compId];
|
|
||||||
|
|
||||||
if (_syncService.TryGetNode(entity, out var node))
|
|
||||||
{
|
|
||||||
node.BuildComponents();
|
|
||||||
var compNode = node.Components.FirstOrDefault(c => c.ComponentType == componentType);
|
|
||||||
if (compNode != null)
|
|
||||||
{
|
|
||||||
compNode.Deserialize(componentElement, s_jsonOptions, (boxed) =>
|
|
||||||
{
|
|
||||||
RemapLocalFieldsToEntity(boxed, componentType, forwardMap);
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to direct deserialization
|
|
||||||
var boxedLegacy = componentElement.Deserialize(componentType, s_jsonOptions);
|
|
||||||
if (boxedLegacy == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemapLocalFieldsToEntity(boxedLegacy, componentType, forwardMap);
|
|
||||||
|
|
||||||
Marshal.StructureToPtr(boxedLegacy, (nint)buffer.GetUnsafePtr(), false);
|
|
||||||
|
|
||||||
world.EntityManager.SetComponent(entity, compId, buffer.GetUnsafePtr());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
scope.Dispose();
|
|
||||||
|
|
||||||
|
var scope = AllocationManager.CreateStackScope();
|
||||||
|
var typeIds = new UnsafeArray<UnsafeList<Identifier<IComponent>>>(entityCount, scope.AllocationHandle);
|
||||||
for (var i = 0; i < typeIds.Length; i++)
|
for (var i = 0; i < typeIds.Length; i++)
|
||||||
{
|
{
|
||||||
typeIds[i].Dispose();
|
typeIds[i] = new UnsafeList<Identifier<IComponent>>(16, scope.AllocationHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
typeIds.Dispose();
|
try
|
||||||
}
|
|
||||||
|
|
||||||
RebuildAndReturn:
|
|
||||||
var initialNames = new Dictionary<Entity, string>();
|
|
||||||
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
|
||||||
{
|
|
||||||
if (forwardMap.TryGetValue(fileIndex, out var entity))
|
|
||||||
{
|
{
|
||||||
initialNames[entity] = data.Entities[fileIndex].Name;
|
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
||||||
|
{
|
||||||
|
var entityData = data.Entities[fileIndex];
|
||||||
|
ref var list = ref typeIds[fileIndex];
|
||||||
|
|
||||||
|
list.Add(ComponentRegistry.GetOrRegisterComponentID<SceneID>());
|
||||||
|
|
||||||
|
foreach (var (typeName, _) in entityData.Components)
|
||||||
|
{
|
||||||
|
var compId = ComponentRegistry.GetComponentIDByName(typeName);
|
||||||
|
if (compId.IsInvalid)
|
||||||
|
{
|
||||||
|
var type = TypeCache.GetTypes().FirstOrDefault(t => t.FullName == typeName);
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
compId = RegisterComponentByType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(compId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentSet = new ComponentSetView(list);
|
||||||
|
var entity = world.EntityManager.CreateEntity(componentSet);
|
||||||
|
forwardMap[fileIndex] = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var buffer = new MemoryBlock(1024, 16, scope.AllocationHandle);
|
||||||
|
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
||||||
|
{
|
||||||
|
if (!forwardMap.TryGetValue(fileIndex, out var entity))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
world.EntityManager.SetSharedComponent(entity, new SceneID { value = activeScene.ID });
|
||||||
|
|
||||||
|
var entityData = data.Entities[fileIndex];
|
||||||
|
|
||||||
|
foreach (var (typeName, componentElement) in entityData.Components)
|
||||||
|
{
|
||||||
|
var compId = ComponentRegistry.GetComponentIDByName(typeName);
|
||||||
|
if (compId.IsInvalid)
|
||||||
|
{
|
||||||
|
var type = TypeCache.GetTypes().FirstOrDefault(t => t.FullName == typeName);
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
compId = ComponentRegistry.GetComponentIDByName(typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compId.IsInvalid)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentType = ComponentRegistry.s_runtimeIDToType[compId];
|
||||||
|
|
||||||
|
if (_syncService.TryGetNode(entity, out var node))
|
||||||
|
{
|
||||||
|
node.BuildComponents();
|
||||||
|
var compNode = node.Components.FirstOrDefault(c => c.ComponentType == componentType);
|
||||||
|
if (compNode != null)
|
||||||
|
{
|
||||||
|
compNode.Deserialize(componentElement, s_jsonOptions, (boxed) =>
|
||||||
|
{
|
||||||
|
RemapLocalFieldsToEntity(boxed, componentType, forwardMap);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to direct deserialization
|
||||||
|
var boxedLegacy = componentElement.Deserialize(componentType, s_jsonOptions);
|
||||||
|
if (boxedLegacy == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemapLocalFieldsToEntity(boxedLegacy, componentType, forwardMap);
|
||||||
|
|
||||||
|
Marshal.StructureToPtr(boxedLegacy, (nint)buffer.GetUnsafePtr(), false);
|
||||||
|
|
||||||
|
world.EntityManager.SetComponent(entity, compId, buffer.GetUnsafePtr());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
finally
|
||||||
_worldService.RebuildSceneGraph(initialNames);
|
{
|
||||||
return activeScene;
|
scope.Dispose();
|
||||||
|
|
||||||
|
for (var i = 0; i < typeIds.Length; i++)
|
||||||
|
{
|
||||||
|
typeIds[i].Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
typeIds.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
RebuildAndReturn:
|
||||||
|
var initialNames = new Dictionary<Entity, string>();
|
||||||
|
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
|
||||||
|
{
|
||||||
|
if (forwardMap.TryGetValue(fileIndex, out var entity))
|
||||||
|
{
|
||||||
|
initialNames[entity] = data.Entities[fileIndex].Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_worldService.RebuildSceneGraph(initialNames);
|
||||||
|
onComplete?.Invoke(activeScene);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Identifier<IComponent> RegisterComponentByType(Type type)
|
private static Identifier<IComponent> RegisterComponentByType(Type type)
|
||||||
@@ -455,19 +458,19 @@ internal class SceneSerializationService : IDisposable
|
|||||||
#region Save Scene from Editor World
|
#region Save Scene from Editor World
|
||||||
|
|
||||||
public unsafe void SaveSceneFromEditorWorld(string filePath, Scene scene)
|
public unsafe void SaveSceneFromEditorWorld(string filePath, Scene scene)
|
||||||
|
{
|
||||||
|
var bytes = SerializeSceneToMemory(scene);
|
||||||
|
File.WriteAllBytes(filePath, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe byte[] SerializeSceneToMemory(Scene scene)
|
||||||
{
|
{
|
||||||
var world = _worldService.EditorWorld;
|
var world = _worldService.EditorWorld;
|
||||||
|
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
using var sceneEntities = SceneManager.GetSceneEntities(world, scene, scope.AllocationHandle);
|
using var entities = SceneManager.GetSceneEntities(world, scene, scope.AllocationHandle);
|
||||||
|
|
||||||
var entities = new List<Entity>(sceneEntities.Count);
|
using var sorted = SortEntitiesByHierarchy(world, entities, scope.AllocationHandle);
|
||||||
for (var i = 0; i < sceneEntities.Count; i++)
|
|
||||||
{
|
|
||||||
entities.Add(sceneEntities[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sorted = SortEntitiesByHierarchy(world, entities);
|
|
||||||
|
|
||||||
var reverseMap = new Dictionary<Entity, int>();
|
var reverseMap = new Dictionary<Entity, int>();
|
||||||
for (var i = 0; i < sorted.Count; i++)
|
for (var i = 0; i < sorted.Count; i++)
|
||||||
@@ -565,57 +568,71 @@ internal class SceneSerializationService : IDisposable
|
|||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
writer.Flush();
|
writer.Flush();
|
||||||
|
|
||||||
File.WriteAllBytes(filePath, stream.ToArray());
|
return stream.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Entity> SortEntitiesByHierarchy(World world, List<Entity> entities)
|
private static UnsafeList<Entity> SortEntitiesByHierarchy(World world, ReadOnlySpan<Entity> entities, AllocationHandle allocationHandle)
|
||||||
{
|
{
|
||||||
var entitySet = new HashSet<Entity>(entities);
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
var roots = new List<Entity>();
|
|
||||||
var childrenMap = new Dictionary<Entity, List<Entity>>();
|
|
||||||
|
|
||||||
foreach (var entity in entities)
|
using var entitySet = new UnsafeHashSet<Entity>(entities.Length, scope.AllocationHandle);
|
||||||
|
using var roots = new UnsafeList<Entity>(32, scope.AllocationHandle);
|
||||||
|
var childrenMap = new UnsafeHashMap<Entity, UnsafeList<Entity>>(32, scope.AllocationHandle);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!world.EntityManager.HasComponent<Hierarchy>(entity))
|
foreach (var entity in entities)
|
||||||
{
|
{
|
||||||
roots.Add(entity);
|
if (!world.EntityManager.HasComponent<Hierarchy>(entity))
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var hierarchy = ref world.EntityManager.GetComponent<Hierarchy>(entity);
|
|
||||||
if (hierarchy.parent.IsValid && entitySet.Contains(hierarchy.parent))
|
|
||||||
{
|
|
||||||
if (!childrenMap.TryGetValue(hierarchy.parent, out var list))
|
|
||||||
{
|
{
|
||||||
list = new List<Entity>();
|
roots.Add(entity);
|
||||||
childrenMap[hierarchy.parent] = list;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Add(entity);
|
ref var hierarchy = ref world.EntityManager.GetComponent<Hierarchy>(entity);
|
||||||
|
if (hierarchy.parent.IsValid && entitySet.Contains(hierarchy.parent))
|
||||||
|
{
|
||||||
|
ref var list = ref childrenMap.GetValueRefOrAddDefault(hierarchy.parent, out var exist);
|
||||||
|
if (!exist)
|
||||||
|
{
|
||||||
|
list = new UnsafeList<Entity>(4, allocationHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
roots.Add(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
var sorted = new UnsafeList<Entity>(entities.Length, allocationHandle);
|
||||||
|
foreach (var root in roots)
|
||||||
{
|
{
|
||||||
roots.Add(entity);
|
AddEntityAndDescendants(ref sorted, root, in childrenMap);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var sorted = new List<Entity>(entities.Count);
|
return sorted;
|
||||||
foreach (var root in roots)
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
AddEntityAndDescendants(sorted, root, childrenMap);
|
foreach (var kvp in childrenMap)
|
||||||
}
|
{
|
||||||
|
kvp.Value.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
return sorted;
|
childrenMap.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddEntityAndDescendants(List<Entity> sorted, Entity entity, Dictionary<Entity, List<Entity>> childrenMap)
|
private static void AddEntityAndDescendants(ref UnsafeList<Entity> sorted, Entity entity, ref readonly UnsafeHashMap<Entity, UnsafeList<Entity>> childrenMap)
|
||||||
{
|
{
|
||||||
sorted.Add(entity);
|
sorted.Add(entity);
|
||||||
if (childrenMap.TryGetValue(entity, out var children))
|
if (childrenMap.TryGetValue(entity, out var children))
|
||||||
{
|
{
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
AddEntityAndDescendants(sorted, child, childrenMap);
|
AddEntityAndDescendants(ref sorted, child, in childrenMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
567
src/Editor/Ghost.Editor.Core/Services/UndoService.cs
Normal file
567
src/Editor/Ghost.Editor.Core/Services/UndoService.cs
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Core.Collections;
|
||||||
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
|
using Ghost.Entities;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
|
public enum LifecycleEvent { Created, Destroyed }
|
||||||
|
|
||||||
|
public interface IUndoService
|
||||||
|
{
|
||||||
|
IEnumerable<UndoOperation> UndoOperations { get; }
|
||||||
|
IEnumerable<UndoOperation> RedoOperations { get; }
|
||||||
|
|
||||||
|
int GlobalVersion { get; }
|
||||||
|
|
||||||
|
event Action? UndoRedoPerformed;
|
||||||
|
|
||||||
|
void RecordObject(GhostObject obj, string actionName);
|
||||||
|
void RecordEntityComponent(ComponentNode node, string actionName);
|
||||||
|
void RecordEntityStructure(EntityNode node, string actionName);
|
||||||
|
void RecordEntityLifecycle(EntityNode node, LifecycleEvent type);
|
||||||
|
|
||||||
|
void BeginTransaction(string name);
|
||||||
|
void EndTransaction();
|
||||||
|
void PerformUndo();
|
||||||
|
void PerformRedo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class UndoOperation
|
||||||
|
{
|
||||||
|
public int GroupId { get; set; }
|
||||||
|
public string ActionName { get; set; } = string.Empty;
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Creates an operation that holds the *current* state, so it can be pushed to Redo.
|
||||||
|
public abstract UndoOperation CreateReciprocal(IEditorWorldService worldService);
|
||||||
|
public abstract void Revert(IEditorWorldService worldService);
|
||||||
|
|
||||||
|
public virtual bool CanMerge(UndoOperation other) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ObjectStateOperation : UndoOperation
|
||||||
|
{
|
||||||
|
public Guid InstanceID { get; set; }
|
||||||
|
public byte[] State { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
|
public override UndoOperation CreateReciprocal(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
var obj = GhostObject.Find(InstanceID);
|
||||||
|
var reciprocal = new ObjectStateOperation { GroupId = GroupId, ActionName = ActionName, InstanceID = InstanceID };
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter(ms);
|
||||||
|
obj.SerializeState(writer);
|
||||||
|
reciprocal.State = ms.ToArray();
|
||||||
|
}
|
||||||
|
return reciprocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Revert(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
var obj = GhostObject.Find(InstanceID);
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream(State);
|
||||||
|
using var reader = new BinaryReader(ms);
|
||||||
|
obj.DeserializeState(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanMerge(UndoOperation other)
|
||||||
|
{
|
||||||
|
if (other is ObjectStateOperation op)
|
||||||
|
{
|
||||||
|
return op.InstanceID == InstanceID && op.GroupId == GroupId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EntityComponentOperation : UndoOperation
|
||||||
|
{
|
||||||
|
public Guid InstanceID { get; set; }
|
||||||
|
public Entity Entity { get; set; }
|
||||||
|
public int ComponentId { get; set; }
|
||||||
|
public byte[] ComponentData { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
|
public unsafe override UndoOperation CreateReciprocal(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
var node = GhostObject.Find(InstanceID) as EntityNode;
|
||||||
|
var targetEntity = node?.Entity ?? Entity;
|
||||||
|
|
||||||
|
var reciprocal = new EntityComponentOperation { GroupId = GroupId, ActionName = ActionName, Entity = targetEntity, InstanceID = InstanceID, ComponentId = ComponentId };
|
||||||
|
var pComp = worldService.EditorWorld.EntityManager.GetComponent(targetEntity, new Identifier<IComponent>(ComponentId));
|
||||||
|
if (pComp != null)
|
||||||
|
{
|
||||||
|
var size = ComponentRegistry.GetComponentInfo(new Identifier<IComponent>(ComponentId)).size;
|
||||||
|
var data = new byte[size];
|
||||||
|
fixed (byte* pDst = data)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pComp, pDst, size, size);
|
||||||
|
}
|
||||||
|
reciprocal.ComponentData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reciprocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Revert(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
var cId = ComponentId;
|
||||||
|
var data = ComponentData;
|
||||||
|
var instId = InstanceID;
|
||||||
|
var fallbackEntity = Entity;
|
||||||
|
|
||||||
|
worldService.Defer(() =>
|
||||||
|
{
|
||||||
|
var node = GhostObject.Find(instId) as EntityNode;
|
||||||
|
var targetEntity = node?.Entity ?? fallbackEntity;
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var pComp = worldService.EditorWorld.EntityManager.GetComponent(targetEntity, new Identifier<IComponent>(cId));
|
||||||
|
if (pComp != null)
|
||||||
|
{
|
||||||
|
fixed (byte* pSrc = data)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrc, pComp, data.Length, data.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanMerge(UndoOperation other)
|
||||||
|
{
|
||||||
|
if (other is EntityComponentOperation op)
|
||||||
|
{
|
||||||
|
if (op.Entity == Entity && op.ComponentId == ComponentId)
|
||||||
|
{
|
||||||
|
// Explicit transaction merge
|
||||||
|
if (op.GroupId != 0 && op.GroupId == GroupId)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time-based merge fallback for non-transactional continuous edits (e.g. 500ms)
|
||||||
|
if (op.GroupId == 0)
|
||||||
|
{
|
||||||
|
return Math.Abs((op.Timestamp - Timestamp).TotalMilliseconds) < 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EntityStructureOperation : UndoOperation
|
||||||
|
{
|
||||||
|
public Guid InstanceID { get; set; }
|
||||||
|
public Entity Entity { get; set; }
|
||||||
|
public int ArchetypeID { get; set; }
|
||||||
|
public byte[] ComponentData { get; set; } = Array.Empty<byte>();
|
||||||
|
public byte[] SharedData { get; set; } = Array.Empty<byte>();
|
||||||
|
public int SharedDataHash { get; set; }
|
||||||
|
|
||||||
|
public unsafe static EntityStructureOperation Capture(IEditorWorldService worldService, EntityNode node)
|
||||||
|
{
|
||||||
|
var entity = node.Entity;
|
||||||
|
var op = new EntityStructureOperation { Entity = entity, InstanceID = node.InstanceID };
|
||||||
|
var locRes = worldService.EditorWorld.EntityManager.GetEntityLocation(entity);
|
||||||
|
if (locRes.IsSuccess)
|
||||||
|
{
|
||||||
|
op.ArchetypeID = locRes.Value.archetypeID;
|
||||||
|
ref var archetype = ref worldService.EditorWorld.ComponentManager.GetArchetypeReference(op.ArchetypeID);
|
||||||
|
ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex);
|
||||||
|
|
||||||
|
// Compute size of all unmanaged components
|
||||||
|
var totalSize = 0;
|
||||||
|
for (var i = 0; i < archetype._layouts.Count; i++)
|
||||||
|
{
|
||||||
|
totalSize += archetype._layouts[i].size;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new byte[totalSize];
|
||||||
|
fixed (byte* pDst = data)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < archetype._layouts.Count; i++)
|
||||||
|
{
|
||||||
|
var layout = archetype._layouts[i];
|
||||||
|
var pSrc = chunk.GetUnsafePtr() + layout.offset + (layout.size * locRes.Value.rowIndex);
|
||||||
|
Buffer.MemoryCopy(pSrc, pDst + offset, layout.size, layout.size);
|
||||||
|
offset += layout.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
op.ComponentData = data;
|
||||||
|
|
||||||
|
if (chunk._groupIndex >= 0 && chunk._groupIndex < archetype._chunkGroups.Count)
|
||||||
|
{
|
||||||
|
var group = archetype._chunkGroups[chunk._groupIndex];
|
||||||
|
op.SharedData = group.sharedData.AsSpan().ToArray();
|
||||||
|
op.SharedDataHash = group.sharedDataHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override UndoOperation CreateReciprocal(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
if (GhostObject.Find(InstanceID) is not EntityNode node)
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reciprocal = Capture(worldService, node);
|
||||||
|
reciprocal.GroupId = GroupId;
|
||||||
|
reciprocal.ActionName = ActionName;
|
||||||
|
return reciprocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe override void Revert(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
var instId = InstanceID;
|
||||||
|
var fallbackEntity = Entity;
|
||||||
|
var archId = ArchetypeID;
|
||||||
|
var compData = ComponentData;
|
||||||
|
var sharedData = SharedData;
|
||||||
|
var sharedHash = SharedDataHash;
|
||||||
|
|
||||||
|
worldService.Defer(() =>
|
||||||
|
{
|
||||||
|
var node = GhostObject.Find(instId) as EntityNode;
|
||||||
|
var targetEntity = node?.Entity ?? fallbackEntity;
|
||||||
|
|
||||||
|
var world = worldService.EditorWorld;
|
||||||
|
var locRes = world.EntityManager.GetEntityLocation(targetEntity);
|
||||||
|
if (!locRes.IsSuccess)
|
||||||
|
{
|
||||||
|
return; // Entity destroyed? Should use Lifecycle undo for that.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locRes.Value.archetypeID != archId)
|
||||||
|
{
|
||||||
|
ref var targetArchetype = ref world.ComponentManager.GetArchetypeReference(archId);
|
||||||
|
|
||||||
|
// Build ComponentSetView from the target archetype
|
||||||
|
var it = targetArchetype._signature.GetIterator();
|
||||||
|
var components = new List<Identifier<IComponent>>();
|
||||||
|
while (it.Next(out var compId))
|
||||||
|
{
|
||||||
|
components.Add(new Identifier<IComponent>(compId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var set = new ComponentSetView(components.ToArray(), sharedData ?? Array.Empty<byte>());
|
||||||
|
world.EntityManager.MigrateEntity(targetEntity, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Overwrite unmanaged memory
|
||||||
|
locRes = world.EntityManager.GetEntityLocation(targetEntity);
|
||||||
|
if (locRes.IsSuccess)
|
||||||
|
{
|
||||||
|
ref var archetype = ref world.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID);
|
||||||
|
ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex);
|
||||||
|
|
||||||
|
fixed (byte* pSrcBase = compData)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < archetype._layouts.Count; i++)
|
||||||
|
{
|
||||||
|
var layout = archetype._layouts[i];
|
||||||
|
var pDst = chunk.GetUnsafePtr() + layout.offset + (layout.size * locRes.Value.rowIndex);
|
||||||
|
Buffer.MemoryCopy(pSrcBase + offset, pDst, layout.size, layout.size);
|
||||||
|
offset += layout.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanMerge(UndoOperation other)
|
||||||
|
{
|
||||||
|
if (other is EntityStructureOperation op)
|
||||||
|
{
|
||||||
|
return op.Entity == Entity && op.GroupId == GroupId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EntityLifecycleOperation : UndoOperation
|
||||||
|
{
|
||||||
|
public Entity Entity { get; set; }
|
||||||
|
public Guid InstanceID { get; set; }
|
||||||
|
public LifecycleEvent EventType { get; set; }
|
||||||
|
|
||||||
|
// State for destruction
|
||||||
|
public int ArchetypeID { get; set; }
|
||||||
|
public byte[] ComponentData { get; set; } = Array.Empty<byte>();
|
||||||
|
public byte[] SharedData { get; set; } = Array.Empty<byte>();
|
||||||
|
public int SharedDataHash { get; set; }
|
||||||
|
|
||||||
|
public override UndoOperation CreateReciprocal(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
var reciprocal = new EntityLifecycleOperation
|
||||||
|
{
|
||||||
|
GroupId = GroupId,
|
||||||
|
ActionName = ActionName,
|
||||||
|
Entity = Entity,
|
||||||
|
InstanceID = InstanceID,
|
||||||
|
EventType = EventType == LifecycleEvent.Created ? LifecycleEvent.Destroyed : LifecycleEvent.Created,
|
||||||
|
ArchetypeID = ArchetypeID,
|
||||||
|
ComponentData = ComponentData,
|
||||||
|
SharedData = SharedData,
|
||||||
|
SharedDataHash = SharedDataHash
|
||||||
|
};
|
||||||
|
return reciprocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Revert(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
worldService.Defer(() =>
|
||||||
|
{
|
||||||
|
if (EventType == LifecycleEvent.Created)
|
||||||
|
{
|
||||||
|
// Revert a Creation = Destroy
|
||||||
|
var node = GhostObject.Find(InstanceID) as EntityNode;
|
||||||
|
var targetEntity = node?.Entity ?? Entity;
|
||||||
|
worldService.EditorWorld.EntityManager.DestroyEntity(targetEntity);
|
||||||
|
// The InstanceID GhostObject will be naturally unlinked, handles become null
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Revert a Destruction = Recreate
|
||||||
|
var newEntity = worldService.EditorWorld.EntityManager.CreateEntity();
|
||||||
|
|
||||||
|
// TODO: Apply the ArchetypeID, ComponentData, SharedData to the newEntity.
|
||||||
|
// We'd add the components using the archetype signature, then memcopy the bytes.
|
||||||
|
|
||||||
|
// Fix the Node reference
|
||||||
|
if (GhostObject.Find(InstanceID) is not EntityNode node)
|
||||||
|
{
|
||||||
|
node = new EntityNode(worldService.EditorWorld, newEntity, "Resurrected", null);
|
||||||
|
// Force the InstanceID using backing field
|
||||||
|
var backingField = typeof(EntityNode).GetField("<InstanceID>k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||||
|
?? typeof(SceneGraphNode).GetField("<InstanceID>k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
backingField?.SetValue(node, InstanceID);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update the entity property of the existing node (using reflection since it's init/readonly)
|
||||||
|
var entityField = typeof(EntityNode).GetField("<Entity>k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
entityField?.SetValue(node, newEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UndoService : IUndoService
|
||||||
|
{
|
||||||
|
public event Action? UndoRedoPerformed;
|
||||||
|
|
||||||
|
private readonly IEditorWorldService _worldService;
|
||||||
|
private readonly RingBuffer<UndoOperation> _undoStack = new(50);
|
||||||
|
private readonly Stack<UndoOperation> _redoStack = new();
|
||||||
|
|
||||||
|
private int _nextGroupId = 1;
|
||||||
|
private int _activeGroupId = 0;
|
||||||
|
|
||||||
|
public int GlobalVersion { get; private set; } = 0;
|
||||||
|
|
||||||
|
public IEnumerable<UndoOperation> UndoOperations => _undoStack;
|
||||||
|
public IEnumerable<UndoOperation> RedoOperations => _redoStack;
|
||||||
|
|
||||||
|
public UndoService(IEditorWorldService worldService)
|
||||||
|
{
|
||||||
|
_worldService = worldService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginTransaction(string name)
|
||||||
|
{
|
||||||
|
_activeGroupId = _nextGroupId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndTransaction()
|
||||||
|
{
|
||||||
|
_activeGroupId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushOperation(UndoOperation op)
|
||||||
|
{
|
||||||
|
bool isTransaction = _activeGroupId != 0;
|
||||||
|
op.GroupId = isTransaction ? _activeGroupId : 0;
|
||||||
|
|
||||||
|
UndoOperation? top = _undoStack.Count > 0 ? _undoStack.Peek() : null;
|
||||||
|
if (top != null && op.CanMerge(top))
|
||||||
|
{
|
||||||
|
// Extend the merge window by updating the timestamp
|
||||||
|
top.Timestamp = op.Timestamp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTransaction)
|
||||||
|
{
|
||||||
|
op.GroupId = _nextGroupId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_undoStack.Push(op);
|
||||||
|
_redoStack.Clear(); // Any new action clears the redo stack
|
||||||
|
GlobalVersion++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordObject(GhostObject obj, string actionName)
|
||||||
|
{
|
||||||
|
var op = new ObjectStateOperation()
|
||||||
|
{
|
||||||
|
ActionName = actionName,
|
||||||
|
InstanceID = obj.InstanceID
|
||||||
|
};
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter(ms);
|
||||||
|
obj.SerializeState(writer);
|
||||||
|
op.State = ms.ToArray();
|
||||||
|
PushOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We may want to have unified api RecordObject that can handle everything.
|
||||||
|
public unsafe void RecordEntityComponent(ComponentNode node, string actionName)
|
||||||
|
{
|
||||||
|
var op = new EntityComponentOperation
|
||||||
|
{
|
||||||
|
ActionName = actionName,
|
||||||
|
Entity = node.EntityNode.Entity,
|
||||||
|
ComponentId = node.Descriptor.ComponentId,
|
||||||
|
InstanceID = node.EntityNode.InstanceID
|
||||||
|
};
|
||||||
|
|
||||||
|
var pComp = node.GetComponentPointer();
|
||||||
|
var size = node.Descriptor.Size;
|
||||||
|
var data = new byte[size];
|
||||||
|
|
||||||
|
fixed (byte* pDst = data)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pComp, pDst, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
op.ComponentData = data;
|
||||||
|
|
||||||
|
PushOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordEntityStructure(EntityNode node, string actionName)
|
||||||
|
{
|
||||||
|
var op = EntityStructureOperation.Capture(_worldService, node);
|
||||||
|
op.ActionName = actionName;
|
||||||
|
PushOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordEntityLifecycle(EntityNode node, LifecycleEvent type)
|
||||||
|
{
|
||||||
|
var op = new EntityLifecycleOperation
|
||||||
|
{
|
||||||
|
ActionName = type == LifecycleEvent.Created ? "Create Entity" : "Destroy Entity",
|
||||||
|
Entity = node.Entity,
|
||||||
|
InstanceID = node.InstanceID,
|
||||||
|
EventType = type
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type == LifecycleEvent.Destroyed)
|
||||||
|
{
|
||||||
|
// Capture state before destruction
|
||||||
|
var structure = EntityStructureOperation.Capture(_worldService, node);
|
||||||
|
op.ArchetypeID = structure.ArchetypeID;
|
||||||
|
op.ComponentData = structure.ComponentData;
|
||||||
|
op.SharedData = structure.SharedData;
|
||||||
|
op.SharedDataHash = structure.SharedDataHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
PushOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PerformUndo()
|
||||||
|
{
|
||||||
|
if (_undoStack.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetGroup = _undoStack.Peek().GroupId;
|
||||||
|
var toUndo = new List<UndoOperation>();
|
||||||
|
|
||||||
|
while (_undoStack.Count > 0 && _undoStack.Peek().GroupId == targetGroup)
|
||||||
|
{
|
||||||
|
toUndo.Add(_undoStack.Pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
var toRedo = new List<UndoOperation>();
|
||||||
|
|
||||||
|
// Revert in reverse order (which is standard for stack pop, but we popped them into a list)
|
||||||
|
// Wait, the list has them in reverse chronological order (newest at index 0).
|
||||||
|
// We should execute them in that order.
|
||||||
|
foreach (var op in toUndo)
|
||||||
|
{
|
||||||
|
// Snapshot current state for Redo BEFORE reverting
|
||||||
|
var reciprocal = op.CreateReciprocal(_worldService);
|
||||||
|
toRedo.Add(reciprocal);
|
||||||
|
|
||||||
|
op.Revert(_worldService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to Redo stack (we push the oldest action first so it comes off last on redo)
|
||||||
|
toRedo.Reverse();
|
||||||
|
foreach (var op in toRedo)
|
||||||
|
{
|
||||||
|
_redoStack.Push(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalVersion--;
|
||||||
|
|
||||||
|
// Flush ECS commands before UI updates
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
|
||||||
|
UndoRedoPerformed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PerformRedo()
|
||||||
|
{
|
||||||
|
if (_redoStack.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetGroup = _redoStack.Peek().GroupId;
|
||||||
|
var toRedo = new List<UndoOperation>();
|
||||||
|
|
||||||
|
while (_redoStack.Count > 0 && _redoStack.Peek().GroupId == targetGroup)
|
||||||
|
{
|
||||||
|
toRedo.Add(_redoStack.Pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
toRedo.Reverse();
|
||||||
|
|
||||||
|
var toUndo = new List<UndoOperation>();
|
||||||
|
|
||||||
|
foreach (var op in toRedo)
|
||||||
|
{
|
||||||
|
var reciprocal = op.CreateReciprocal(_worldService);
|
||||||
|
toUndo.Add(reciprocal);
|
||||||
|
op.Revert(_worldService); // Revert actually means Apply in this symmetric design
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var op in toUndo)
|
||||||
|
{
|
||||||
|
_undoStack.Push(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalVersion++;
|
||||||
|
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
|
||||||
|
UndoRedoPerformed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,31 +6,56 @@ namespace Ghost.Editor.Core.Utilities;
|
|||||||
|
|
||||||
public static class BindingUtility
|
public static class BindingUtility
|
||||||
{
|
{
|
||||||
public static void BindTwoWay<T>(this ValueControl<T> control, PropertyNode<T> model)
|
public static void BindTwoWay<T>(this INotifyValueChanged<T> control, PropertyNode<T> node)
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
control.OnValueChanged += (s, e) => model.SetValueFromUI(e.NewValue);
|
control.SetValueWithoutNotify(node.Value);
|
||||||
model.OnValueChanged += control.SetValue;
|
control.OnValueChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
node.ComponentNode.EntityNode.Modify();
|
||||||
|
node.SetValueFromUI(e.NewValue);
|
||||||
|
};
|
||||||
|
node.OnValueChanged += control.SetValueWithoutNotify;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void BindOneWay<T>(this ValueControl<T> control, PropertyNode<T> model)
|
public static void BindTwoWay<T, U>(this INotifyValueChanged<T> control, PropertyNode<U> node, Func<PropertyNode<U>, T> getter, Action<PropertyNode<U>, T> setter)
|
||||||
where T : unmanaged
|
where U : unmanaged
|
||||||
{
|
{
|
||||||
model.OnValueChanged += control.SetValue;
|
control.SetValueWithoutNotify(getter(node));
|
||||||
|
control.OnValueChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
node.ComponentNode.EntityNode.Modify();
|
||||||
|
setter(node, args.NewValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
node.OnValueChanged += (newVal) =>
|
||||||
|
{
|
||||||
|
control.SetValueWithoutNotify(getter(node));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void BindOneWay<T>(this FrameworkElement element, DependencyProperty dp, PropertyNode<T> model)
|
public static void BindOneWay<T>(this INotifyValueChanged<T> control, PropertyNode<T> node)
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
model.OnValueChanged += (newVal) =>
|
control.SetValueWithoutNotify(node.Value);
|
||||||
|
node.OnValueChanged += control.SetValueWithoutNotify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void BindOneWay<T, U>(this INotifyValueChanged<T> control, PropertyNode<U> node, Func<PropertyNode<U>, T> getter)
|
||||||
|
where U : unmanaged
|
||||||
|
{
|
||||||
|
node.OnValueChanged += (newVal) =>
|
||||||
|
{
|
||||||
|
control.SetValueWithoutNotify(getter(node));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void BindOneWay<T>(this FrameworkElement element, DependencyProperty dp, PropertyNode<T> node)
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
node.OnValueChanged += (newVal) =>
|
||||||
{
|
{
|
||||||
element.SetValue(dp, newVal);
|
element.SetValue(dp, newVal);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void BindOneWay<T>(this FrameworkElement element, PropertyNode<T> model, Action<T> action)
|
|
||||||
where T : unmanaged
|
|
||||||
{
|
|
||||||
model.OnValueChanged += action;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
using Ghost.Editor.Models;
|
using Ghost.Editor.Models;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
@@ -68,6 +69,9 @@ internal static class ActivationHandler
|
|||||||
|
|
||||||
var assetRegistry = App.GetService<IAssetRegistry>();
|
var assetRegistry = App.GetService<IAssetRegistry>();
|
||||||
var engineCore = App.GetService<EngineCore>();
|
var engineCore = App.GetService<EngineCore>();
|
||||||
|
var editorTick = App.GetService<EditorTickEngine>();
|
||||||
|
|
||||||
|
editorTick.Start();
|
||||||
|
|
||||||
assetRegistry.OnAssetImported += (sender, e) =>
|
assetRegistry.OnAssetImported += (sender, e) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Ghost.Editor.ViewModels.Windows;
|
|||||||
using Ghost.Editor.Views.Windows;
|
using Ghost.Editor.Views.Windows;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
using Ghost.Engine.Streaming;
|
using Ghost.Engine.Streaming;
|
||||||
|
using Ghost.Graphics.Core;
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -64,9 +65,13 @@ public partial class App : Application
|
|||||||
services.AddSingleton<IInspectorService, InspectorService>();
|
services.AddSingleton<IInspectorService, InspectorService>();
|
||||||
services.AddSingleton<IPreviewService, PreviewService>();
|
services.AddSingleton<IPreviewService, PreviewService>();
|
||||||
services.AddSingleton<IAssetRegistry, AssetRegistry>();
|
services.AddSingleton<IAssetRegistry, AssetRegistry>();
|
||||||
services.AddSingleton<InspectorSyncService>();
|
services.AddSingleton<IShaderCompiler, DXCShaderCompiler>();
|
||||||
|
services.AddSingleton<IEditorWorldService, EditorWorldService>();
|
||||||
|
services.AddSingleton<IUndoService, UndoService>();
|
||||||
|
services.AddSingleton<IDirtyTrackerService, DirtyTrackerService>();
|
||||||
|
|
||||||
services.AddSingleton<EditorWorldService>();
|
services.AddSingleton<InspectorSyncService>();
|
||||||
|
services.AddSingleton<EditorTickEngine>();
|
||||||
services.AddSingleton<SceneSerializationService>();
|
services.AddSingleton<SceneSerializationService>();
|
||||||
services.AddSingleton<SceneGraphSyncService>();
|
services.AddSingleton<SceneGraphSyncService>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using Ghost.Editor.Core;
|
using Ghost.Editor.Core;
|
||||||
using Ghost.Editor.Core.Controls;
|
using Ghost.Editor.Core.Controls;
|
||||||
using Ghost.Editor.Core.Inspector;
|
using Ghost.Editor.Core.Inspector;
|
||||||
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
|
using Ghost.Editor.Core.Utilities;
|
||||||
using Ghost.Engine.Components;
|
using Ghost.Engine.Components;
|
||||||
using Ghost.Engine.Utilities;
|
using Ghost.Engine.Utilities;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@@ -15,53 +17,60 @@ internal class LocalToWorldEditor : ComponentEditor
|
|||||||
private Float3Field _rotationField = null!;
|
private Float3Field _rotationField = null!;
|
||||||
private Float3Field _scaleField = null!;
|
private Float3Field _scaleField = null!;
|
||||||
|
|
||||||
public override void Create(Panel container)
|
public override void Create(Panel root, ComponentNode componentNode)
|
||||||
{
|
{
|
||||||
_translationField = new Float3Field();
|
_translationField = new Float3Field();
|
||||||
_rotationField = new Float3Field();
|
_rotationField = new Float3Field();
|
||||||
_scaleField = new Float3Field();
|
_scaleField = new Float3Field();
|
||||||
|
|
||||||
container.Children.Add(new PropertyField() { Label = "Position", Content = _translationField });
|
root.Children.Add(new PropertyField() { Label = "Position", Content = _translationField });
|
||||||
container.Children.Add(new PropertyField() { Label = "Rotation", Content = _rotationField });
|
root.Children.Add(new PropertyField() { Label = "Rotation", Content = _rotationField });
|
||||||
container.Children.Add(new PropertyField() { Label = "Scale", Content = _scaleField });
|
root.Children.Add(new PropertyField() { Label = "Scale", Content = _scaleField });
|
||||||
|
|
||||||
Bind(_translationField,
|
var property = componentNode.GetProperty<float4x4>(nameof(LocalToWorld.matrix));
|
||||||
getter: obj =>
|
|
||||||
|
_translationField.BindTwoWay(property,
|
||||||
|
getter: node =>
|
||||||
{
|
{
|
||||||
obj.GetData<LocalToWorld>().matrix.GetTRS(out var position, out _, out _);
|
return node.Value.c3.xyz;
|
||||||
return position;
|
|
||||||
},
|
},
|
||||||
setter: (obj, val) =>
|
setter: (node, val) =>
|
||||||
{
|
{
|
||||||
ref var data = ref obj.GetData<LocalToWorld>();
|
var data = node.Value;
|
||||||
data.matrix.c3.xyz = val;
|
data.c3.xyz = val;
|
||||||
|
node.SetValueFromUI(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
Bind(_rotationField,
|
_rotationField.BindTwoWay(property,
|
||||||
getter: obj =>
|
getter: node =>
|
||||||
{
|
{
|
||||||
obj.GetData<LocalToWorld>().matrix.GetTRS(out _, out var rotation, out _);
|
node.Value.GetTRS(out _, out var rotation, out _);
|
||||||
return math.degrees(math.EulerXYZ(rotation));
|
return math.degrees(math.EulerXYZ(rotation));
|
||||||
},
|
},
|
||||||
setter: (obj, val) =>
|
setter: (node, val) =>
|
||||||
{
|
{
|
||||||
ref var data = ref obj.GetData<LocalToWorld>();
|
var data = node.Value;
|
||||||
var newRotation = quaternion.EulerXYZ(val * math.TORADIANS);
|
var newRotation = quaternion.EulerXYZ(val * math.TORADIANS);
|
||||||
data.matrix.GetTRS(out var oldTranslation, out _, out var oldScale);
|
data.GetTRS(out var oldTranslation, out _, out var oldScale);
|
||||||
data.matrix = float4x4.TRS(oldTranslation, newRotation, oldScale);
|
data = float4x4.TRS(oldTranslation, newRotation, oldScale);
|
||||||
|
node.SetValueFromUI(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
Bind(_scaleField,
|
_scaleField.BindTwoWay(property,
|
||||||
getter: obj =>
|
getter: node =>
|
||||||
{
|
{
|
||||||
obj.GetData<LocalToWorld>().matrix.GetTRS(out _, out _, out var scale);
|
var matrix = node.Value;
|
||||||
return scale;
|
var scaleX = math.length(matrix.c0.xyz);
|
||||||
|
var scaleY = math.length(matrix.c1.xyz);
|
||||||
|
var scaleZ = math.length(matrix.c2.xyz);
|
||||||
|
return new float3(scaleX, scaleY, scaleZ);
|
||||||
},
|
},
|
||||||
setter: (obj, val) =>
|
setter: (node, val) =>
|
||||||
{
|
{
|
||||||
ref var data = ref obj.GetData<LocalToWorld>();
|
var data = node.Value;
|
||||||
data.matrix.GetTRS(out var oldTranslation, out var oldRotation, out _);
|
data.GetTRS(out var oldTranslation, out var oldRotation, out _);
|
||||||
data.matrix = float4x4.TRS(oldTranslation, oldRotation, val);
|
data = float4x4.TRS(oldTranslation, oldRotation, val);
|
||||||
|
node.SetValueFromUI(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
using Ghost.Editor.Core;
|
using Ghost.Editor.Core;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
using Ghost.Editor.Core.Utilities;
|
using Ghost.Editor.Core.Utilities;
|
||||||
|
using Ghost.Editor.Views.Controls;
|
||||||
using Ghost.Engine.Core;
|
using Ghost.Engine.Core;
|
||||||
|
|
||||||
namespace Ghost.Editor.Views.Controls;
|
namespace Ghost.Editor.ContextMenu;
|
||||||
|
|
||||||
internal partial class ContentBrowser
|
internal static class ContentBrowserContextMenu
|
||||||
{
|
{
|
||||||
[ContextMenuItem("project-browser", "Show in Explorer")]
|
[ContextMenuItem("project-browser", "Show in Explorer")]
|
||||||
private static void ShowInExplorer()
|
private static void ShowInExplorer()
|
||||||
{
|
{
|
||||||
var path = LastFocused?.ViewModel.CurrentDirectoryPath;
|
var path = ContentBrowser.LastFocused?.ViewModel.CurrentDirectoryPath;
|
||||||
if (!Directory.Exists(path))
|
if (!Directory.Exists(path))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -29,7 +30,7 @@ internal partial class ContentBrowser
|
|||||||
{
|
{
|
||||||
// TODO: Use AssetService
|
// TODO: Use AssetService
|
||||||
|
|
||||||
var viewModel = LastFocused?.ViewModel;
|
var viewModel = ContentBrowser.LastFocused?.ViewModel;
|
||||||
if (viewModel is null)
|
if (viewModel is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -57,7 +58,7 @@ internal partial class ContentBrowser
|
|||||||
[ContextMenuItem("project-browser", "Create/Asset/Scene")]
|
[ContextMenuItem("project-browser", "Create/Asset/Scene")]
|
||||||
private static void CreateSceneAsset()
|
private static void CreateSceneAsset()
|
||||||
{
|
{
|
||||||
var viewModel = LastFocused?.ViewModel;
|
var viewModel = ContentBrowser.LastFocused?.ViewModel;
|
||||||
if (viewModel is null)
|
if (viewModel is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -75,6 +76,6 @@ internal partial class ContentBrowser
|
|||||||
var sceneSerializationService = App.GetService<SceneSerializationService>();
|
var sceneSerializationService = App.GetService<SceneSerializationService>();
|
||||||
sceneSerializationService.SaveSceneFromEditorWorld(newScenePath, tempScene);
|
sceneSerializationService.SaveSceneFromEditorWorld(newScenePath, tempScene);
|
||||||
|
|
||||||
SceneManager.DestroyScene(tempScene, App.GetService<EditorWorldService>().EditorWorld);
|
SceneManager.DestroyScene(tempScene, App.GetService<IEditorWorldService>().EditorWorld);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
34
src/Editor/Ghost.Editor/ContextMenu/EditPageMenu.cs
Normal file
34
src/Editor/Ghost.Editor/ContextMenu/EditPageMenu.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Editor.Core;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.ContextMenu;
|
||||||
|
|
||||||
|
internal static class EditPageContextMenu
|
||||||
|
{
|
||||||
|
[Shortcut(VirtualKey.S, VirtualKeyModifiers.Control)]
|
||||||
|
[ContextMenuItem("edit-page-menu", "File/Save")]
|
||||||
|
private static async void MenuBar_Save()
|
||||||
|
{
|
||||||
|
if (EditorApplication.State != EditorState.Idle)
|
||||||
|
{
|
||||||
|
Logger.Warning("Cannot save while the editor is busy.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await App.GetService<Ghost.Editor.Core.Contracts.IAssetRegistry>().SaveDirtyAssetsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ContextMenuItem("edit-page-menu", "Edit/Undo", priority: 1, group: 1)]
|
||||||
|
private static void MenuBar_Undo()
|
||||||
|
{
|
||||||
|
App.GetService<IUndoService>().PerformUndo();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ContextMenuItem("edit-page-menu", "Edit/Redo", priority: 0, group: 1)]
|
||||||
|
private static void MenuBar_Redo()
|
||||||
|
{
|
||||||
|
App.GetService<IUndoService>().PerformRedo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -203,7 +203,6 @@
|
|||||||
</Page>
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="ContextMenu\" />
|
|
||||||
<Folder Include="ViewModels\Pages\" />
|
<Folder Include="ViewModels\Pages\" />
|
||||||
<Folder Include="Views\Pages\" />
|
<Folder Include="Views\Pages\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</GridView.ItemTemplate>
|
</GridView.ItemTemplate>
|
||||||
<GridView.ContextFlyout>
|
<GridView.ContextFlyout>
|
||||||
<ghost:ContextFlyout Tag="project-browser" />
|
<ghost:ContextFlyout ContextMenuTag="project-browser" />
|
||||||
</GridView.ContextFlyout>
|
</GridView.ContextFlyout>
|
||||||
</GridView>
|
</GridView>
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ namespace Ghost.Editor.Views.Controls;
|
|||||||
public sealed partial class Hierarchy : UserControl
|
public sealed partial class Hierarchy : UserControl
|
||||||
{
|
{
|
||||||
private readonly IInspectorService _inspectorService;
|
private readonly IInspectorService _inspectorService;
|
||||||
|
private readonly IEditorWorldService _worldService;
|
||||||
private readonly SceneGraphSyncService _syncService;
|
private readonly SceneGraphSyncService _syncService;
|
||||||
private readonly EditorWorldService _worldService;
|
|
||||||
private EntityNode? _draggedNode;
|
private EntityNode? _draggedNode;
|
||||||
|
|
||||||
public Hierarchy()
|
public Hierarchy()
|
||||||
@@ -27,7 +27,7 @@ public sealed partial class Hierarchy : UserControl
|
|||||||
// This ensures the singleton hooks into EditorWorldService events and starts populating RootNodes.
|
// This ensures the singleton hooks into EditorWorldService events and starts populating RootNodes.
|
||||||
_syncService = App.GetService<SceneGraphSyncService>();
|
_syncService = App.GetService<SceneGraphSyncService>();
|
||||||
|
|
||||||
_worldService = App.GetService<EditorWorldService>();
|
_worldService = App.GetService<IEditorWorldService>();
|
||||||
|
|
||||||
SceneTreeView.ItemsSource = _worldService.RootNodes;
|
SceneTreeView.ItemsSource = _worldService.RootNodes;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:Ghost.Editor.Views.Controls"
|
xmlns:controls="using:Ghost.Editor.Views.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ghost="using:Ghost.Editor.Core.Controls"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||||
NavigationCacheMode="Enabled"
|
NavigationCacheMode="Enabled"
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
|
|
||||||
<Border Height="12" Style="{StaticResource VerticalDivider}" />
|
<Border Height="12" Style="{StaticResource VerticalDivider}" />
|
||||||
|
|
||||||
<MenuBar>
|
<!--<MenuBar x:Name="TopMenuBar">
|
||||||
<MenuBarItem Title="File">
|
<MenuBarItem Title="File">
|
||||||
<MenuFlyoutItem Text="New" />
|
<MenuFlyoutItem Text="New" />
|
||||||
<MenuFlyoutItem Text="Open..." />
|
<MenuFlyoutItem Text="Open..." />
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
|
|
||||||
<MenuBarItem Title="Edit">
|
<MenuBarItem Title="Edit">
|
||||||
<MenuFlyoutItem Text="Undo" />
|
<MenuFlyoutItem Text="Undo" />
|
||||||
|
<MenuFlyoutItem Text="Redo" />
|
||||||
<MenuFlyoutItem Text="Cut" />
|
<MenuFlyoutItem Text="Cut" />
|
||||||
<MenuFlyoutItem Text="Copy" />
|
<MenuFlyoutItem Text="Copy" />
|
||||||
<MenuFlyoutItem Text="Paste" />
|
<MenuFlyoutItem Text="Paste" />
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
<MenuBarItem Title="Help">
|
<MenuBarItem Title="Help">
|
||||||
<MenuFlyoutItem Text="About" />
|
<MenuFlyoutItem Text="About" />
|
||||||
</MenuBarItem>
|
</MenuBarItem>
|
||||||
</MenuBar>
|
</MenuBar>-->
|
||||||
|
<ghost:MenuContextBar ContextMenuTag="edit-page-menu" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Ghost.Editor.Views.Controls;
|
using Ghost.Editor.Views.Controls;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Ghost.Editor.Views.Pages;
|
namespace Ghost.Editor.Views.Pages;
|
||||||
|
|
||||||
@@ -15,6 +16,17 @@ public sealed partial class EditPage : Page
|
|||||||
ContentBrowserPresenter.Content = GetContentBrowser();
|
ContentBrowserPresenter.Content = GetContentBrowser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MenuFlyoutItem CreateNewMenuItem(string name, MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
var menuItem = new MenuFlyoutItem { Text = name };
|
||||||
|
menuItem.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
methodInfo.Invoke(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
private ContentBrowser GetContentBrowser()
|
private ContentBrowser GetContentBrowser()
|
||||||
{
|
{
|
||||||
return _contentBrowser ??= new ContentBrowser();
|
return _contentBrowser ??= new ContentBrowser();
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Test/">
|
<Folder Name="/Test/">
|
||||||
<Project Path="Test/Ghost.MicroTest/Ghost.MicroTest.csproj" Id="8c8ffa4b-e1e4-46a1-9221-7b508a109edd" />
|
<Project Path="Test/Ghost.MicroTest/Ghost.MicroTest.csproj" Id="8c8ffa4b-e1e4-46a1-9221-7b508a109edd" />
|
||||||
<Project Path="Test/Ghost.Shader.Test/Ghost.Shader.Test.csproj" />
|
|
||||||
<Project Path="Test/Ghost.TestCore/Ghost.TestCore.csproj" />
|
<Project Path="Test/Ghost.TestCore/Ghost.TestCore.csproj" />
|
||||||
<Project Path="Test/Ghost.UnitTest/Ghost.UnitTest.csproj" Id="4da45668-456b-4dcc-acd8-6bfe154e6837">
|
<Project Path="Test/Ghost.UnitTest/Ghost.UnitTest.csproj" Id="4da45668-456b-4dcc-acd8-6bfe154e6837">
|
||||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||||
|
|||||||
123
src/Runtime/Ghost.Core/Collections/RingBuffer.cs
Normal file
123
src/Runtime/Ghost.Core/Collections/RingBuffer.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Core.Collections;
|
||||||
|
|
||||||
|
public class RingBuffer<T> : IEnumerable<T>
|
||||||
|
{
|
||||||
|
public struct Enumerator : IEnumerator<T>
|
||||||
|
{
|
||||||
|
private readonly RingBuffer<T> _ringBuffer;
|
||||||
|
private int _index;
|
||||||
|
public Enumerator(RingBuffer<T> ringBuffer)
|
||||||
|
{
|
||||||
|
_ringBuffer = ringBuffer;
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly T Current => _ringBuffer._buffer[(_ringBuffer._head + _index) % _ringBuffer._buffer.Length];
|
||||||
|
readonly object? IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_index + 1 >= _ringBuffer._count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_index++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly void Dispose()
|
||||||
|
{
|
||||||
|
// No resources to dispose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly T[] _buffer;
|
||||||
|
private int _head;
|
||||||
|
private int _count;
|
||||||
|
|
||||||
|
public int Count => _count;
|
||||||
|
|
||||||
|
public RingBuffer(int capacity)
|
||||||
|
{
|
||||||
|
_buffer = new T[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Push(T item)
|
||||||
|
{
|
||||||
|
if (_count < _buffer.Length)
|
||||||
|
{
|
||||||
|
_buffer[(_head + _count) % _buffer.Length] = item;
|
||||||
|
_count++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_buffer[_head] = item;
|
||||||
|
_head = (_head + 1) % _buffer.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Pop()
|
||||||
|
{
|
||||||
|
if (_count == 0) throw new InvalidOperationException("Ring buffer is empty.");
|
||||||
|
_count--;
|
||||||
|
var item = _buffer[(_head + _count) % _buffer.Length];
|
||||||
|
_buffer[(_head + _count) % _buffer.Length] = default!; // Clear reference
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryPop(out T? item)
|
||||||
|
{
|
||||||
|
if (_count == 0)
|
||||||
|
{
|
||||||
|
item = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_count--;
|
||||||
|
item = _buffer[(_head + _count) % _buffer.Length];
|
||||||
|
_buffer[(_head + _count) % _buffer.Length] = default!; // Clear reference
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Peek()
|
||||||
|
{
|
||||||
|
if (_count == 0) throw new InvalidOperationException("Ring buffer is empty.");
|
||||||
|
return _buffer[(_head + _count - 1) % _buffer.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryPeek(out T? item)
|
||||||
|
{
|
||||||
|
if (_count == 0)
|
||||||
|
{
|
||||||
|
item = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = _buffer[(_head + _count - 1) % _buffer.Length];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_head = 0;
|
||||||
|
_count = 0;
|
||||||
|
Array.Clear(_buffer, 0, _buffer.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
{
|
||||||
|
return new Enumerator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -294,7 +294,7 @@ public static class Logger
|
|||||||
public static void DebugAssert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = null)
|
public static void DebugAssert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = null)
|
||||||
{
|
{
|
||||||
s_logger.Assert(condition, message?.ToString() ?? "null");
|
s_logger.Assert(condition, message?.ToString() ?? "null");
|
||||||
#if DEBUG || GHOST_EDITOR
|
#if DEBUG
|
||||||
if (!condition)
|
if (!condition)
|
||||||
{
|
{
|
||||||
throw new Exception(message ?? "Assertion failed.");
|
throw new Exception(message ?? "Assertion failed.");
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ public struct Scene : IEquatable<Scene>
|
|||||||
_id = id;
|
_id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Scene instance from a raw ID. Use with caution.
|
||||||
|
/// </summary>
|
||||||
|
public static Scene FromID(ushort id)
|
||||||
|
{
|
||||||
|
return new Scene(id);
|
||||||
|
}
|
||||||
|
|
||||||
public readonly bool Equals(Scene other)
|
public readonly bool Equals(Scene other)
|
||||||
{
|
{
|
||||||
return ID == other.ID;
|
return ID == other.ID;
|
||||||
|
|||||||
@@ -6,36 +6,24 @@ namespace Ghost.Engine;
|
|||||||
|
|
||||||
public static class HierarchyUtility
|
public static class HierarchyUtility
|
||||||
{
|
{
|
||||||
|
public static Error IsValidParent(World world, Entity child, Entity parent)
|
||||||
|
{
|
||||||
|
if (!child.IsValid) return Error.InvalidArgument;
|
||||||
|
if (!parent.IsValid) return Error.InvalidArgument;
|
||||||
|
if (child == parent) return Error.InvalidArgument;
|
||||||
|
if (!world.EntityManager.HasComponent<Components.Hierarchy>(child)) return Error.NotFound;
|
||||||
|
if (!world.EntityManager.HasComponent<Components.Hierarchy>(parent)) return Error.NotFound;
|
||||||
|
if (IsAncestor(world, parent, child)) return Error.InvalidArgument;
|
||||||
|
|
||||||
|
return Error.None;
|
||||||
|
}
|
||||||
|
|
||||||
public static Error SetParent(World world, Entity child, Entity parent)
|
public static Error SetParent(World world, Entity child, Entity parent)
|
||||||
{
|
{
|
||||||
if (!child.IsValid)
|
var validError = IsValidParent(world, child, parent);
|
||||||
|
if (validError != Error.None)
|
||||||
{
|
{
|
||||||
return Error.InvalidArgument;
|
return validError;
|
||||||
}
|
|
||||||
|
|
||||||
if (!parent.IsValid)
|
|
||||||
{
|
|
||||||
return Error.InvalidArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child == parent)
|
|
||||||
{
|
|
||||||
return Error.InvalidArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!world.EntityManager.HasComponent<Components.Hierarchy>(child))
|
|
||||||
{
|
|
||||||
return Error.NotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!world.EntityManager.HasComponent<Components.Hierarchy>(parent))
|
|
||||||
{
|
|
||||||
return Error.NotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsAncestor(world, parent, child))
|
|
||||||
{
|
|
||||||
return Error.InvalidArgument;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var childHierarchy = ref world.EntityManager.GetComponent<Components.Hierarchy>(child);
|
ref var childHierarchy = ref world.EntityManager.GetComponent<Components.Hierarchy>(child);
|
||||||
|
|||||||
@@ -425,6 +425,36 @@ internal unsafe struct Archetype : IDisposable
|
|||||||
return newChunk;
|
return newChunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private readonly ComponentMemoryLayout GetLayoutUnsafe(int componentID)
|
||||||
|
{
|
||||||
|
return _layouts[_componentIDToLayoutIndex[componentID]];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ref Chunk GetChunkReference(int index)
|
||||||
|
{
|
||||||
|
return ref _chunks[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly Result<ComponentMemoryLayout, Error> GetLayout(int componentID)
|
||||||
|
{
|
||||||
|
if (componentID >= _componentIDToLayoutIndex.Count)
|
||||||
|
{
|
||||||
|
return Error.InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layoutIndex = _componentIDToLayoutIndex[componentID];
|
||||||
|
if (layoutIndex == -1)
|
||||||
|
{
|
||||||
|
return Error.NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _layouts[layoutIndex];
|
||||||
|
}
|
||||||
|
|
||||||
public void AllocateEntity(ReadOnlySpan<byte> sharedData, int sharedDataHash, out int chunkIndex, out int rowIndex)
|
public void AllocateEntity(ReadOnlySpan<byte> sharedData, int sharedDataHash, out int chunkIndex, out int rowIndex)
|
||||||
{
|
{
|
||||||
var world = World.GetWorldUncheck(_worldID);
|
var world = World.GetWorldUncheck(_worldID);
|
||||||
@@ -605,13 +635,12 @@ internal unsafe struct Archetype : IDisposable
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var r = GetLayout(componentID);
|
if (!_signature.IsSet(componentID))
|
||||||
if (r.Error != Error.None)
|
|
||||||
{
|
{
|
||||||
return r.Error;
|
return Error.InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = r.Value.offset;
|
var offset = GetLayoutUnsafe(componentID).offset;
|
||||||
ref var chunk = ref _chunks[chunkIndex];
|
ref var chunk = ref _chunks[chunkIndex];
|
||||||
|
|
||||||
var chunkBase = chunk.GetUnsafePtr();
|
var chunkBase = chunk.GetUnsafePtr();
|
||||||
@@ -636,13 +665,12 @@ internal unsafe struct Archetype : IDisposable
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var r = GetLayout(componentID);
|
if (!_signature.IsSet(componentID))
|
||||||
if (r.Error != Error.None)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = r.Value.offset;
|
var offset = GetLayoutUnsafe(componentID).offset;
|
||||||
var chunk = _chunks[chunkIndex];
|
var chunk = _chunks[chunkIndex];
|
||||||
|
|
||||||
var chunkBase = chunk.GetUnsafePtr();
|
var chunkBase = chunk.GetUnsafePtr();
|
||||||
@@ -650,33 +678,6 @@ internal unsafe struct Archetype : IDisposable
|
|||||||
return chunkBase + offset + (size * rowIndex);
|
return chunkBase + offset + (size * rowIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public ref Chunk GetChunkReference(int index)
|
|
||||||
{
|
|
||||||
return ref _chunks[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public readonly Result<ComponentMemoryLayout, Error> GetLayout(int componentID)
|
|
||||||
{
|
|
||||||
#if GHOST_SAFETY_CHECKS
|
|
||||||
if (componentID >= _componentIDToLayoutIndex.Count)
|
|
||||||
{
|
|
||||||
return Error.InvalidArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
var layoutIndex = _componentIDToLayoutIndex[componentID];
|
|
||||||
if (layoutIndex == -1)
|
|
||||||
{
|
|
||||||
return Error.NotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _layouts[layoutIndex];
|
|
||||||
#else
|
|
||||||
return _layouts[_componentIDToLayoutIndex[componentID]];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Returns the shared component layout for the given component ID, or an error if not found.</summary>
|
/// <summary>Returns the shared component layout for the given component ID, or an error if not found.</summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly Result<SharedComponentLayout, Error> GetSharedLayout(int componentID)
|
public readonly Result<SharedComponentLayout, Error> GetSharedLayout(int componentID)
|
||||||
@@ -695,14 +696,13 @@ internal unsafe struct Archetype : IDisposable
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly Error MarkChanged(int chunkIndex, int componentTypeId, uint globalVersion)
|
public readonly Error MarkChanged(int chunkIndex, int componentTypeId, uint globalVersion)
|
||||||
{
|
{
|
||||||
var layoutResult = GetLayout(componentTypeId);
|
if (!_signature.IsSet(componentTypeId))
|
||||||
if (layoutResult.IsFailure)
|
|
||||||
{
|
{
|
||||||
return layoutResult.Error;
|
return Error.InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var chunk = ref _chunks[chunkIndex];
|
ref var chunk = ref _chunks[chunkIndex];
|
||||||
chunk.GetVersionUnsafePtr()[layoutResult.Value.versionIndex] = globalVersion;
|
chunk.GetVersionUnsafePtr()[GetLayoutUnsafe(componentTypeId).versionIndex] = globalVersion;
|
||||||
|
|
||||||
return Error.None;
|
return Error.None;
|
||||||
}
|
}
|
||||||
@@ -710,14 +710,13 @@ internal unsafe struct Archetype : IDisposable
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly Result<uint, Error> GetVersion(int chunkIndex, int componentTypeId)
|
public readonly Result<uint, Error> GetVersion(int chunkIndex, int componentTypeId)
|
||||||
{
|
{
|
||||||
var layoutResult = GetLayout(componentTypeId);
|
if (!_signature.IsSet(componentTypeId))
|
||||||
if (layoutResult.Error != Error.None)
|
|
||||||
{
|
{
|
||||||
return layoutResult.Error;
|
return Error.InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var chunk = ref _chunks[chunkIndex];
|
ref var chunk = ref _chunks[chunkIndex];
|
||||||
return chunk.GetVersionUnsafePtr()[layoutResult.Value.versionIndex];
|
return chunk.GetVersionUnsafePtr()[GetLayoutUnsafe(componentTypeId).versionIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
public Error RemoveEntity(int chunkIndex, int rowIndex)
|
public Error RemoveEntity(int chunkIndex, int rowIndex)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ internal static class ComponentRegistry
|
|||||||
// NOTE: Can we remove the lock? Ideally all the component registeration will happend during module init, way before the first get.
|
// NOTE: Can we remove the lock? Ideally all the component registeration will happend during module init, way before the first get.
|
||||||
private static readonly Lock s_registerLock = new();
|
private static readonly Lock s_registerLock = new();
|
||||||
|
|
||||||
#if DEBUG || GHOST_EDITOR
|
#if GHOST_EDITOR
|
||||||
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ internal static class ComponentRegistry
|
|||||||
|
|
||||||
s_typeHandleToID[typeHandle] = newID;
|
s_typeHandleToID[typeHandle] = newID;
|
||||||
s_nameToRuntimeID[stableName] = newID;
|
s_nameToRuntimeID[stableName] = newID;
|
||||||
#if DEBUG || GHOST_EDITOR
|
#if GHOST_EDITOR
|
||||||
s_runtimeIDToType[newID.Value] = typeof(T);
|
s_runtimeIDToType[newID.Value] = typeof(T);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public readonly record struct Entity
|
|||||||
public bool IsValid
|
public bool IsValid
|
||||||
{
|
{
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
get => Generation > 0;
|
get => Generation != 0; // Temp entities have negative generation, valid entities have positive generation, invalid entities have 0 generation
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Entity Invalid
|
public static Entity Invalid
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Core.Utilities;
|
using Ghost.Core.Utilities;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Ghost.Entities;
|
namespace Ghost.Entities;
|
||||||
@@ -19,27 +20,83 @@ public unsafe struct EntityCommandBuffer : IDisposable
|
|||||||
AddSharedComponent,
|
AddSharedComponent,
|
||||||
RemoveSharedComponent,
|
RemoveSharedComponent,
|
||||||
SetSharedComponent,
|
SetSharedComponent,
|
||||||
|
MigrateEntity,
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferWriter _writer;
|
private BufferWriter _writer;
|
||||||
|
private int _nextTempId;
|
||||||
|
|
||||||
public EntityCommandBuffer(int capacity, AllocationHandle allocationHandle)
|
public EntityCommandBuffer(int capacity, AllocationHandle allocationHandle)
|
||||||
{
|
{
|
||||||
_writer = new BufferWriter(capacity, allocationHandle);
|
_writer = new BufferWriter(capacity, allocationHandle);
|
||||||
|
_nextTempId = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void CreateEntity(int count = 1)
|
public Entity CreateEntity()
|
||||||
{
|
{
|
||||||
_writer.Write(ECBOpCode.CreateEntity);
|
_writer.Write(ECBOpCode.CreateEntity);
|
||||||
_writer.Write(count);
|
_writer.Write(1);
|
||||||
|
var tempId = _nextTempId--;
|
||||||
|
return new Entity(tempId, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void CreateEntity(int count, ComponentSetView set)
|
public void CreateEntities(Span<Entity> entities)
|
||||||
|
{
|
||||||
|
_writer.Write(ECBOpCode.CreateEntity);
|
||||||
|
_writer.Write(entities.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < entities.Length; i++)
|
||||||
|
{
|
||||||
|
var tempId = _nextTempId--;
|
||||||
|
entities[i] = new Entity(tempId, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void CreateEntities(int count)
|
||||||
|
{
|
||||||
|
_writer.Write(ECBOpCode.CreateEntity);
|
||||||
|
_writer.Write(-count); // Negative count indicates multiple entities without returning temp IDs.
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public Entity CreateEntity(ComponentSetView set)
|
||||||
{
|
{
|
||||||
_writer.Write(ECBOpCode.CreateEntityWithComponents);
|
_writer.Write(ECBOpCode.CreateEntityWithComponents);
|
||||||
_writer.Write(count);
|
_writer.Write(1);
|
||||||
|
_writer.Write(set.Components.Length);
|
||||||
|
_writer.WriteSpan(set.Components);
|
||||||
|
_writer.Write(set.SharedComponentData.Length);
|
||||||
|
_writer.WriteSpan(set.SharedComponentData);
|
||||||
|
var tempId = _nextTempId--;
|
||||||
|
return new Entity(tempId, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void CreateEntities(Span<Entity> entities, ComponentSetView set)
|
||||||
|
{
|
||||||
|
_writer.Write(ECBOpCode.CreateEntityWithComponents);
|
||||||
|
_writer.Write(entities.Length);
|
||||||
|
_writer.Write(set.Components.Length);
|
||||||
|
_writer.WriteSpan(set.Components);
|
||||||
|
_writer.Write(set.SharedComponentData.Length);
|
||||||
|
_writer.WriteSpan(set.SharedComponentData);
|
||||||
|
|
||||||
|
for (int i = 0; i < entities.Length; i++)
|
||||||
|
{
|
||||||
|
var tempId = _nextTempId--;
|
||||||
|
entities[i] = new Entity(tempId, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void CreateEntities(int count, ComponentSetView set)
|
||||||
|
{
|
||||||
|
_writer.Write(ECBOpCode.CreateEntityWithComponents);
|
||||||
|
_writer.Write(-count); // Negative count indicates multiple entities without returning temp IDs.
|
||||||
_writer.Write(set.Components.Length);
|
_writer.Write(set.Components.Length);
|
||||||
_writer.WriteSpan(set.Components);
|
_writer.WriteSpan(set.Components);
|
||||||
_writer.Write(set.SharedComponentData.Length);
|
_writer.Write(set.SharedComponentData.Length);
|
||||||
@@ -128,10 +185,40 @@ public unsafe struct EntityCommandBuffer : IDisposable
|
|||||||
_writer.WriteMemory(data, ComponentRegistry.GetComponentInfo(componentID).size);
|
_writer.WriteMemory(data, ComponentRegistry.GetComponentInfo(componentID).size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void MigrateEntity(Entity entity, ComponentSetView set)
|
||||||
|
{
|
||||||
|
_writer.Write(ECBOpCode.MigrateEntity);
|
||||||
|
_writer.Write(entity);
|
||||||
|
_writer.Write(set.Components.Length);
|
||||||
|
_writer.WriteSpan(set.Components);
|
||||||
|
_writer.Write(set.SharedComponentData.Length);
|
||||||
|
_writer.WriteSpan(set.SharedComponentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static Entity MapEntity(Entity entity, UnsafeList<Entity>* map)
|
||||||
|
{
|
||||||
|
if (entity.ID < 0)
|
||||||
|
{
|
||||||
|
int index = (-entity.ID) - 1;
|
||||||
|
if (index >= 0 && index < map->Count)
|
||||||
|
{
|
||||||
|
return ((Entity*)map->GetUnsafePtr())[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
public readonly void Playback(EntityManager entityManager)
|
public readonly void Playback(EntityManager entityManager)
|
||||||
{
|
{
|
||||||
var reader = _writer.AsReader();
|
var reader = _writer.AsReader();
|
||||||
|
|
||||||
|
using var scope = AllocationManager.CreateStackScope();
|
||||||
|
using var tempMap = new UnsafeList<Entity>(16, scope.AllocationHandle);
|
||||||
|
using var tempEntities = new UnsafeList<Entity>(16, scope.AllocationHandle);
|
||||||
|
|
||||||
while (reader.RemainingBytes > 0)
|
while (reader.RemainingBytes > 0)
|
||||||
{
|
{
|
||||||
var op = reader.Read<ECBOpCode>();
|
var op = reader.Read<ECBOpCode>();
|
||||||
@@ -140,11 +227,31 @@ public unsafe struct EntityCommandBuffer : IDisposable
|
|||||||
{
|
{
|
||||||
case ECBOpCode.CreateEntity:
|
case ECBOpCode.CreateEntity:
|
||||||
var count = reader.Read<int>();
|
var count = reader.Read<int>();
|
||||||
entityManager.CreateEntities(count);
|
var needRemap = count > 0;
|
||||||
|
count = count < 0 ? -count : count;
|
||||||
|
|
||||||
|
if (needRemap)
|
||||||
|
{
|
||||||
|
if (tempEntities.Capacity < count)
|
||||||
|
{
|
||||||
|
tempEntities.Resize(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
tempEntities.UnsafeSetCount(count);
|
||||||
|
entityManager.CreateEntities(tempEntities);
|
||||||
|
tempMap.AddRange(tempEntities);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entityManager.CreateEntities(count);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.CreateEntityWithComponents:
|
case ECBOpCode.CreateEntityWithComponents:
|
||||||
var entityCount = reader.Read<int>();
|
var entityCount = reader.Read<int>();
|
||||||
|
var entityNeedRemap = entityCount > 0;
|
||||||
|
entityCount = entityCount < 0 ? -entityCount : entityCount;
|
||||||
|
|
||||||
var compCount = reader.Read<int>();
|
var compCount = reader.Read<int>();
|
||||||
var components = reader.ReadSpan<Identifier<IComponent>>(compCount);
|
var components = reader.ReadSpan<Identifier<IComponent>>(compCount);
|
||||||
@@ -152,60 +259,93 @@ public unsafe struct EntityCommandBuffer : IDisposable
|
|||||||
var sharedData = reader.ReadSpan<byte>(sharedDataLength);
|
var sharedData = reader.ReadSpan<byte>(sharedDataLength);
|
||||||
|
|
||||||
var set = new ComponentSetView(components, sharedData);
|
var set = new ComponentSetView(components, sharedData);
|
||||||
entityManager.CreateEntities(entityCount, set);
|
|
||||||
|
if (entityNeedRemap)
|
||||||
|
{
|
||||||
|
if (tempEntities.Capacity < entityCount)
|
||||||
|
{
|
||||||
|
tempEntities.Resize(entityCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
tempEntities.UnsafeSetCount(entityCount);
|
||||||
|
entityManager.CreateEntities(tempEntities, set);
|
||||||
|
tempMap.AddRange(tempEntities);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entityManager.CreateEntities(entityCount, set);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.DestroyEntity:
|
case ECBOpCode.DestroyEntity:
|
||||||
var entityToDestroy = reader.Read<Entity>();
|
var entityToDestroy = reader.Read<Entity>();
|
||||||
entityManager.DestroyEntity(entityToDestroy);
|
entityManager.DestroyEntity(MapEntity(entityToDestroy, &tempMap));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.DestroyEntities:
|
case ECBOpCode.DestroyEntities:
|
||||||
var removeCount = reader.Read<int>();
|
var removeCount = reader.Read<int>();
|
||||||
var entitiesToRemove = reader.ReadSpan<Entity>(removeCount);
|
var entitiesToRemove = reader.ReadSpan<Entity>(removeCount);
|
||||||
entityManager.DestroyEntities(entitiesToRemove);
|
// Mapped destruction for multiple entities not typically used with temp IDs,
|
||||||
|
// but we map them just in case.
|
||||||
|
for (int i = 0; i < removeCount; i++)
|
||||||
|
{
|
||||||
|
entityManager.DestroyEntity(MapEntity(entitiesToRemove[i], &tempMap));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.AddComponent:
|
case ECBOpCode.AddComponent:
|
||||||
var entityToAdd = reader.Read<Entity>();
|
var entityToAdd = reader.Read<Entity>();
|
||||||
var addCompTypeID = reader.Read<Identifier<IComponent>>();
|
var addCompTypeID = reader.Read<Identifier<IComponent>>();
|
||||||
var pAddCompData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(addCompTypeID).size);
|
var pAddCompData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(addCompTypeID).size);
|
||||||
entityManager.AddComponent(entityToAdd, addCompTypeID, pAddCompData);
|
entityManager.AddComponent(MapEntity(entityToAdd, &tempMap), addCompTypeID, pAddCompData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.RemoveComponent:
|
case ECBOpCode.RemoveComponent:
|
||||||
var entityToRemove = reader.Read<Entity>();
|
var entityToRemove = reader.Read<Entity>();
|
||||||
var removeCompTypeID = reader.Read<Identifier<IComponent>>();
|
var removeCompTypeID = reader.Read<Identifier<IComponent>>();
|
||||||
entityManager.RemoveComponent(entityToRemove, removeCompTypeID);
|
entityManager.RemoveComponent(MapEntity(entityToRemove, &tempMap), removeCompTypeID);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.SetComponent:
|
case ECBOpCode.SetComponent:
|
||||||
var entityToSet = reader.Read<Entity>();
|
var entityToSet = reader.Read<Entity>();
|
||||||
var setCompTypeID = reader.Read<Identifier<IComponent>>();
|
var setCompTypeID = reader.Read<Identifier<IComponent>>();
|
||||||
var pSetCompData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(setCompTypeID).size);
|
var pSetCompData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(setCompTypeID).size);
|
||||||
entityManager.SetComponent(entityToSet, setCompTypeID, pSetCompData);
|
entityManager.SetComponent(MapEntity(entityToSet, &tempMap), setCompTypeID, pSetCompData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.AddSharedComponent:
|
case ECBOpCode.AddSharedComponent:
|
||||||
var entityToAddShared = reader.Read<Entity>();
|
var entityToAddShared = reader.Read<Entity>();
|
||||||
var addSharedTypeID = reader.Read<Identifier<IComponent>>();
|
var addSharedTypeID = reader.Read<Identifier<IComponent>>();
|
||||||
var pAddSharedData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(addSharedTypeID).size);
|
var pAddSharedData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(addSharedTypeID).size);
|
||||||
entityManager.AddSharedComponent(entityToAddShared, addSharedTypeID, pAddSharedData);
|
entityManager.AddSharedComponent(MapEntity(entityToAddShared, &tempMap), addSharedTypeID, pAddSharedData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.RemoveSharedComponent:
|
case ECBOpCode.RemoveSharedComponent:
|
||||||
var entityToRemoveShared = reader.Read<Entity>();
|
var entityToRemoveShared = reader.Read<Entity>();
|
||||||
var removeSharedTypeID = reader.Read<Identifier<IComponent>>();
|
var removeSharedTypeID = reader.Read<Identifier<IComponent>>();
|
||||||
entityManager.RemoveSharedComponent(entityToRemoveShared, removeSharedTypeID);
|
entityManager.RemoveSharedComponent(MapEntity(entityToRemoveShared, &tempMap), removeSharedTypeID);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ECBOpCode.SetSharedComponent:
|
case ECBOpCode.SetSharedComponent:
|
||||||
var entityToSetShared = reader.Read<Entity>();
|
var entityToSetShared = reader.Read<Entity>();
|
||||||
var setSharedTypeID = reader.Read<Identifier<IComponent>>();
|
var setSharedTypeID = reader.Read<Identifier<IComponent>>();
|
||||||
var pSetSharedData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(setSharedTypeID).size);
|
var pSetSharedData = reader.ReadBuffer((nuint)ComponentRegistry.GetComponentInfo(setSharedTypeID).size);
|
||||||
entityManager.SetSharedComponent(entityToSetShared, setSharedTypeID, pSetSharedData);
|
entityManager.SetSharedComponent(MapEntity(entityToSetShared, &tempMap), setSharedTypeID, pSetSharedData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ECBOpCode.MigrateEntity:
|
||||||
|
var entityToMigrate = reader.Read<Entity>();
|
||||||
|
var migrateCompCount = reader.Read<int>();
|
||||||
|
var migrateComponents = reader.ReadSpan<Identifier<IComponent>>(migrateCompCount);
|
||||||
|
var migrateSharedDataLength = reader.Read<int>();
|
||||||
|
var migrateSharedData = reader.ReadSpan<byte>(migrateSharedDataLength);
|
||||||
|
var migrateSet = new ComponentSetView(migrateComponents, migrateSharedData);
|
||||||
|
entityManager.MigrateEntity(MapEntity(entityToMigrate, &tempMap), migrateSet);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tempEntities.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +353,7 @@ public unsafe struct EntityCommandBuffer : IDisposable
|
|||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_writer.Reset();
|
_writer.Reset();
|
||||||
|
_nextTempId = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|||||||
@@ -877,6 +877,82 @@ public unsafe partial class EntityManager : IDisposable
|
|||||||
{
|
{
|
||||||
return RemoveComponent(entity, ComponentTypeID<T>.Value);
|
return RemoveComponent(entity, ComponentTypeID<T>.Value);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Migrate the specified entity to a new structural state defined by the component set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">The entity to migrate.</param>
|
||||||
|
/// <param name="set">The target component set view defining the new structural state.</param>
|
||||||
|
/// <returns>The result status of the operation.</returns>
|
||||||
|
public Error MigrateEntity(Entity entity, ComponentSetView set)
|
||||||
|
{
|
||||||
|
ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist);
|
||||||
|
if (!exist)
|
||||||
|
{
|
||||||
|
return Error.NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = set.ComponentHashCode;
|
||||||
|
var newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(hash);
|
||||||
|
|
||||||
|
if (newArcID.IsInvalid)
|
||||||
|
{
|
||||||
|
newArcID = _world.ComponentManager.CreateArchetype(set.Components, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.archetypeID == newArcID)
|
||||||
|
{
|
||||||
|
ref var currentArchetype = ref _world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
||||||
|
|
||||||
|
// Check if shared data is exactly the same
|
||||||
|
if (currentArchetype._sharedLayouts.Count > 0)
|
||||||
|
{
|
||||||
|
var sharedHash = set.SharedDataHashCode;
|
||||||
|
var currentChunk = currentArchetype.GetChunkReference(location.chunkIndex);
|
||||||
|
if (currentChunk._groupIndex >= 0)
|
||||||
|
{
|
||||||
|
var currentGroup = currentArchetype._chunkGroups[currentChunk._groupIndex];
|
||||||
|
if (currentGroup.sharedDataHash == sharedHash && currentGroup.sharedData.AsSpan().SequenceEqual(set.SharedComponentData))
|
||||||
|
{
|
||||||
|
// Completely identical structure and shared group, no migration needed!
|
||||||
|
return Error.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No shared data, and archetype matches. No migration needed!
|
||||||
|
return Error.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var oldArchetype = ref _world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
||||||
|
ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID);
|
||||||
|
|
||||||
|
// Allocate the entity in the new archetype
|
||||||
|
newArchetype.AllocateEntity(set.SharedComponentData, set.SharedDataHashCode, out var newChunkIndex, out var newRowIndex);
|
||||||
|
|
||||||
|
// Copy overlapping data from old to new
|
||||||
|
CopyData(ref oldArchetype, location.chunkIndex, location.rowIndex,
|
||||||
|
ref newArchetype, newChunkIndex, newRowIndex);
|
||||||
|
|
||||||
|
// Set entity identity in the new chunk
|
||||||
|
newArchetype.SetEntity(newChunkIndex, newRowIndex, entity);
|
||||||
|
|
||||||
|
// Remove from the old archetype
|
||||||
|
var r = oldArchetype.RemoveEntity(location.chunkIndex, location.rowIndex);
|
||||||
|
Logger.DebugAssert(r == Error.None);
|
||||||
|
if (r != Error.None)
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update location
|
||||||
|
location.archetypeID = newArcID;
|
||||||
|
location.chunkIndex = newChunkIndex;
|
||||||
|
location.rowIndex = newRowIndex;
|
||||||
|
|
||||||
|
return Error.None;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the component data for the specified entity.
|
/// Set the component data for the specified entity.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ internal static class ManagedComponentRegistry
|
|||||||
private static readonly List<ManagedComponentInfo> s_registeredComponents = new();
|
private static readonly List<ManagedComponentInfo> s_registeredComponents = new();
|
||||||
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
|
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
|
||||||
private static readonly Dictionary<string, int> s_nameToRuntimeID = new();
|
private static readonly Dictionary<string, int> s_nameToRuntimeID = new();
|
||||||
#if DEBUG || GHOST_EDITOR
|
#if GHOST_EDITOR
|
||||||
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ internal static class ManagedComponentRegistry
|
|||||||
|
|
||||||
s_typeHandleToID[typeHandle] = newID;
|
s_typeHandleToID[typeHandle] = newID;
|
||||||
s_nameToRuntimeID[stableName] = newID;
|
s_nameToRuntimeID[stableName] = newID;
|
||||||
#if DEBUG || GHOST_EDITOR
|
#if GHOST_EDITOR
|
||||||
s_runtimeIDToType[newID.Value] = typeof(T);
|
s_runtimeIDToType[newID.Value] = typeof(T);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<PublishAot>True</PublishAot>
|
|
||||||
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Ghost.DSL.ShaderCompiler;
|
|
||||||
using Misaki.HighPerformance.Mathematics;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
//ShaderStructGenerator.GenerateHLSL([typeof(TestStruct), typeof(TestEnum), typeof(TestEnumFlags)], PackingRules.Exact, "C:/Users/Misaki/Downloads/Archive/Test.cs.hlsl");
|
|
||||||
|
|
||||||
//return;
|
|
||||||
#if true
|
|
||||||
var result = DSLShaderCompiler.CompileComputeShaderCode("F:\\csharp\\GhostEngine\\src\\Runtime\\Ghost.Graphics\\TestCompute.gcomp");
|
|
||||||
if (result.IsFailure)
|
|
||||||
{
|
|
||||||
Console.WriteLine(result.Message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public struct TestStruct
|
|
||||||
{
|
|
||||||
public int A;
|
|
||||||
public float B;
|
|
||||||
public Vector3 C;
|
|
||||||
public float3x4 D;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum TestEnum
|
|
||||||
{
|
|
||||||
First,
|
|
||||||
Second,
|
|
||||||
Third
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum TestEnumFlags
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
First = 1 << 0,
|
|
||||||
Second = 1 << 1,
|
|
||||||
Third = 1 << 2,
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ using Ghost.Editor.Core.Services;
|
|||||||
|
|
||||||
namespace Ghost.UnitTest.AssetSystem;
|
namespace Ghost.UnitTest.AssetSystem;
|
||||||
|
|
||||||
|
#if false
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class AssertRegistryTest
|
public class AssertRegistryTest
|
||||||
{
|
{
|
||||||
@@ -55,3 +56,4 @@ public class AssertRegistryTest
|
|||||||
Assert.AreEqual(meta.Guid, guid);
|
Assert.AreEqual(meta.Guid, guid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Ghost.Editor.Core.Assets;
|
|||||||
|
|
||||||
namespace Ghost.UnitTest.AssetSystem;
|
namespace Ghost.UnitTest.AssetSystem;
|
||||||
|
|
||||||
|
#if false
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class AssetMetaTests
|
public class AssetMetaTests
|
||||||
{
|
{
|
||||||
@@ -26,33 +27,36 @@ public class AssetMetaTests
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task TestAssetMeta_ReadWrite()
|
public async Task TestAssetMeta_ReadWrite()
|
||||||
{
|
{
|
||||||
var metaPath = Path.Combine(_testDir, "test.png.gmeta");
|
var meta = new AssetMeta
|
||||||
var originalMeta = new AssetMeta
|
|
||||||
{
|
{
|
||||||
Guid = Guid.NewGuid(),
|
Guid = Guid.NewGuid(),
|
||||||
AssetTypeId = Guid.NewGuid(),
|
AssetTypeId = Guid.NewGuid(),
|
||||||
HandlerVersion = 1,
|
HandlerVersion = 1,
|
||||||
Labels = ["test", "hero"]
|
Settings = new GenericAssetSettings()
|
||||||
};
|
};
|
||||||
|
|
||||||
await AssetMetaIO.WriteAsync(metaPath, originalMeta);
|
var metaPath = Path.Combine(_testDir, "test.meta");
|
||||||
|
|
||||||
|
await AssetMetaIO.WriteAsync(metaPath, meta, CancellationToken.None);
|
||||||
|
|
||||||
Assert.IsTrue(File.Exists(metaPath));
|
Assert.IsTrue(File.Exists(metaPath));
|
||||||
|
|
||||||
var loadedMeta = await AssetMetaIO.ReadAsync(metaPath);
|
var readMeta = await AssetMetaIO.ReadAsync(metaPath, CancellationToken.None);
|
||||||
Assert.IsNotNull(loadedMeta);
|
|
||||||
Assert.AreEqual(originalMeta.Guid, loadedMeta.Guid);
|
Assert.IsNotNull(readMeta);
|
||||||
Assert.AreEqual(originalMeta.AssetTypeId, loadedMeta.AssetTypeId);
|
Assert.AreEqual(meta.Guid, readMeta.Guid);
|
||||||
Assert.AreEqual(originalMeta.HandlerVersion, loadedMeta.HandlerVersion);
|
Assert.AreEqual(meta.AssetTypeId, readMeta.AssetTypeId);
|
||||||
CollectionAssert.AreEqual(originalMeta.Labels, loadedMeta.Labels);
|
Assert.AreEqual(meta.HandlerVersion, readMeta.HandlerVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestAssetMetaIO_Paths()
|
public void TestAssetMetaIO_GetPaths()
|
||||||
{
|
{
|
||||||
var sourcePath = "f:/assets/hero.png";
|
var sourcePath = "Assets/Textures/logo.png";
|
||||||
var expectedMetaPath = "f:/assets/hero.png.gmeta";
|
var metaPath = "Assets/Textures/logo.png" + AssetMetaIO.META_EXTENSION;
|
||||||
|
|
||||||
Assert.AreEqual(expectedMetaPath, AssetMetaIO.GetMetaPath(sourcePath));
|
Assert.AreEqual(metaPath, AssetMetaIO.GetMetaPath(sourcePath));
|
||||||
Assert.AreEqual(sourcePath, AssetMetaIO.GetSourcePath(expectedMetaPath));
|
Assert.AreEqual(sourcePath, AssetMetaIO.GetSourcePath(metaPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -4,56 +4,71 @@ using Microsoft.Data.Sqlite;
|
|||||||
|
|
||||||
namespace Ghost.UnitTest.AssetSystem;
|
namespace Ghost.UnitTest.AssetSystem;
|
||||||
|
|
||||||
|
#if false
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class ImportCoordinatorTests
|
public class ImportCoordinatorTests
|
||||||
{
|
{
|
||||||
private string _assetsRoot = null!;
|
private string _testDir = null!;
|
||||||
private string _libraryRoot = null!;
|
private AssetCatalog _catalog = null!;
|
||||||
private string _dbPath = null!;
|
private ImportCoordinator _coordinator = null!;
|
||||||
|
|
||||||
[TestInitialize]
|
[TestInitialize]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
var testDir = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
|
_testDir = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
|
||||||
_assetsRoot = Path.Combine(testDir, "Assets");
|
Directory.CreateDirectory(_testDir);
|
||||||
_libraryRoot = Path.Combine(testDir, "Library");
|
|
||||||
_dbPath = Path.Combine(_libraryRoot, "AssetDB.sqlite");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(_assetsRoot);
|
EditorApplication.Initialize(null!, _testDir, "Test");
|
||||||
Directory.CreateDirectory(_libraryRoot);
|
|
||||||
|
var dbPath = Path.Combine(_testDir, "AssetDB.sqlite");
|
||||||
|
_catalog = new AssetCatalog(dbPath);
|
||||||
|
_coordinator = new ImportCoordinator(_catalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TestCleanup]
|
||||||
public void Cleanup()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
var connectionString = new SqliteConnectionStringBuilder
|
_coordinator.Dispose();
|
||||||
{
|
_catalog.Dispose();
|
||||||
DataSource = _dbPath,
|
|
||||||
ForeignKeys = true,
|
|
||||||
Pooling = true
|
|
||||||
}.ToString();
|
|
||||||
|
|
||||||
using var connection = new SqliteConnection(connectionString);
|
if (Directory.Exists(_testDir))
|
||||||
SqliteConnection.ClearPool(connection);
|
{
|
||||||
|
Directory.Delete(_testDir, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task TestImportCoordinator_BasicImport()
|
public async Task TestImportCoordinator_BasicImport()
|
||||||
{
|
{
|
||||||
var catalog = new AssetCatalog(_dbPath);
|
var sourcePath = "Assets/test.text";
|
||||||
using var coordinator = new ImportCoordinator(catalog);
|
var fullSourcePath = Path.Combine(_testDir, sourcePath);
|
||||||
|
|
||||||
var assetGuid = Guid.NewGuid();
|
Directory.CreateDirectory(Path.GetDirectoryName(fullSourcePath)!);
|
||||||
var sourcePath = "test.png";
|
await File.WriteAllBytesAsync(fullSourcePath, [1, 2, 3], CancellationToken.None);
|
||||||
var fullSourcePath = Path.Combine(_assetsRoot, sourcePath);
|
|
||||||
await File.WriteAllBytesAsync(fullSourcePath, [1, 2, 3]);
|
var meta = new AssetMeta
|
||||||
|
{
|
||||||
|
Guid = Guid.NewGuid(),
|
||||||
|
AssetTypeId = Guid.NewGuid(),
|
||||||
|
HandlerVersion = 1,
|
||||||
|
Settings = new GenericAssetSettings()
|
||||||
|
};
|
||||||
|
|
||||||
var meta = new AssetMeta { Guid = assetGuid };
|
|
||||||
var metaPath = AssetMetaIO.GetMetaPath(fullSourcePath);
|
var metaPath = AssetMetaIO.GetMetaPath(fullSourcePath);
|
||||||
await AssetMetaIO.WriteAsync(metaPath, meta);
|
await AssetMetaIO.WriteAsync(metaPath, meta, CancellationToken.None);
|
||||||
|
|
||||||
catalog.Upsert(meta, sourcePath);
|
var job = new ImportJob(meta.Guid, sourcePath, AssetMetaIO.GetMetaPath(sourcePath), ImportReason.NewAsset);
|
||||||
|
await _coordinator.EnqueueAsync(job);
|
||||||
|
|
||||||
await coordinator.EnqueueAsync(new ImportJob(assetGuid, sourcePath, metaPath, ImportReason.NewAsset));
|
// Wait for the import to complete. The importer for .text will just copy the file to the library.
|
||||||
|
var cachePath = EditorApplication.GetAssetCachePath(meta.Guid);
|
||||||
|
using var cts = new CancellationTokenSource(5000);
|
||||||
|
while (!File.Exists(cachePath) && !cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(50, cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(File.Exists(cachePath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class MeshAssetHandlerTests
|
|||||||
|
|
||||||
var parentGuid = Guid.NewGuid();
|
var parentGuid = Guid.NewGuid();
|
||||||
var targetPath = ImportCoordinator.GetImportedAssetPath(parentGuid);
|
var targetPath = ImportCoordinator.GetImportedAssetPath(parentGuid);
|
||||||
var handler = new MeshAssetHandler();
|
var handler = new ModelAssetHandler();
|
||||||
|
|
||||||
var result = await handler.ImportAsync(sourcePath, targetPath, parentGuid, new ObjAssetSettings(), TestContext.CancellationToken);
|
var result = await handler.ImportAsync(sourcePath, targetPath, parentGuid, new ObjAssetSettings(), TestContext.CancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class EntityCommandBufferTests
|
|||||||
public void TestECB_CreateEntity()
|
public void TestECB_CreateEntity()
|
||||||
{
|
{
|
||||||
using var ecb = new EntityCommandBuffer(1024, AllocationHandle.Persistent);
|
using var ecb = new EntityCommandBuffer(1024, AllocationHandle.Persistent);
|
||||||
ecb.CreateEntity(3);
|
ecb.CreateEntities(3);
|
||||||
ecb.Playback(_world.EntityManager);
|
ecb.Playback(_world.EntityManager);
|
||||||
|
|
||||||
var queryID = QueryBuilder.New().Build(_world);
|
var queryID = QueryBuilder.New().Build(_world);
|
||||||
@@ -138,4 +138,30 @@ public class EntityCommandBufferTests
|
|||||||
|
|
||||||
Assert.IsFalse(_world.EntityManager.HasComponent<SharedComp>(entity));
|
Assert.IsFalse(_world.EntityManager.HasComponent<SharedComp>(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestECB_TempEntity()
|
||||||
|
{
|
||||||
|
using var ecb = new EntityCommandBuffer(1024, AllocationHandle.Persistent);
|
||||||
|
var tempEntity = ecb.CreateEntity();
|
||||||
|
|
||||||
|
Assert.IsLessThan(0, tempEntity.ID); // Temp entities should have negative IDs
|
||||||
|
Assert.IsLessThan(0, tempEntity.Generation); // Temp entities should have negative generations
|
||||||
|
|
||||||
|
ecb.AddComponent(tempEntity, new CompA { value = 123 });
|
||||||
|
|
||||||
|
ecb.Playback(_world.EntityManager);
|
||||||
|
|
||||||
|
var queryID = QueryBuilder.New().WithAll<CompA>().Build(_world);
|
||||||
|
ref readonly var query = ref _world.ComponentManager.GetEntityQueryReference(queryID);
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
foreach (var (entity, compA) in query.GetEntityComponentIterator<CompA>())
|
||||||
|
{
|
||||||
|
Assert.AreEqual(123, compA.Get().value);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(found);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/Test/Ghost.UnitTest/ECS/EntityManagerTests.cs
Normal file
131
src/Test/Ghost.UnitTest/ECS/EntityManagerTests.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using Ghost.Entities;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest.ECS;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
[DoNotParallelize]
|
||||||
|
public class EntityManagerTests
|
||||||
|
{
|
||||||
|
private struct CompA : IComponentData { public int value; }
|
||||||
|
private struct CompB : IComponentData { public int value; }
|
||||||
|
|
||||||
|
private World _world = null!;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_world = World.Create(null, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_world.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_CreateEntity()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
Assert.IsTrue(_world.EntityManager.Exists(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_CreateEntities()
|
||||||
|
{
|
||||||
|
var entities = new Entity[3];
|
||||||
|
_world.EntityManager.CreateEntities(entities);
|
||||||
|
|
||||||
|
foreach (var e in entities)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(_world.EntityManager.Exists(e));
|
||||||
|
}
|
||||||
|
Assert.AreEqual(3, _world.EntityManager.EntityCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_AddComponent()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompA { value = 42 });
|
||||||
|
|
||||||
|
Assert.IsTrue(_world.EntityManager.HasComponent<CompA>(entity));
|
||||||
|
Assert.AreEqual(42, _world.EntityManager.GetComponent<CompA>(entity).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_RemoveComponent()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompA { value = 42 });
|
||||||
|
Assert.IsTrue(_world.EntityManager.HasComponent<CompA>(entity));
|
||||||
|
|
||||||
|
_world.EntityManager.RemoveComponent<CompA>(entity);
|
||||||
|
Assert.IsFalse(_world.EntityManager.HasComponent<CompA>(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_SetComponent()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompA { value = 42 });
|
||||||
|
|
||||||
|
_world.EntityManager.SetComponent(entity, new CompA { value = 84 });
|
||||||
|
Assert.AreEqual(84, _world.EntityManager.GetComponent<CompA>(entity).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_DestroyEntity()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
Assert.IsTrue(_world.EntityManager.Exists(entity));
|
||||||
|
|
||||||
|
_world.EntityManager.DestroyEntity(entity);
|
||||||
|
Assert.IsFalse(_world.EntityManager.Exists(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_MultipleOperations()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompA { value = 10 });
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompB { value = 20 });
|
||||||
|
|
||||||
|
Assert.IsTrue(_world.EntityManager.HasComponent<CompA>(entity));
|
||||||
|
Assert.IsTrue(_world.EntityManager.HasComponent<CompB>(entity));
|
||||||
|
|
||||||
|
_world.EntityManager.RemoveComponent<CompA>(entity);
|
||||||
|
|
||||||
|
Assert.IsFalse(_world.EntityManager.HasComponent<CompA>(entity));
|
||||||
|
Assert.IsTrue(_world.EntityManager.HasComponent<CompB>(entity));
|
||||||
|
Assert.AreEqual(20, _world.EntityManager.GetComponent<CompB>(entity).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_Singleton()
|
||||||
|
{
|
||||||
|
_world.EntityManager.CreateSingleton(new CompA { value = 99 });
|
||||||
|
|
||||||
|
Assert.AreEqual(99, _world.EntityManager.GetSingleton<CompA>().value);
|
||||||
|
|
||||||
|
_world.EntityManager.GetSingleton<CompA>().value = 100;
|
||||||
|
Assert.AreEqual(100, _world.EntityManager.GetSingleton<CompA>().value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEntityManager_MigrateEntity()
|
||||||
|
{
|
||||||
|
var entity = _world.EntityManager.CreateEntity();
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompA { value = 10 });
|
||||||
|
_world.EntityManager.AddComponent(entity, new CompB { value = 20 });
|
||||||
|
|
||||||
|
using var newSet = new ComponentSet(AllocationHandle.Temp, ComponentTypeID<CompB>.Value);
|
||||||
|
_world.EntityManager.MigrateEntity(entity, newSet);
|
||||||
|
|
||||||
|
Assert.IsFalse(_world.EntityManager.HasComponent<CompA>(entity));
|
||||||
|
Assert.IsTrue(_world.EntityManager.HasComponent<CompB>(entity));
|
||||||
|
Assert.AreEqual(20, _world.EntityManager.GetComponent<CompB>(entity).value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Ghost.UnitTest.ECS;
|
namespace Ghost.UnitTest.ECS;
|
||||||
|
|
||||||
@@ -9,8 +8,6 @@ namespace Ghost.UnitTest.ECS;
|
|||||||
[DoNotParallelize]
|
[DoNotParallelize]
|
||||||
public class SharedComponentTests
|
public class SharedComponentTests
|
||||||
{
|
{
|
||||||
// ── Test components ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private struct Tag : IComponentData { }
|
private struct Tag : IComponentData { }
|
||||||
|
|
||||||
private struct Tag2 : IComponentData { }
|
private struct Tag2 : IComponentData { }
|
||||||
@@ -30,7 +27,6 @@ public class SharedComponentTests
|
|||||||
public int subID;
|
public int subID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fixture ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private World _world = null!;
|
private World _world = null!;
|
||||||
|
|
||||||
@@ -46,7 +42,6 @@ public class SharedComponentTests
|
|||||||
_world.Dispose();
|
_world.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// <summary>Creates an entity that carries Tag + SharedGroup(groupID).</summary>
|
/// <summary>Creates an entity that carries Tag + SharedGroup(groupID).</summary>
|
||||||
private unsafe Entity CreateWithSharedGroup(int groupID)
|
private unsafe Entity CreateWithSharedGroup(int groupID)
|
||||||
@@ -494,7 +489,7 @@ public class SharedComponentTests
|
|||||||
_world.EntityManager.CreateEntities(result2, set2);
|
_world.EntityManager.CreateEntities(result2, set2);
|
||||||
|
|
||||||
var groups = CollectGroupCounts();
|
var groups = CollectGroupCounts();
|
||||||
|
|
||||||
Assert.AreEqual(1, groups.Count, "Expected exactly 1 chunk group due to canonical sorting of SharedComponentSet.");
|
Assert.AreEqual(1, groups.Count, "Expected exactly 1 chunk group due to canonical sorting of SharedComponentSet.");
|
||||||
Assert.AreEqual(2, groups[42], "Both entities should be grouped under groupID 42.");
|
Assert.AreEqual(2, groups[42], "Both entities should be grouped under groupID 42.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ public unsafe class ComponentDescriptorTests
|
|||||||
var p0 = descriptor.Properties[0];
|
var p0 = descriptor.Properties[0];
|
||||||
Assert.AreEqual("intValue", p0.Name);
|
Assert.AreEqual("intValue", p0.Name);
|
||||||
Assert.AreEqual("IntValue", p0.DisplayName);
|
Assert.AreEqual("IntValue", p0.DisplayName);
|
||||||
Assert.AreEqual(typeof(int), p0.FieldType);
|
Assert.AreEqual(typeof(int), p0.ValueType);
|
||||||
Assert.AreEqual(0, p0.OffsetInComponent);
|
Assert.AreEqual(0, p0.OffsetInComponent);
|
||||||
|
|
||||||
var p1 = descriptor.Properties[1];
|
var p1 = descriptor.Properties[1];
|
||||||
Assert.AreEqual("doubleValue", p1.Name);
|
Assert.AreEqual("doubleValue", p1.Name);
|
||||||
Assert.AreEqual("Custom Name", p1.DisplayName);
|
Assert.AreEqual("Custom Name", p1.DisplayName);
|
||||||
Assert.AreEqual(typeof(double), p1.FieldType);
|
Assert.AreEqual(typeof(double), p1.ValueType);
|
||||||
// Offset of double after int+float is 8 (with alignment)
|
// Offset of double after int+float is 8 (with alignment)
|
||||||
Assert.AreEqual((int)Marshal.OffsetOf<TestComponent>("doubleValue"), p1.OffsetInComponent);
|
Assert.AreEqual((int)Marshal.OffsetOf<TestComponent>("doubleValue"), p1.OffsetInComponent);
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ public unsafe class ComponentDescriptorTests
|
|||||||
var p3 = descriptor.Properties[3];
|
var p3 = descriptor.Properties[3];
|
||||||
Assert.AreEqual("position", p3.Name);
|
Assert.AreEqual("position", p3.Name);
|
||||||
Assert.AreEqual("Position", p3.DisplayName);
|
Assert.AreEqual("Position", p3.DisplayName);
|
||||||
Assert.AreEqual(typeof(float3), p3.FieldType);
|
Assert.AreEqual(typeof(float3), p3.ValueType);
|
||||||
Assert.IsNull(p3.Children); // float3 is a primitive so it has no children
|
Assert.IsNull(p3.Children); // float3 is a primitive so it has no children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
|
||||||
@@ -52,7 +52,11 @@ public class SceneGraphSyncTests
|
|||||||
{ child, "ChildEntity" }
|
{ child, "ChildEntity" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
world.AdvanceVersion();
|
||||||
|
|
||||||
_worldService.RebuildSceneGraph(names);
|
_worldService.RebuildSceneGraph(names);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
Assert.AreEqual(1, _worldService.RootNodes.Count);
|
Assert.AreEqual(1, _worldService.RootNodes.Count);
|
||||||
var sceneNode = _worldService.RootNodes[0];
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
@@ -74,12 +78,15 @@ public class SceneGraphSyncTests
|
|||||||
{
|
{
|
||||||
var scene = SceneManager.CreateScene();
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
var entity = _worldService.CreateEntity("NewEntity", scene.ID);
|
_worldService.CreateEntity("NewEntity", scene.ID);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
Assert.AreEqual(1, _worldService.RootNodes.Count);
|
Assert.AreEqual(1, _worldService.RootNodes.Count);
|
||||||
var sceneNode = _worldService.RootNodes[0];
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
Assert.AreEqual(1, sceneNode.Children.Count);
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
var entityNode = (EntityNode)sceneNode.Children[0];
|
var entityNode = (EntityNode)sceneNode.Children[0];
|
||||||
|
var entity = entityNode.Entity;
|
||||||
Assert.AreEqual("NewEntity", entityNode.Name);
|
Assert.AreEqual("NewEntity", entityNode.Name);
|
||||||
Assert.AreEqual(entity, entityNode.Entity);
|
Assert.AreEqual(entity, entityNode.Entity);
|
||||||
}
|
}
|
||||||
@@ -89,11 +96,17 @@ public class SceneGraphSyncTests
|
|||||||
{
|
{
|
||||||
var scene = SceneManager.CreateScene();
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
var entity = _worldService.CreateEntity("NewEntity", scene.ID);
|
_worldService.CreateEntity("NewEntity", scene.ID);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var sceneNode = _worldService.RootNodes[0];
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
|
var entity = ((EntityNode)sceneNode.Children[0]).Entity;
|
||||||
Assert.AreEqual(1, sceneNode.Children.Count);
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
|
|
||||||
_worldService.DestroyEntity(entity);
|
_worldService.DestroyEntity(entity);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
Assert.AreEqual(0, sceneNode.Children.Count);
|
Assert.AreEqual(0, sceneNode.Children.Count);
|
||||||
}
|
}
|
||||||
@@ -103,13 +116,21 @@ public class SceneGraphSyncTests
|
|||||||
{
|
{
|
||||||
var scene = SceneManager.CreateScene();
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
var parent = _worldService.CreateEntity("Parent", scene.ID);
|
_worldService.CreateEntity("Parent", scene.ID);
|
||||||
var child = _worldService.CreateEntity("Child", scene.ID);
|
_worldService.CreateEntity("Child", scene.ID);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var sceneNode = _worldService.RootNodes[0];
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
Assert.AreEqual(2, sceneNode.Children.Count);
|
Assert.AreEqual(2, sceneNode.Children.Count);
|
||||||
|
|
||||||
|
var parent = ((EntityNode)sceneNode.Children[0]).Entity;
|
||||||
|
var child = ((EntityNode)sceneNode.Children[1]).Entity;
|
||||||
|
|
||||||
var err = _worldService.SetParent(child, parent);
|
var err = _worldService.SetParent(child, parent);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
Assert.AreEqual(Error.None, err);
|
Assert.AreEqual(Error.None, err);
|
||||||
|
|
||||||
Assert.AreEqual(1, sceneNode.Children.Count);
|
Assert.AreEqual(1, sceneNode.Children.Count);
|
||||||
@@ -127,13 +148,19 @@ public class SceneGraphSyncTests
|
|||||||
{
|
{
|
||||||
var scene = SceneManager.CreateScene();
|
var scene = SceneManager.CreateScene();
|
||||||
|
|
||||||
var entity = _worldService.CreateEntity("OriginalName", scene.ID);
|
_worldService.CreateEntity("OriginalName", scene.ID);
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var sceneNode = _worldService.RootNodes[0];
|
var sceneNode = _worldService.RootNodes[0];
|
||||||
var entityNode = (EntityNode)sceneNode.Children[0];
|
var entityNode = (EntityNode)sceneNode.Children[0];
|
||||||
|
var entity = entityNode.Entity;
|
||||||
|
|
||||||
Assert.AreEqual("OriginalName", entityNode.Name);
|
Assert.AreEqual("OriginalName", entityNode.Name);
|
||||||
|
|
||||||
_worldService.RenameEntity(entity, "NewName");
|
_worldService.RenameEntity(entity, "NewName");
|
||||||
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
Assert.AreEqual("NewName", entityNode.Name);
|
Assert.AreEqual("NewName", entityNode.Name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,15 +119,20 @@ public class SceneSerializationTests
|
|||||||
Assert.IsGreaterThanOrEqualTo(0, ent.Components.Count, "Entity has no components"); // Can be 0 because we might have entities without components, but should not be negative
|
Assert.IsGreaterThanOrEqualTo(0, ent.Components.Count, "Entity has no components"); // Can be 0 because we might have entities without components, but should not be negative
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data);
|
_serializationService.LoadSceneIntoEditorWorld(data);
|
||||||
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var world = _worldService.EditorWorld;
|
var world = _worldService.EditorWorld;
|
||||||
scene = loadResult.Value;
|
|
||||||
|
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
|
||||||
using var entities = SceneManager.GetSceneEntities(world, scene, scope.AllocationHandle);
|
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
||||||
Assert.AreEqual(3, entities.Count, $"Expected 3 entities for scene {scene.ID} but found {entities.Count}");
|
var loadedCount = 0;
|
||||||
|
foreach (var chunk in query.GetChunkIterator())
|
||||||
|
{
|
||||||
|
loadedCount += chunk.EntityCount;
|
||||||
|
}
|
||||||
|
Assert.AreEqual(3, loadedCount, $"Expected 3 entities but found {loadedCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -148,8 +153,9 @@ public class SceneSerializationTests
|
|||||||
Assert.IsNotNull(data);
|
Assert.IsNotNull(data);
|
||||||
Assert.HasCount(3, data.Entities);
|
Assert.HasCount(3, data.Entities);
|
||||||
|
|
||||||
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data);
|
_serializationService.LoadSceneIntoEditorWorld(data);
|
||||||
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var queryID = new QueryBuilder().WithAll<SceneID, Hierarchy>().Build(world);
|
var queryID = new QueryBuilder().WithAll<SceneID, Hierarchy>().Build(world);
|
||||||
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
||||||
@@ -280,8 +286,9 @@ public class SceneSerializationTests
|
|||||||
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
|
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
|
||||||
Assert.IsNotNull(data);
|
Assert.IsNotNull(data);
|
||||||
|
|
||||||
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data, SceneLoadingType.Single);
|
_serializationService.LoadSceneIntoEditorWorld(data, SceneLoadingType.Single);
|
||||||
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var afterCount = 0;
|
var afterCount = 0;
|
||||||
query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
||||||
@@ -326,8 +333,9 @@ public class SceneSerializationTests
|
|||||||
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
|
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
|
||||||
Assert.IsNotNull(data);
|
Assert.IsNotNull(data);
|
||||||
|
|
||||||
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data);
|
_serializationService.LoadSceneIntoEditorWorld(data);
|
||||||
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var world = _worldService.EditorWorld;
|
var world = _worldService.EditorWorld;
|
||||||
var queryID = new QueryBuilder().WithAll<Hierarchy>().Build(world);
|
var queryID = new QueryBuilder().WithAll<Hierarchy>().Build(world);
|
||||||
@@ -367,8 +375,9 @@ public class SceneSerializationTests
|
|||||||
Assert.IsNotNull(data);
|
Assert.IsNotNull(data);
|
||||||
Assert.AreEqual(999u, data.FormatVersion);
|
Assert.AreEqual(999u, data.FormatVersion);
|
||||||
|
|
||||||
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data);
|
_serializationService.LoadSceneIntoEditorWorld(data);
|
||||||
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -445,8 +454,9 @@ public class SceneSerializationTests
|
|||||||
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
|
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
|
||||||
Assert.IsNotNull(data);
|
Assert.IsNotNull(data);
|
||||||
|
|
||||||
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data);
|
_serializationService.LoadSceneIntoEditorWorld(data);
|
||||||
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
|
_worldService.FlushCommands();
|
||||||
|
_worldService.FirePendingEvents();
|
||||||
|
|
||||||
var queryID = new QueryBuilder().WithAll<LocalToWorld>().Build(world);
|
var queryID = new QueryBuilder().WithAll<LocalToWorld>().Build(world);
|
||||||
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
|
||||||
|
|||||||
119
src/Test/Ghost.UnitTest/UndoServiceEcsTests.cs
Normal file
119
src/Test/Ghost.UnitTest/UndoServiceEcsTests.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Ghost.Entities;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class UndoServiceEcsTests
|
||||||
|
{
|
||||||
|
private struct CompA : IComponentData { public int value; }
|
||||||
|
private struct CompB : IComponentData { public int value; }
|
||||||
|
|
||||||
|
private EditorWorldService _worldService = null!;
|
||||||
|
private UndoService _undoService = null!;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_worldService = new EditorWorldService();
|
||||||
|
_undoService = new UndoService(_worldService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_worldService.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRecordEntityStructure()
|
||||||
|
{
|
||||||
|
var world = _worldService.EditorWorld;
|
||||||
|
var e = world.EntityManager.CreateEntity();
|
||||||
|
world.EntityManager.AddComponent<CompA>(e);
|
||||||
|
|
||||||
|
// Initial state: Entity has CompA
|
||||||
|
ref var compA = ref world.EntityManager.GetComponent<CompA>(e);
|
||||||
|
compA.value = 10;
|
||||||
|
|
||||||
|
var node = new EntityNode(world, e, "TestEntity", null);
|
||||||
|
|
||||||
|
_undoService.BeginTransaction("Add CompB");
|
||||||
|
_undoService.RecordEntityStructure(node, "Before Add CompB");
|
||||||
|
|
||||||
|
// Modify structure
|
||||||
|
world.EntityManager.AddComponent<CompB>(e);
|
||||||
|
ref var compB = ref world.EntityManager.GetComponent<CompB>(e);
|
||||||
|
compB.value = 20;
|
||||||
|
|
||||||
|
// Re-fetch CompA because AddComponent moves the entity to a new chunk,
|
||||||
|
// invalidating the previous ref!
|
||||||
|
ref var compA_new = ref world.EntityManager.GetComponent<CompA>(e);
|
||||||
|
compA_new.value = 15; // also modify compA
|
||||||
|
|
||||||
|
_undoService.EndTransaction();
|
||||||
|
|
||||||
|
// Perform Undo
|
||||||
|
_undoService.PerformUndo();
|
||||||
|
|
||||||
|
Assert.IsTrue(world.EntityManager.HasComponent<CompA>(e), "Should have CompA");
|
||||||
|
Assert.IsFalse(world.EntityManager.HasComponent<CompB>(e), "Should NOT have CompB");
|
||||||
|
Assert.AreEqual(10, world.EntityManager.GetComponent<CompA>(e).value, "CompA value should be reverted to 10");
|
||||||
|
|
||||||
|
// Perform Redo
|
||||||
|
_undoService.PerformRedo();
|
||||||
|
|
||||||
|
Assert.IsTrue(world.EntityManager.HasComponent<CompA>(e), "Should have CompA");
|
||||||
|
Assert.IsTrue(world.EntityManager.HasComponent<CompB>(e), "Should have CompB");
|
||||||
|
Assert.AreEqual(15, world.EntityManager.GetComponent<CompA>(e).value, "CompA value should be restored to 15");
|
||||||
|
Assert.AreEqual(20, world.EntityManager.GetComponent<CompB>(e).value, "CompB value should be restored to 20");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRecordEntityLifecycle_CreateAndDestroy()
|
||||||
|
{
|
||||||
|
var world = _worldService.EditorWorld;
|
||||||
|
|
||||||
|
// Step 1: Create Entity
|
||||||
|
var e = world.EntityManager.CreateEntity();
|
||||||
|
world.EntityManager.AddComponent<CompA>(e);
|
||||||
|
world.EntityManager.GetComponent<CompA>(e).value = 42;
|
||||||
|
var node = new EntityNode(world, e, "TestEntity", null);
|
||||||
|
|
||||||
|
_undoService.BeginTransaction("Create Entity");
|
||||||
|
_undoService.RecordEntityLifecycle(node, LifecycleEvent.Created);
|
||||||
|
_undoService.EndTransaction();
|
||||||
|
|
||||||
|
// Undo Creation (Expect destruction)
|
||||||
|
_undoService.PerformUndo();
|
||||||
|
Assert.IsFalse(world.EntityManager.Exists(e), "Entity should be destroyed by Undo of Creation");
|
||||||
|
|
||||||
|
// Redo Creation (Expect resurrection)
|
||||||
|
_undoService.PerformRedo();
|
||||||
|
|
||||||
|
// Note: The entity ID might be different, but the EntityNode should be updated
|
||||||
|
var resurrectedEntity = node.Entity;
|
||||||
|
Assert.IsTrue(world.EntityManager.Exists(resurrectedEntity), "Entity should be resurrected by Redo of Creation");
|
||||||
|
// In our current implementation, restoring components for created entities isn't fully robust yet,
|
||||||
|
// but we verify the entity is alive.
|
||||||
|
|
||||||
|
// Step 2: Destroy Entity
|
||||||
|
_undoService.BeginTransaction("Destroy Entity");
|
||||||
|
_undoService.RecordEntityLifecycle(node, LifecycleEvent.Destroyed);
|
||||||
|
world.EntityManager.DestroyEntity(resurrectedEntity);
|
||||||
|
_undoService.EndTransaction();
|
||||||
|
|
||||||
|
Assert.IsFalse(world.EntityManager.Exists(resurrectedEntity), "Entity destroyed manually");
|
||||||
|
|
||||||
|
// Undo Destruction (Expect resurrection)
|
||||||
|
_undoService.PerformUndo();
|
||||||
|
|
||||||
|
var undoneDestroyEntity = node.Entity;
|
||||||
|
Assert.IsTrue(world.EntityManager.Exists(undoneDestroyEntity), "Entity should be resurrected by Undo of Destruction");
|
||||||
|
|
||||||
|
// Redo Destruction (Expect destruction)
|
||||||
|
_undoService.PerformRedo();
|
||||||
|
Assert.IsFalse(world.EntityManager.Exists(undoneDestroyEntity), "Entity should be destroyed by Redo of Destruction");
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/Test/Ghost.UnitTest/UndoServiceTests.cs
Normal file
105
src/Test/Ghost.UnitTest/UndoServiceTests.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using Ghost.Editor.Core;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class UndoServiceTests
|
||||||
|
{
|
||||||
|
private class TestGhostObject : GhostObject
|
||||||
|
{
|
||||||
|
public string Data { get; set; } = "Initial";
|
||||||
|
|
||||||
|
public TestGhostObject()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SerializeState(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DeserializeState(BinaryReader reader)
|
||||||
|
{
|
||||||
|
Data = reader.ReadString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditorWorldService _worldService = null!;
|
||||||
|
private UndoService _undoService = null!;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_worldService = new EditorWorldService();
|
||||||
|
_undoService = new UndoService(_worldService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_worldService.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestObjectStateUndoRedo()
|
||||||
|
{
|
||||||
|
var obj = new TestGhostObject();
|
||||||
|
obj.Data = "State 1";
|
||||||
|
|
||||||
|
_undoService.RecordObject(obj, "Change Data");
|
||||||
|
obj.Data = "State 2";
|
||||||
|
|
||||||
|
_undoService.PerformUndo();
|
||||||
|
Assert.AreEqual("State 1", obj.Data);
|
||||||
|
|
||||||
|
_undoService.PerformRedo();
|
||||||
|
Assert.AreEqual("State 2", obj.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestTransactionGrouping()
|
||||||
|
{
|
||||||
|
var obj = new TestGhostObject();
|
||||||
|
|
||||||
|
_undoService.BeginTransaction("Slider Drag");
|
||||||
|
_undoService.RecordObject(obj, "Drag Start");
|
||||||
|
obj.Data = "Drag 1";
|
||||||
|
|
||||||
|
_undoService.RecordObject(obj, "Drag Mid");
|
||||||
|
obj.Data = "Drag 2";
|
||||||
|
|
||||||
|
_undoService.RecordObject(obj, "Drag End");
|
||||||
|
obj.Data = "Drag Final";
|
||||||
|
_undoService.EndTransaction();
|
||||||
|
|
||||||
|
// Perform undo should jump all the way back to "Initial"
|
||||||
|
_undoService.PerformUndo();
|
||||||
|
Assert.AreEqual("Initial", obj.Data);
|
||||||
|
|
||||||
|
_undoService.PerformRedo();
|
||||||
|
Assert.AreEqual("Drag Final", obj.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRingBufferOverflow()
|
||||||
|
{
|
||||||
|
// Internal capacity is 50. Let's push 60 items.
|
||||||
|
var obj = new TestGhostObject();
|
||||||
|
|
||||||
|
for (var i = 0; i < 60; i++)
|
||||||
|
{
|
||||||
|
_undoService.RecordObject(obj, $"Action {i}");
|
||||||
|
obj.Data = $"State {i}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can only undo 50 times.
|
||||||
|
for (var i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
_undoService.PerformUndo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// It should have stopped at State 9 because State 0-9 were overwritten in the buffer.
|
||||||
|
Assert.AreEqual("State 9", obj.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user