`UploaderModalContent` was defined inline in each upload modal's
function body and rendered as `<UploaderModalContent />`. Each parent
re-render created a fresh component identity, which React treats as
a new component type — unmounting the old subtree and mounting a new
one. PageScene's many zustand subscriptions fire often enough that
this remount sometimes happens between mousedown and mouseup of a
click on the file uploader's drag-drop zone.
Result: original `<u>` ("Upload a file" link) is destroyed mid-click,
mouseup lands on the modal wrapper instead of `<u>`, WebView2 refuses
to synthesize `click`, and the library's native click listener that
opens the OS file picker never fires. Other pages have lighter
parent re-render frequency, so the remount usually doesn't land
inside a click window.
Render `UploaderModalContent` as a function call (`{UploaderModalContent()}`)
instead of a component instance. The function reference still
changes per render but React just sees the returned JSX inline —
no component identity, no remount.
Same fix in all five upload modals so the bug doesn't recur on any
of them.
Also strips the temporary [click-trace] debug logger from
PageEditor.tsx (which identified the cause via mid-click DOM
mutations).
Root cause of the upload-modal file-picker regression: the modal's
React Spring enter spring transitions
`transform: scale(0.95) translateY(-10px)` → `scale(1) translateY(0)`.
With PageScene's heavier render loop, the spring takes long enough
that a click on a small deep child (the file uploader's "Upload a
file" `<u>` link, ~10px tall) lands its mousedown on the `<u>` and
its mouseup on a different element after the modal shifted. WebView2
strictly requires same-target mousedown/mouseup to synthesize
`click`, so no `click` fires, the library's native
`addEventListener("click", u)` on the label never runs, and
`inputRef.current.click()` is never called.
Animate opacity only on enter/leave. Same fade visual, no element
movement under the cursor.
Also strip the now-unused TEMP click tracer from PageEditor.tsx.
Logs click / mousedown / pointerdown at document capture + bubble
phases for every event reaching PageScene while it's mounted. Each
log entry shows target, currentTarget, defaultPrevented, and the
first 6 elements of composedPath.
Goal: figure out which listener is silencing the upload-modal
file-picker click. Will be reverted in a follow-up commit.
Two PageScene-only divergences from how the working pages
(PageDraw / PageImage / PageVideo) handle the same upload modals:
1. `handleOverlayClick` on the top/bottom controls overlays called
`event.stopPropagation()` for every click that bubbled through
them. Modals rendered as JSX children of `Controls3D` (the
upload-3D / upload-image / upload-splat dialogs) are
React-tree descendants even though `Modal`'s `Dialog.Portal`
puts them at `document.body` in the DOM. React synthetic
stopPropagation calls native `Event.stopPropagation()`, which
silences `react-drag-drop-files`'s native `addEventListener("click", u)`
on the file-input label — so clicking the drop zone never opens
the OS file picker.
Narrow the handler to `event.target === event.currentTarget` so
it only fires on the transparent overlay area itself, never on
bubbles from children. The original "control buttons don't
interact with canvas" intent is preserved without the side
effect — the canvas listener is bound to the canvas element only,
so clicks on overlay siblings never reach it regardless.
2. `tabIndex={0}` + `outline: none` on the editor canvas. Added in
`61c4936c29` "so [the canvas] can receive keyboard focus," but
`useViewportKeyboard` listens at `document` (not on canvas) so
the focus isn't actually required. Other pages don't make their
canvas focusable; remove for parity.
PopoverMenu's toggle mode renders the trigger label from whichever
item has `selected: true` — and AspectRatioPicker only marks an item
selected when `useAspectRatio === ratio` for some ratio in
`model.aspectRatios`. When the user picks an aspect ratio supported
by model A and then switches to model B that doesn't include it,
no item matches, the trigger has no label, and the aspect-ratio text
silently disappears.
Add an effect that, on every `selectedImageModel` change, resets
`commonAspectRatio` to the new model's default if the current pick
isn't in the new model's `aspectRatios`. Routes through
`handleCommonAspectRatioSelect` so the editor letterbox follows the
reset (the user's pick was unrenderable on this model anyway, so the
fallback is correct here — distinct from the cold-load case).
Almost every entry in ImageModels.ts declares
`defaultAspectRatio: Square`, so the per-model-switch sync was
forcing the letterbox to Square on every model swap and overriding
whatever the user had picked.
Replace `lastSyncedModelIdRef` with a single `didColdSyncRef` so
the sync fires exactly once — when the engine and a
`supportsNewAspectRatio()` model are both ready — and then never
again. Subsequent letterbox changes flow only through user picks via
PromptBox3D's `handleCommonAspectRatioSelect`.
The PromptBox3D-side cold-load sync raced: the effect fires when
`selectedImageModel` becomes defined, but `editorEngine` is still
null at that point because EngineProvider initializes asynchronously
(waits on canvas refs landing in the store). `onAspectRatioSelect`
early-returns when there's no engine, so the letterbox never gets
the model's default applied.
Move the sync into PageEditor with `[editorEngine, selectedImageModel]`
deps, gated by a ref that records the last synced model id so a later
user pick on the same model isn't clobbered.
For models with `supportsNewAspectRatio()`, the picker only stored a
`CommonAspectRatio` in local state for the API request payload — it
never called `onAspectRatioSelect`, so the editor's render-camera
aspect and the Letterbox stayed on the previous setting.
Wrap the picker callback to also map the chosen `CommonAspectRatio`
to the closest `CameraAspectRatio` and forward it through
`onAspectRatioSelect`. `Auto*` ratios have no fixed shape, so they
leave the letterbox alone.
Export `commonToCameraAspect` so PageEditor can reuse the same
mapping for the cold-load sync.
- 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.