Skip to content

Claude/nervous hamilton a271ee#57

Open
kisstp2006 wants to merge 67 commits into
elnormous:masterfrom
kisstp2006:claude/nervous-hamilton-a271ee
Open

Claude/nervous hamilton a271ee#57
kisstp2006 wants to merge 67 commits into
elnormous:masterfrom
kisstp2006:claude/nervous-hamilton-a271ee

Conversation

@kisstp2006
Copy link
Copy Markdown

No description provided.

kisstp2006 and others added 20 commits January 14, 2026 21:11
Wires Dear ImGui into the Fluxion render loop on D3D11, OpenGL 2/3/4 + ES 2/3,
and Metal via stock backends; routes input through Fluxion's EventDispatcher
instead of imgui_impl_win32 so the same code works on every platform without
racing ImGui state from the window message thread.

Engine plumbing:
- RenderDevice gains a thread-safe preSwapHook (mutex-protected) called by
  each renderer right before backbuffer swap, with the back buffer rebound
  and viewport set so ImGui draws into the final image.
- Engine constructor accepts -graphics <opengl|direct3d11|metal|empty>
  to override settings.ini for runtime driver switching.
- Graphics gains getDriverName() helper for diagnostics.

ImGui module (engine/gui/imgui/):
- Context/Backend split with PIMPL public API and per-renderer Backend impls.
- ImGuiInputBridge: high-priority EventHandler buffers mouse/keyboard events
  under a mutex on the platform thread, drains them into ImGuiIO on the
  render thread before NewFrame, and consumes events when WantCaptureMouse/
  Keyboard is set so the game scenes don't double-process focused input.
- Synchronous shutdown via executeOnRenderThread + std::promise, so OGL
  and Metal release their device objects on the correct thread without leaks.
- Master switch (Context::setEnabled) makes the backend skip the entire
  ImGui frame for zero overhead when the editor UI is hidden.

Sample:
- samples/main.cpp draws three demo windows whose visibility is driven by
  atomic toggles in samples/ImGuiHost.hpp.
- New ImGuiSample (samples/ImGuiSample.{hpp,cpp}) gives the user a panel
  of native Fluxion buttons that flip those toggles and the master switch
  from the engine's own UI, exercising both rendering paths together.
- MainMenu picks up an "ImGui" entry; main.cpp adds "imgui" to the
  -sample dispatch.

Implementation plan for the rest of the editor lives in
docs/EDITOR_IMPLEMENTATION_PLAN.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…itor

Lays the editor's metadata foundation (phase 2 of the editor plan).
Components opt in to reflection by adding FLUXION_COMPONENT_DECL to the
header and FLUXION_COMPONENT_REGISTER to the .cpp; non-reflective
components keep compiling unchanged because the new Component virtuals
default to a no-op.

engine/reflection/:
- TypeRegistry: process-wide name -> factory map with create() and
  listTypes(). AutoRegistrar enables one-line registration from any TU.
- Inspector: UI-independent property-bus interface (float, vector,
  color, enum, group fields). Same calls feed an editor panel today
  and a serializer in the next phase.
- Macros: FLUXION_COMPONENT_DECL injects typeName(), staticTypeName()
  and a default-construction factory hook in the class body;
  FLUXION_COMPONENT_REGISTER drops a static AutoRegistrar inside the
  class's namespace so registration runs at static-init time.

engine/gui/imgui/ImGuiInspector: concrete Inspector implementation that
maps each typed-field call to the appropriate ImGui widget (DragFloat,
SliderFloat, Checkbox, ColorEdit4, BeginCombo, etc.). Color values
round-trip through float[4] for ColorEdit4; strings use a resize
callback so the editor can grow them in place.

scene::Component gains two virtuals (typeName, inspect) with no-op
defaults. SpriteRenderer and Camera are wired up as proof of concept:
- SpriteRenderer: hidden, offset, playing, animation time, anim count.
- Camera: hidden, fov / near / far, scale mode, wireframe, clear group
  (color buffer, depth buffer, clear color). Projection mode and depth
  test are read-only in this phase because mutating them on a temporary
  orphan Camera (the type-registry demo path) would either divide by
  zero in setPerspective or allocate an engine-bound DepthStencilState
  without a valid layer; phase 5 will edit live scene cameras instead.

Sample (samples/main.cpp + ImGuiSample): new "Reflection demo" ImGui
window lists the registered types and exposes a live property editor
for a temporary instance picked from the dropdown. ImGuiSample picks
up a "Reflection: on/off" native button alongside the existing toggles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 foundation. The same inspect() bodies that drive the editor UI
now also drive serialization, so a component author writes its property
list once and gets editor + save + load for free. Full Scene save/load
(actor traversal, atomic file write, AssetReference resolution) is
deferred to a follow-up phase; this commit only delivers the per-
component round trip and the Uuid primitive that scene IDs will use.

engine/utils/Uuid:
- 128-bit value with default-constructed nil sentinel, RFC 4122 v4
  generator, canonical 36-char parse/toString and a std::hash spec.
- Random source: BCryptGenRandom on Windows (cryptographic), with a
  std::random_device fallback for the platforms we have not yet
  specialised. The fallback path is documented and intentionally
  best-effort.

engine/reflection/JsonInspector:
- JsonWriter and JsonReader implement the Inspector interface and walk
  the same callback sequence the ImGui editor uses. JsonReader is
  deliberately tolerant of missing fields and type mismatches: an
  unknown field is left at its current value, so a component schema
  can grow without breaking saved scenes.
- enumField persists option NAMES, not their numeric indices, so
  reordering or inserting enum values in C++ does not silently change
  the meaning of saved data.
- Free functions serializeComponent / deserializeComponent provide the
  one-line round-trip path callers want.

samples/main.cpp:
- Reflection panel grows a Serialize / Load section with a multiline
  text area showing the JSON, a Generate UUID button to demo the new
  type, and a status string for parse errors.
- An in-process startup self-test round-trips a Camera (fov, near, far,
  clear color) through JsonWriter and JsonReader and surfaces the
  result in the smoke-test panel ("PASS (scene.Camera round-trip)"),
  so a regression in serialization is visible immediately even without
  the Reflection panel open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ouch input, platform CSPRNGs

The previous three commits added the ImGui integration, the reflection
TypeRegistry / Inspector, and the Uuid + JsonInspector primitives, but
hooked the new sources only into engine/libfluxion.vcxproj. That broke
every non-Windows build (Linux make, macOS Xcode/CLI, iOS, tvOS,
Android NDK, Emscripten) because the new translation units were never
compiled into libouzel.a / libfluxion.

This commit closes that gap on every build system we own.

engine/Makefile (Linux / macOS CLI / Emscripten):
- Add ../external/imgui to all four FLAGS variables so the imgui
  headers are findable from C, C++, Objective-C, and Objective-C++.
- New cross-platform sources land in the main SOURCES list:
  gui/imgui/ImGui{Backend,BackendD3D11,BackendFactory,BackendOGL,
  Context,InputBridge,Inspector,KeyMap}, reflection/{JsonInspector,
  TypeRegistry}, utils/Uuid, plus the imgui core .cpps and the
  cross-platform imgui_impl_opengl3 backend. The D3D11 / Metal
  backend wrappers carry their own #if FLUXION_COMPILE_* guards, so
  putting them in the cross-platform list compiles to empty TUs on
  the wrong platform.
- Windows section adds external/imgui/backends/imgui_impl_dx11.cpp
  (this one has no internal guard and would fail to find d3d11.h
  elsewhere).
- macOS / iOS / tvOS sections add gui/imgui/ImGuiBackendMetal.mm and
  external/imgui/backends/imgui_impl_metal.mm. ObjC++ is not
  available on Linux/Windows toolchains, so these can't live in
  cross-platform SOURCES.

engine/jni/Android.mk (Android NDK ndkBuild):
- Same source-list and include-path changes, mirrored into the NDK
  build (LOCAL_C_INCLUDES + LOCAL_SRC_FILES).

engine/libouzel.xcodeproj NOT updated — .pbxproj uses internal UUID
references that hand-editing tends to corrupt. Documented in the
implementation plan that Xcode-side updates should go through Xcode UI
or the xcodeproj Ruby gem; macOS CLI builds keep working through the
Makefile path.

engine/utils/Uuid.cpp — strengthen the random source per platform so
we are not relying on std::random_device's implementation quality:
- macOS / iOS / tvOS: arc4random_buf (libSystem, cannot fail)
- Linux: getrandom() syscall first, /dev/urandom fallback
- Android: /dev/urandom (always present on supported NDK levels)
- Emscripten: crypto.getRandomValues via EM_ASM (browser CSPRNG)
- Windows: BCryptGenRandom (unchanged)
- std::random_device remains the very last fallback for any platform
  we have not specialised, with a comment about its known-good
  implementations on the standard libraries we ship against.

