Claude/nervous hamilton a271ee#57
Open
kisstp2006 wants to merge 67 commits into
Open
Conversation
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>
Author
|
asd |
Claude/nervous hamilton a271ee
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.