diff --git a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs index 4604cd1..278a8b1 100644 --- a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs +++ b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs @@ -60,12 +60,21 @@ public sealed class EditorPlugin : IPlugin { _controller!.Update(_time!.DeltaTime); + // Nothing selected yet and there's something to select: default to + // the first root rather than opening on an empty, useless + // Inspector. Only fires once — any real click overwrites it, and + // it never fights a deliberate deselect because there's no way to + // deselect yet. + if (_state.Selected is null && world.Roots.Count > 0) + _state.Selected = world.Roots[0]; + ImGui.Begin("Lingua Editor"); ImGui.Text($"FPS: {1f / MathF.Max(_time.DeltaTime, 0.0001f):F0}"); ImGui.Text($"Frame: {_time.FrameCount}"); ImGui.End(); HierarchyPanel.Draw(world, _state); + InspectorPanel.Draw(_state); _controller.Render(); } diff --git a/plugins/engine.editor/Engine.Editor/InspectorPanel.cs b/plugins/engine.editor/Engine.Editor/InspectorPanel.cs new file mode 100644 index 0000000..b48dd72 --- /dev/null +++ b/plugins/engine.editor/Engine.Editor/InspectorPanel.cs @@ -0,0 +1,108 @@ +using System.Numerics; +using System.Reflection; +using Engine.Kernel.World; +using ImGuiNET; + +namespace Engine.Editor; + +/// +/// Shows EditorState.Selected's Transform (direct field access — Transform +/// is a known struct, not something to reflect over) and, below it, every +/// Component's public fields via reflection: there's no per-component-type +/// editor code anywhere in engine.editor, on purpose — a new component +/// type in any plugin gets an Inspector for free, which is the entire +/// point of doing this by reflection instead of a registry of per-type +/// drawers. Only int/float/bool/string/Vector3 fields are editable; +/// anything else falls back to a read-only ToString() so an unsupported +/// field type degrades to "visible but not editable" instead of being +/// silently hidden. +/// +internal static class InspectorPanel +{ + public static void Draw(EditorState state) + { + ImGui.Begin("Inspector"); + + var go = state.Selected; + if (go is null) + { + ImGui.TextDisabled("Nothing selected."); + ImGui.End(); + return; + } + + ImGui.Text(go.Name); + ImGui.Separator(); + + if (ImGui.CollapsingHeader("Transform", ImGuiTreeNodeFlags.DefaultOpen)) + { + var t = go.Transform; + var changed = ImGui.DragFloat3("Position", ref t.LocalPosition, 0.1f); + changed |= ImGui.DragFloat3("Scale", ref t.LocalScale, 0.1f); + ImGui.Text($"Rotation (quat): {t.LocalRotation}"); + + if (changed) + go.Transform = t; + } + + foreach (var component in go.Components) + DrawComponent(component); + + ImGui.End(); + } + + private static void DrawComponent(Component component) + { + var type = component.GetType(); + if (!ImGui.CollapsingHeader(type.Name, ImGuiTreeNodeFlags.DefaultOpen)) + return; + + ImGui.PushID(component.GetHashCode()); + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + if (fields.Length == 0) + ImGui.TextDisabled("(no fields)"); + + foreach (var field in fields) + DrawField(component, field); + + ImGui.PopID(); + } + + private static void DrawField(Component component, FieldInfo field) + { + var value = field.GetValue(component); + + switch (value) + { + case int i: + if (ImGui.DragInt(field.Name, ref i)) + field.SetValue(component, i); + break; + + case float f: + if (ImGui.DragFloat(field.Name, ref f)) + field.SetValue(component, f); + break; + + case bool b: + if (ImGui.Checkbox(field.Name, ref b)) + field.SetValue(component, b); + break; + + case string s: + if (ImGui.InputText(field.Name, ref s, 256)) + field.SetValue(component, s); + break; + + case Vector3 v: + if (ImGui.DragFloat3(field.Name, ref v, 0.1f)) + field.SetValue(component, v); + break; + + default: + ImGui.Text($"{field.Name}: {value}"); + break; + } + } +}