engine/gui/imgui/ImGuiInputBridge — add a touchHandler so the editor
UI is operable on iOS / Android / touchscreen-Emscripten without a
mouse. The primary touch (first finger down) is forwarded as a left-
mouse drag (touchBegin -> mouseMove + button-down, touchMove ->
mouseMove, touchEnd / touchCancel -> button-up). Secondary fingers
are ignored; multi-touch gestures are out of scope for this iteration
but can be added later by feeding ImGui::AddMouseSourceEvent /
viewport touch state.

Verified: Windows build still passes with zero new warnings or errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3.5 closes the loop on the editor's persistence story: a Scene
can now be written to disk and read back identically on every platform
Fluxion supports. The serializer reuses the JsonReader / JsonWriter
Inspectors from phase 3, so adding a property to a Component
automatically flows through to the on-disk format with no extra work.

engine/scene/Actor:
- Lazy Uuid identity (mutable, generated on first getId() call) plus
  setId() for the deserializer. Keeps the syscall cost off any actor
  that the editor never touches.
- Editor-friendly name (std::string), with no runtime semantics.
- using ActorContainer::addChild brings the unique_ptr-taking overload
  back into scope; the reference-taking override hid it via name lookup
  and the serializer needs the owning version.

engine/scene/Layer: same `using ActorContainer::addChild` fix.

engine/scene/SceneSerializer:
- Versioned JSON schema (fluxion_scene_version: 1). Unknown major
  versions throw SerializerError instead of guessing.
- Forwards-compat: missing fields keep their constructor defaults,
  newer-than-known fields are ignored, unknown component types log
  a warning and are skipped (the rest of the scene still loads).
- Layers are not constructed from JSON because they own engine-bound
  state (cameras, render-pass setup); the loader populates the
  Scene's existing layers in order. This keeps the serializer free
  of graphics-device coupling and makes the API testable in
  headless contexts.
- Free helpers for the four levels (componentToJson / actorToJson /
  layerToJson / sceneToJson) plus the file-level saveToFile /
  loadFromFile, all going through the cross-platform AtomicFile.

engine/storage/AtomicFile:
- Atomic write strategy: write to "<path>.tmp", flush to disk, then
  rename. Crash-safety guarantees the destination either keeps the
  old contents or gets the new ones, never a half-written file.
- Windows: CreateFileW + WriteFile + FlushFileBuffers + MoveFileExW
  with MOVEFILE_REPLACE_EXISTING and MOVEFILE_WRITE_THROUGH.
- POSIX: open + write loop + fsync + rename. EINTR is retried in
  the write loop. fsync is skipped on Emscripten because the
  in-process VFS does not implement it.
- macOS note: fsync(2) on Apple does not flush all the way to
  physical media (that needs F_FULLFSYNC); we accept the standard
  POSIX guarantee in exchange for keeping the same code shape on
  every POSIX target.
- iOS / tvOS / Android sandbox notes captured in the header so
  callers know which directories are writable.

samples/main.cpp:
- Startup runs runSceneRoundTrip(): builds a tiny scene with two
  actors and one nested child, serializes to JSON and parses it
  back into a fresh Scene, then asserts actor count, IDs, names,
  position and opacity survive. Result surfaces in the smoke-test
  panel as "Scene round-trip self-test: PASS (...)".
- Reflection panel grows "Save test scene" / "Load test scene"
  buttons that exercise the on-disk path (storage::AtomicFile)
  with a known-good two-actor scene, displaying the round-tripped
  position to the user as proof.

Build files:
- engine/libfluxion.vcxproj: SceneSerializer.cpp + AtomicFile.cpp
  added (ClCompile + ClInclude).
- engine/Makefile: same files added to the cross-platform SOURCES
  list — they compile on Linux / macOS CLI / Emscripten too.
- engine/jni/Android.mk: same, mirrored into the NDK source list
  so Android builds pick up the new code.
- engine/libouzel.xcodeproj NOT touched (UUID-keyed pbxproj would
  corrupt under hand-editing); use Xcode UI or the xcodeproj Ruby
  gem to wire the new files into the Xcode build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… only

Setup commit before phase 4. The editor was previously hosted inside
samples/main.cpp + samples/ImGuiSample for fast iteration; now it lives
in its own editor/ directory with its own .vcxproj / Makefile, builds
to editor.exe alongside samples.exe, and the samples binary is back to
demonstrating only engine-level features (ImGui integration smoke test,
text-rendering reference, the stock ImGui demo). Reflection / save-load
demos move into the editor where they belong.

Architecture:
- editor/Panel: tiny base class for ImGui panels (atomic visibility flag,
  display name). Each panel owns its layout and draws inside Begin/End.
- editor/EditorApp: Application subclass that owns the ImGui Context,
  runs the reflection + scene round-trip self-tests on startup, and
  registers the initial panel set. Phase 5+ panels (SceneTree, Inspector,
  AssetBrowser, LogPanel) plug into the same panels_ vector.
- editor/main.cpp: fluxion::main returning EditorApp. Same shape as
  samples/main.cpp, deliberately minimal so the cross-platform engine
  entry (WinMain on Windows, SystemMacOS::main on Apple, etc.) needs
  no editor-specific changes.
- editor/panels/InfoPanel: top-of-window status panel (renderer name,
  display, frame stats, WantCapture, self-test results). Replaces the
  samples smoke-test reflection lines.
- editor/panels/ReflectionPanel: TypeRegistry list, live ImGui
  Inspector for the chosen component type, JSON serialize/load round
  trip, AtomicFile-backed Save/Load test scene. All the editor-y bits
  that used to live in samples/main.cpp.

Build:
- editor/editor.vcxproj: Application target on x86/x64 Debug+Release;
  shares libfluxion via ProjectReference; SubSystem Windows; same
  IncludePath layout as samples (engine + external/imgui).
- editor/windows/editor.rc: VS_VERSION_INFO resource. Required because
  Engine reads its own exe's file-version metadata at startup; the
  matching samples.rc has carried this for years and was the missing
  piece (without it the engine throws "Failed to get file version size"
  before fluxion::main is reached).
- editor/Makefile: Linux + macOS CLI build. Mobile / web are not
  editor targets; the documentation already records this.
- samples/samples.sln: editor.vcxproj added so a single MSBuild
  invocation builds samples.exe and editor.exe together.

Trimmed samples:
- samples/main.cpp loses drawReflectionWindow + the in-process
  reflection / scene self-tests; smoke-test panel keeps the runtime
  diagnostics that match the engine-side ImGui demo it represents.
- samples/ImGuiSample loses the "Reflection: on/off" toggle button
  (and the matching showReflection flag in ImGuiHost), since that
  panel no longer lives in the samples binary.

Verification: editor.exe boots, both startup self-tests log PASS
("scene.Camera round-trip" and "2 actors + 1 nested, ids preserved"),
ImGui panels render. Samples.exe still runs identically (regression-
free): the four engine-level ImGui demos and all original game samples
(sprites, gui, animations, input, sound, render-target, perspective).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p UI

Phase 4. The engine grows two atomic primitives that let an external
host (the editor, or a future debugging tool) freeze and single-step
the simulation without intercepting the update thread directly. The
editor wires them up to a Play / Pause / Resume / Step / Stop button
strip with a live time-scale slider, plus snapshot/restore around
play sessions so Stop reverts every actor's state to the moment Play
was pressed.

engine/core/Engine.{hpp,cpp}:
- setTimeScale(float) / getTimeScale(): atomic multiplier applied to
  the per-frame UpdateEvent delta. 1.0 = real time (default), 0.0 =
  frozen simulation but live render (the editor's "Edit" and "Pause"
  states), 0.5 / 2.0 / etc = slow-mo / fast-forward.
- requestStep(int n) / getPendingSteps(): atomic counter that, on
  each engine tick, takes precedence over timeScale and delivers
  exactly one real-time delta per pending step. The compare-exchange
  loop in update() guarantees the counter never decrements past zero
  even if the editor and the engine race on the same tick.
- Both primitives live in libfluxion and therefore work on every
  Fluxion-supported platform; the desktop-only editor is just the
  first consumer. A future runtime "instant replay" or "bullet time"
  feature can reuse the same APIs from a game-side scene.

editor/panels/PlayControlPanel.{hpp,cpp}:
- State machine: Editing (timeScale=0, snapshot off) -> Playing
  (timeScale=slider, snapshot taken) -> Paused (timeScale=0, snapshot
  retained, Step works) -> back to Editing on Stop (snapshot restored).
- Snapshot is a json::Value taken via SceneSerializer::sceneToJson,
  not the encoded string, so restore round-trip avoids float decode
  drift. Restore goes through sceneFromJson onto the same Layer.
- Buttons grey out when their transition is invalid for the current
  state (BeginDisabled/EndDisabled), so the layout never jumps.
