Reworks scene-cache-store from a single playground-scoped string to a
Record<string, { original?, current }> keyed by sceneToken (with a
PLAYGROUND_KEY constant for the no-token slot). Visiting /edit-3d/A,
editing, jumping to /edit-3d/B, then coming back to /edit-3d/A
restores A's in-progress state — the cache hit short-circuits the
server fetch inside Editor.initialize.
The adapter's loadSceneViaApi seeds `original` on first server fetch
so the lib's "Reset to original" menu item has the snapshot it needs.
resetToOriginal pulls that snapshot, hands it to editor.applyJson
(which replaces the scene and clears undo history), and mirrors
`current = original` so a pre-serialization reload can't drag the
discarded edits back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Editor.applyJson(jsonString) that delegates to the existing
save_manager.loadCache JSON-deserialize path and additionally clears
the undo stack and emits SceneResetEvent. Used by the new Reset flow
so the user can't "undo" a reset back into their discarded edits.
PageSceneAdapter gains an optional resetToOriginal() hook. When the
host implements it AND a sceneToken is loaded, the File menu shows a
destructive "Reset to original" item that opens a confirm modal and
then defers to the host (which knows where to find the original
snapshot — the webapp's per-token cache lands in a follow-up commit).
Tauri leaves the hook off so nothing changes there.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A visitor of someone else's scene now sees the File menu's primary
"Save scene" item relabeled to "Save copy". Click → opens the existing
Save-as-copy dialog (name input + Save button) which forks the scene to
the visitor's own account via the existing saveAsCopy code path. Anon
visitors still get the signup CTA on click instead of the dialog.
The standalone "Save scene as copy" entry is hidden when visiting so
the two paths don't read as redundant for owners. canSave is loosened
to always-true for visitors so forking an unmodified scene
("favorite to my account") works as well.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Visitors can now fully interact with someone else's scene — move objects,
add shapes, edit colors, etc. The previous strict view-only mode is gone.
Save remains the only auth/ownership-gated action; its rewiring lands in
a follow-up.
Renames computeIsViewOnly to computeIsVisitingOthersScene to reflect the
remaining role (it only drives the PreviewBox today; Save-copy label and
Reset button consume it next). Drops getIsViewOnly()/useIsViewOnly()
early-returns from engine actions, drag-drop, gizmo attach, keybinds,
and the Outliner row controls. Strips !isViewOnly wrappers from chrome
in Stage3DBody so the prompt box, controls, panel, and model selector
show for everyone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two unrelated noise sources after wiring the editor into the webapp:
1. FontLoader was fetching /resources/fonts/CarterOneRegular.json,
which exists in the Tauri public/ but not in the webapp's. Vite's
SPA fallback returns index.html (200, HTML body), the FontLoader's
progress callback fires "Font Loaded: 1" three times as it streams,
then JSON.parse chokes on the leading `<` — surfaces as an
unhandled promise rejection from LoadingPlaceHolderManager.
Copy the same font asset into the webapp's public/ tree (same
pattern Tauri and editor3d already use).
2. Editor.initialize() reads sceneContainerEl.offsetWidth/offsetHeight
to size the WebGLRenderer + post-processing pipeline. On the
webapp the flex chain (SidebarInset → wrapper → SceneContainer)
isn't always laid out by first call, so the dimensions are
transiently 0×0. setSize(0,0) then configureMain(0,0) allocates
six post-process framebuffer attachments at 0 size, each emitting
INVALID_FRAMEBUFFER_OPERATION until the next window.resize event
cascades through and resizes them properly. Clamp the initial
container offset to ≥1 (Math.max(1, ...)) and replace the
`?? 0` fallbacks on the canvas dimensions with `|| 1`. First
frame renders into a harmless 1×1, next resize fixes everything.
The 3D editor lib already exposes cacheJsonString + onSceneSerialized
props (Tauri wires them through useTabStore so per-tab scene state
survives tab switches). The webapp wasn't using them, so navigating
to /gallery, /create-image, etc. and back blew away whatever the user
had been building in the playground.
Add a small zustand store (mirrors enter-to-generate-store.ts) backed
by sessionStorage so the serialized JSON survives both SPA navigation
and tab reloads. Read cacheJsonString once via getState() on mount so
re-renders don't restart the engine; write back via the lib's
onSceneSerialized callback on unmount.
Scoped to the no-token playground: when sceneToken is in the URL the
server-loaded scene is the source of truth, so we neither restore
from cache (would override a shared scene) nor write the session back
into it (would later resurrect a stranger's scene as the playground).
PromptBox3D's handleWebEnqueue was POSTing to /v1/image_studio/prompt
via PromptsApi.enqueueImageGeneration, but that endpoint does not exist
on the Rust backend — image_studio_routes.rs only registers
/scene_snapshot and /update_job_status under /v1/image_studio (the
remaining routes live under legacy_routes/). The webapp's /create-image
and other generation flows already use OmniGenApi.generateImage hitting
/v1/omni_gen/image/generate; this commit aligns the 3D editor with that
modern path.
- Snapshot upload still goes through PromptsApi.uploadSceneSnapshot
(that endpoint is the one image_studio route still in use).
- The returned snapshot media token is prepended to image_media_tokens
ahead of the user's reference images, matching how the Tauri path
treats the scene snapshot as the primary input.
- aspect_ratio uses commonAspectRatio for models that support the new
shape, falling back to getCurrentAspectRatio() otherwise.
- On success: toast "Image added to queue" plus the same
__storeTaskEnqueueMeta telemetry the Tauri path fires.
- Failure path routes through the existing handleError modal.
- Drops a dead 2-second setTimeout that delayed the success toast.
Tauri path unchanged — IsDesktopApp() still gates handleTauriEnqueue,
which goes through Tauri IPC and bypasses storyteller-web entirely.
The Color section in ControlPanelSceneObject had two issues that
compounded into a "no picker shows up" symptom in Safari:
1. setColor(editorEngine?.selected?.userData.color) wrote `undefined`
into React state for shapes that never had a color set, which made
<input value={undefined}> uncontrolled and gave the Button no inline
background color (it fell back to the primary variant, so the swatch
looked like a generic button instead of a white swatch).
2. The hidden <input type="color"> was a sibling of the swatch
<label htmlFor={id}>, with h-0 w-0 opacity-0. Safari refuses to open
the native color picker when the label's target input has no layout
box — Chrome/Firefox tolerate it, Safari doesn't.
Fixes:
- Default the color state to "#ffffff" when userData.color is unset
(both at setColor time and at value={} time for belt-and-braces).
- Seed userData.color = "#ffffff" at shape creation in addShape so
the default carries through save/load roundtrips too.
- Move the <input type="color"> to be a CHILD of the Button label
and position it absolute inset-0 over the swatch. Click lands
directly on the input (no label[htmlFor] indirection Safari is
flaky about) and the input now has a real full-size layout box.
- Gate File → Load my scene for anon: route through promptSignup
instead of opening a dialog that would just error-toast on the
silent listUserMediaFiles failure.
- Remove dead SettingsModal mount in Controls3D (no UI ever triggered
it). Drops the @storyteller/ui-settings-modal import from this file.
- New AnonHintChip pill in the top-right corner of the playground
("Sign up to save"). Renders only when currentUserToken is undefined
AND not in view-only mode; click routes to promptSignup. Tauri users
never see it.
- Scaffolding for view-only PreviewBox: new component renders a small
card with the author's generation result; returns null when no URL
is supplied. Mounted in Stage3DBody under the isViewOnly guard,
reading sceneMeta.previewImageUrl (new optional field). Data-path
wiring (populating previewImageUrl from adapter.loadScene's
response) is the next step — leaving the UI plumbing in place so
that follow-up is purely data, not layout surgery.
useFeaturedObjects' auto-fetch effect synchronously flipped
firstFetch.current to IN_PROGRESS before calling fetchFeaturedObjects.
On AssetModal's first mount the EngineContext editor is null, so the
fetch bailed at `if (!editor) return`. When the editor later became
available the effect re-fired (editor is in fetchFeaturedObjects'
useCallback deps), but the IN_PROGRESS guard blocked it — the network
request never fired for any featured category. CHARACTER/OBJECT tabs
appeared populated only because they're merged with local demo arrays
inside AssetModal's assetTabs useMemo; Sets and Creatures have no demo
fallback so the bug was visible there.
Move the editor check into the auto-fetch effect itself and add editor
to its deps, so IN_PROGRESS isn't burned on the no-op call. Regression
from PR #1492.
- styles.css: move .glass into @layer components so Tailwind's
.absolute utility wins the cascade. OnboardingHelper was rendering
position: relative and pushing the editor down on first paint until
clicking dismissed it.
- SceneContainer + Stage3DBody: drop hardcoded calc(100vh - 68px) and
height - 56 topbar offsets; the lib now fills its parent container.
- Webapp adapter: drop the +56 getViewportSize compensation now that
the lib doesn't subtract a topbar from the supplied height.
- Tauri PageScene: wrap Stage3D in a calc(100vh - 56px) container
(matching .topbar-spacer) so its shape is preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds /pagescene (and /pagescene/:sceneToken) under the webapp's public
chrome (inside ProtectedLayout, outside RequireAuth) so anonymous users
can view scenes too. Adapter implementation mirrors the website's, with
two webapp-specific tweaks: imports the local useSession / showToast /
invalidateSession helpers, and getViewportSize returns the wrapper's
live ResizeObserver-measured rect so DnD hit-testing matches the visible
canvas (not window.innerWidth).
Sidebar handling: the lib's position-fixed UI is scoped to a wrapper
div with transform: translateZ(0), which creates a new containing block.
The wrapper renders inside SidebarInset's flex flow, so the editor sits
naturally to the right of the sidebar and below the topbar; expanded-
sidebar behavior is deferred (currently shrinks the page further per the
flex layout).
Sidebar nav gets an "Edit 3D" entry between Image and Video. Tailwind
config gets brand-primary/secondary/tertiary, action color, red/green/
blue DEFAULTs; styles.css adds .bg-ui-panel-gradient, .prevent-select,
.dot-typing — same gap-fills landed on the website.
No new endpoints. tsconfig churn from nx sync.
useTauriPageSceneAdapter, GlobalFileDropHandler, and UploadImagesButton
now pull UploadModal3D/Image/Splat from the shared lib. Removes the
host-local UploadModal3D/UploadModalImage/UploadModalSplat directories
since they're no longer referenced anywhere.
UploadModalMedia/ stays — it backs the host-only image-plane upload
adapter helpers, not the modal slots.
Hoists the three feature-specific upload modals out of the artcraft
Tauri host into @storyteller/ui-upload-modal so the website (and any
future host) can render the same flows through PageScene's adapter
slots. Replaces direct `~/Classes/ApiManager` calls with @storyteller/api's
MediaUploadApi/MediaFilesApi.
Removes the old lib-internal UploadModal/UploadFiles/uploadAssets
placeholders (unused) and inlines the host-local enums
(MediaFileAnimationType, UploaderState/States, file-type enums) into
Types.ts. Adds UploadSpzFile to @storyteller/api's MediaUploadApi and
the SPLAT variant to FilterEngineCategories.
When the stored `commonAspectRatio` (e.g. `SquareHd`) isn't in the
newly-selected model's `aspectRatios`, `PopoverMenu` toggle mode found
no selected item and collapsed the trigger to icon-only.
Reset to `model.defaultAspectRatio` from inside `AspectRatioPicker` so
the fix applies to all three callers (image/edit/3D). Drops the
duplicate workaround from `PromptBox3D`.
- added make video button to the create image page gallery card action
button row and the lightbox modal
- clicking it will redirect to the create video page and insert the
image as a reference automatically