Color picker is the third interactive surface (after gizmo drag and
outliner toggles) that mutates an object. Wire it the same way:
record in the onChange handler (input type="color" fires once per
committed pick, so one undo entry per user choice).
Object Panel transform inputs (position/rotation/scale text fields)
are still on the legacy diff path. Wiring them is one more pass —
they need a coalescer because each keystroke flushes to the engine.
Tracking that as a follow-up.
The gizmo's onDraggingChanged fires once with dragging=true at
dragstart and once with dragging=false at dragend. Wire those
boundaries to history.beginTransform / endTransform so each drag
becomes one coalesced undo entry. No-op moves (mouse-down + drag-up
without movement) are dropped automatically by HistoryManager.
Object Panel inputs and color edits still ride the legacy
check_scene_for_updates diff path; those are migrated separately
before the diff machinery is removed.
Wires creation, deletion, lock, visibility, and paste through the new
HistoryManager. Mutator-recorded — every external call site keeps
the same signature it had before; recording happens inside the mutator
or its dep callback, so callers don't grow new responsibilities.
Recordings added:
- actions/addObject, addCharacter, addShape: editor.history.recordCreate(obj)
replaces the legacy add_creation_undostack call.
- editor.deleteObject: snapshots the target before delegating to
utils.deleteObject so undo can fully reconstruct it.
- SelectionBridge.lockUnlockObject: takes a recordLockChange dep
callback, records before/after locked state inline. No coupling
back to HistoryManager from the bridge.
- Outliner.handleToggleVisibility: records visible before/after at the
call site (hideObject lives on SceneManager which has no history dep).
- SceneManager.paste: now returns the new Object3D so keymap's paste
handler can record it via editor.history.recordCreate. The legacy
add_creation_undostack call inside paste is removed.
keymap.ts: undo/redo target editor.history; deleteSelected routes
through editor.deleteObject (so history records); paste captures the
returned object and records.
Transform recording (gizmo drag, panel inputs) and color stay on the
old check_scene_for_updates diff path until follow-up commits. Old
SceneManager undo machinery (undoStack, undo/redo methods,
add_creation_undostack, the diff helpers) is still in place but only
the diff path drives transforms now.
New peer subsystem under engine/editor/ that owns the undo/redo stack.
Lands the foundation only — old SceneManager-based undo machinery
still drives undo/redo for now. A follow-up commit migrates every
mutation site over and removes the legacy diffing.
Design:
- Pure-data HistoryEntry: { label, apply(ctx), revert(ctx) }. Entries
receive a HistoryContext when replayed; they don't hold subsystem
references. Easy to test in isolation, immune to subsystem
re-creation.
- The external API is the recordX methods (recordCreate, recordDelete,
begin/endTransform, recordSetColor/Locked/Visible). Mutation sites
call ONE method — entry construction stays in the manager. Adding
a future action is "add an entry factory + a recorder method"; no
churn at action sites.
- Async-serialized replay via a Promise chain: a Ctrl+Z mash during
an in-flight asset reload can't interleave entry replays.
- onChange callback hook for a future store-bridge to publish
canUndo/canRedo into PageSceneStore.
- Capacity 64 (vs. main's 16); transform/userdata no-ops are dropped.
Editor wires the HistoryContext inline in its constructor (Phase 2
deps idiom — same shape as CameraControllerDeps etc.). Closures over
`this` resolve sceneManager/activeScene lazily so construction order
inside the editor constructor doesn't matter.
Pre-refactor, the queue handler (timeline.addShape/addObject/addCharacter)
called sceneManager.add_creation_undostack(obj) after creating the
THREE.Object3D. The actions/ extraction collapsed the queue path but
forgot to carry the undo push along, so dragging a new asset onto the
3D canvas left the undo stack untouched.
Push a CreationSceneItem from each of addObject, addCharacter, and
addShape after the engine call succeeds. check_scene_for_updates only
detects transforms / deletions / userdata diffs on mouseup; it does
not record creations, which is why this has to be an explicit call.
useViewportPointer.onPointerUp called editor.mouse_controls.onMouseUp
directly. That ran the interaction state machine (gizmo end, FK toggle,
selection click) but skipped editor.sceneManager.onMouseUp, which is
the only thing that calls check_scene_for_updates. Result: transform,
deletion, and userdata edits (color, lock, visibility) never made it
into the undo stack, so Ctrl+Z found nothing to undo.
Pre-refactor, SceneManager.attachEventListeners attached a window-level
mouseup that went through SceneManager.onMouseUp; the canvas-scoped
hook lost the same pass-through. Calling sceneManager.onMouseUp here
preserves all the mouse_controls behavior (it forwards internally) and
restores the post-mouseup undo capture.
Phase 1 moved camera, freeCamState, and focalLengthToFov off Editor and
onto CameraController, but two camera-update blocks in PageEditor
(handleSelectCamera and handleAddCamera) still read editorEngine.camera,
editorEngine.focalLengthToFov, and editorEngine.freeCamState. Those
fields are undefined now, so the cam-update guards short-circuited and
the reset/random-camera buttons silently did nothing.
Sweep both blocks to read from editorEngine.cameraController.* and
collapse the chain into a local `cam` const.
DndAsset.onPointerUp captured event.pageX/Y but threw them away; both
add* actions then defaulted position to origin. PageEditor's gallery
onImageDrop tried an NDC unproject but gated on editorEngine.camera,
which Phase 1's CameraController extraction left undefined — the guard
short-circuited and worldPosition stayed undefined.
Add pickDropPosition: a deps-pattern helper that mirrors the
pre-refactor timeline.getPos() — raycasts from the camera through the
cursor against scene children and returns the first hit (or origin if
the ray misses or the camera/canvas isn't ready). Same idiom as
CameraControllerDeps / GizmoControllerDeps from Phase 2; callers build
the deps inline so the helper stays decoupled from Editor and any
subsystem's import surface.
Both drop sites — DndAsset.onPointerUp and PageEditor's 3D gallery
onImageDrop — now call the helper and forward worldPosition to
addCharacter / addObject / addShape.
create_parim was a one-line passthrough to activeScene.instantiate
that the pre-refactor queue path used. After actions/addShape was
rewired to call sceneManager.create with the "Parim" sentinel,
create_parim has zero callers anywhere in the artcraft frontend.
actions/addShape passed item.media_id ("Box", "Sphere", ...) as the
routing token to SceneManager.create. That hits the
loadObject(media_token, ...) branch and tries to fetch a media URL
for "Box", which fails — the shape never lands in the scene.
Pass "Parim" as the routing sentinel and item.media_id as the
geometry name so create() forwards to scene.instantiate(name, pos),
which has the geometry switch ("Box" -> BoxGeometry, etc.). Then
overwrite obj.name with the display label so the outliner reads
"Cube" instead of "Box".
Restores the pre-Phase-4 behavior of editorEngine.create_parim(media_id, pos)
that the queue path used.
- Move sound catalog registration off PageScene mount and into a single
SoundManager.install() call in GlobalSettingsManager so sounds exist at
app boot (previously silent until PageScene mounted).
- Honor the play_sounds master toggle in all five event playback sites
via SoundManager.playEvent helpers. Settings preview keeps firing
regardless (handled by SoundManager.playPreview).
- Replace per-callsite GetAppPreferences + SoundRegistry boilerplate in
the four generation event hooks and the file-deleted handler with
one-line SoundManager.* calls.
- Dedupe SOUND_OPTIONS: AudioSettingsPane now reads
SoundManager.OPTIONS, derived from a single declarative SOUNDS array
(label-bearing entries are user-selectable; others are still registered
for code-driven use).
- Narrow the soundboard public barrel to SoundManager + SoundOption
only; SoundRegistry and SoundEffect are now lib-internal.
The last batch of PageScene-owned signals — `is3DPageMounted`,
`is3DEditorInitialized`, `is3DSceneLoaded`, and the three canvas-ref
signals (sceneContainer, editorCanvas, camViewCanvas) — now live on
the Zustand store. EngineProvider, the canvas components, the
useEditorCanvas hook, and the cross-app TopBar consumer all read from
the store instead. The helper signals in editor.ts are gone; the
`set3DPageMounted` / `setIs3DEditorInitialized` / `setIs3DSceneLoaded`
exports stay as thin wrappers around the store actions for existing
callers.
PromptBox3D and CameraSettingsModal now accept plain values + setters
instead of `Signal<T>` props. The camera/focalLengthDragging/
isPromptBoxFocused signals are gone — store is the single source of
truth. Engine code (editor/save_manager/scene) reads/writes via
`usePageSceneStore.getState()` and store actions. Demo asset signals
are now plain constants (they were never reactive).
api_manager.ts goes from 460 lines to ~135. The video-render
pipeline (uploadMediaFrameGeneration, stylizeVideo,
StylizeVideoParams, getMediaBatch, getMediaFile, uploadMedia,
gltfExport) was unreachable after videoGeneration was deleted.
Surviving public methods:
- saveSceneState (called by save_manager)
- loadSceneState (called by save_manager)
- uploadMediaSceneThumbnail (used internally by saveSceneState)
Also drops the local ArtStyle / Visibility / MediaFile types —
they only fed the deleted methods. The ArtStyle enum has a
separate copy in ~/enums/ArtStyle.ts that other code uses.
Delete two files that became unreferenced:
- engine/APIManagerResponseError.ts (duplicate of the local class
in api_manager.ts; nothing imported it)
- models/generationOptions.ts (IGenerationOptions/GenerationOptions
fed editor.generation_options, deleted last commit)
Net: -1 typecheck error, -326 lines.
Files with zero importers across the artcraft app:
- constants/RandomTexts.ts — RandomTextsPositive/Negative seeded
prompt suggestions in the deleted StylizeTab.
- composable_engines/scene_format.tsx — empty (1 line).
- constants/page.ts and the matching currentPage signal in
signals/global.ts — Pages enum + signal, neither read.
- enums/StylizeTabPages.ts and enums/AudioTabPages.ts — re-exported
via enums/index.ts but no consumer.
- models/track.ts, models/message.ts, models/voice.ts, models/tts.ts
— timeline / TTS / audio-panel types from deleted features.
- styleList.ts — used to seed selectedArtStyle's default; that
store field is gone.
- models/types.tsx had a duplicate AudioTabPages enum (separate
from the deleted one in enums/) — drop the duplicate too.
models/index.ts and enums/index.ts trimmed to match. Error count
unchanged (151) — none of these had broken type errors anyway.
Strip the stylize fields that nothing reads anymore now that the
StyleMoreOptions / StyleSelection UI is gone. From PageSceneStore:
selectedArtStyle, upscale, faceDetail, styleStrength, lipSync,
cinematic, enginePreProcessing, globalIPAMediaToken, adapterImage,
positivePrompt, negativePrompt, isUserInputPositive,
isUserInputNegative, showNegativePrompt — and the matching setters
+ resetSceneGenerationMetadata. Drop the styleList / ArtStyle
imports; both go unused here.
From editor.ts: drop negative_prompt, art_style,
engine_preprocessing, generation_options, globalIpAdapterImage.
Keep positive_prompt — PromptBox3D writes it via setEnginePrompt
(PageEditor.tsx) and save_manager round-trips it.
Trim save_manager.loadFromJson to the prompt-only path (legacy
saves with stylize keys load fine; the now-removed if-guards just
silently ignored them anyway). Trim SceneMetadata to
{ positivePrompt, cameraAspectRatio } and the matching
SceneGenereationMetaData interface.
ControlsTopButtons.handleResetScene drops its now-removed store
call.
Both component trees survived earlier cleanup but never render
from PageEditor or PageScene. They were the last UI consumers
of the stylize store fields; deleting them sets up a follow-up
that drops those store fields entirely.
Net: -17 typecheck errors (the orphan files were the source of
many missing-import errors).
cardLayout.ts, MoodboardHtmlColorPicker.tsx, findDropTarget.ts, and
CardNodeHtml.tsx were swept into the PageEnigma -> PageScene rename
commit (76add858c9) on this 3D-refactor branch. They belong on
moodboard-page-html.
The file content is preserved on the local-only moodboard-rescue
branch (pointing at 01e0e27037). To carry them back over:
git checkout moodboard-page-html
git checkout moodboard-rescue -- frontend/apps/artcraft/app/src/pages/PageMoodboard/cardLayout.ts frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/MoodboardHtmlColorPicker.tsx frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/interactions/findDropTarget.ts frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/nodes/CardNodeHtml.tsx
git commit
The Enter Pose Mode button reached through a window.__mouseControls
global handle that MouseControls' constructor set up. With
getActiveEditor() already exposing the live editor instance for
non-React callers (used in the camera-view toggle, the keymap),
the global is unnecessary.
Drop the (window as any).__mouseControls = this assignment in
MouseControls and the matching Window augmentation in
PoseModeSelector. Replace the click handler's reach with
getActiveEditor()?.mouse_controls?.toggleFKMode() — same shape
the keymap entry already uses.
The Tab interface in this file was the type for the deleted
SidePanel/tabList.tsx tab list. Grep across app/src/ confirms no
consumer remains; AssetModal uses its own local AssetTab type.
Drop the file and the matching re-export in models/index.ts.
EditorStates is no longer referenced from editor.ts now that
PREVIEW is gone from the enum and switchPreview/switchEdit are
deleted. EngineCanvases.tsx never used useContext/EngineContext;
the imports were leftover from earlier work.
Net: -3 typecheck errors.
The mouseOverEventHandler / enableFreeCamControls /
disableFreeCamControls trio existed to track when the cursor was on
the viewport canvas and toggle FreeCam.enabled. With useFreeCam
gating itself on editorState === CAMERA_VIEW, that gate is
redundant; the only remaining consumer of selectedCanvas was
MouseControls.onkeydown's space-bar branch, which is unreachable
after the previous keyboard rewrite.
Removes the last document.addEventListener call from PageScene.
Replace MouseControls.onkeydown's giant if-else chain with a
declarative keymap:
- engine/keymap.ts: KeyBinding type + buildKeymap() returning every
one-shot viewport shortcut as one entry. Each carries label /
group metadata so a future Ctrl-hold cheatsheet overlay can read
the same array. dispatchBinding() does the modifier match
(treating Ctrl and Meta as equivalent for macOS).
- hooks/useViewportKeyboard.ts: attaches a single keydown listener
that gates on hotkeyStatus + editable-element checks, then walks
the keymap. Held movement keys (W/A/S/D etc.) live in useFreeCam
since they're continuous; this hook is for one-shots only.
The listener is on document, not the canvas, deliberately: canvas
focus is finicky and would require the user to click the viewport
before T/R/G work. The editable-element guard prevents the keymap
from firing while inputs are focused.
MouseControls.onkeydown is now unreachable (SceneManager's window
listeners are no-op since the previous commit, and useViewportKeyboard
goes straight to keymap dispatch). The class itself stays for now —
it still implements selection / FK / pointer-lock state.
The old SceneManager.attachEventListeners() attached window-level
mousemove / click / mousedown / mouseup / keydown handlers in dev
mode and dispatched them into MouseControls. Replace with two pieces:
- hooks/useViewportPointer.ts: canvas-scoped pointer listeners
that call into editor.mouse_controls. NDC pointer coords are
computed via the new engine/pointer.ts helpers (ndcFromClient,
applyNdcToVector2). Selection clicks suppress while the
free-cam owns input (CAMERA_VIEW state).
- engine/pointer.ts: pure NDC coordinate translation, no DOM
side-effects.
SceneManager.attachEventListeners / detachEventListeners are now
no-ops; kept for back-compat with the editor lifecycle so future
follow-ups can carve them out cleanly.
MouseControls itself still owns selection / FK / drag state — its
dispatch methods (onMouseMove, onMouseClick, etc.) are now invoked
by the React hook instead of window listeners. A future PR can
dissolve MouseControls further; the goal here is "no window
listeners" + "React owns lifecycle".
The 446-line FreeCam class is gone. Its responsibilities split:
- Math (move/rotate vectors, pan/zoom from inputs, lookAt) lives in
cameraMath.ts as pure functions, plus a freeCamFrameTick() that
integrates the per-frame state.
- Listener attachment + drag/key state ownership moves into
hooks/useFreeCam.ts. Listeners are scoped to the canvas (no
window/document handlers); held-key state is reset on
CAMERA_VIEW exit so a stale W keypress can't drift the camera.
- The shared FreeCamControlState struct flows through Editor via
editor.setFreeCamState(state); the render loop reads it and
calls freeCamFrameTick() each frame.
Side effects: keybinds_controls.ts now imports
isPointerLockSupported from browserChecks; helper.ts and
PageEditor's camera-select handler stop poking
.cameraViewControls and reset velocity via freeCamState instead.
PreviewEngineCamera's camera-view toggle now just flips the store
state — useFreeCam picks it up on its own (no more
enableFreeCamControls/disableFreeCamControls call site).
Pure-function helpers carved out of FreeCam in preparation for the
hook rewrite:
- engine/cameraMath.ts: HeldMoveKeys / HeldRotateKeys types,
moveVectorFromKeys, rotationVectorFromKeys, panFromDrag,
zoomFromWheel, lookAtFromCamera, lerpVelocity. No DOM access, no
signals.
- engine/browserChecks.ts: isSafariOrWebKit, hasApplePay, isSafari,
isPointerLockSupported. Unrelated to camera math; were just
hitchhiking in free_cam.ts.
free_cam.ts re-exports the browser checks for back-compat with
keybinds_controls.ts; both consumers will switch to the direct
imports when free_cam.ts is deleted in the next commit.
The Phase 6 hooks (useFreeCam, useViewportPointer, useViewportKeyboard)
need a React-shaped accessor for the canvas DOM node so their
useEffects re-run when the canvas mounts/unmounts. Promote the
existing editorCanvasSignal / camViewCanvasSignal into useEditorCanvas
/ useCamViewCanvas hooks that bridge signal -> React state via a
subscribe.
Add tabIndex={0} and outline:none to the editor canvas so it can
receive keyboard focus without showing a focus ring.
handleButtonCameraView in PreviewEngineCamera was a TODO no-op.
Flip editorState (EDIT <-> CAMERA_VIEW) via the store, then call
editor.enableFreeCamControls() / disableFreeCamControls() on the
active editor. Drop the stale console.log useEffect that fired on
every state change.
The 3D editor no longer generates videos. Remove:
- engine/video_generation.ts (the VideoGeneration class)
- comps/GenerateModals/ (modal + MyMovies/Sharing/Activity/cards)
- comps/PreviewFrameImage.tsx (PREVIEW-state-only preview overlay)
- comps/ControlsTopButtons/TestFeaturesButtons.tsx (dev-only,
rendered with debug={false})
- ~/components/signaled/TopBar/MyMoviesButton.tsx (was orphaned;
no consumer)
editor.ts: drop the VideoGeneration import, field, instantiation,
and the dead helpers it powered (useCachedMediaTokens,
stopPlaybackAndUploadVideo, switchPreview/switchEdit, generateFrame,
generateVideo, checkAndUseCache).
PageSceneStore: drop previewSrc/viewMyMovies/generateMovieId/
generateProgress fields and setters.
EditorStates: drop the PREVIEW variant; PageEditor's state machine is
now EDIT <-> CAMERA_VIEW only. PREVIEW was set only by the deleted
video_generation.ts and read by PreviewFrameImage (deleted) and a few
gates in PreviewEngineCamera + ControlPanelSceneObject (now removed).
Net: -8 typecheck errors.
sidePanelWidth, sidePanelVisible, dndSidePanelWidth and their setters
are no longer read by any rendered component. Drop them from
PageSceneStore and the lone setSidePanelWidth(340) call in
PageEditor's mount effect. PreviewFrameImage's width calc no longer
subtracts the (always-zero) panel width.
The <SidePanel /> component was never rendered from PageEditor.tsx; the
entire comps/SidePanel/, comps/SidePanelMenu/, and most of comps/SidePanelTabs/
tree was unreachable. Relocate the few files still consumed by
AssetModal (sharedComps/ItemElement{,s}.tsx, hooks/useUserObjects,
hooks/useFeaturedObjects, utilities/fetchMediaItems + misc) to
comps/AssetMenu/{shared,hooks,utilities}/, and the StyleOptions panel
used by StyleMoreOptions to its parent folder. Then delete the rest.
Net: -33 typecheck errors (the dead tree had its own type problems).
Editor/ -> engine/:
- Rename pages/PageScene/Editor/ -> pages/PageScene/engine/
- Update every "./Editor/...", "../Editor/...", "../../Editor/...",
"~/pages/PageScene/Editor/..." import path. The class name `Editor`,
`EditorState` types, and `editorState` field are unchanged -- only
the directory is renamed
- ImageTo3DExperience also gets the new path
useSignals() drops where the component reads zero .value signals:
- comps/GenerateModals/Activity.tsx
- comps/SidePanelTabs/hooks/useFeatureFlags.tsx
- comps/SidePanelTabs/sharedComps/ItemElement.tsx
- comps/SidePanelTabs/tabComps/StylizeTab/PageStyleSelection.tsx
- comps/StyleMoreOptions/StyleMoreOptions.tsx
The remaining useSignals() callers still subscribe to genuine signals
(pageHeight/pageWidth from ~/signals, outlinerIsShowing,
sceneContainerSignal, etc.) and need to stay until those signals migrate.
Final grep audit confirms no residue:
- "PageEnigma" -- 0 hits
- "from \"...Editor/...\"" import paths -- 0 hits
- Queue.publish / Queue.subscribe / fromEngineActions /
toEngineActions / toTimelineActions -- 0 hits
- TimeLine, AudioEngine, TransformEngine, EmotionEngine, LipSyncEngine,
CharacterAnimationEngine, ClipUI, TimelineState -- 0 hits
Typecheck unchanged at 144 errors in PageScene; zero new errors.
The directory was named after a stale codename. The route is
/$sceneToken, the top bar reads "Edit Scene", and the page is the main
scene-editing workspace. Renaming the directory, top-level component,
and Zustand store accordingly:
- pages/PageEnigma/ -> pages/PageScene/
- pages/PageScene/PageEnigma.tsx -> PageScene.tsx
- pages/PageScene/PageEnigmaStore.ts -> PageSceneStore.ts
- usePageEnigmaStore -> usePageSceneStore (function rename across the
whole codebase via sed)
- All ~/pages/PageEnigma/* import paths -> ~/pages/PageScene/*
- Component name PageEnigma -> PageScene in routes/_index.tsx and
routes/$sceneToken.tsx
234 files touched, all mechanical. Typecheck error count holds at 144;
zero new errors introduced by the rename.
stylizeTab.ts (deleted). 11 consumers migrated:
UI components:
- StyleOptions: cinematic / upscale / faceDetail / lipSync /
enginePreProcessing toggles drive the matching store setters; the
upscale<->cinematic mutual-exclusion logic preserved
- StyleStrength: slider + number input read styleStrength from store,
call setStyleStrength
- Prompts: textBufferPositive / textBufferNegative now positivePrompt /
negativePrompt on the store; isUserInputPositive /
isUserInputNegative / showNegativePrompt as fields with setters
- IPAdapter: adapterImage + globalIPAMediaToken from store; useShallow
subscribes to all four needed fields/setters in one selector
- StylizeTab: selectedArtStyle from store, setSelectedArtStyle replaces
the old setArtStyleSelection helper
- StyleSelection: same pattern -- selectedArtStyle and
setSelectedArtStyle
- GenerateMovieButton: snapshots the entire stylize bag from the store
with usePageEnigmaStore.getState() inside the click handler
Engine + persistence:
- SceneMetadata.getSceneGenerationMetaData reads cameraAspectRatio,
globalIPAMediaToken, upscale, faceDetail, styleStrength, lipSync,
cinematic, enginePreProcessing all from the store in one getState()
- save_manager.loadFromJson restores all stylize state through the
store's setters instead of the old restoreSceneGenerationMetadata
signal helper. globalIpAdapterImageMediaToken restoration also goes
through setGlobalIPAMediaToken
- video_generation: setGlobalIPAMediaToken on upload + reads
globalIPAMediaToken from store for the gen request payload
- ControlsTopButtons: new-scene flow calls
usePageEnigmaStore.getState().resetSceneGenerationMetadata()
Bonus cleanup:
- SidePanelTabs reverted to inlining pageHeight - 64 (the previous
edit got lost in a stash round-trip)
- IPAdapter dropped useSignals and uses useShallow for its 4-field
selector
Net effect on typecheck: 145 -> 144 errors in PageEnigma.
sizing.ts (deleted). Was already minimal -- just sidePanelWidth, the
last survivor after the timeline-related sizing exports were stripped
in Phase 2. Three consumers migrated:
- PageEditor's mount-time setup: store.setSidePanelWidth(340)
- PreviewFrameImage: pulls sidePanelWidth off the store alongside
sidePanelVisible
- SidePanel: same pattern (already reading sidePanelVisible /
dndSidePanelWidth from the store; sidePanelWidth joins them)
- StyleSelection: subscribes to sidePanelWidth via the store
signals/ down to 10 files: camera, demoAssets, dragAndDrop, engine,
objectPanel, outliner, precisionSelectorMenu, promptBox, stylizeTab.
selectedMode.ts (deleted):
- PoseModeSelector reads poseMode + showPoseControls from store, calls
setPoseMode to toggle
- keybinds_controls migrates the t/r/g hotkeys (mode change),
x (transform space toggle), 'Escape' (exits pose mode), object
selection (sets showPoseControls), and FK toggle to the store
- editor.change_mode + toggleTransformSpace read/write transformSpace
through usePageEnigmaStore.getState()
- Controls3D's mode dropdown + world/local pill read selectedMode and
transformSpace from the store
- actions/setTransformMode no longer dual-writes the legacy signal
- store's PoseMode union changed from "select" | "edit" to
"select" | "pose" to match what the engine actually checks
sidePanel.ts (deleted):
- SidePanel reads sidePanelVisible + dndSidePanelWidth from store
- TabTitle's close button calls setSidePanelVisible
- SidePanelMenu pulls sidePanelVisible + setSidePanelVisible
- PreviewFrameImage reads sidePanelVisible (was a signal, now store)
- sidePanelHeight (was a computed signal of pageHeight - 64) inlined
at its three call sites: PreviewEngineCamera, SidePanelTabs,
StyleSelection. cameraFilter has no consumers; it lives on the store
for the next round.
signals/ down to 11 leaves: camera, demoAssets, dragAndDrop, engine,
objectPanel, outliner, precisionSelectorMenu, promptBox, sizing,
stylizeTab.
hotkeys.ts (deleted):
- keybinds_controls reads usePageEnigmaStore.getState().hotkeyStatus.disabled
- ControlPanelSceneObject pulls disableHotkeyInput / enableHotkeyInput
off the store and passes them to InputVector
- PageEditor pulls the same and passes them to LoginModal +
PromptBox3D. DomLevels imported from PageEnigmaStore (replacing the
signals/hotkeys re-export)
selectedItem.ts (deleted):
- No external consumers (dead since the timeline removal).
selectedObject lives on the store; the legacy signal had no readers
outside its own file.
signals/index.ts no longer re-exports either slice. Down to 13 files
in signals/.
assetModal.ts (deleted):
- AssetMenu: setAssetModalVisibleDuringDrag(true) on open
- AssetModal: reads assetModalVisible / assetModalVisibleDuringDrag /
reopenAfterDrag from the store, writes via the matching setters.
Modal close drives setAssetModalVisible(false); the reopen-after-add
checkbox writes through setReopenAfterDrag (note: store field is
reopenAfterDrag, not the old reopenAfterDragSignal name)
- Controls3D: handleOpenModal flips both setters from the store; the
empty-scene tooltip useEffect reads assetModalVisible from the store
- keybinds_controls: 'b' shortcut opens the asset modal via the store
- DragAndDrop/DndAsset: pointerdown clears
assetModalVisibleDuringDrag, pointerup restores it from
reopenAfterDrag, click (no drag) restores it to true. All through
the store now.
signals/index.ts no longer re-exports assetModal. Four signal files
gone so far in Phase 5; signals/ now down to 13 leaves (camera,
demoAssets, dragAndDrop, engine, hotkeys, objectPanel, outliner,
precisionSelectorMenu, promptBox, selectedItem, selectedMode,
sidePanel, sizing, stylizeTab).
Three more signal files retired in favour of the Zustand store.
error.ts (deleted):
- ErrorDialog (in ~/components/signaled): reads showErrorDialog /
errorDialogTitle / errorDialogMessage from the store, calls
setShowErrorDialog
- SceneTitleInput (in TopBar): handleShowErrorDialog uses the store's
setErrorDialog action
myMovies.ts (deleted):
- MyMoviesButton (in TopBar): writes via setGenerateMovieId /
setViewMyMovies
- GenerateModals: useEffect-on-store replaces useSignalEffect; reads
generateMovieId from store
- MyMovies and Sharing: read viewMyMovies from store, close via
setViewMyMovies
- Sharing also reads generateMovieId from store
editorOverlays.ts (deleted):
- PageEditor's LoadingDots: reads editorLoader from store
- SceneContainer: reads editorLetterBox from store and passes through
to Letterbox
- PreviewEngineCamera: reads editorLetterBox + toggleEditorLetterBox
from store
- save_manager: showEditorLoader/hideEditorLoader become thin wrappers
over the store actions, keeping the same call sites
signals/index.ts no longer re-exports any of the three deleted files.
Migrates components and engine code from reading signals/engine.ts
(cameraAspectRatio, editorState, gridVisibility, previewSrc) to reading
the Zustand store.
Components migrated to usePageEnigmaStore reads:
- AspectRatioMenu (also dropped useSignals + collapsed two
duplicated lookup ladders into ICON_BY_RATIO / LABEL_BY_RATIO)
- PreviewFrameImage
- Letterbox (no longer reactive via signals; reads camAspect from store)
- StyleButtons (editorState, previewSrc, setEditorState)
- Outliner (cameraAspectRatio)
- SidePanelMenu (editorState + setEditorState)
- ControlPanelSceneObject (editorState)
- PreviewEngineCamera (cameraAspectRatio + editorState)
- PageEditor (cameraAspectRatio for getScale; the signal version still
flows into PromptBox3D as a prop)
Engine code migrated to usePageEnigmaStore.getState():
- editor.ts: snapShotOfCurrentFrame reads cameraAspectRatio + grid;
switchPreview / switchEdit set editorState
- helper.ts: switchCameraView writes editorState
- video_generation.ts: generateFrame writes previewSrc + editorState
- SceneMetadata.ts: cameraAspectRatio for snapshot metadata
- scene.ts: subscribes to store gridVisible (not the signal)
signals/engine.ts becomes a thin compatibility layer for PromptBox3D's
signal-typed props. Its setters dual-write into the store so any
PromptBox3D-driven changes (grid toggle, aspect ratio dropdown) keep
the store authoritative. editorState / previewSrc / setEditorState
removed entirely -- nothing outside the store needs them anymore.
Net effect on typecheck: 148 -> 147 errors in PageEnigma. Zero new
errors introduced.
The auto-success useEffect in UploadModalSplat / UploadModalImage /
UploadModal3D was firing on UploaderStates.success and immediately
calling onClose(), which short-circuited the UploaderModalContent
switch before the UploaderStates.success branch (UploadSuccess) could
render. The OK button on UploadSuccess was effectively dead code.
Move the success-side actions (open the gallery, fire onSuccess, close
the modal) into the UploadSuccess onOk handler. The user now sees the
"upload successful" dialog and can dismiss it intentionally; the
gallery opens at dismissal so they can drag the freshly uploaded asset
onto the canvas.