- Time slider 0..4x is interactive only while Playing; in Paused the
  slider value is preserved but the actual scale is 0 until Resume.

editor/EditorApp:
- buildTestScene(): creates a deterministic 2-actor placeholder scene
  (Player at origin, Enemy at (10,0,0)) so the PlayControlPanel has
  something concrete to snapshot/restore. Phase 5 will replace this
  with a load-from-disk scene.
- runPlayControlSelfTest(): startup verification of the
  snapshot/restore semantics. Mutates an actor's position, restores
  from the captured json::Value, asserts the original position came
  back; cleans up the duplicates the loader appends so the panel
  sees the original scene state.

Cross-platform note (per recent confirmation): the editor app itself
ships only for Windows / macOS / Linux; mobile/web are not editor
targets. The engine-side time-scale and step APIs build and run on
every platform regardless.

Verified: full samples.sln build succeeds (libfluxion + samples +
editor + fluxion tool, zero new warnings). editor.exe boots, all three
self-tests log PASS (reflection, scene round-trip, play-control
snapshot/restore).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The editor was previously buildable only through samples/samples.sln
(which I added it to in the editor-split commit) or via its own
editor/Makefile. Anyone running the canonical "build the project"
incantation from the repo root — `msbuild fluxion.sln` on Windows or
`make` on Linux/macOS — got libfluxion + the fluxion shader tool but
no editor. This closes that gap.

fluxion.sln (root):
- editor/editor.vcxproj added with the same {8E7C0B11-...-6F70} GUID
  used in samples/samples.sln, so both solutions reference the
  identical project file.
- ProjectDependencies declares libfluxion as a build-order
  dependency, matching how samples.sln wires the tool dependency.
- Per-config rows added for Debug/Release on Win32/x64.

Makefile (root):
- New editor target depends on engine and recurses into editor/.
- Skipped on PLATFORM=ios / tvos / android / emscripten with an
  echo so the same `make` invocation still does the right thing on
  mobile / web (the editor is desktop-only by design); on Windows /
  Linux / macOS it builds editor.exe alongside engine and tools.
