From e120d009ae27ff9ef4fba1bea958a7cfcd6abcbd Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Tue, 12 May 2026 17:41:18 -0700 Subject: [PATCH] pagescene: add view-only mode + anon signup CTA - view-only mode (driven by ownerToken !== currentUserToken): gates engine mutators (add/delete/toggle/transform/color), gizmo attach, delete/undo/redo keybinds, drag-to-add; hides editor chrome (OnboardingHelper, Controls3D, ControlsTopButtons, PromptBox3D, ControlPanelSceneObject, Outliner row toggles, model/cost buttons). - new optional adapter.promptSignup; webapp adapter wires to existing useSignupCta modal. Save / Save-as-copy / Generate / Upload buttons call it when currentUserToken is undefined, so anon visitors hit a signup modal instead of silent failures. - PromptBox3D gains onBeforeSubmit?: () => boolean hook; Stage3DBody uses it to route anon Generate clicks through promptSignup, since the Generate flow bypasses adapter.enqueueGeneration today. --- .../src/pages/pagescene/pagescene.tsx | 3 + .../src/pages/pagescene/web-adapter.tsx | 32 +++++++-- .../libs/components/pagescene/src/index.ts | 2 +- .../pagescene/src/lib/DragAndDrop/DndAsset.ts | 3 +- .../pagescene/src/lib/PageSceneStore.ts | 13 ++++ .../pagescene/src/lib/Stage3DBody.tsx | 69 ++++++++++++------- .../pagescene/src/lib/actions/addCharacter.ts | 2 + .../pagescene/src/lib/actions/addObject.ts | 2 + .../pagescene/src/lib/actions/addShape.ts | 2 + .../src/lib/actions/setObjectColor.ts | 5 ++ .../src/lib/actions/toggleObjectLock.ts | 2 + .../src/lib/actions/toggleObjectVisibility.ts | 2 + .../src/lib/actions/transformObject.ts | 4 ++ .../components/pagescene/src/lib/adapter.ts | 6 ++ .../src/lib/comps/Controls3D/Controls3D.tsx | 11 +++ .../ControlsTopButtons/ControlsTopButtons.tsx | 8 +++ .../src/lib/comps/Outliner/Outliner.tsx | 67 +++++++++--------- .../pagescene/src/lib/engine/editor.ts | 3 +- .../src/lib/engine/editor/GizmoController.ts | 2 + .../src/lib/engine/keybinds_controls.ts | 5 ++ .../promptbox/src/lib/PromptBox3D.tsx | 7 ++ 21 files changed, 184 insertions(+), 66 deletions(-) diff --git a/frontend/apps/artcraft-webapp/src/pages/pagescene/pagescene.tsx b/frontend/apps/artcraft-webapp/src/pages/pagescene/pagescene.tsx index 711646dcdc..dd2518e061 100644 --- a/frontend/apps/artcraft-webapp/src/pages/pagescene/pagescene.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/pagescene/pagescene.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { Stage3D, usePageSceneStore } from "@storyteller/ui-pagescene"; import { useSession } from "../../lib/session"; import { useSidebar } from "../../components/ui/sidebar"; +import { useSignupCta } from "../../components/signup-cta-modal"; import { useWebAppPageSceneAdapter } from "./web-adapter"; // Stage3D fills its parent box; this wrapper is that box. It clamps @@ -27,6 +28,7 @@ export default function PageScene() { const { user } = useSession(); const navigate = useNavigate(); const { setOpen, isMobile } = useSidebar(); + const { openSignupCta } = useSignupCta(); const didAutoCollapseRef = useRef(false); useEffect(() => { @@ -78,6 +80,7 @@ export default function PageScene() { initialSceneToken: sceneToken, navigateToImageTo3D, getViewportSize, + promptSignup: openSignupCta, }); return ( diff --git a/frontend/apps/artcraft-webapp/src/pages/pagescene/web-adapter.tsx b/frontend/apps/artcraft-webapp/src/pages/pagescene/web-adapter.tsx index 45e28f4e50..0ca2dbb7de 100644 --- a/frontend/apps/artcraft-webapp/src/pages/pagescene/web-adapter.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/pagescene/web-adapter.tsx @@ -102,16 +102,27 @@ export interface WebAppPageSceneAdapterOptions { // Wrapper size — kept in a ref so the closure sees live values without // rebuilding the adapter on every resize. getViewportSize: () => { width: number; height: number }; + // Open the host's signup CTA modal. Called from inside the lib when an + // anonymous visitor clicks Save / Generate / Upload / My Library. + promptSignup: (reason?: string) => void; } export const useWebAppPageSceneAdapter = ( options: WebAppPageSceneAdapterOptions, ): PageSceneAdapter => { - const { userToken, initialSceneToken, navigateToImageTo3D, getViewportSize } = options; + const { userToken, initialSceneToken, navigateToImageTo3D, getViewportSize, promptSignup } = options; return useMemo( () => ({ - enqueueGeneration: async () => ({ status: "fail" }), + enqueueGeneration: async () => { + if (!userToken) { + promptSignup("generate"); + return { status: "fail" }; + } + // Generation through the webapp adapter isn't wired up yet — + // signed-in users still hit this path during the editor port. + return { status: "fail" }; + }, saveScene: ({ saveJson, sceneTitle, sceneToken, sceneThumbnail }) => saveSceneViaApi(saveJson, sceneTitle, sceneToken, sceneThumbnail), @@ -124,8 +135,17 @@ export const useWebAppPageSceneAdapter = ( getApiSchemeAndHost: apiHost, getCurrentUserToken: () => userToken, - getCdnUrl: (bucketPath, _width, _quality) => - `${GetCdnOrigin()}${bucketPath}`, + getCdnUrl: (bucketPath, width, quality) => { + const base = GetCdnOrigin(); + const path = bucketPath?.startsWith("/") + ? bucketPath + : `/${bucketPath ?? ""}`; + const params: string[] = []; + if (width) params.push(`width=${width}`); + if (quality) params.push(`quality=${quality}`); + if (params.length === 0) return `${base}${path}`; + return `${base}/cdn-cgi/image/${params.join(",")}${path}`; + }, // The wrapper around creates a fixed-positioning containing // block sized via the webapp's SidebarInset flex layout. Return its @@ -247,6 +267,8 @@ export const useWebAppPageSceneAdapter = ( navigateToImageTo3D, + promptSignup, + performLogout: async () => { try { await new UsersApi().Logout(); @@ -259,6 +281,6 @@ export const useWebAppPageSceneAdapter = ( initialSceneToken, }), - [userToken, initialSceneToken, navigateToImageTo3D, getViewportSize], + [userToken, initialSceneToken, navigateToImageTo3D, getViewportSize, promptSignup], ); }; diff --git a/frontend/libs/components/pagescene/src/index.ts b/frontend/libs/components/pagescene/src/index.ts index 48aad427a4..b2a556e1bd 100644 --- a/frontend/libs/components/pagescene/src/index.ts +++ b/frontend/libs/components/pagescene/src/index.ts @@ -15,7 +15,7 @@ export type { } from "./lib/adapter"; // Store -export { usePageSceneStore } from "./lib/PageSceneStore"; +export { usePageSceneStore, useIsViewOnly, getIsViewOnly } from "./lib/PageSceneStore"; export type { Camera, FocalLengthDragging, diff --git a/frontend/libs/components/pagescene/src/lib/DragAndDrop/DndAsset.ts b/frontend/libs/components/pagescene/src/lib/DragAndDrop/DndAsset.ts index dd39a6304c..af4ff139c2 100644 --- a/frontend/libs/components/pagescene/src/lib/DragAndDrop/DndAsset.ts +++ b/frontend/libs/components/pagescene/src/lib/DragAndDrop/DndAsset.ts @@ -1,6 +1,6 @@ import React from "react"; import { MediaItem } from "../models"; -import { usePageSceneStore } from "../PageSceneStore"; +import { usePageSceneStore, getIsViewOnly } from "../PageSceneStore"; import { AssetType } from "../enums"; import type Editor from "../engine/editor"; import { pickDropPosition } from "../engine/pickDropPosition"; @@ -37,6 +37,7 @@ class DndAsset { item: MediaItem, editor: Editor | null, ) { + if (getIsViewOnly()) return; if (event.button === 0) { this.editor = editor; const store = usePageSceneStore.getState(); diff --git a/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts b/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts index ada650ed9b..b67a8bb043 100644 --- a/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts +++ b/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts @@ -490,3 +490,16 @@ export const usePageSceneStore = create((set, get) => ({ setEditorCanvasEl: (el) => set({ editorCanvasEl: el }), setCamViewCanvasEl: (el) => set({ camViewCanvasEl: el }), })); + +// View-only mode: visitor is looking at someone else's scene (owner is +// known and isn't them). A blank scene has no owner so this returns +// false; the scene owner viewing their own scene returns false too. +const computeIsViewOnly = (s: PageSceneState): boolean => + s.sceneMeta.ownerToken !== undefined && + s.sceneMeta.ownerToken !== s.currentUserToken; + +export const useIsViewOnly = (): boolean => + usePageSceneStore(computeIsViewOnly); + +export const getIsViewOnly = (): boolean => + computeIsViewOnly(usePageSceneStore.getState()); diff --git a/frontend/libs/components/pagescene/src/lib/Stage3DBody.tsx b/frontend/libs/components/pagescene/src/lib/Stage3DBody.tsx index 567f2d6a20..c2f17a5a28 100644 --- a/frontend/libs/components/pagescene/src/lib/Stage3DBody.tsx +++ b/frontend/libs/components/pagescene/src/lib/Stage3DBody.tsx @@ -52,7 +52,7 @@ import { useViewportSize } from "./hooks/useViewportSize"; import { GridVisibleChangedEvent } from "./engine/events/EngineEvent"; import { pickDropPosition } from "./engine/pickDropPosition"; import { AssetType, CameraAspectRatio } from "./enums"; -import { usePageSceneStore } from "./PageSceneStore"; +import { usePageSceneStore, useIsViewOnly } from "./PageSceneStore"; import type { MediaItem } from "./models/assets"; const PAGE_ID: ModelPage = ModelPage.Stage3D; @@ -74,6 +74,7 @@ export const Stage3DBody = () => { (s) => s.setIsPromptBoxFocused, ); const gridVisible = usePageSceneStore((s) => s.gridVisible); + const isViewOnly = useIsViewOnly(); const addCamera = usePageSceneStore((s) => s.addCamera); const updateCamera = usePageSceneStore((s) => s.updateCamera); const deleteCamera = usePageSceneStore((s) => s.deleteCamera); @@ -320,7 +321,7 @@ export const Stage3DBody = () => { return (
- + {!isViewOnly && }
{ - + {!isViewOnly && } -
-
- - + {!isViewOnly && ( +
+
+ + +
-
+ )}
{
- + {!isViewOnly && }
- slot when isViewOnly — shows the + author's generated still next to the camera. */} + + {!isViewOnly && { editor.positive_prompt = prompt; }} snapshotCurrentFrame={editor?.snapShotOfCurrentFrame.bind(editor)} - /> + onBeforeSubmit={() => { + const currentUserToken = + usePageSceneStore.getState().currentUserToken; + if (!currentUserToken && editor?.adapter.promptSignup) { + editor.adapter.promptSignup("generate"); + return false; + } + return true; + }} + />} { message={editorLoader.message} /> -
- -
+ {!isViewOnly && ( +
+ +
+ )}
- + {!isViewOnly && }
diff --git a/frontend/libs/components/pagescene/src/lib/actions/addCharacter.ts b/frontend/libs/components/pagescene/src/lib/actions/addCharacter.ts index 0a3b8eecc6..58e811966b 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/addCharacter.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/addCharacter.ts @@ -3,12 +3,14 @@ import type Editor from "../engine/editor"; import { MediaItem } from "../models"; import { CreateAction } from "../engine/editor/actions/CreateAction"; import { ObjectAddedEvent } from "../engine/events/EngineEvent"; +import { getIsViewOnly } from "../PageSceneStore"; export async function addCharacter( editor: Editor, item: MediaItem, position?: THREE.Vector3, ): Promise { + if (getIsViewOnly()) return undefined; const obj = await editor.sceneManager?.create( item.media_id, item.name ?? "character", diff --git a/frontend/libs/components/pagescene/src/lib/actions/addObject.ts b/frontend/libs/components/pagescene/src/lib/actions/addObject.ts index 48cb5f0209..4056424189 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/addObject.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/addObject.ts @@ -3,12 +3,14 @@ import type Editor from "../engine/editor"; import { MediaItem } from "../models"; import { CreateAction } from "../engine/editor/actions/CreateAction"; import { ObjectAddedEvent } from "../engine/events/EngineEvent"; +import { getIsViewOnly } from "../PageSceneStore"; export async function addObject( editor: Editor, item: MediaItem, position?: THREE.Vector3, ): Promise { + if (getIsViewOnly()) return undefined; const obj = await editor.sceneManager?.create( item.media_id, item.name ?? "object", diff --git a/frontend/libs/components/pagescene/src/lib/actions/addShape.ts b/frontend/libs/components/pagescene/src/lib/actions/addShape.ts index d65542987d..c10cbad858 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/addShape.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/addShape.ts @@ -3,12 +3,14 @@ import type Editor from "../engine/editor"; import { MediaItem } from "../models"; import { CreateAction } from "../engine/editor/actions/CreateAction"; import { ObjectAddedEvent } from "../engine/events/EngineEvent"; +import { getIsViewOnly } from "../PageSceneStore"; export async function addShape( editor: Editor, item: MediaItem, position?: THREE.Vector3, ): Promise { + if (getIsViewOnly()) return undefined; const obj = await editor.sceneManager?.create( "Parim", item.media_id, diff --git a/frontend/libs/components/pagescene/src/lib/actions/setObjectColor.ts b/frontend/libs/components/pagescene/src/lib/actions/setObjectColor.ts index 76263612b0..c0074a347c 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/setObjectColor.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/setObjectColor.ts @@ -1,5 +1,6 @@ import type Editor from "../engine/editor"; import { ColorAction } from "../engine/editor/actions/ColorAction"; +import { getIsViewOnly } from "../PageSceneStore"; // One-shot apply: use when the change is atomic (e.g. a button click // or programmatic setter). Records a single ColorAction. @@ -14,6 +15,7 @@ export function setObjectColor( uuid: string, color: string, ): void { + if (getIsViewOnly()) return; const obj = editor.activeScene.scene.getObjectByProperty("uuid", uuid); if (!obj) return; const before = (obj.userData.color as string) ?? "#ffffff"; @@ -36,6 +38,9 @@ export function beginColorSession( editor: Editor, uuid: string, ): ColorSession { + if (getIsViewOnly()) { + return { apply() {}, commit() {} }; + } const startObj = editor.activeScene.scene.getObjectByProperty( "uuid", uuid, diff --git a/frontend/libs/components/pagescene/src/lib/actions/toggleObjectLock.ts b/frontend/libs/components/pagescene/src/lib/actions/toggleObjectLock.ts index 2f5eb3c660..21c49822e2 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/toggleObjectLock.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/toggleObjectLock.ts @@ -1,6 +1,7 @@ import type Editor from "../engine/editor"; import { LockAction } from "../engine/editor/actions/LockAction"; import { OutlinerItemLockToggledEvent } from "../engine/events/EngineEvent"; +import { getIsViewOnly } from "../PageSceneStore"; // Toggle the locked state of an object: flips userData.locked, runs // the gizmo attach/detach side effect, syncs the outliner row icon in @@ -9,6 +10,7 @@ import { OutlinerItemLockToggledEvent } from "../engine/events/EngineEvent"; // View callers (Outliner) only need to provide editor + uuid; the // action handles the engine + store + history orchestration. export function toggleObjectLock(editor: Editor, uuid: string): void { + if (getIsViewOnly()) return; const before = editor.selection.isObjectLocked(uuid); editor.bus.emit(new OutlinerItemLockToggledEvent(uuid)); editor.selection.lockUnlockObject(uuid); diff --git a/frontend/libs/components/pagescene/src/lib/actions/toggleObjectVisibility.ts b/frontend/libs/components/pagescene/src/lib/actions/toggleObjectVisibility.ts index 54591584c5..439c8723f7 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/toggleObjectVisibility.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/toggleObjectVisibility.ts @@ -1,6 +1,7 @@ import type Editor from "../engine/editor"; import { VisibilityAction } from "../engine/editor/actions/VisibilityAction"; import { OutlinerItemVisibilityToggledEvent } from "../engine/events/EngineEvent"; +import { getIsViewOnly } from "../PageSceneStore"; // Toggle the visibility of an object: flips obj.visible + // userData.visible, syncs the outliner row icon in the Zustand store, @@ -10,6 +11,7 @@ import { OutlinerItemVisibilityToggledEvent } from "../engine/events/EngineEvent // helper reads the before-state from the live engine object so the // view doesn't have to plumb it. export function toggleObjectVisibility(editor: Editor, uuid: string): void { + if (getIsViewOnly()) return; const obj = editor.activeScene.scene.getObjectByProperty("uuid", uuid); if (!obj) return; const before = obj.visible; diff --git a/frontend/libs/components/pagescene/src/lib/actions/transformObject.ts b/frontend/libs/components/pagescene/src/lib/actions/transformObject.ts index 4cdf449645..ede008d0f4 100644 --- a/frontend/libs/components/pagescene/src/lib/actions/transformObject.ts +++ b/frontend/libs/components/pagescene/src/lib/actions/transformObject.ts @@ -1,5 +1,6 @@ import type Editor from "../engine/editor"; import { TransformAction } from "../engine/editor/actions/TransformAction"; +import { getIsViewOnly } from "../PageSceneStore"; // A transform "session" — opened on the first edit of a multi-input // editing flow (e.g. Object Panel position/rotation/scale text fields), @@ -24,6 +25,9 @@ export function beginTransformSession( editor: Editor, uuid: string, ): TransformSession { + if (getIsViewOnly()) { + return { commit() {} }; + } const action = new TransformAction(editor, uuid); return { commit() { diff --git a/frontend/libs/components/pagescene/src/lib/adapter.ts b/frontend/libs/components/pagescene/src/lib/adapter.ts index 228e07afab..3168d3826a 100644 --- a/frontend/libs/components/pagescene/src/lib/adapter.ts +++ b/frontend/libs/components/pagescene/src/lib/adapter.ts @@ -203,6 +203,12 @@ export interface PageSceneAdapter { // callback. Tauri host: setLogoutStates. Web host: web auth flow. performLogout(): void; + // Open the host's signup/login modal. Called when an anonymous + // visitor clicks a feature that requires an account (Save, Generate, + // Upload, "My Library"). Tauri host leaves it undefined — its user + // is always signed in. Webapp host wires this to its auth modal. + promptSignup?(reason?: string): void; + // Optional event hooks — telemetry, host-side modals, tab title sync. onSelectionChange?( sel: { uuid: string; assetType: string } | null, diff --git a/frontend/libs/components/pagescene/src/lib/comps/Controls3D/Controls3D.tsx b/frontend/libs/components/pagescene/src/lib/comps/Controls3D/Controls3D.tsx index 22f7683bd9..602a6cc2f0 100644 --- a/frontend/libs/components/pagescene/src/lib/comps/Controls3D/Controls3D.tsx +++ b/frontend/libs/components/pagescene/src/lib/comps/Controls3D/Controls3D.tsx @@ -40,6 +40,7 @@ export const Controls3D = () => { setAssetModalVisibleDuringDrag, selectedMode, transformSpace, + currentUserToken, } = usePageSceneStore( useShallow((s) => ({ assetModalVisible: s.assetModalVisible, @@ -47,6 +48,7 @@ export const Controls3D = () => { setAssetModalVisibleDuringDrag: s.setAssetModalVisibleDuringDrag, selectedMode: s.selectedMode, transformSpace: s.transformSpace, + currentUserToken: s.currentUserToken, })), ); const [showEmptySceneTooltip, setShowEmptySceneTooltip] = useState(false); @@ -102,6 +104,15 @@ export const Controls3D = () => { }; const handleAddAssetAction = (action: string) => { + const requiresAuth = + action === "library" || + action === "upload-3d" || + action === "upload-image" || + action === "upload-splat"; + if (requiresAuth && !currentUserToken && editor?.adapter.promptSignup) { + editor.adapter.promptSignup(action); + return; + } switch (action) { case "presets": handleOpenModal(); diff --git a/frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/ControlsTopButtons.tsx b/frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/ControlsTopButtons.tsx index 8a25de5e7a..e06a9e041b 100644 --- a/frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/ControlsTopButtons.tsx +++ b/frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/ControlsTopButtons.tsx @@ -59,6 +59,10 @@ export const ControlsTopButtons = () => { // No-op without an editor; the toast is informational only. return; } + if (!currentUserToken && editor.adapter.promptSignup) { + editor.adapter.promptSignup("save"); + return; + } const sceneGenerationMetadata = getSceneGenerationMetaData(editor); const retSceneMediaToken = await editor.saveScene({ @@ -89,6 +93,10 @@ export const ControlsTopButtons = () => { const handleButtonSaveAsCopy = useCallback(async () => { if (!editor) return; + if (!currentUserToken && editor.adapter.promptSignup) { + editor.adapter.promptSignup("save"); + return; + } const sceneGenerationMetadata = getSceneGenerationMetaData(editor); const retSceneMediaToken = await editor.saveScene({ sceneTitle: sceneTitleInput, diff --git a/frontend/libs/components/pagescene/src/lib/comps/Outliner/Outliner.tsx b/frontend/libs/components/pagescene/src/lib/comps/Outliner/Outliner.tsx index 5b99dccc20..d6af441130 100644 --- a/frontend/libs/components/pagescene/src/lib/comps/Outliner/Outliner.tsx +++ b/frontend/libs/components/pagescene/src/lib/comps/Outliner/Outliner.tsx @@ -15,7 +15,7 @@ import { Transition } from "@headlessui/react"; import { twMerge } from "tailwind-merge"; import { useSignals } from "@preact/signals-react/runtime"; import { EngineContext } from "../../contexts/EngineContext"; -import { OutlinerItem, usePageSceneStore } from "../../PageSceneStore"; +import { OutlinerItem, usePageSceneStore, useIsViewOnly } from "../../PageSceneStore"; import { useViewportSize } from "../../hooks/useViewportSize"; import { CameraAspectRatio } from "../../enums"; import { toggleObjectLock } from "../../actions/toggleObjectLock"; @@ -26,6 +26,7 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => { const isSelected = usePageSceneStore( (s) => s.outlinerSelectedItem?.id === item.id, ); + const isViewOnly = useIsViewOnly(); const editorEngine = useContext(EngineContext); @@ -79,37 +80,39 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => {
{item.name} -
- - -
+ {!isViewOnly && ( +
+ + +
+ )}
); }; diff --git a/frontend/libs/components/pagescene/src/lib/engine/editor.ts b/frontend/libs/components/pagescene/src/lib/engine/editor.ts index 5b8c1d1b8f..49cd04c2d1 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/editor.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/editor.ts @@ -20,7 +20,7 @@ import { TransformAction } from "./editor/actions/TransformAction"; import Stats from "three/examples/jsm/libs/stats.module.js"; import { SparkRenderer } from "@sparkjsdev/spark"; -import { usePageSceneStore } from "../PageSceneStore"; +import { usePageSceneStore, getIsViewOnly } from "../PageSceneStore"; import { ensureAmmoLoaded } from "./ammoLoader"; import { EngineEventBus } from "./events/EngineEventBus"; import { EngineStoreBridge } from "./EngineStoreBridge"; @@ -780,6 +780,7 @@ class Editor { } deleteObject(uuid: string) { + if (getIsViewOnly()) return; const obj = this.activeScene.scene.getObjectByProperty("uuid", uuid); if (obj) this.history.record(new DeleteAction(this, obj)); this.mouse_controls?.clearFKVisuals(); diff --git a/frontend/libs/components/pagescene/src/lib/engine/editor/GizmoController.ts b/frontend/libs/components/pagescene/src/lib/engine/editor/GizmoController.ts index 75fe2cc6d4..6e828c94dd 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/editor/GizmoController.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/editor/GizmoController.ts @@ -9,6 +9,7 @@ import type * as THREE from "three"; import { TransformControls } from "../TransformControls.js"; +import { getIsViewOnly } from "../../PageSceneStore"; export type TransformMode = "translate" | "rotate" | "scale"; export type TransformSpace = "world" | "local"; @@ -90,6 +91,7 @@ export class GizmoController { } attach(object: THREE.Object3D) { + if (getIsViewOnly()) return; this.control?.attach(object); } diff --git a/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts b/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts index 223598c709..dddb25825a 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts @@ -21,6 +21,7 @@ import { TransformSpaceChangedEvent, } from "./events/EngineEvent"; import type { PoseMode, TransformSpace } from "../PageSceneStore"; +import { getIsViewOnly } from "../PageSceneStore"; const EDITABLE_INPUT_TYPES = new Set([ "text", @@ -344,6 +345,7 @@ export class MouseControls { this.focus(); return; } else if (event.key === "Backspace" || event.key === "Delete") { + if (getIsViewOnly()) return; if (this.selected) { this.selected.forEach((selected) => { this.deleteObject(selected.uuid); @@ -384,15 +386,18 @@ export class MouseControls { this.deps.bus.emit(new SelectedModeChangedEvent("scale")); return; } else if (event.key === "k") { + if (getIsViewOnly()) return; this.toggleFKMode(); return; } else if (event.key === "b") { + if (getIsViewOnly()) return; // Open asset modal this.deps.bus.emit(new AssetModalVisibilityChangedEvent(true, true)); return; } if ((event.ctrlKey || event.metaKey) && !this.isProcessing) { + if (getIsViewOnly()) return; const keyLower = event.key.toLowerCase(); if (keyLower === "z" && !event.shiftKey) { // undo diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx index 70fcf54ddc..aa81eaba97 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx @@ -74,6 +74,9 @@ interface PromptBox3DProps { } | null) | undefined; credits?: number | null; + // Optional pre-submit gate. Returns false to abort the generation + // (e.g. host wants to open a signup modal for anon visitors). + onBeforeSubmit?: () => boolean; } export const PromptBox3D = ({ @@ -100,6 +103,7 @@ export const PromptBox3D = ({ selectedProvider, snapshotCurrentFrame, credits, + onBeforeSubmit, }: PromptBox3DProps) => { //const fileInputRef = useRef(null); const textareaRef = useRef(null); @@ -275,6 +279,9 @@ export const PromptBox3D = ({ const maxLen = selectedImageModel?.maxPromptLength ?? 1000; const handleEnqueue = async () => { + if (onBeforeSubmit && !onBeforeSubmit()) { + return; + } if (isFinite(maxLen) && prompt.length > maxLen) { toast.error(`Prompt exceeds the ${maxLen} character limit for this model`); return;