feat: implement scene graph system with ECS hierarchy support

Add hierarchical scene graph for editor with TreeView UI, runtime
HierarchyUtility for parent/child linked-list management, and
incremental sync between editor world and scene graph nodes.

- SceneGraphNode/SceneNode/EntityNode with World, Scene, Entity refs
- SceneGraphBuilder — construct tree from ECS World queries
- HierarchyUtility — SetParent, RemoveParent, IsAncestor, cascade destroy
- EditorWorldService + SceneGraphSyncService — editor world lifecycle & incremental sync
- Hierarchy.xaml — TreeView with DataTemplate + SceneGraphTemplateSelector
- 25 unit tests covering hierarchy ops and scene graph building
This commit is contained in:
2026-05-10 00:14:55 +09:00
parent e2bc68d359
commit 2ea3c509b0
19 changed files with 1841 additions and 62 deletions

View File

@@ -38,10 +38,10 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,9 +5,40 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.Views.Controls"
xmlns:sg="using:Ghost.Editor.Core.SceneGraph"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Resources>
<local:SceneGraphTemplateSelector
x:Key="SceneGraphTemplateSelector"
SceneNodeTemplate="{StaticResource SceneNodeTemplate}"
EntityNodeTemplate="{StaticResource EntityNodeTemplate}" />
<DataTemplate x:Key="SceneNodeTemplate" x:DataType="sg:SceneNode">
<TreeViewItem
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF156;" />
<TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
<DataTemplate x:Key="EntityNodeTemplate" x:DataType="sg:EntityNode">
<TreeViewItem
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF158;" />
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -50,19 +81,17 @@
Margin="0,0,4,0"
FontSize="{StaticResource ToolbarFontIconFontSize}"
Glyph="&#xE721;" />
<TextBox Grid.Column="1" PlaceholderText="Sreach item..." />
<TextBox Grid.Column="1" PlaceholderText="Search item..." />
</Grid>
<Border Margin="-8,8,-4,-4" Style="{StaticResource HorizontalStrongDivider}" />
</StackPanel>
<ListView Grid.Row="1" Padding="4,2,0,2">
<ListViewItem Content="Test" />
<ListViewItem Content="Test" />
<ListViewItem Content="Test" />
<ListViewItem Content="Test" />
<ListViewItem Content="Test" />
<ListViewItem Content="Test" />
</ListView>
<TreeView
x:Name="SceneTreeView"
Grid.Row="1"
Padding="4,2,0,2"
ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}"
SelectionMode="Single" />
</Grid>
</UserControl>

View File

@@ -1,14 +1,62 @@
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor.Views.Controls;
public sealed partial class Hierarchy : UserControl
{
private readonly SceneGraphSyncService _syncService;
private readonly IInspectorService _inspectorService;
private readonly EditorWorldService _worldService;
private DispatcherQueueTimer? _syncTimer;
public Hierarchy()
{
InitializeComponent();
_syncService = App.GetService<SceneGraphSyncService>();
_inspectorService = App.GetService<IInspectorService>();
_worldService = App.GetService<EditorWorldService>();
SceneTreeView.ItemsSource = _worldService.RootNodes;
SceneTreeView.ItemInvoked += OnTreeViewItemInvoked;
SceneTreeView.SelectionChanged += OnTreeViewSelectionChanged;
_syncTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
_syncTimer.Interval = TimeSpan.FromMilliseconds(100);
_syncTimer.Tick += OnSyncTick;
_syncTimer.Start();
Unloaded += OnUnloaded;
}
private void OnSyncTick(DispatcherQueueTimer sender, object args)
{
if (_syncService.Tick())
{
}
}
private void OnTreeViewItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
{
if (args.InvokedItem is IInspectable inspectable)
{
_inspectorService.SetSelected(inspectable, this);
}
}
private void OnTreeViewSelectionChanged(object sender, TreeViewSelectionChangedEventArgs args)
{
}
private void OnUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
_syncTimer?.Stop();
SceneTreeView.ItemInvoked -= OnTreeViewItemInvoked;
SceneTreeView.SelectionChanged -= OnTreeViewSelectionChanged;
Unloaded -= OnUnloaded;
}
}

View File

@@ -0,0 +1,35 @@
using Ghost.Editor.Core.SceneGraph;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls;
public partial class SceneGraphTemplateSelector : DataTemplateSelector
{
public DataTemplate? SceneNodeTemplate
{
get; set;
}
public DataTemplate? EntityNodeTemplate
{
get; set;
}
protected override DataTemplate SelectTemplateCore(object item)
{
var result = item switch
{
SceneNode => SceneNodeTemplate,
EntityNode => EntityNodeTemplate,
_ => base.SelectTemplateCore(item)
};
return result!;
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
}