- `clean` cleans the editor too (with leading '-' so a missing
  editor target doesn't fail the parent clean).

Verified: msbuild fluxion.sln /p:Configuration=Debug /p:Platform=x64
builds libfluxion + fluxion + editor.exe end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes MSB8020 ("The build tools for Visual Studio 2017 (Platform
Toolset = 'v141') cannot be found") for users on a stock VS 2022
installation that does not include the legacy v141 toolset.

The repo's build flow already used VS 2022 — we have been passing
/p:PlatformToolset=v143 on every msbuild invocation in CI. Updating
the .vcxproj defaults removes the override requirement so the
canonical "open the .sln in Visual Studio and hit Build" path works
without first prompting the user to retarget. Affected projects:

- editor/editor.vcxproj      — the project that surfaced the issue;
                               new since the editor split commit and
                               had not been retargeted in any user's
                               local VS solution state yet.
- engine/libfluxion.vcxproj  — engine library; depended on by all
                               three executables.
- tools/fluxion.vcxproj      — shader/asset processor.
- samples/samples.vcxproj    — game samples binary.

Verified with both fluxion.sln (root) and samples/samples.sln using
plain `msbuild ... /p:Configuration=Debug /p:Platform=x64` without
any /p:PlatformToolset override; libfluxion + tool + editor + samples
all build clean.

Note: the libouzel.xcodeproj on Apple platforms uses Xcode's own
toolset selection and is unaffected by this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs surfaced together when launching the editor:

1. Window title showed "sample" because FLUXION_APPLICATION_NAME is a
   compile-time constant baked into libfluxion; the editor shares the
   same library as the samples binary, so it inherited that name. Fixed
   by calling engine->getWindow().setTitle("Fluxion Editor") in the
   editor constructor.

2. The window rendered fully white because no Scene was registered with
   the SceneManager. With no active scene, Scene::draw is never called,
   Graphics::present is never queued, and the render device's
   preSwapHook (where ImGui draws) never fires. Fixed by handing the
   editor's working scene to SceneManager during startup.

Also fixes a related bug in scene::serialization::layerFromJson: it was
appending actors instead of replacing them, which caused the editor's
Play -> Stop cycle to double the actor count every round. The Play-
control self-test was previously written around that bug; with the
serializer now correct, the self-test simplifies to "actor count and
position match the pre-snapshot state".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a Camera with clearColorBuffer=true in the working layer, the
back buffer was never cleared between frames; ImGui drew its panels on
top of the previous frame's content, smearing every window outline
into a stack.

The camera is held by value as an EditorApp member alongside its host
actor so the editor controls its lifetime and doesn't have to recreate
it on every Play/Stop snapshot. Added at the end of the layer so the
existing play-control self-test (which uses kids[0] for Player) keeps
its indexing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the first real editor interaction surface: pick an actor in the
hierarchy, edit its live data in an inspector. Three new pieces:

* SelectionContext (editor/SelectionContext.{hpp,cpp})
  Single-actor selection state shared between panels via
  pointer-to-EditorApp-member. Listener pattern for future panels that
  want to react. validateAgainst(scene) drops the selection if the
  pointed-to actor is no longer reachable from any layer, so a
  removal that didn't go through the editor's own remove path can't
  leave a dangling pointer.

* SceneTreePanel (editor/panels/SceneTreePanel.{hpp,cpp})
  Layers as collapsing headers, actors as recursive ImGui tree nodes.
  Click to select; the selected row picks up
  ImGuiTreeNodeFlags_Selected so the inspector + tree agree visually.
  Component count badge ("[Nc]") next to actors with components, so
  the user can see at a glance which entries carry behaviour.

* InspectorPanel (editor/panels/InspectorPanel.{hpp,cpp})
  Replaces the old ReflectionPanel. Edits the *actual* selected
  Actor, not a TypeRegistry-spawned temporary. Renders:
    - read-only UUID
    - editable name
    - transform (position, rotation as Euler degrees, scale)
    - opacity / hidden / pickable flags
    - per-component CollapsingHeader that calls Component::inspect
      via gui::imgui::Inspector — so anything that overrides inspect()
      shows up automatically with no panel-side wiring.

The old ReflectionPanel is removed: its purpose (proving reflection +
JSON round-trip + storage::AtomicFile work) is now covered by the
startup self-tests surfaced through the InfoPanel. Disk save/load
will return as a proper File menu in a later phase.

Cross-platform: vcxproj and Makefile both updated; no platform-
specific code in the new panels (everything is ImGui + reflection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed help and license sections from README.
The editor now lets the user click an actor in the back-buffer area
to select it, with screen-space AABB outlines that show where every
actor is. New pieces:

* EditorPicking (editor/EditorPicking.{hpp,cpp})
  Pure editor-side helpers: pickActor() projects each actor's bounding
  box (or a fallback unit cube for component-less actors) into
  normalized window space and returns the topmost hit;
  drawAabbOverlay() renders the same boxes as ImGui background
  drawlist lines plus a name label, with the selected actor in a
  brighter highlight. Background draw list keeps overlay anchored to
  the world while ImGui windows render on top of it.

* Click-to-pick wiring in EditorApp
  A held EventHandler subscribes to mousePress; when ImGui doesn't
  want the mouse (the click landed outside any panel) the picker runs
  against the working scene and updates the shared SelectionContext.
  Already-existing tree + inspector panels follow the new selection
  with no further wiring.

* Editor-internal layer split
  Adds editorLayer_ alongside workingLayer_. The editor camera moves
  off the user-facing layer so it never appears in the Scene tree,
  never gets a visible AABB outline, and is never returned by
  click-to-pick. Both panels and EditorPicking take an optional
  excludeLayer parameter so the editor only filters its own internal
  layer; multi-user-layer scenes are unaffected.

Why pick on the editor side instead of using Scene::pickActor:
  The engine's pickActor only matches actors whose components have
  bounding boxes (Actor::pointOn iterates components). Bare actors —
  the ones the user creates and then attaches behaviour to — would be
  invisible AND unpickable. The editor falls back to a unit cube
  centred on each actor's local origin so every actor in the
  hierarchy has a deterministic, transform-aware click target,
  without polluting the runtime engine with editor-only pick shapes.

Cross-platform: no engine changes; ImGui's draw list and the existing
Camera::convertWorldToNormalized do all the heavy lifting on every
backend Fluxion supports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a per-backend method that converts a graphics::Texture into the
renderer-native ImTextureID payload, packed into a uint64_t so the
public interface stays platform-agnostic:

  * Direct3D 11 — ID3D11ShaderResourceView* (cast from pointer)
  * OpenGL      — GLuint texture name (zero-extended)
  * Metal       — id<MTLTexture> (the raw pointer the existing
                  MetalRenderTarget plumbing already stores as void*)

Returns 0 when the texture has no GPU resource yet; callers must
check before passing the value to ImGui::Image (zero is undefined
behaviour for ImGui).

Foundation for the editor's upcoming ViewportPanel which will create
a render-target Texture, point a viewport camera at it, and display
the result with ImGui::Image inside a dockable panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the embedded scene viewport: a panel that owns its own camera,
color + depth render-target textures, and shows the scene as an
ImGui::Image inside a dockable window. Uses the texture-handle
accessor introduced in 5b-2.a to bridge the per-renderer GPU handle
through to ImTextureID.

KNOWN ISSUE: the editor aborts with a Microsoft VC++ Runtime dialog
during EditorApp construction once the ViewportPanel is wired in.
RTSample's near-identical Texture + RenderTarget setup runs cleanly,
so the bug is somewhere in how the panel introduces a second camera
into the working scene. To diagnose, build in Visual Studio and run
under the debugger — the abort surfaces with an actionable stack.

Bring-up plan once the stack is captured:
  1. Identify the failing call (likely inside D3D11 backend during
     the first frame's clear-RT pass).
  2. Either gate the camera/RT introduction until the next frame
     boundary, or fix the underlying initialization race.
  3. Re-enable click-to-pick within the viewport rect (this is
     5b-2.c, deferred until the panel renders cleanly).

Other pieces in this commit, all complete and independently testable:
  * pickActor / drawAabbOverlay / SceneTreePanel now take an
    optional excludeActor parameter; EditorApp passes the viewport
    camera-actor pointer so it stays out of the user-facing surfaces.
  * ViewportPanel raw pointer stashed in EditorApp so the exclude
    threading can resolve to a stable address.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kisstp2006
Copy link
Copy Markdown
Author

asd

kisstp2006 and others added 9 commits May 8, 2026 20:19
The per-backend getResource<T>(id) helpers indexed `resources[id-1]`
unconditionally. Internal callers always invoke them with ids whose
Init command has already been processed, so out-of-range never
showed up. The new ImGui texture-handle accessor (introduced in the
ViewportPanel work) crosses thread boundaries: the editor allocates
a Texture's resource id on the host thread, the InitTextureCommand
is queued, and on the very next frame the render thread fires the
preSwap hook which calls our textureNativeHandle. If the host thread
hasn't flushed yet the queued Init command lands in a later buffer,
so resources.size() trails the requested id by one — and the old
indexed access ran straight off the end of the vector, producing the
debug-CRT abort observed during 5b-2.b bring-up.

Returning null for "id beyond current resources" preserves existing
internal usage (those callers always pass valid ids) and lets the
imgui handle accessor degrade cleanly: textureNativeHandle returns
0, ViewportPanel::draw skips ImGui::Image, and the next frame the
texture exists and rendering proceeds normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The viewport panel now owns the entire scene-interaction surface:
clicks land on the embedded ImGui::Image, and the actor outlines
draw on top of that same image. The back-buffer area is just clear
colour now — no editor decorations bleed onto it.

Concrete moves:

* drawAabbOverlay (editor/EditorPicking.{hpp,cpp})
  Now takes an explicit screen-pixel rect (rectMin/rectMax) and
  renders into the current ImGui window's draw list with a clip-rect
  pinned to that rect. Projection is rect-relative instead of
  display-relative, so the same code naturally tracks panel resizes
  and DPI scaling.

* ViewportPanel
  After ImGui::Image, captures GetItemRectMin/Max:
    - On left click via IsItemClicked, converts mouse pos to image-
      local normalized [0,1] and runs pickActor() against the
      viewport's own camera (not the back-buffer-clear camera).
    - Calls drawAabbOverlay with the same rect so outlines and click
      targets line up to the pixel.
  Excludes its own camera-actor from picking + overlay so the user
  never selects or sees a frame around it.

* EditorApp
  Drops the back-buffer EventDispatcher pickHandler_ and the post-
  panel drawAabbOverlay call — both jobs now belong to the panel.
  The pick + overlay path no longer touches editorCamera_ at all;
  that camera's only job is clearing the back buffer behind panels.

Cross-platform: only ImGui APIs (IsItemClicked, GetItemRectMin/Max,
GetMousePos, GetWindowDrawList, PushClipRect) plus the existing
camera/scene math — runs identically on D3D11, OpenGL, and Metal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the first transform manipulator: three colour-coded axis lines
drawn at the selected actor's pivot, click-and-drag to translate
along that axis. Hover highlights the axis under the cursor; the
white centre dot marks the pivot.

Math summary: project actor origin + each axis end through the
viewport camera into image-space pixels; on drag, project the
mouse-pixel delta onto the chosen axis's screen vector and convert
back to world units. Works for both orthographic and perspective
cameras. Axes whose projected length collapses below 6 px (e.g. Z
in the default ortho view that looks down +Z) are auto-hidden so
the user can't aim at a degenerate handle.

The gizmo claims the click on grab-start so picking and gizmo-drag
don't fight: ViewportPanel runs Gizmo::update() before the pick
path and skips pickActor() when update() returns true.

Bonus, related to Tibor's report that the panel followed the gizmo
when grabbed: ImGui::Image is a passive widget — it draws, but it
does not register as a clickable item. Without an interactive item
under the click, ImGui's window-move logic treats the image area
as empty space and starts dragging the panel. Stacking an
InvisibleButton at the same rect makes ImGui see the click as
"on an item", so window-drag is suppressed. Picking + gizmo logic
keep working unchanged because they read mouse state via the global
ImGui::IsMouseClicked / GetMousePos APIs.

Cross-platform: pure ImGui draw + Camera math, runs identically on
D3D11 / OpenGL / Metal. Editor-only file (Gizmo.{hpp,cpp})
registered in editor.vcxproj and editor/Makefile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the rotate manipulator: three axis-coloured rings around the
selected actor's pivot, click-and-drag to rotate around the world
axis. Q switches the viewport gizmo to translate, W to rotate.

Math summary:
  * Each ring projects 48 points around its world-space circle (in
    the plane perpendicular to the axis) into image-space pixels;
    hit-testing walks closed-polyline segments so a click anywhere
    on the rim wins regardless of camera angle.
  * Drag tracks a screen-plane atan2 from the pivot to the cursor
    and accumulates the per-frame delta wrapped into (-pi, pi]. That
    handles both the natural +/-pi atan2 boundary and full circles
    around the pivot — drag once, drag twice, both work.
  * The accumulated angle becomes a quaternion via setRotation
    (axis-angle), pre-multiplied with the captured start rotation
    so the actor rotates in WORLD space. Local-space mode will land
    with the toolbar in 5c-4.

Rings whose projected bounding box collapses below 12 px in either
axis (typical for X/Y in the default ortho camera looking down +Z)
are skipped — the user can't aim at a degenerate ring, and they'd
fight the visible Z ring for hit-priority anyway.

Mode switch:
  * Gizmo grew a `Mode` enum and a `setMode` that cancels any
    in-flight drag. Translate logic moved into updateTranslate,
    rotate into updateRotate; update() dispatches.
  * ViewportPanel watches Q/W with ImGui::IsKeyPressed when the
    viewport window is focused, and prints a "Gizmo: <mode> (<key>)"
    line above the image so the active mode is obvious. 5c-4 will
    swap this for a clickable toolbar.

Cross-platform: pure ImGui draw + Camera/Quaternion math, runs
identically on D3D11/OpenGL/Metal. Editor-only files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the third manipulator: three axis lines terminated with small
square handles. Click on a line or its end-cap and drag to scale the
selected actor along that axis. E switches into scale mode alongside
the existing Q (translate) / W (rotate) shortcuts.

Math: project the cursor onto the axis's screen-direction at click
time and at every drag-frame. The ratio of current-projection to
start-projection multiplies the captured start-scale on that axis.
Pulling away from the pivot grows; sliding through the pivot to the
opposite side flips the sign — useful for mirroring and intentional.

The hot region matches translate's (axis-line within kHitThreshold)
so the muscle memory carries over; the visual cue swaps the arrow
for a 10x10 px box at the axis end so the user sees at a glance
which manipulator is active.

Cross-platform: pure ImGui draw + Camera math, identical on
D3D11/OpenGL/Metal. Editor-only changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on fixes

Replaces the Q/W/E text label above the viewport with a proper
toolbar: three mode buttons (Translate / Rotate / Scale) with the
active one tinted blue, and a Local/World button that switches the
gizmo's reference frame for the translate + rotate manipulators.
Keyboard shortcuts (Q/W/E) still work when the viewport window has
focus.

Local-space behaviour:
  * Translate handles point along the actor's rotated basis (right /
    up / forward of getRotation()) so dragging X moves along the
    actor's local X regardless of orientation.
  * Rotate rings tilt with the actor and the delta quaternion is
    post-multiplied (startRotation * delta) so the rotation happens
    in the local frame.
  * Scale always uses the local basis for visualisation — scale is
    component-wise on Vector3 setScale, so World-aligned scale
    handles would mismatch the numbers they edit. Tooltip on the
    Local/World button explains this.

Engine fixes (header-only, applies to every backend / platform):
  * math::rotatedVector dropped a stray `constexpr` on a runtime
    expression (`quat.v[0]`), which tripped C2131 when the template
    was finally instantiated by the gizmo.
  * math::getRightVector / getUpVector / getForwardVector / the
    Quaternion-times-Vector operator* called the in-place
    `rotateVector` (which returns void) and tried to use its return
    value. Swapped them to the by-value `rotatedVector`.
  These helpers were unused by the engine itself, so the bugs lay
  dormant until the editor wired them in.

KNOWN ISSUE: the editor crashes after some interaction with the new
toolbar (exit 139). Pushing this WIP so we can debug it under VS —
push pull this branch, run with the debugger, and the call stack
will pinpoint the offending code path. Likely candidates: the
local-mode rotate post-multiply, or the toolbar's PushStyleColor
path interacting with something downstream. Commit boundary chosen
so the math-fix part is separate from any future stability fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous WIP rotate apply used the screen-plane atan2 delta
unmodified. That happens to match the on-screen rotation direction
ONLY when the rotation axis points out of the screen — for the
default editor camera that is +Z viewer-side. The engine's editor
camera looks down +Z, which means +Z actually points INTO the
screen from the viewer; right-handed positive rotation around any
axis appears MIRRORED to the user, so dragging clockwise rotated
the actor counterclockwise on every ring.

Negate the accumulated angle once at quaternion-construction time
so cursor direction == visible rotation direction for X, Y, and Z
alike. Replaces the per-axis sign hack from the previous WIP — the
correction is global, not axis-dependent.

Caveat: this is the right answer for the current top-down ortho
camera convention. A free-perspective viewport (future phase) with
the camera looking from -Z would need the opposite sign; promoting
the convention to "viewer is on -axis side of screen" with a proper
screen-tangent measure will replace this when the viewport gains
camera navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the user build and tear down scenes from inside the editor.
Three additions, all editor-only, deferred-mutation:

* SceneTreePanel context menus
  Right-click on a layer header -> "Add Actor". Right-click on an
  actor row -> "Add Child Actor" / "Delete". The handler queues a
  PendingOp; the queue drains after the tree walk so we never mutate
  layer/actor children mid-iteration. Selection is cleared eagerly
  when the deleted actor was selected, then validateAgainst() runs
  again so panels drawn later in the same frame (Inspector) never
  see a dangling pointer.

* InspectorPanel component management
  An "X" button on each component header queues a remove. An
  "Add Component" button opens a popup driven by
  TypeRegistry::listTypes(), so anything registered via
  FLUXION_COMPONENT_REGISTER appears with no editor-side wiring.
  Both deferred until after the components vector iteration ends.

* Click-capture fix in the Scene tree
  TreeNodeEx submits an item; right after that, the row's
  IsItemClicked / IsItemToggledOpen reflect the row. Earlier this
  function then submitted a "[Nc]" badge with TextDisabled and a
  context menu, so by the time IsItemClicked() ran at the bottom it
  was reading the BADGE's state — and badges aren't interactive,
  so click-to-select silently failed for any actor that owned at
  least one component. Capturing nodeClicked/nodeToggled directly
  after TreeNodeEx restores correct row click handling regardless
  of how many sibling widgets follow on the row.

Cross-platform: editor-only, no engine changes; the engine APIs
(addChild / removeChild / addComponent / removeComponent /
TypeRegistry::create / listTypes) all already shipped on every
backend in earlier phases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kisstp2006 and others added 30 commits May 8, 2026 23:54
Add stable Uuid identity to loaded assets so editor scenes can
reference them across renames and rebuilds. Each Bundle now keeps a
reverse-index per resource type; setX accepts an optional Uuid (default
null = no binding), getX(Uuid) and getXId(name) round-trip both
directions. Loader typedef gained a Uuid parameter; Bundle::loadAsset
mints one when the caller doesn't supply one, so existing samples keep
working with no code change. AssetReference{ Uuid, Type } added to
Asset.hpp as the scene-file value type for phase 11b/c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Editor-side scanner + atomic JSON .meta sidecar writer. On startup
the editor walks samples/Resources, mints a v4 Uuid for every
unmetafied file, and writes a sibling <file>.meta with the id, the
sniffed Asset::Type (extension-driven, with a content peek for .json
to disambiguate sprite vs cue), and the import options. The sidecar
becomes the persistent identity store: a second run reads the
existing .meta and reuses the id, so scene references stay stable
across renames or rebuilds.

The new editor::AssetDatabase exposes lookup by id and by relative
path. AssetImportPipeline is a thin dispatcher over Bundle::loadAsset
that threads the .meta-stored Uuid through to the (phase 11a) Cache
reverse index, so cache.getX(id) starts working as soon as a project
asset is imported.

EditorApp adds the project root to FileSystem::resourcePaths so the
loader chain can resolve relative paths, and surfaces the scan stats
in InfoPanel (PASS scanned=N loaded=K created=M).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an Inspector::assetField virtual so a component can declare it
holds a reference to an AssetDatabase asset. Default impl is a
no-op, keeping every existing inspector subclass binary-compatible —
JsonWriter / JsonReader / ImGuiInspector each override it.

JsonInspector persists the reference as { "id": "<uuid>", "type":
"<typeName>" } using shared assets::typeName / parseTypeName helpers
(now in engine/assets/Asset.hpp, replacing the duplicated copies in
editor/asset/AssetDatabase.cpp). JsonReader is tolerant: missing
field, missing id, or unparseable id all leave the AssetReference
at its constructor default — same forwards-compat policy the rest
of JsonReader follows. So a scene saved before phase 11c keeps
loading, and a scene saved at 11c re-resolves the asset id at
load time (resolution UI ships in 11d).

ImGuiInspector renders a minimal "[type] uuid" / "(none, type)"
label as a placeholder; the editor-side rich receptor (drag-drop
target + name resolution via AssetDatabase) lands in phase 11d.

A new EditorApp self-test exercises the JsonWriter -> JsonReader
round-trip directly with a fabricated AssetReference and verifies
the id and type survive, plus the missing-field default path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pane folder browser (left: collapsible folder tree, right:
wrapping tile grid) backed by the AssetDatabase from phase 11b.
Image-type tiles render their texture via the engine's ImGui
context texture-handle accessor — same path ViewportPanel uses for
its render target — so the panel stays renderer-agnostic across
D3D11 / OpenGL / Metal. Other types fall back to a coloured tile +
single-letter glyph until per-type previews ship.

Each tile is the drag-source for a "FLUXION_ASSET_REF" payload that
serialises an AssetReference value. The Inspector receptor accepts
the same payload via a new editor::AssetAwareInspector subclass of
gui::imgui::Inspector that overrides assetField to render a button-
like drop zone showing the resolved asset's relative path. ImGui
delivers the payload only when the mouse releases over the button;
the inspector mutates the field live AND records the swap so the
panel can convert it into an undoable SetAssetCommand on the
existing CommandStack.

To make this possible:
  - gui::imgui::Inspector dropped its `final` so editor-side hosts
    can extend its vocabulary.
  - editor/commands/AssetCommands adds SetAssetCommand, identifying
    the target by (actor uuid, component index, field label) and
    re-applying via a SingleAssetFieldWriter that drives the
    component's inspect() with a tailored Inspector.
  - InspectorPanel's ctor now takes an optional AssetDatabase ptr
    so the receptor can resolve names; the panel pops the
    inspector's pendingDrop and pushes the command after each
    component's inspect() returns.
  - EditorApp drives AssetImportPipeline::importAll on startup so
    cache.getTexture(id) works for browser thumbnails. Failures
    are reported in the InfoPanel ("imported=N (M import-fail)").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Click an asset tile in the AssetBrowser to select it: the tile gets
a yellow outline and the Inspector switches from actor-properties
view to a read-only .meta panel showing the resolved name, the
canonical type string, the project-relative path, the importable
flag, and the Asset::Options.mipmaps. Image-type selections also
get a square preview rendered through the engine's ImGui texture-
handle accessor (same path the AssetBrowser tiles + ViewportPanel
use, so the preview works on every backend).

SelectionContext gains an asset channel alongside the existing
actor channel; the two are kept mutually exclusive at the setter
boundary so the Inspector branches on a single hasAssetSelection()
check. Setting an actor implicitly clears any asset selection, and
vice versa, so tree-row clicks and tile clicks toggle the inspector
view without leaving stale highlights.

AssetBrowserPanel restructures tile rendering: the backdrop, image
or fallback glyph, and selection outline now go through the window
draw list, while a stacked InvisibleButton owns click + drag-source
semantics — uniform across image and non-image tiles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the foundation for Unity-style 2D draw ordering. Renderer
components carry a sortingLayer name + an orderInLayer int (defaults
"Default" / 0); the project-wide layer list lives in a tiny
fluxion::scene::sortingLayers namespace with default
{Background, Default, Foreground, UI}. indexOf() resolves a name to
its position with a Default fallback so scenes saved with future
layer names still load in older binaries.

Component grows the two fields plus a protected inspectSorting()
helper that subclasses' inspect() override can call to surface the
controls — kept opt-in because non-renderer components (Animator,
Camera, Light) don't care about z-order.

This commit lands the data + serialization surface only. Wiring the
draw queue's sort to use sortingLayers::indexOf and exposing the
fields in SpriteRenderer's inspector ships in 12b. Samples keep
their existing draw order because every renderer defaults to
"Default" / 0, which sorts as a no-op against today's queue.

Build: registered in libfluxion.vcxproj, engine/Makefile, and
engine/jni/Android.mk (D3D11 / OpenGL / Metal / Android share the
same translation unit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SpriteRenderer grows an AssetReference for its sprite/texture and a
setSpriteReference() that re-initialises the renderer from
cache.getSpriteData(id) (or, as a fallback for raw images,
cache.getTexture(id)). The reference round-trips through the scene
file via the JsonInspector path landed in 11c — a saved scene
re-resolves the sprite by uuid on load.

inspect() exposes the reference as an Inspector::assetField so the
editor's AssetAwareInspector renders it as a drag-drop receptor.
On drop the editor's InspectorPanel pushes a SetAssetCommand
(phase 11d) onto the CommandStack, so the assignment is undoable.
inspect() also calls Component::inspectSorting() (phase 12a) to
surface Sorting Layer + Order in Layer.

Layer::draw stable-sorts the draw queue by (sortingLayerIndex,
orderInLayer) using each actor's first non-default-keyed component
— actors with all-defaulted renderers keep their previous visit
order, so existing samples render unchanged. Indices come from
sortingLayers::indexOf so future renames in the project's layer
list propagate without code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The press that begins a drag from an AssetBrowser tile also selects
the asset, which (correctly) swaps the InspectorPanel to its .meta
view. That fired BEFORE the drag began, so the SpriteRenderer's
receptor was already off-screen by the time the user moved the mouse
toward it.

Fix: keep the asset-selection-driven view suppressed while a drag is
in flight. ImGui::GetDragDropPayload() returns non-null only between
BeginDragDropSource (drag started) and the drop or cancel — exactly
the window where the inspector should keep showing the actor's
components so the drop receptor remains a target. After the drop
the inspector flips back to .meta view if the asset is still
selected, which is the right resting state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selecting on press cleared any actor selection the moment the user
clicked a tile — before they had a chance to start dragging. By
the time the drag began, the SpriteRenderer drop receptor was
already gone, and the in-flight override could not bring it back
because the actor pointer had already been wiped by
SelectionContext::setSelectedAsset.

Move the selection trigger to (IsItemHovered + IsMouseReleased).
A pure click + release fires it as before. A drag press leaves
the selection alone, because ImGui clears IsItemHovered on the
drag source while the drag is active — so even if the user
releases the button somewhere else, the source tile's hover state
is false and no selection fires. End-to-end: actor stays selected
through the drag, the receptor stays visible, the drop succeeds.

The drag-source block now precedes the click test so ImGui
recognises the hover-suppression in time for the same frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace external/imgui core sources (imgui.{h,cpp},
imgui_internal.h, imgui_demo/draw/tables/widgets.cpp, imstb_*.h)
with the corresponding files from ocornut/imgui's docking branch.
Master 1.92.6 WIP -> docking 1.92.8 WIP. The branches share
identical end-user APIs for everything we currently use; the
docking branch is strictly additive (extra tags IMGUI_HAS_DOCK,
IMGUI_HAS_VIEWPORT and the DockSpace family).

Cross-platform: the change touches only header-only / .cpp ImGui
sources that compile identically on every supported platform
(Windows / macOS / Linux / iOS / tvOS / Android / Emscripten).
Fluxion ships its own ImGui backends (engine/gui/imgui/ImGuiBackend
{D3D11,OGL,Metal}.{cpp,mm}) on top of the engine's render device,
not the official imgui_impl_*; the backend interface only consumes
ImDrawList draw commands, which docking emits in the same vertex /
index / texture-cmd format as before. So all renderers (D3D11,
OpenGL 2/3/4, OpenGL ES 2/3, Metal) keep working unmodified.

Multi-viewport (separate OS windows for floating panels) is
deliberately NOT enabled — that mode needs platform-backend code
for creating new native windows that we don't ship for mobile/web.
Intra-window docking is enough for the editor's UX and doesn't
need any backend support.

Editor wiring:
  - EditorApp ctor: set ImGuiConfigFlags_DockingEnable so panel
    title-bars become draggable into a dockspace.
  - imgui_.setDrawCallback: call DockSpaceOverViewport(0, nullptr,
    PassthruCentralNode) at the top of every frame. The fullscreen
    invisible host window covers the main viewport; the central
    node stays transparent so the engine's back-buffer clear
    (editor camera) shows through wherever no panel is docked.

Layout persists to imgui.ini in the working directory between
runs, so the user's arrangement survives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop a sprite or image asset from the AssetBrowser onto the
viewport's empty area to spawn a new actor at the cursor's world
position with a SpriteRenderer already pointed at the dropped
asset. Single CommandStack step — Ctrl+Z removes the actor and
Ctrl+Y re-creates it (the unique_ptr alternates between command
and layer, mirroring AddActorCommand's pattern).

ViewportPanel grows a BeginDragDropTarget block over the
InvisibleButton stack. Accepts the same FLUXION_ASSET_REF payload
the Inspector receptor consumes (24 bytes: Uuid + Asset::Type).
The drop position is converted from screen-space to world via
camera_.convertNormalizedToWorld using the panel's content rect
(cursorBeforeImage + avail), so panning / zooming stays consistent
with where the actor lands.

CreateActorWithSpriteCommand lives in editor/commands/AssetCommands
alongside SetAssetCommand. Captures the layer pointer, the
spriteRef, the world position, and a name. Builds the Actor +
SpriteRenderer at construction; redo() hands it to the layer,
undo() takes it back via releaseChild. Selection follows the new
actor on redo so the Inspector immediately shows the SpriteRenderer
for tweaks; the selection clears on undo to match AddActor's
pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The viewport's render target is fixed at 1024x1024 (square) and
ImGui::Image stretches it to whatever rectangular panel size the
user docks the viewport into. With showAll the camera kept world
aspect intact within the RT (letterboxing as needed), but the
subsequent RT-to-panel stretch then re-multiplied the panel's
aspect on top — a unit world square ended up panelAspect:1 on
screen (visible as horizontally-stretched sprites in wide panels).

exactFit anisotropically fills the RT regardless of world aspect,
so the per-axis stretch is exactly inverse to the panel stretch
when we set targetContentSize = (panelAspect * H, H) every frame.
World 1:1 sprites now appear 1:1 on the panel for any panel shape.

Confirmed visually: dropped sprite tiles render at their authored
aspect even when the viewport is docked as a wide rectangle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
exactFit clamps contentScale to (1,1) and lets contentSize fall
back to renderViewport.size — the projection covers a fixed
1024x1024 world rect regardless of targetContentSize. Result:
sprites rendered at correct aspect (the RT-to-panel stretch undid
the anisotropic ortho projection), but the wheel zoom path changed
contentHeight_ / targetContentSize for nothing because exactFit
ignores both.

noScale keeps contentScale = renderViewport.size /
targetContentSize and contentSize = targetContentSize, so the
orthographic projection covers exactly the (panelAspect * H, H)
world rect we set every frame. Zoom mutates H, the projection
follows. The RT-to-panel stretch still cancels the projection's
panelAspect anisotropy, so sprites stay aspect-correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-click an image in the Asset Browser, pick "Open in Sprite
Editor", set columns x rows, hit Apply Grid + Save: every cell
becomes a sub-sprite with its own Uuid, draggable into a
SpriteRenderer.Sprite receptor on its own. Covers the typical
sprite-sheet workflow without needing a TexturePacker JSON.

Storage: the parent's .meta sidecar gains optional sprite_mode
("single" | "sliced"), grid {columns, rows}, and a slices array of
{id, name, rect}. Slice ids are minted once and preserved across
re-applies (matched by index), so already-saved scene refs survive
when the user tweaks the grid.

AssetDatabase rescan emits a sub-AssetEntry per slice: type=sprite,
parentId=<image>, sliceRectPx=<cell rect in parent texture pixels>,
relativePath="<parent>#<sliceName>". AssetImportPipeline learns a
two-pass importAll (parents first, then slices) plus a new
importSlicesFor(parentId) so the SpriteEditor can re-register only
the slices it just changed instead of re-running the whole pipeline.

Slice import builds a single-frame SpriteData using the parent
texture + slice rect (no extra disk read — the parent texture is
already in the Cache). cache.getSpriteData(sliceId) resolves it,
so SpriteRenderer::setSpriteReference picks up the slice via the
existing 11a→12b drag-drop path.

UI bits:
  - SpriteEditorPanel: mode dropdown, columns/rows DragInts, Apply
    Grid + Save buttons, texture preview with cell-line overlay,
    compact slice list (count + first few names).
  - AssetBrowserPanel: per-tile right-click context menu with
    "Open in Sprite Editor" (single-image parents only). Sub-slice
    tiles render their own thumbnail by sampling a UV sub-rect of
    the parent texture, so the browser shows each cell separately.
  - EditorApp wires the new panel + the back-pointer the browser
    needs for its context menu, sharing the project bundle.

The interactive freeform editor (Multiple mode with click-drag
rectangles + naming) is a follow-up; the .meta schema already
allows it (slices is just a list — Multiple mode would populate
the same field with arbitrary rects instead of a uniform grid).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Inspector::actionField virtual (returns true on the frame the
user clicks; default false so JsonInspector and silent inspectors
ignore it — buttons are pure UI affordance, not state). ImGui's
inspector implements it as ImGui::Button.

Animator base class grows a non-virtual default inspect() that
exposes the controls every animator shares: Hidden, Length, Play,
Stop, Reset, and a read-only state line (running / done / idle
plus current time). Move / Rotate / Scale / Fade now call
Animator::inspect first and only emit their type-specific fields
afterward — drops 4 copies of the same boilerplate from
Animators.cpp.

Play wires through Animator::start so the dispatcher
subscription bookkeeping happens alongside the play() call —
matches how sample code drives animations and gives the editor a
working "click Play, see actor lerp in viewport" loop without any
extra wiring (the engine's update event fires every frame in edit
mode too).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New dockable panel that, for the currently-selected actor's
SpriteRenderer, lists every named animation in its map with:
  - frame count
  - frame interval slider (mutates SpriteData::Animation directly
    via the new SpriteRenderer::getAnimationsMutable() accessor)
  - "Set as current" button that switches the playing animation
    without restarting playback
  - Frame strip — one SmallButton per frame index; clicking jumps
    the renderer to that frame's start time via setAnimationTime
    and pauses, so the user can scrub to a specific frame.

Top of the panel: Play / Pause / Stop / Reset transport buttons
that mirror what the Inspector's "Playing" boolField triggers,
plus the explicit Stop (turn off + reset to t=0) and Reset
(rewind without changing playback state) split that the panel
UX expects.

Engine surface: SpriteRenderer grows getAnimationsMutable() —
const overload of getAnimations() stays the canonical accessor
for game code, the mutable one is editor-only and lets the panel
write back the new frameInterval. No other engine changes.

13b minimum cut: text-only frame strip. Per-frame UV thumbnails
are a follow-up — they need SpriteData::Frame to expose its
frameRectangle + textureSize, which is a separate engine change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SpriteRenderer::play() is a no-op when `playing` is already true,
which stays the case after a non-looping animation reaches its
natural end (running flips to false but playing stays true). With
the panel's Play button calling sprite->play() directly, the
second click did nothing — the user saw a dead button.

Restructure Play to do a stop(true) + setAnimation(curName,
repeat=true) + play() sequence: clears the queue, primes the
current animation as a loop, restarts the dispatcher
subscription. Pause still calls stop(false) for "freeze on the
current frame without resetting time." Stop and Reset stay as
they were.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PlayControlPanel's ctor sets engine.timeScale = 0 in the
default Editing state, so the EventDispatcher fires UpdateEvents
with delta=0 and the SpriteRenderer's update() never advances
currentTime. Result: clicking Play in SpriteAnimationPanel reset
the animation to frame 0 (we re-set the queue + called play())
but the sprite then sat there forever — visually broken.

Two fixes:

1. Edit-time tick. SpriteAnimationPanel::draw, when the panel is
   visible AND timeScale==0 AND the renderer is in playing state,
   manually calls sprite->update(io.DeltaTime) once per frame so
   playback progresses regardless of the engine's frozen tick.
   The dispatcher path still fires with delta=0 (no-op), so we
   don't double-advance. Phase 14's Play Mode (timeScale != 0)
   short-circuits the manual tick automatically.

2. Per-animation Loop checkbox. SpriteData::Animation grows a
   `bool repeat = true` field that the panel exposes as a
   checkbox per animation row. Play uses this value when it
   re-issues setAnimation; toggling Loop while the animation is
   playing re-applies the new flag immediately. Repeat used to
   be only on QueuedAnimation (runtime-only) — the Animation
   field gives the editor a place to hang the user preference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The viewport panel attaches its own scene::Actor (named
"<viewport camera>") to the working layer because the engine ties
Camera component membership to layer.children — without that the
camera doesn't render the user's scene. The SceneTreePanel had
an excludeActor pointer for this case, but it broke down once
scene save/load entered the picture: layerToJson serialised the
camera into the .flux file, and on load a fresh Actor with a
non-matching pointer slipped past the exclude filter.

Add a name-prefix convention: any actor whose name starts with
"<" is treated as editor-internal. SceneTreePanel skips them in
the tree alongside the existing pointer-based exclude. SceneIO's
saveUserScene calls a new stripEditorActors pass on the
serialised layer JSON to drop them before write, so the .flux
file stays free of editor scaffolding and round-trips cleanly.

The pointer-based excludeActor stays in place as a fast path
for the live (no-save) case where the actor is exactly the one
the panel knows about.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds currentTime / progress / running / done to Animator's
default inspect() under a beginGroup("Runtime State") section.
JsonInspector reads + writes them transparently — so a Play
Mode snapshot taken mid-animation, then restored on Stop, brings
the animator back to where it was rather than to t=0.

The destructor "leak" the original phase plan called out turned
out to be a non-issue: EventHandler's own destructor unsubscribes
from the dispatcher, and SceneSerializer's layerFromJson calls
removeAllChildren before re-adding, which cascades through
~Actor → ~Component → ~Animator → ~EventHandler. So no explicit
handler.remove() in the dtors is needed; the bug-fix half of 14a
is "nothing to fix".

ParticleSystem mid-flight particle state is too big to round-
trip cleanly without a dedicated serialiser pass; that stays as
a follow-up. For 14a's purpose (Pause / Step / Resume continuity
across the scene-snapshot cycle), Animator state is the piece
that matters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlayControlPanel grows public enterPlay / enterPause / enterResume /
enterStop / stepOne entry points (renamed from the private
enterEditing) and a updateWindowTitle helper that runs on every
state transition. Title prefix follows Unity: "" / "[PLAYING] " /
"[PAUSED] " before "Fluxion Editor". The panel itself stays as the
"Time" details surface (timeScale slider + status string).

EditorApp drawMainMenu adds Play / Pause / Resume / Step / Stop
buttons after the Edit menu. Each button is gated on the current
PlayControlPanel state and routes through the panel's public API,
so the panel and the toolbar share a single state machine without
needing duplicated enable-rules. Play gets a green tint when
already running so the eye finds it.

ViewportPanel grows an optional setPlayControl(const PlayControlPanel*)
back-pointer plus a 2-pixel border drawn on the foreground draw
list at the end of draw(). Colour: gray (Editing), green (Playing),
yellow (Paused). Wired up by EditorApp after both panels are
constructed (viewport is built first, the back-pointer pushed in
when PlayControlPanel becomes available).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pressing Play in the top toolbar / Time panel snapshotted the
scene and bumped timeScale, but every Animator and SpriteRenderer
sat there inert because none had had play() called on them — the
user had to click Play in each component's inspector / panel
first, which is unlike anything else (Unity, Godot, etc.) and
defeats the point of a single Play button.

Add a recursive scene walker that, when entering Play, calls
Animator::start() on every animator and SpriteRenderer::play() on
every sprite renderer in every layer. The dispatcher's
addEventHandler removes any existing subscription before re-
adding, so doubling-up on already-running components is safe.

Symmetric autoStopContainer pass on enterStop detaches the
dispatcher subscriptions before the snapshot/restore destroys
the actors, avoiding a transient frame where both the about-to-
die and the freshly-restored components could both tick the
same scene.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user can now toggle "Loop" directly on the SpriteRenderer in
the Inspector — the simple "make this animation keep playing"
toggle most setups want. The per-animation Loop checkbox in the
SpriteAnimationPanel stays around for cases where different
animations on the same renderer need different loop behaviour.

Implementation: a `bool loop = true` member on SpriteRenderer.
play() syncs every queued animation's repeat flag to it, so the
Inspector's Loop wins over whatever the queue was last set to
(via init() with its hardcoded `false`, or via setAnimation
called by the panel). setLoop also re-applies to the live queue
so a mid-play toggle takes effect on the very next animation
cycle. inspect() exposes the field through a plain boolField,
which JsonInspector round-trips on scene save/load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audited every editor-registered component for play / pause / stop /
step coverage and found ParticleSystem missing from the
autoPlayContainer / autoStopContainer walker — the only other
component besides Animator and SpriteRenderer that subscribes to
the engine UpdateEvent dispatcher. Now Play Mode entry calls
ParticleSystem::resume() on every particle system in the scene,
and Stop calls ParticleSystem::stop() symmetrically before the
snapshot/restore destroys the actors.

Pause and Step are uniform across all components — they're
driven by engine.timeScale and engine.requestStep(), so any
component that ticks via UpdateEvent automatically obeys them.
The remaining editor-registered components (Camera, Light,
ShapeRenderer, TextRenderer, StaticMeshRenderer,
SkinnedMeshRenderer) are passive renderers with no play/stop
lifecycle — they just keep drawing whatever state they're in,
which is the right behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pause and Step both report engine.timeScale == 0 (the engine's
freeze mechanism), and the panel's edit-time manual update()
tick was gated on exactly that — so during Pause / between Step
ticks the panel kept advancing the SpriteRenderer's currentTime
on its own, defeating the freeze. The user noticed: hitting
Pause didn't pause sprite animation.

Fix: the panel takes an optional PlayControlPanel back-pointer
and gates the manual tick on state == Editing instead of on
timeScale alone. In Editing mode the manual tick is the only
thing advancing the sprite (timeScale=0 zeroes the dispatcher
delta), so the SpriteAnimationPanel's Play button still works.
In Playing mode timeScale is non-zero and the dispatcher drives
normally — the manual tick correctly skips. In Paused / Step
mode neither path runs, so the sprite freezes the way the user
expects. Defensive fallback: when no PlayControlPanel is wired
the panel falls back to the old timeScale check.

EditorApp passes playControl_ at SpriteAnimationPanel construction
now that PlayControlPanel exists earlier in the ctor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mutation pathways that would diverge the live scene from the
pre-Play snapshot are gated when state != Editing:

  * File menu: New / Open / Save / Save As — disabled. New /
    Open would wipe the snapshot's referent; Save would
    serialise the post-Play, possibly already-mutated state.
  * Edit menu: Undo / Redo — disabled. Reverting commands mid-
    Play would either rewind into engine-driven mutations
    (transform updates from Animators) or apply commands whose
    target UUIDs no longer exist after Stop's restore.
  * Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z keyboard shortcuts —
    disabled with the same gate so the menu and the keyboard
    stay in sync.
  * Gizmo manipulation in ViewportPanel — gizmo.update() short-
    circuits, so the translate / rotate / scale handles don't
    drag actors. Picking still runs (left-click selection) so
    the user can inspect actor state mid-Play.

Auto-clear the CommandStack on Play -> Editing transition (i.e.
Stop). Snapshot/restore returns the scene to its pre-Play
state, but commands the user happened to push DURING Play
(asset drag-drop pushes SetAssetCommand, drop-to-create-actor
in viewport pushes CreateActorWithSpriteCommand) still
reference actors that won't exist after restore — undo/redo on
those would be no-ops at best and crashes at worst. Wiping the
stack at the transition keeps the history aligned with the
post-Stop scene. The transition is detected by polling
playControl_->state() once per frame against a stored last-
frame value, avoiding any panel-side callback machinery.

The AssetBrowser's Reimport / Rename / Delete operations the
plan listed don't exist yet (we never implemented them); when
they land they should pick up the same isEditing gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hooks the engine's OS-window close button to a host-side
confirmation callback (engine->setCloseConfirmHandler), wired on
Windows via WM_CLOSE: when the editor's handler returns false,
the WndProc skips DestroyWindow so the window stays alive long
enough to pop the modal. Other platforms keep the existing
"close immediately" path until their native handlers learn the
same hook (macOS NSWindow shouldClose: + Linux WM_DELETE_WINDOW
flagged for follow-up).

Editor side:
  - CommandStack gets undoCount() so EditorApp can detect "stack
    drifted from the last save."
  - Per-frame updateDirty() recomputes dirty_ from the count
    delta against savedUndoCount_; doNewScene / doSaveScene /
    doOpenScene reset that baseline.
  - Right-side menubar text gains a trailing "*" inside the
    [path*] / [unsaved*] when dirty_.
  - File menu's New Scene + Open Scene defer when dirty_ — they
    set pendingDiscardAction_ instead of executing immediately.
  - The engine close handler does the same for Close.
  - drawDiscardModal pops a "Discard unsaved changes?" popup the
    next frame; Discard executes the queued action (doNewScene /
    open file dialog / userConfirmedClose_+engine->exit()),
    Cancel clears it.

For the close case specifically: the user's first X click sets
pendingDiscardAction_=Close and refuses the close. They confirm
in the modal; we set userConfirmedClose_=true and call
engine->exit(), which triggers the next WM_CLOSE — this time the
handler sees userConfirmedClose_ and returns true, so the
window destroys cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
engine->exit() only flips the engine's `active` flag — it
doesn't destroy the OS window, so the editor's main loop drops
out but the window stays on screen showing whatever it last
drew. The OS marks the unresponsive title bar "Not Responding"
because the window's message pump never sent WM_DESTROY.

Engine::getWindow().close() sends a fresh WM_CLOSE through the
platform window. The next time our requestCloseConfirmation
handler runs, it sees userConfirmedClose_=true (set by the
modal's Discard click) and returns true, so the WndProc lets
DefWindowProc -> DestroyWindow -> WM_DESTROY -> PostQuitMessage
proceed and the main loop tears down the engine cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d on SpriteRenderer

Phase 10 leftover: the five Animator subclasses Ease, Parallel,
Repeat, Sequence and Shake were never registered with the
TypeRegistry, so the editor's Add Component popup couldn't spawn
them. Add a default constructor to each (TypeRegistry::create
needs a no-arg path) plus FLUXION_COMPONENT_DECL/REGISTER and an
inspect() override:

  - Ease: easing function + mode enum pickers (sine/quad/.../bounce
    cross easeIn/easeOut/easeInOut), plus a wrapped-animator
    count read-out.
  - Parallel / Sequence: child-count read-out — full child-list
    editor stays a follow-up.
  - Repeat: count int field (0 = infinite per the runtime semantics)
    plus a wrapped-animator count read-out.
  - Shake: distance Vector3 + time-scale float fields, on top of
    the base Animator inspect().

EditorApp's force-link table picks up the five new ForceLink
symbols so the static-link drops don't strip the AutoRegistrar
.objs out of libfluxion.

Phase 12 leftover: SpriteRenderer's inspect() now exposes the
material-side controls the plan called for:

  - Color tint via colorField on material->diffuseColor (white =
    pass-through, anything else multiplies the rendered sprite).
  - Blend mode enum picker over the engine's five cached presets
    (None/Alpha/Add/Multiply/Screen). A "Custom" sentinel appears
    when the current blendState pointer doesn't match any preset
    — e.g. when the user supplied their own via setMaterial.

Pivot was deliberately left out: SpriteRenderer's Offset field
already covers the per-renderer pivot adjustment; the per-frame
pivot lives in SpriteData::Frame and is a different concept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-click on a top-level asset tile now opens a fuller menu:

  - Open in Sprite Editor (image only) — already there.
  - Reimport: re-runs the loader chain for this asset against the
    project bundle, picking up edits the user made on the source
    file outside the editor.
  - Rename...: pops a modal with the current stem prefilled. On
    Confirm, AssetDatabase::renameAsset moves the file + .meta
    sidecar to the new name (extension preserved when omitted)
    and rescans.
  - Delete...: pops a confirmation modal that warns about scene
    refs going dangling. AssetDatabase::deleteAsset removes the
    file + .meta and rescans; sub-slice entries vanish naturally
    because their parent is gone.

Mutating ops (Reimport / Rename / Delete) are gated on play
state == Editing per phase 14c. Sub-slice tiles keep getting no
context menu — their identity lives inside the parent's .meta
and the SpriteEditor is the right surface for editing them.

AssetDatabase grows two new public methods (renameAsset,
deleteAsset) backed by std::filesystem::rename / remove. Both
trigger a rescan so entries_ + sliceSets_ stay in sync after
the on-disk change.

EditorApp wires the AssetBrowserPanel with the project bundle
and PlayControlPanel back-pointers it now needs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant