diff --git a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs index 5312b5d..4604cd1 100644 --- a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs +++ b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs @@ -19,14 +19,15 @@ namespace Engine.Editor; /// split exists at all. /// /// Nothing here reads or writes a Component through GetComponent/Query — -/// the Hierarchy and Inspector panels (not built yet) will walk -/// IWorld.Roots and GameObject.Components directly instead, which is why -/// this plugin never needs to declare Reads/Writes on its system: those -/// checks only guard the typed accessors, not plain property reads. See -/// GameObject.AddComponent's own doc comment on the same gap. +/// HierarchyPanel walks IWorld.Roots/GameObject.Children directly, and the +/// not-yet-built Inspector will walk GameObject.Components the same way — +/// which is why this plugin never needs to declare Reads/Writes on its +/// system: those checks only guard the typed accessors, not plain property +/// reads. See GameObject.AddComponent's own doc comment on the same gap. /// public sealed class EditorPlugin : IPlugin { + private readonly EditorState _state = new(); private GL? _gl; private ImGuiController? _controller; private ITime? _time; @@ -64,6 +65,8 @@ public sealed class EditorPlugin : IPlugin ImGui.Text($"Frame: {_time.FrameCount}"); ImGui.End(); + HierarchyPanel.Draw(world, _state); + _controller.Render(); } } diff --git a/plugins/engine.editor/Engine.Editor/EditorState.cs b/plugins/engine.editor/Engine.Editor/EditorState.cs new file mode 100644 index 0000000..59113e0 --- /dev/null +++ b/plugins/engine.editor/Engine.Editor/EditorState.cs @@ -0,0 +1,15 @@ +using Engine.Kernel.World; + +namespace Engine.Editor; + +/// +/// Shared, per-session editor state — currently just which GameObject the +/// Hierarchy panel last clicked, so the Inspector panel (built against this +/// same instance) knows what to show. One instance per EditorPlugin, not +/// static: reloading engine.editor should start with nothing selected, not +/// hold a reference to a GameObject that may not even exist anymore. +/// +internal sealed class EditorState +{ + public GameObject? Selected { get; set; } +} diff --git a/plugins/engine.editor/Engine.Editor/HierarchyPanel.cs b/plugins/engine.editor/Engine.Editor/HierarchyPanel.cs new file mode 100644 index 0000000..8609cee --- /dev/null +++ b/plugins/engine.editor/Engine.Editor/HierarchyPanel.cs @@ -0,0 +1,50 @@ +using Engine.Kernel.World; +using ImGuiNET; + +namespace Engine.Editor; + +/// +/// Walks IWorld.Roots/GameObject.Children directly — not Query<T>(), +/// there's no component type to query for here — so, like EditorPlugin +/// itself, this never touches SystemAccessScope and needs no declared +/// Reads/Writes. +/// +internal static class HierarchyPanel +{ + public static void Draw(IWorld world, EditorState state) + { + ImGui.Begin("Hierarchy"); + + foreach (var root in world.Roots) + DrawNode(root, state); + + ImGui.End(); + } + + private static void DrawNode(GameObject go, EditorState state) + { + // PushID/PopID, not relying on go.Name for identity: sibling + // GameObjects can share a name (nothing stops it), and ImGui's + // default ID-from-label would then merge their open/selected state. + ImGui.PushID(go.GetHashCode()); + + var flags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.SpanAvailWidth; + if (go.Children.Count == 0) + flags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; + if (ReferenceEquals(state.Selected, go)) + flags |= ImGuiTreeNodeFlags.Selected; + + var open = ImGui.TreeNodeEx(go.Name, flags); + if (ImGui.IsItemClicked()) + state.Selected = go; + + if (open && go.Children.Count > 0) + { + foreach (var child in go.Children) + DrawNode(child, state); + ImGui.TreePop(); + } + + ImGui.PopID(); + } +} diff --git a/samples/WindowDemo/scene.json b/samples/WindowDemo/scene.json index 85d4edb..f104f17 100644 --- a/samples/WindowDemo/scene.json +++ b/samples/WindowDemo/scene.json @@ -9,6 +9,19 @@ "components": [ { "type": "Engine.Render.Contracts.QuadRenderer, Engine.Render.Contracts", "data": {} } ], - "children": [] + "children": [ + { + "name": "ChildQuad", + "transform": { + "position": [1.5, 0, 0], + "rotation": [0, 0, 0, 1], + "scale": [0.5, 0.5, 1] + }, + "components": [ + { "type": "Engine.Render.Contracts.QuadRenderer, Engine.Render.Contracts", "data": {} } + ], + "children": [] + } + ] } ]