Add component editors and UI controls

Added the `HierarchyEditor` and `LocalToWorldEditor` classes to implement custom component editing functionality.
Added the `Vector3Field` control for 3D vector manipulation and its corresponding XAML definition.
Added the `ComponentDataView` and `ComponentObject` classes to manage component data display and access.
Added the `CustomEditorAttribute` to mark classes as custom editors for specific components.

Changed the `IInspectable` interface to use properties for `Icon`, `HeaderContent`, and `InspectorContent`.
Changed the `PropertyField` class to enhance UI control binding capabilities.
Changed the `EditorWorldManager` to improve world data loading and deserialization processes.
Changed the `EntityNode` and `WorldNode` classes to update entity construction and component querying.
Changed the `StaticResource` class to include new binding flags for component properties.
Changed the `InspectorService` to remove old contract references and adopt new interfaces.
Changed the `QueryEnumerable` and related files to update generic constraints for improved type safety.
Changed the `QueryItem` class to reflect new generic constraints and enhance deconstruction.
Changed the `World.Query` methods to utilize the updated generic constraints.

Updated the `SerializationTest` to align with new entity creation and management practices.
This commit is contained in:
2025-06-20 20:19:14 +09:00
parent fc44c73ca8
commit 1724072f7e
54 changed files with 1474 additions and 554 deletions

View File

@@ -0,0 +1,29 @@
using Ghost.Entities;
using Ghost.Entities.Components;
using Ghost.Entities.Query;
namespace Ghost.Editor.Core.Inspector;
public unsafe 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 _world.EntityManager.GetComponent<T>(_entity);
}
public void SetData<T>(in T data)
where T : unmanaged, IComponentData
{
_world.EntityManager.SetComponent(_entity, in data);
}
}

View File

@@ -0,0 +1,10 @@
namespace Ghost.Editor.Core.Inspector;
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute(Type targetType) : Attribute
{
internal Type TargetType
{
get;
} = targetType;
}

View File

@@ -0,0 +1,25 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
interface IComponentEditor
{
/// <summary>
/// Called when the component editor is created.
/// </summary>
/// <param name="componentObject">The component data to edit.</param>
/// <param name="container">The container to add the editor controls to.</param>
public void Create(ComponentObject componentObject, StackPanel container);
/// <summary>
/// Called when the component editor needs to update its UI based on the current state of the component data.
/// </summary>
/// <param name="componentObject">The component data to edit.</param>
public void Update(ComponentObject componentObject);
/// <summary>
/// Called when the component editor is destroyed.
/// </summary>
/// <param name="componentObject">The component data to edit.</param>
public void Destroy(ComponentObject componentObject);
}

View File

@@ -0,0 +1,22 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public interface IInspectable
{
public IconSource? Icon
{
get;
}
public UIElement? HeaderContent
{
get;
}
public UIElement? InspectorContent
{
get;
}
}

View File

@@ -1,6 +1,5 @@
using Ghost.Editor.Resources;
using Ghost.Editor.Services.Contracts;
using Ghost.Engine.Resources;
using System.Text.Json;
namespace Ghost.Editor.Core.SceneGraph;
@@ -40,7 +39,7 @@ public static class EditorWorldManager
}
await using var readStream = new FileStream(worldPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var deserializedScene = await JsonSerializer.DeserializeAsync<WorldNode>(readStream, StaticResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed.");
var deserializedScene = await JsonSerializer.DeserializeAsync<WorldNode>(readStream, Engine.Resources.StaticResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed.");
_loadedWorlds.Clear();

View File

@@ -1,70 +1,102 @@
using Ghost.Editor.Contracts;
using Ghost.Editor.Controls.Internal;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Resources;
using Ghost.Engine.Editor;
using Ghost.Entities;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System.Reflection;
namespace Ghost.Editor.Core.SceneGraph;
public partial class EntityNode : SceneGraphNode
{
private WorldNode _owner;
private readonly Entity _entity;
public Entity Entity => _entity;
public override SceneGraphNodeType NodeType => SceneGraphNodeType.Entity;
public EntityNode(Entity entity, string name)
public EntityNode(WorldNode owner, Entity entity, string name)
{
_owner = owner;
_entity = entity;
Name = name;
}
internal EntityNode()
{
}
}
public partial class EntityNode : IInspectable
{
public IconSource? Icon => EditorIconSource.entity_24;
public UIElement? HeaderContent()
public UIElement? HeaderContent
{
var root = new StackPanel()
get
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center
};
var root = new StackPanel()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center
};
var nameText = new TextBox
{
Text = Name,
FontWeight = FontWeights.Bold,
};
var idText = new TextBlock
{
Text = $"ID: {_entity.ID}",
Margin = new Thickness(0, 5, 0, 0),
};
var nameText = new TextBox
{
Text = Name,
FontWeight = FontWeights.Bold,
};
var idText = new TextBlock
{
Text = $"ID: {_entity.ID} Generation: {_entity.Generation}",
Margin = new Thickness(5, 7, 0, 0),
Opacity = 0.75,
Style = Application.Current.Resources["CaptionTextBlockStyle"] as Style
};
nameText.SetBinding(TextBox.TextProperty, new Binding
{
Source = this,
Path = new PropertyPath(nameof(Name)),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
});
nameText.SetBinding(TextBox.TextProperty, new Binding
{
Source = this,
Path = new PropertyPath(nameof(Name)),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
});
root.Children.Add(nameText);
root.Children.Add(idText);
root.Children.Add(nameText);
root.Children.Add(idText);
return root;
return root;
}
}
public UIElement? InspectorContent()
public UIElement? InspectorContent
{
return null;
get
{
var root = new StackPanel()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Top
};
foreach (var (typeHandle, componentPtr) in _owner.World.EntityManager.GetComponentsUnsafe(_entity))
{
if (componentPtr == IntPtr.Zero)
{
continue;
}
var type = Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(typeHandle));
if (type == null || type.GetCustomAttribute<HideEditorAttribute>() != null)
{
continue;
}
var dataView = new ComponentDataView(type.Name, _owner.World, _entity, type);
root.Children.Add(dataView);
}
return root;
}
}
}

