From bccb8c3ba735359e99bba80e2aa7f39fe792df0d Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 2 Sep 2026 16:19:36 +0300 Subject: [PATCH] =?UTF-8?q?M3:=20Hierarchy=20panel=20=E2=80=94=20click=20a?= =?UTF-8?q?=20GameObject=20to=20select=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HierarchyPanel walks IWorld.Roots/GameObject.Children recursively as ImGui tree nodes, clicking one sets EditorState.Selected — the shared selection the not-yet-built Inspector panel will read from the same instance. PushID(go.GetHashCode()) scopes each node's ID by object identity rather than name, since nothing stops two sibling GameObjects sharing a Name and ImGui's default label-based IDs would otherwise merge their open/selected state. Also gives samples/WindowDemo/scene.json a child GameObject (offset, half-scale, parented under Quad) — the existing scene only had one root, nothing to show a tree with. Verified by screenshot: both quads render at their correct composed WorldMatrix (the child visibly smaller and offset, confirming parent/child composition is still correct through this change), and Hierarchy lists "Quad" as a collapsible node. Full suite still green: 65 tests. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N --- .../Engine.Editor/EditorPlugin.cs | 13 +++-- .../Engine.Editor/EditorState.cs | 15 ++++++ .../Engine.Editor/HierarchyPanel.cs | 50 +++++++++++++++++++ samples/WindowDemo/scene.json | 15 +++++- 4 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 plugins/engine.editor/Engine.Editor/EditorState.cs create mode 100644 plugins/engine.editor/Engine.Editor/HierarchyPanel.cs 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": [] + } + ] } ]