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,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)
{
}
}

View File

@@ -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<LocalToWorld>();
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<LocalToWorld>();
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<LocalToWorld>();
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<LocalToWorld>();
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)
{
}
}

View File

@@ -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();
}

View File

@@ -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<Type, DependencyProperty> _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<TField>(PropertyField propertyField, FieldInfo fieldInfo, object sourceObject, Func<TField> 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;
}
}
}

View File

@@ -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<Vector3>? 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<Vector3>(oldValue, vector3Field.Value));
}
}
public Vector3Field()
{
DefaultStyleKey = typeof(Vector3Field);
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Controls">
<Style TargetType="local:Vector3Field">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Vector3Field">
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="X" />
<NumberBox Grid.Column="1" Value="{Binding X, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Text="Y" />
<NumberBox Grid.Column="3" Value="{TemplateBinding Y}" />
<TextBlock
Grid.Column="4"
VerticalAlignment="Center"
Text="Z" />
<NumberBox Grid.Column="5" Value="{TemplateBinding Z}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -2,6 +2,8 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Controls/BasicInput/PropertyField.xaml" />
<ResourceDictionary Source="/Controls/BasicInput/Vector3Field.xaml" />
<ResourceDictionary Source="/Controls/Internal/InternalControls.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -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<object>? _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<CustomEditorAttribute>()?.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;
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Controls.Internal">
<Style TargetType="local:ComponentDataView">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ComponentDataView">
<StackPanel Margin="0,0,0,16">
<Border
Padding="8"
HorizontalAlignment="Stretch"
Background="{ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{TemplateBinding HeaderText}" />
</Border>
<StackPanel
x:Name="ContentContainer"
Margin="8,2,2,0"
Spacing="2" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries />
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Controls/Internal/ComponentDataView.xaml" />
<ResourceDictionary Source="/Controls/Internal/NavigationTabView.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

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;

View File

@@ -0,0 +1,22 @@
namespace Ghost.Editor.Event;
public delegate void ValueChangedEventHandler<T>(object? sender, ValueChangedEventArgs<T> args);
public class ValueChangedEventArgs<T> : EventArgs
{
public T OldValue
{
get;
}
public T NewValue
{
get;
}
public ValueChangedEventArgs(T oldValue, T newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}

View File

@@ -40,7 +40,9 @@
<None Remove="Assets\Icon.targetsize-48.png" />
<None Remove="Assets\Icon.targetsize-48_altform-unplated.png" />
<None Remove="Controls\BasicInput\PropertyField.xaml" />
<None Remove="Controls\BasicInput\Vector3Field.xaml" />
<None Remove="Controls\EditorControls.xaml" />
<None Remove="Controls\Internal\ComponentDataView.xaml" />
<None Remove="Controls\Internal\InternalControls.xaml" />
<None Remove="Controls\Internal\NavigationTabView.xaml" />
<None Remove="View\Pages\EngineEditor\ConsolePage.xaml" />
@@ -159,6 +161,16 @@
<ItemGroup>
<Folder Include="Controls\Layout\" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\BasicInput\Vector3Field.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\Internal\ComponentDataView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\Internal\NavigationTabView.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,8 @@
using System.Reflection;
namespace Ghost.Editor.Resources;
internal static class StaticResource
{
public static readonly BindingFlags componentPropertyBindingFlags = BindingFlags.Public | BindingFlags.Instance;
}

View File

@@ -1,4 +1,4 @@
using Ghost.Editor.Contracts;
using Ghost.Editor.Core.Inspector;
namespace Ghost.Editor.Services.Contracts;

View File

@@ -1,4 +1,4 @@
using Ghost.Editor.Contracts;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Services.Contracts;
namespace Ghost.Editor.Services;

View File

@@ -12,7 +12,9 @@
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style TargetType="internal:NavigationTabView">
<Setter Property="TabWidthMode" Value="Compact" />
</Style>
<Style TargetType="NumberBox" />
</ResourceDictionary>

View File

@@ -0,0 +1,27 @@
using Ghost.Engine.Utilities;
using Microsoft.UI.Xaml.Data;
using System.Numerics;
namespace Ghost.Editor.Utilities.Converters;
public partial class Vector3ToQuaternionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Vector3 vector)
{
return Quaternion.CreateFromYawPitchRoll(vector.Y, vector.X, vector.Z);
}
throw new ArgumentException("Value must be of type System.Numerics.Vector3.", nameof(value));
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is Quaternion quaternion)
{
return VectorUtility.CreateFromQuaternion(quaternion);
}
throw new ArgumentException("Value must be of type System.Numerics.Quaternion.", nameof(value));
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
namespace Ghost.Editor.Utilities;
public class ReflectionBinding
{
private void RefreshField(FieldInfo field, FrameworkElement control, object source)
{
var value = field.GetValue(source);
switch (control)
{
case TextBox tb:
tb.Text = value?.ToString();
break;
case NumberBox nb when value is double d:
nb.Value = d;
break;
// Add more controls...
}
}
public void StartPollingField(FieldInfo field, FrameworkElement control, object component)
{
var lastValue = field.GetValue(component);
DispatcherTimer timer = new()
{
Interval = TimeSpan.FromMilliseconds(200)
};
timer.Tick += (_, _) =>
{
var currentValue = field.GetValue(component);
if (!Equals(currentValue, lastValue))
{
RefreshField(field, control, component);
lastValue = currentValue;
}
};
timer.Start();
}
}