View File

@@ -10,21 +10,21 @@ public class SceneGraphHelpers
/// </summary>
/// <param name="world">The world context where the entity will be created.</param>
/// <param name="entity">The entity to be wrapped in the <see cref="EntityNode"/>.</param>
public static EntityNode CreateEntityNode(World world, Entity entity, string name)
public static EntityNode CreateEntityNode(WorldNode owner, Entity entity, string name)
{
world.EntityManager.AddComponent(entity, LocalToWorld.Identity);
world.EntityManager.AddComponent(entity, Hierarchy.Root);
return new EntityNode(entity, name);
owner.World.EntityManager.AddComponent(entity, LocalToWorld.Identity);
owner.World.EntityManager.AddComponent(entity, Hierarchy.Root);
return new EntityNode(owner, entity, name);
}
/// <summary>
/// Creates a new <see cref="Entity"/> and <see cref="EntityNode"/> entity with default components.
/// </summary>
/// <param name="world">The world context where the entity will be created.</param>
public static EntityNode CreateEntityNode(World world, string name)
/// <param name="owner">The world context where the entity will be created.</param>
public static EntityNode CreateEntityNode(WorldNode owner, string name)
{
var entity = world.EntityManager.CreateEntity();
return CreateEntityNode(world, entity, name);
var entity = owner.World.EntityManager.CreateEntity();
return CreateEntityNode(owner, entity, name);
}
/// <summary>

View File

@@ -1,5 +1,5 @@
using Ghost.Editor.Contracts;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Serializer;
using Ghost.Editor.Resources;
using Ghost.Engine.Components;
@@ -76,21 +76,21 @@ public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
return result;
}
private EntityNode BuildNodeRecursive(Entity entity, World world)
private EntityNode BuildNodeRecursive(Entity entity)
{
if (!_entityNodeLookup.TryGetValue(entity, out var node))
{
node = new EntityNode(entity, "New Entity");
node = new EntityNode(this, entity, "New Entity");
_entityNodeLookup[entity] = node;
}
var hc = world.EntityManager.GetComponent<Hierarchy>(entity);
var hc = _world.EntityManager.GetComponent<Hierarchy>(entity);
var child = hc.ValueRO.firstChild;
while (child != Entity.Invalid)
{
node.AddChild(BuildNodeRecursive(child, world));
var childHC = world.EntityManager.GetComponent<Hierarchy>(child);
node.AddChild(BuildNodeRecursive(child));
var childHC = _world.EntityManager.GetComponent<Hierarchy>(child);
child = childHC.ValueRO.nextSibling;
}
@@ -103,7 +103,7 @@ public partial class WorldNode : SceneGraphNode, IEquatable<WorldNode>
{
if (hierarchy.ValueRO.parent == Entity.Invalid)
{
var node = BuildNodeRecursive(entity, _world);
var node = BuildNodeRecursive(entity);
AddChild(node);
}
}
@@ -178,18 +178,7 @@ public partial class WorldNode : IInspectable
await EditorWorldManager.LoadWorld(path);
}
public UIElement? HeaderContent()
{
return new TextBlock
{
Text = Name,
Style = Application.Current.Resources["SubtitleTextBlockStyle"] as Style,
VerticalAlignment = VerticalAlignment.Center
};
}
public UIElement? HeaderContent => null;
public UIElement? InspectorContent()
{
return null;
}
public UIElement? InspectorContent => null;
}

View File

@@ -39,7 +39,7 @@ internal class WorldNodeSerializer : JsonConverter<WorldNode>
var entityName = entityElement.GetProperty(Property.NAME).GetString() ?? "New Entity";
var entityID = entityElement.GetProperty(Property.ID).GetInt32();
var entity = new Entity(entityID, 0);
var node = new EntityNode(entity, entityName);
var node = new EntityNode(result, entity, entityName);
world.EntityManager.AddEntityInternal(entity);
result.EntityNodeLookup[entity] = node;