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