View File

@@ -1,9 +1,25 @@
using Ghost.Entities;
using System;
using System.Linq;
using System.Reflection;
namespace Ghost.Editor.Utilities;
public static class TypeCache
{
private static readonly TypeInfo[] _types;
static TypeCache()
{
_types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.DefinedTypes)
.ToArray();
}
public static Type[] GetTypes()
{
return _types;
}
}
public static class ComponentTypeCache
{
private static readonly Type?[][] _componentTypes;

View File

@@ -13,22 +13,22 @@
<internal:NavigationTabPage.Resources>
<DataTemplate x:Key="SceneTemplate" x:DataType="sg:SceneGraphNode">
<TreeViewItem
AutomationProperties.Name="{x:Bind Name}"
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
Background="{ThemeResource ControlSolidFillColorDefaultBrush}"
IsExpanded="True"
ItemsSource="{x:Bind Children}">
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF159;" />
<TextBlock Margin="10,0" Text="{x:Bind Name}" />
<TextBlock Margin="10,0" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
<DataTemplate x:Key="EntityTemplate" x:DataType="sg:SceneGraphNode">
<TreeViewItem AutomationProperties.Name="{x:Bind Name}" ItemsSource="{x:Bind Children}">
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}" ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Margin="10,0" Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF158;" />
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name}" />
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>

View File

@@ -1,5 +1,5 @@
using Ghost.Editor.Contracts;
using Ghost.Editor.Controls.Internal;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.SceneGraph;
using Ghost.Editor.Services.Contracts;
using Ghost.Editor.ViewModels.Pages.EngineEditor;

View File

@@ -32,13 +32,13 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IconSource="{x:Bind ViewModel.Inspectable.Icon, Mode=OneWay}" />
<ContentPresenter Grid.Column="1" Content="{x:Bind ViewModel.Inspectable.HeaderContent(), Mode=OneWay}" />
<ContentPresenter Grid.Column="1" Content="{x:Bind ViewModel.Inspectable.HeaderContent, Mode=OneWay}" />
</Grid>
<!-- Content -->
<Grid Grid.Row="1" Padding="10,0,10,0">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentPresenter Content="{x:Bind ViewModel.Inspectable.InspectorContent(), Mode=OneWay}" />
<Grid Grid.Row="1" Padding="0,0,0,0">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ContentPresenter Content="{x:Bind ViewModel.Inspectable.InspectorContent, Mode=OneWay}" />
</ScrollViewer>
</Grid>
</Grid>

View File

@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Contracts;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Services.Contracts;
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;