The lib now owns the canonical PageSceneStore (Zustand), the
PageScene-specific enums (AssetType, AssetFilterOption, ClipGroup,
CameraAspectRatio, EditorStates), the common datastructures
(XYZ + Simple3DVector), and a minimal models surface (MediaItem +
AudioMediaItem -- enough for the store; richer model types stay in
apps/PageScene/models for now and migrate alongside their consumers).
Old artcraft locations become re-export shims so 56+ existing
importers keep working unchanged during the staged migration:
- apps/.../enums/{AssetType,AssetFilterOption,ClipGroup}.ts re-export
from @storyteller/ui-pagescene. Single nominal identity for each
enum across both lib and host -- avoids the "two AssetType types
that look the same but TypeScript treats as distinct" trap when
values flow across the boundary (e.g. ImageTo3DExperience passes
AssetType.SPLAT into PageScene's addObject).
- apps/.../pages/PageScene/PageSceneStore.ts re-exports the store +
types from the lib.
artcraft/tsconfig.json gains a `references` entry for
libs/components/pagescene so TypeScript resolves the package.
node_modules/@storyteller/ui-pagescene gets its workspace symlink
on first `npm install`; created manually here for the typecheck.
Verified: total typecheck error count unchanged (124, all pre-existing)
-- no new errors from the move.
The mirror ref served two purposes that turned out to be unnecessary:
the double-create guard `if (activeEditorRef.current) return;` is
unreachable (cleanup nulls the ref before the next effect body runs,
under both normal lifecycle and React 18 StrictMode), and the
cleanup-side fetch can read the editor through closure capture of
the local `newEditor` variable instead.
cacheRef and onSerializeRef stay -- those are latest-prop holders so
the effect can read fresh values on cleanup without putting the
props in deps (which would tear down + rebuild the engine on every
prop change).
PageScene's engine lifecycle was previously tab-aware: EngineProvider
read tabStore.activeTabId to decide whether to construct or dispose
the Editor, and reached into tabStore.getTabData / updateTabData for
in-memory scene save+restore across tab switches. That hack predates
the post-Stage-A architecture; with the engine event bus + decoupled
Zustand store in place, we can drive the lifecycle from React's own
mount/unmount + a clean prop interface.
Changes:
- EngineProvider takes cacheJsonString + onSceneSerialized props
instead of importing useTabStore. The construct branch fires once
the three canvas DOM nodes are populated (sceneContainerEl,
editorCanvasEl, camViewCanvasEl); the cleanup branch (returned from
useEffect) snapshots the scene via save_manager.getSceneJson and
hands the serialized JSON back through onSceneSerialized before
unmountEngine. No knowledge of tabs.
- SceneContainer + EditorCanvas + CameraViewCanvas callback refs no
longer drop the unmount-side null. Previously `if (node) { ... }`
swallowed the null on canvas unmount, leaving stale DOM refs in the
store and leaking the Editor. Now they pass null through, which
drives the EngineProvider cleanup effect when the canvases unmount
(e.g. on tab switch away from 3D).
- PageScene.tsx wraps the new prop interface: reads cacheJsonString
from useTabStore.getTabData("3D") on render, supplies an
onSceneSerialized that calls tabStore.updateTabData("3D", json).
The tab integration moves up one layer; the engine itself is
storage-agnostic and ready to drop into the @storyteller/ui-pagescene
lib without modification.
- Deletes pose/CachedPoseLandmarker.ts and pose/CachedFilesetResolver.ts.
These wrapped @mediapipe/tasks-vision but were fully orphaned (only
referenced each other; no callers anywhere). FK + KinHelpers stay --
bone manipulation is still in use, only the MediaPipe-driven pose
detection is gone.
Empty Nx library at frontend/libs/components/pagescene/ mirroring the
@storyteller/ui-pagedraw shape (package.json with development export,
tsconfig.lib.json with workspace lib references, vite.config.ts).
Defines the platform-abstraction surface PageSceneAdapter:
- Generation enqueue (model/provider/prompt/tokens/aspect/resolution)
- Scene I/O (saveScene, loadScene, fetchAsset, host CDN/API getters)
- Slot renderers for host-owned modals (renderAssetBrowser,
renderSceneLoader)
- Optional event hooks (onError, onSceneSaved, onSceneTitleChange,
onEnqueueMeta, onSelectionChange, onSceneDirty)
- Tab-cache integration (cacheJsonString + onSceneSerialized) so the
artcraft useTabStore can roundtrip serialized scene JSON without
the lib knowing about tabs
Library code is empty for now -- file migrations (PageSceneStore +
engine + comps + actions + hooks etc.) land in follow-up commits on
this branch.
PageScene engine no longer writes to PageSceneStore directly. Engine
subsystems emit typed EngineEvents on a per-event-type bus; a single
EngineStoreBridge subscribes per event class and is the only file in
engine/ that imports usePageSceneStore. Write flow is one-way: UI
toggles emit on the bus, the bridge updates the store, an engine-side
reactor updates the THREE side. No store->engine subscriptions remain.
EngineEventBus is keyed by constructor reference (Map<EventCtor,
Set<Listener>>) so emit() walks only listeners for that event type --
no global fan-out, no instanceof chains. Mirrors Chromium's per-type
EventListenerMap pattern.
MouseControls constructor refactored from 20 positional Function args
to a MouseControlsDeps object before adding bus. SceneUtils,
SaveManager, CameraController, and Scene now take Deps objects with
an explicit bus field.
Lifecycle helpers (set3DPageMounted etc.) moved out of editor.ts into
a non-engine engineLifecycle.ts so engine/ stays store-free. Same for
SceneMetadata (now sceneMetadata.ts at the page root).
Action dispatchers no longer dual-write: addCharacter/addObject/
addShape/setCameraAspect/setTransformMode/toggleObjectLock/
toggleObjectVisibility now emit a single bus event each.
Prerequisite for componentizing PageScene into @storyteller/ui-pagescene.
- Move to zustand stores from preact signals
- Delete dead code and unused files
- Remove addLocalSplat and follow gallery flow
- Get rid of Timeline components and queues, move to actions
- Lightbox now shows full media metadata: Aspect Ratio, Resolution,
Size, Duration, Audio, and Created (with timestamp).
- Added Author header at the top so the close button no longer overlaps
the Prompt copy button.
- Extracted aspect ratio / resolution / duration formatters into
`@storyteller/common` so the website and desktop app share them.
Customers occasionally saw "0 Credits" with no signal that the fetch had
failed silently. Add a status badge on the coin (slow/failed/ recovered,
hidden in the happy path), a 60s background refresh, and gate the
initial fetch on AUTH_STATUS.LOGGED_IN to remove the 1s setTimeout
band-aid that was racing intermediate auth states.
Only the submit button triggers generation by default. The "Enter to
generate" toggle (available in both app and website settings top right
nav) still allows opting into Enter-to-submit.