TranslateGizmo draws three axis handles at the selected GameObject's
world position by projecting real 3D points through the real camera's
View/Projection (GizmoMath.WorldToScreen) onto ImGui's foreground draw
list. Nothing OpenGL-side draws lines yet, so this isn't 3D geometry
in the GL sense — but it IS driven by the actual camera matrices, which
is what the earlier scoped-2D-vs-full-3D-pipeline choice was actually
about: a handle dragged in screen space has to map onto a real 3D axis,
and that only means something once there's a real camera to project
through. The screenshot below shows exactly that — the axes aren't
screen-perpendicular, because the camera at (0,3,6) looking at the
origin means they shouldn't be.
Dragging a handle re-projects the mouse delta onto the axis's own
screen-space direction (GizmoMath.ProjectDragOntoAxis) and writes the
result back through GizmoMath.WorldToLocalPosition, which inverts the
parent's WorldMatrix rather than writing LocalPosition directly — a
parent with non-identity scale or rotation means "move 1 world unit"
and "add 1 to LocalPosition" are different amounts, and
samples/WindowDemo's ChildQuad (parented under a (2,2,1)-scaled Quad)
is exactly that case.
Split the actual math into GizmoMath (Engine.Editor.Contracts, no GL/
ImGui/mouse dependency) so it's unit-testable without a window or a real
mouse — neither exists in a headless test run, and dragging is exactly
the kind of interaction that's easy to get subtly wrong (screen-space
ratio direction, perspective sign, parent-scale correctness) without
something to check it against beyond eyeballing a screenshot. New
Engine.Editor.Tests project, 8 tests: screen-center projection, a
behind-camera point returning null, drag-ratio math on both an
axis-aligned and a diagonal screen direction, and the parent-scale/
parent-translation inverse-transform cases. All 8 passed on the first
run — including the non-uniform-scale case, the one most likely to be
subtly wrong.
Full suite: 73 tests, all green (65 previous + 8 new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
IPlayModeController (Engine.Editor.Contracts) wraps IWorld.Snapshot()/
Restore() as EnterPlay/ExitPlay — the entire mechanism is that one
snapshot, per Snapshot's own doc comment. Exposed as a service, not just
something engine.editor's UI calls directly, so a future non-UI driver
(a test harness, a headless "play for N frames" CLI command) can drive
Play mode without depending on ImGui.
Engine.Host now looks up IPlayModeController once and checks IsPlaying
every frame before running Stage.Update: Edit mode still renders the
scene (so the view isn't frozen and the editor UI stays responsive) but
never ticks it, same distinction Unity draws between its Scene and Game
views. A project with no engine.editor loaded sees no behavior change —
Update runs unconditionally, same as before this existed.
Added "play"/"stop" as stdin commands alongside the existing "r" and
"screenshot", both real controls (not just test scaffolding) for driving
Play mode without a mouse — which is also how this got verified: an
interactive run sent "play", screenshotted, sent "stop", and the log
shows "Entered Play mode in 13.2ms", the real editor path exercising the
same under-100ms budget WorldSnapshotTests already proves at the kernel
level. The Lingua Editor panel's button/label flip Play/(Edit mode) to
Stop/(Playing) correctly across both screenshots.
Also fixes a real layout bug hit while verifying this: SetNextWindowPos
with ImGuiCond.FirstUseEver only applies with no prior imgui.ini entry
for that window title, and this project had already accumulated one from
earlier runs (all three panels stacked exactly on top of each other) —
gave each panel its own default position and .gitignore'd imgui.ini,
which is per-machine session state, not source.
Full suite still green: 65 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
Introduces engine.editor, a new plugin that renders an ImGui panel on top
of whatever the scene is drawing, using Silk.NET.OpenGL.Extensions.ImGui's
ImGuiController against the same GL context and window engine.render
already owns.
Getting the overlay to actually show up in the same frame (not delayed by
one, which is what happens if UI draws after SwapBuffers) required
splitting engine.render's Draw system: SwapBuffers moves out into its own
Present system on a new Stage.Present, run by Engine.Host after every
Stage.Render system — this plugin's Draw and, now, engine.editor's ImGui
pass — has drawn into the same back buffer. Stage.Present is the second
addition to the frame stage set (after FixedUpdate was scoped out for M4);
docs/kernel-contract.md already committed to the set being fixed and
kernel-defined, not plugin-extensible, and to only adding a stage when
there's something real to run in it — SwapBuffers already needed
somewhere to live once a second Render-stage system existed to race it.
Also extends IEngineInput with a Native IInputContext property, the same
escape hatch IEngineWindow.Native already is — ImGuiController's
constructor needs the raw Silk.NET input context directly.
Verified with a real windowed run: --screenshot after 5 frames shows the
"Lingua" ImGui panel (FPS/frame counters) correctly composited over the
still-correct 3D checkerboard quad from the M3 camera pipeline, in the
same frame. Full suite still green: 65 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N