diff --git a/Ghost.App/Components/HierarchyEditor.cs b/Ghost.App/Components/HierarchyEditor.cs new file mode 100644 index 0000000..706025e --- /dev/null +++ b/Ghost.App/Components/HierarchyEditor.cs @@ -0,0 +1,20 @@ +using Ghost.Editor.Core.Inspector; +using Microsoft.UI.Xaml.Controls; + +namespace Ghost.Editor.Components; + +//[CustomEditor(typeof(Hierarchy))] +internal class HierarchyEditor : IComponentEditor +{ + public void Create(ComponentObject componentObject, StackPanel container) + { + } + + public void Update(ComponentObject componentObject) + { + } + + public void Destroy(ComponentObject componentObject) + { + } +} \ No newline at end of file diff --git a/Ghost.App/Components/LocalToWorldEditor.cs b/Ghost.App/Components/LocalToWorldEditor.cs new file mode 100644 index 0000000..6083106 --- /dev/null +++ b/Ghost.App/Components/LocalToWorldEditor.cs @@ -0,0 +1,72 @@ +using Ghost.Editor.Controls; +using Ghost.Editor.Core.Inspector; +using Ghost.Engine.Components; +using Ghost.Engine.Utilities; +using Microsoft.UI.Xaml.Controls; + +namespace Ghost.Editor.Components; + +[CustomEditor(typeof(LocalToWorld))] +internal class LocalToWorldEditor : IComponentEditor +{ + private Vector3Field _translationField = null!; + private Vector3Field _rotationField = null!; + private Vector3Field _scaleField = null!; + + public void Create(ComponentObject componentObject, StackPanel container) + { + _translationField = new Vector3Field(); + _rotationField = new Vector3Field(); + _scaleField = new Vector3Field(); + + _translationField.OnValueChanged += (s, e) => + { + var data = componentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var oldScale); + data.ValueRW.matrix = MatrixUtility.CreateTRS(e.NewValue, oldRotation, oldScale); + }; + + _rotationField.OnValueChanged += (s, e) => + { + var data = componentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var oldScale); + data.ValueRW.matrix = MatrixUtility.CreateTRS(oldTranslation, e.NewValue.ToQuaternion(), oldScale); + }; + + _scaleField.OnValueChanged += (s, e) => + { + var data = componentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var oldScale); + data.ValueRW.matrix = MatrixUtility.CreateTRS(oldTranslation, oldRotation, e.NewValue); + }; + + container.Children.Add(new PropertyField() { Label = "Position", Content = _translationField }); + container.Children.Add(new PropertyField() { Label = "Rotation", Content = _rotationField }); + container.Children.Add(new PropertyField() { Label = "Scale", Content = _scaleField }); + } + + public void Update(ComponentObject componentObject) + { + var data = componentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var translation, out var rotation, out var scale); + + if (_translationField.FocusState == Microsoft.UI.Xaml.FocusState.Unfocused) + { + _translationField.Value = translation; + } + + if (_rotationField.FocusState == Microsoft.UI.Xaml.FocusState.Unfocused) + { + _rotationField.Value = VectorUtility.CreateFromQuaternion(rotation); + } + + if (_scaleField.FocusState == Microsoft.UI.Xaml.FocusState.Unfocused) + { + _scaleField.Value = scale; + } + } + + public void Destroy(ComponentObject componentObject) + { + } +} \ No newline at end of file diff --git a/Ghost.App/Contracts/IInspectable.cs b/Ghost.App/Contracts/IInspectable.cs deleted file mode 100644 index 958d40c..0000000 --- a/Ghost.App/Contracts/IInspectable.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Contracts; - -public interface IInspectable -{ - public IconSource? Icon - { - get; - } - - public UIElement? HeaderContent(); - public UIElement? InspectorContent(); -} \ No newline at end of file diff --git a/Ghost.App/Controls/BasicInput/PropertyField.cs b/Ghost.App/Controls/BasicInput/PropertyField.cs index 34d2f83..f3a099e 100644 --- a/Ghost.App/Controls/BasicInput/PropertyField.cs +++ b/Ghost.App/Controls/BasicInput/PropertyField.cs @@ -1,10 +1,30 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using System.Reflection; +using Windows.Globalization.NumberFormatting; namespace Ghost.Editor.Controls; public sealed partial class PropertyField : ContentControl { + private static readonly Dictionary _valueProperties = new() + { + { typeof(TextBox), TextBox.TextProperty }, + { typeof(NumberBox), NumberBox.ValueProperty }, + { typeof(ToggleButton), ToggleButton.IsCheckedProperty }, + { typeof(ToggleSwitch), ToggleSwitch.IsOnProperty }, + { typeof(ComboBox), Selector.SelectedValueProperty }, + { typeof(RangeBase), RangeBase.ValueProperty }, + }; + + private object? sourceObject; + private FieldInfo? propertyInfo; + private Type? _fieldType; + + private object? _lastValue; + public string Label { get => (string)GetValue(LabelProperty); @@ -21,4 +41,106 @@ public sealed partial class PropertyField : ContentControl { DefaultStyleKey = typeof(PropertyField); } + + private static DependencyProperty? GetValueProperty(Type? fieldType) + { + while (fieldType != null) + { + if (_valueProperties.TryGetValue(fieldType, out var dp)) + { + return dp; + } + fieldType = fieldType.BaseType; + } + + return null; + } + + private static TField ConfigureField(PropertyField propertyField, FieldInfo fieldInfo, object sourceObject, Func factory) + where TField : FrameworkElement + { + propertyField.sourceObject = sourceObject; + propertyField.propertyInfo = fieldInfo; + propertyField._fieldType = typeof(TField); + + var field = factory(); + + var dp = GetValueProperty(typeof(TField)); + field.SetBinding(dp, new Binding + { + Source = sourceObject, + Path = new PropertyPath(fieldInfo.Name), + Mode = BindingMode.TwoWay, + }); + return field; + } + + public static PropertyField Create(string label, FieldInfo fieldInfo, object sourceObject) + { + var propertyField = new PropertyField + { + Label = label + }; + + FrameworkElement content; + switch (fieldInfo.FieldType) + { + case Type t when t == typeof(string): + content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new TextBox()); + break; + case Type t when t == typeof(int) || t == typeof(float) || t == typeof(double): + content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new NumberBox + { + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Hidden, + AcceptsExpression = true, + NumberFormatter = new DecimalFormatter + { + FractionDigits = t == typeof(int) ? 0 : 9, + } + }); + break; + case Type t when t == typeof(bool): + content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new ToggleSwitch()); + break; + case Type t when t == typeof(Enum): + content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new ComboBox + { + ItemsSource = Enum.GetValues(t), + SelectedValuePath = "Value", + }); + break; + default: + content = new TextBlock + { + Text = $"Unsupported type: {fieldInfo.FieldType.Name}", + VerticalAlignment = VerticalAlignment.Center, + Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Red) + }; + break; + } + + propertyField.Content = content; + return propertyField; + } + + public void UpdateValue() + { + if (sourceObject == null || propertyInfo == null || _fieldType == null) + { + return; + } + + var currentValue = propertyInfo.GetValue(sourceObject); + if (Equals(currentValue, _lastValue)) + { + return; + } + + var dp = GetValueProperty(_fieldType); + if (dp != null) + { + SetValue(dp, propertyInfo.GetValue(sourceObject)); + _lastValue = currentValue; + } + } } \ No newline at end of file diff --git a/Ghost.App/Controls/BasicInput/Vector3Field.cs b/Ghost.App/Controls/BasicInput/Vector3Field.cs new file mode 100644 index 0000000..fa31410 --- /dev/null +++ b/Ghost.App/Controls/BasicInput/Vector3Field.cs @@ -0,0 +1,96 @@ +using Ghost.Editor.Event; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Numerics; + +namespace Ghost.Editor.Controls; + +// TODO: value update event +public sealed partial class Vector3Field : Control +{ + private bool _suppressCallback; + + public Vector3 Value + { + get => new((float)X, (float)Y, (float)Z); + set + { + if (value == Value) + { + return; + } + + _suppressCallback = true; + + X = value.X; + Y = value.Y; + Z = value.Z; + + _suppressCallback = false; + } + } + + public double X + { + get => (double)GetValue(XProperty); + set => SetValue(XProperty, value); + } + + public static readonly DependencyProperty XProperty = + DependencyProperty.Register(nameof(X), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged)); + + public double Y + { + get => (double)GetValue(YProperty); + set => SetValue(YProperty, value); + } + + public static readonly DependencyProperty YProperty = + DependencyProperty.Register(nameof(Y), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged)); + + public double Z + { + get => (double)GetValue(ZProperty); + set => SetValue(ZProperty, value); + } + + public static readonly DependencyProperty ZProperty = + DependencyProperty.Register(nameof(Z), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged)); + + public event ValueChangedEventHandler? OnValueChanged; + + private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is Vector3Field vector3Field) + { + if (vector3Field._suppressCallback) + { + return; + } + + var oldValue = vector3Field.Value; + if (e.Property == XProperty) + { + var f = (float)(double)e.OldValue; + oldValue.X = f; + } + else if (e.Property == YProperty) + { + var f = (float)(double)e.OldValue; + oldValue.Y = f; + } + else if (e.Property == ZProperty) + { + var f = (float)(double)e.OldValue; + oldValue.Z = f; + } + + vector3Field.OnValueChanged?.Invoke(vector3Field, new ValueChangedEventArgs(oldValue, vector3Field.Value)); + } + } + + public Vector3Field() + { + DefaultStyleKey = typeof(Vector3Field); + } +} \ No newline at end of file diff --git a/Ghost.App/Controls/BasicInput/Vector3Field.xaml b/Ghost.App/Controls/BasicInput/Vector3Field.xaml new file mode 100644 index 0000000..f41c3be --- /dev/null +++ b/Ghost.App/Controls/BasicInput/Vector3Field.xaml @@ -0,0 +1,41 @@ + + + + + \ No newline at end of file diff --git a/Ghost.App/Controls/EditorControls.xaml b/Ghost.App/Controls/EditorControls.xaml index ac96cd7..b2f05a0 100644 --- a/Ghost.App/Controls/EditorControls.xaml +++ b/Ghost.App/Controls/EditorControls.xaml @@ -2,6 +2,8 @@ + + diff --git a/Ghost.App/Controls/Internal/ComponentDataView.cs b/Ghost.App/Controls/Internal/ComponentDataView.cs new file mode 100644 index 0000000..bf96072 --- /dev/null +++ b/Ghost.App/Controls/Internal/ComponentDataView.cs @@ -0,0 +1,122 @@ +using Ghost.Editor.Core.Inspector; +using Ghost.Editor.Resources; +using Ghost.Editor.Utilities; +using Ghost.Entities; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using System.Reflection; + +namespace Ghost.Editor.Controls.Internal; + +internal unsafe sealed partial class ComponentDataView : Control +{ + private StackPanel? _contentContainer; + + private readonly World? _world; + private readonly Entity _entity = Entity.Invalid; + private readonly Type? _componentType; + + private EventHandler? _updateHandler; + private IComponentEditor? _customEditor; + private PropertyField[]? _propertyFields; + + public string HeaderText + { + get => (string)GetValue(HeaderTextProperty); + set => SetValue(HeaderTextProperty, value); + } + + public static readonly DependencyProperty HeaderTextProperty = + DependencyProperty.Register(nameof(HeaderText), typeof(string), typeof(ComponentDataView), new PropertyMetadata(string.Empty)); + + internal ComponentDataView() + { + DefaultStyleKey = typeof(ComponentDataView); + + Unloaded += (s, e) => + { + CompositionTarget.Rendering -= _updateHandler; + _contentContainer = null; + _customEditor = null; + _propertyFields = null; + }; + } + + public ComponentDataView(string header, World world, Entity entity, Type componentType) : this() + { + HeaderText = header; + _world = world; + _entity = entity; + _componentType = componentType; + } + + protected override void OnApplyTemplate() + { + _contentContainer = (StackPanel)GetTemplateChild("ContentContainer"); + + base.OnApplyTemplate(); + ReBuild(); + } + + private void ReflectionUpdate(object? sender, object e) + { + if (_propertyFields == null) + { + return; + } + + foreach (var propertyField in _propertyFields) + { + propertyField.UpdateValue(); + } + } + + private void CustomEditorUpdate(object? sender, object e) + { + _customEditor!.Update(new ComponentObject(_world!, _entity)); + } + + public void ReBuild() + { + if (_contentContainer == null) + { + return; + } + + _contentContainer.Children.Clear(); + if (_world == null || _componentType == null || _entity == Entity.Invalid) + { + return; + } + + var componentObject = new ComponentObject(_world, _entity); + var editorType = TypeCache.GetTypes().FirstOrDefault(t => + typeof(IComponentEditor).IsAssignableFrom(t) && + t.GetCustomAttribute()?.TargetType.IsAssignableFrom(_componentType) == true); + + if (editorType != null) + { + _customEditor = (IComponentEditor)Activator.CreateInstance(editorType)!; + _customEditor.Create(componentObject, _contentContainer); + } + else + { + var fields = _componentType.GetFields(StaticResource.componentPropertyBindingFlags); + _propertyFields = new PropertyField[fields.Length]; + + for (var i = 0; i < fields.Length; i++) + { + var field = fields[i]; + var component = _world.ComponentStorage.ComponentPools[_componentType.TypeHandle.Value].Get(_entity); + var propertyField = PropertyField.Create(field.Name, field, component); + + _propertyFields[i] = propertyField; + _contentContainer.Children.Add(propertyField); + } + } + + _updateHandler = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate; + CompositionTarget.Rendering += _updateHandler; + } +} \ No newline at end of file diff --git a/Ghost.App/Controls/Internal/ComponentDataView.xaml b/Ghost.App/Controls/Internal/ComponentDataView.xaml new file mode 100644 index 0000000..1949bd5 --- /dev/null +++ b/Ghost.App/Controls/Internal/ComponentDataView.xaml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/Ghost.App/Controls/Internal/InternalControls.xaml b/Ghost.App/Controls/Internal/InternalControls.xaml index 795c1fb..b543b4e 100644 --- a/Ghost.App/Controls/Internal/InternalControls.xaml +++ b/Ghost.App/Controls/Internal/InternalControls.xaml @@ -1,4 +1,7 @@ - + + + + diff --git a/Ghost.App/Core/Inspector/ComponentObject.cs b/Ghost.App/Core/Inspector/ComponentObject.cs new file mode 100644 index 0000000..96e0577 --- /dev/null +++ b/Ghost.App/Core/Inspector/ComponentObject.cs @@ -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 GetData() + where T : unmanaged, IComponentData + { + return _world.EntityManager.GetComponent(_entity); + } + + public void SetData(in T data) + where T : unmanaged, IComponentData + { + _world.EntityManager.SetComponent(_entity, in data); + } +} \ No newline at end of file diff --git a/Ghost.App/Core/Inspector/CustomEditorAttribute.cs b/Ghost.App/Core/Inspector/CustomEditorAttribute.cs new file mode 100644 index 0000000..37c9d81 --- /dev/null +++ b/Ghost.App/Core/Inspector/CustomEditorAttribute.cs @@ -0,0 +1,10 @@ +namespace Ghost.Editor.Core.Inspector; + +[AttributeUsage(AttributeTargets.Class)] +public class CustomEditorAttribute(Type targetType) : Attribute +{ + internal Type TargetType + { + get; + } = targetType; +} \ No newline at end of file diff --git a/Ghost.App/Core/Inspector/IComponentEditor.cs b/Ghost.App/Core/Inspector/IComponentEditor.cs new file mode 100644 index 0000000..9ce7849 --- /dev/null +++ b/Ghost.App/Core/Inspector/IComponentEditor.cs @@ -0,0 +1,25 @@ +using Microsoft.UI.Xaml.Controls; + +namespace Ghost.Editor.Core.Inspector; + +interface IComponentEditor +{ + /// + /// Called when the component editor is created. + /// + /// The component data to edit. + /// The container to add the editor controls to. + public void Create(ComponentObject componentObject, StackPanel container); + + /// + /// Called when the component editor needs to update its UI based on the current state of the component data. + /// + /// The component data to edit. + public void Update(ComponentObject componentObject); + + /// + /// Called when the component editor is destroyed. + /// + /// The component data to edit. + public void Destroy(ComponentObject componentObject); +} \ No newline at end of file diff --git a/Ghost.App/Core/Inspector/IInspectable.cs b/Ghost.App/Core/Inspector/IInspectable.cs new file mode 100644 index 0000000..bf31433 --- /dev/null +++ b/Ghost.App/Core/Inspector/IInspectable.cs @@ -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; + } +} \ No newline at end of file diff --git a/Ghost.App/Core/SceneGraph/EditorWorldManager.cs b/Ghost.App/Core/SceneGraph/EditorWorldManager.cs index 79da3f5..566c5d7 100644 --- a/Ghost.App/Core/SceneGraph/EditorWorldManager.cs +++ b/Ghost.App/Core/SceneGraph/EditorWorldManager.cs @@ -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(readStream, StaticResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed."); + var deserializedScene = await JsonSerializer.DeserializeAsync(readStream, Engine.Resources.StaticResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed."); _loadedWorlds.Clear(); diff --git a/Ghost.App/Core/SceneGraph/EntityNode.cs b/Ghost.App/Core/SceneGraph/EntityNode.cs index dd38ed1..edc9bd7 100644 --- a/Ghost.App/Core/SceneGraph/EntityNode.cs +++ b/Ghost.App/Core/SceneGraph/EntityNode.cs @@ -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() != null) + { + continue; + } + + var dataView = new ComponentDataView(type.Name, _owner.World, _entity, type); + root.Children.Add(dataView); + } + + return root; + } } } \ No newline at end of file diff --git a/Ghost.App/Core/SceneGraph/SceneGraphHelpers.cs b/Ghost.App/Core/SceneGraph/SceneGraphHelpers.cs index 3254ac0..8ab2c32 100644 --- a/Ghost.App/Core/SceneGraph/SceneGraphHelpers.cs +++ b/Ghost.App/Core/SceneGraph/SceneGraphHelpers.cs @@ -10,21 +10,21 @@ public class SceneGraphHelpers /// /// The world context where the entity will be created. /// The entity to be wrapped in the . - 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); } /// /// Creates a new and entity with default components. /// - /// The world context where the entity will be created. - public static EntityNode CreateEntityNode(World world, string name) + /// The world context where the entity will be created. + 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); } /// diff --git a/Ghost.App/Core/SceneGraph/WorldNode.cs b/Ghost.App/Core/SceneGraph/WorldNode.cs index 944087e..3ca7952 100644 --- a/Ghost.App/Core/SceneGraph/WorldNode.cs +++ b/Ghost.App/Core/SceneGraph/WorldNode.cs @@ -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 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(entity); + var hc = _world.EntityManager.GetComponent(entity); var child = hc.ValueRO.firstChild; while (child != Entity.Invalid) { - node.AddChild(BuildNodeRecursive(child, world)); - var childHC = world.EntityManager.GetComponent(child); + node.AddChild(BuildNodeRecursive(child)); + var childHC = _world.EntityManager.GetComponent(child); child = childHC.ValueRO.nextSibling; } @@ -103,7 +103,7 @@ public partial class WorldNode : SceneGraphNode, IEquatable { 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; } \ No newline at end of file diff --git a/Ghost.App/Core/Serializer/WorldNodeSerializer.cs b/Ghost.App/Core/Serializer/WorldNodeSerializer.cs index 5004c76..d669a2a 100644 --- a/Ghost.App/Core/Serializer/WorldNodeSerializer.cs +++ b/Ghost.App/Core/Serializer/WorldNodeSerializer.cs @@ -39,7 +39,7 @@ internal class WorldNodeSerializer : JsonConverter 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; diff --git a/Ghost.App/Event/ValueChangedEventHandler.cs b/Ghost.App/Event/ValueChangedEventHandler.cs new file mode 100644 index 0000000..f04242d --- /dev/null +++ b/Ghost.App/Event/ValueChangedEventHandler.cs @@ -0,0 +1,22 @@ +namespace Ghost.Editor.Event; + +public delegate void ValueChangedEventHandler(object? sender, ValueChangedEventArgs args); + +public class ValueChangedEventArgs : EventArgs +{ + public T OldValue + { + get; + } + + public T NewValue + { + get; + } + + public ValueChangedEventArgs(T oldValue, T newValue) + { + OldValue = oldValue; + NewValue = newValue; + } +} \ No newline at end of file diff --git a/Ghost.App/Ghost.Editor.csproj b/Ghost.App/Ghost.Editor.csproj index e0e9bee..3e316a6 100644 --- a/Ghost.App/Ghost.Editor.csproj +++ b/Ghost.App/Ghost.Editor.csproj @@ -40,7 +40,9 @@ + + @@ -159,6 +161,16 @@ + + + MSBuild:Compile + + + + + MSBuild:Compile + + MSBuild:Compile diff --git a/Ghost.App/Resources/StaticResource.cs b/Ghost.App/Resources/StaticResource.cs new file mode 100644 index 0000000..247849e --- /dev/null +++ b/Ghost.App/Resources/StaticResource.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Ghost.Editor.Resources; + +internal static class StaticResource +{ + public static readonly BindingFlags componentPropertyBindingFlags = BindingFlags.Public | BindingFlags.Instance; +} \ No newline at end of file diff --git a/Ghost.App/Services/Contracts/IInspectorService.cs b/Ghost.App/Services/Contracts/IInspectorService.cs index d2881c6..8dccc97 100644 --- a/Ghost.App/Services/Contracts/IInspectorService.cs +++ b/Ghost.App/Services/Contracts/IInspectorService.cs @@ -1,4 +1,4 @@ -using Ghost.Editor.Contracts; +using Ghost.Editor.Core.Inspector; namespace Ghost.Editor.Services.Contracts; diff --git a/Ghost.App/Services/InspectorService.cs b/Ghost.App/Services/InspectorService.cs index 6447a25..b1bb2be 100644 --- a/Ghost.App/Services/InspectorService.cs +++ b/Ghost.App/Services/InspectorService.cs @@ -1,4 +1,4 @@ -using Ghost.Editor.Contracts; +using Ghost.Editor.Core.Inspector; using Ghost.Editor.Services.Contracts; namespace Ghost.Editor.Services; diff --git a/Ghost.App/Themes/Override.xaml b/Ghost.App/Themes/Override.xaml index ae0ad06..69d1d2d 100644 --- a/Ghost.App/Themes/Override.xaml +++ b/Ghost.App/Themes/Override.xaml @@ -12,7 +12,9 @@ + +