diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index 89e72cec88..69f4b8c9d7 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -259,6 +259,9 @@ export interface EditorProps { sidebarOverlay?: ReactNode viewerBanner?: ReactNode + /** Shown in the preview header. */ + projectName?: string | null + // Panel config (passed through to sidebar panels — v1 only) settingsPanelProps?: SettingsPanelProps sitePanelProps?: SitePanelProps @@ -1254,12 +1257,14 @@ function PreviewStage({ isFirstPersonMode, mode, onModeChange, + projectName, showLoader, viewerContent, }: { isFirstPersonMode: boolean mode: ViewerStageMode onModeChange: (mode: ViewerStageMode) => void + projectName?: string | null showLoader: boolean viewerContent: ReactNode }) { @@ -1286,6 +1291,7 @@ function PreviewStage({ useEditor.getState().setPreviewMode(false)} + projectName={projectName} /> )} @@ -1333,6 +1339,7 @@ function EditorContent({ immersive, sidebarOverlay, viewerBanner, + projectName, settingsPanelProps, sitePanelProps, extraSidebarPanels, @@ -1688,6 +1695,7 @@ function EditorContent({ isFirstPersonMode={isFirstPersonMode} mode={previewStageMode} onModeChange={setPreviewStageMode} + projectName={projectName} showLoader={visibleLoader} viewerContent={previewViewerContent} /> @@ -1767,6 +1775,7 @@ function EditorContent({ isFirstPersonMode={isFirstPersonMode} mode={previewStageMode} onModeChange={setPreviewStageMode} + projectName={projectName} showLoader={visibleLoader} viewerContent={previewViewerContent} /> diff --git a/packages/viewer/src/components/viewer/selection-manager.tsx b/packages/viewer/src/components/viewer/selection-manager.tsx index 48d0e14313..830b6dd683 100644 --- a/packages/viewer/src/components/viewer/selection-manager.tsx +++ b/packages/viewer/src/components/viewer/selection-manager.tsx @@ -302,6 +302,9 @@ export const SelectionManager = () => { // re-subscribe when plugin kinds register after mount (async plugin load) void registryVersion const onEnter = (event: NodeEvent) => { + // Walkthrough is camera-only: first-person controls own hover for the + // interactable target, and pointer lock keeps raycasting from a frozen cursor. + if (useViewer.getState().walkthroughMode) return const strategy = getStrategy() if (!strategy) return // Ceilings are selected via their floor-plan helper and the @@ -325,6 +328,7 @@ export const SelectionManager = () => { } const onLeave = (event: NodeEvent) => { + if (useViewer.getState().walkthroughMode) return const strategy = getStrategy() if (!strategy) return if (event.node.type === 'ceiling') return @@ -341,6 +345,7 @@ export const SelectionManager = () => { } const onClick = (event: NodeEvent) => { + if (useViewer.getState().walkthroughMode) return const strategy = getStrategy() if (!strategy) return if (event.node.type === 'ceiling') return @@ -411,6 +416,7 @@ const PointerMissedHandler = ({ // Only handle left clicks const viewerState = useViewer.getState() if (viewerState.cameraDragging || viewerState.inputDragging) return + if (viewerState.walkthroughMode) return if (event.button !== 0) return // Use requestAnimationFrame to check after R3F event handlers diff --git a/packages/viewer/src/store/use-viewer.test.ts b/packages/viewer/src/store/use-viewer.test.ts index 8548b21cf2..94a85aeffc 100644 --- a/packages/viewer/src/store/use-viewer.test.ts +++ b/packages/viewer/src/store/use-viewer.test.ts @@ -91,3 +91,27 @@ describe('unit focus', () => { expect(useViewer.getState().selection).toBe(selection) }) }) + +describe('scene theme', () => { + afterEach(() => useViewer.getState().setSceneTheme('studio')) + + test('a shown theme renders without replacing the saved one', () => { + useViewer.getState().setSceneTheme('paper') + useViewer.getState().showSceneTheme('night') + + expect(useViewer.getState().sceneTheme).toBe('night') + expect(useViewer.getState().savedSceneTheme).toBe('paper') + + useViewer.getState().showSceneTheme(null) + expect(useViewer.getState().sceneTheme).toBe('paper') + }) + + test('picking a theme while another is shown saves the pick', () => { + useViewer.getState().showSceneTheme('night') + useViewer.getState().setSceneTheme('sunset') + useViewer.getState().showSceneTheme(null) + + expect(useViewer.getState().sceneTheme).toBe('sunset') + expect(useViewer.getState().savedSceneTheme).toBe('sunset') + }) +}) diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts index 834a671866..651bbf6b60 100644 --- a/packages/viewer/src/store/use-viewer.ts +++ b/packages/viewer/src/store/use-viewer.ts @@ -51,7 +51,12 @@ type ViewerState = { setCameraMode: (mode: 'perspective' | 'orthographic') => void sceneTheme: string + /** The user's own pick, and the only theme that persists. */ + savedSceneTheme: string setSceneTheme: (id: string) => void + /** Show a theme without saving it (e.g. a project's own mood); `null` + * returns to the saved one. */ + showSceneTheme: (id: string | null) => void renderContext: RenderContext setRenderContext: (context: RenderContext) => void @@ -368,7 +373,9 @@ const useViewer = create()( setCameraMode: (mode) => set({ cameraMode: mode }), sceneTheme: 'studio', - setSceneTheme: (id) => set({ sceneTheme: id }), + savedSceneTheme: 'studio', + setSceneTheme: (id) => set({ sceneTheme: id, savedSceneTheme: id }), + showSceneTheme: (id) => set((state) => ({ sceneTheme: id ?? state.savedSceneTheme })), renderContext: 'editor', setRenderContext: (context) => set({ renderContext: context }), @@ -560,13 +567,17 @@ const useViewer = create()( }), { name: 'viewer-preferences', - merge: (persistedState, currentState) => ({ - ...currentState, - ...normalizePersistedViewerState(persistedState), - }), + merge: (persistedState, currentState) => { + const persisted = normalizePersistedViewerState(persistedState) + return { + ...currentState, + ...persisted, + savedSceneTheme: persisted.sceneTheme ?? currentState.savedSceneTheme, + } + }, partialize: (state) => ({ cameraMode: state.cameraMode, - sceneTheme: state.sceneTheme, + sceneTheme: state.savedSceneTheme, shadingByContext: state.shadingByContext, textures: state.textures, colorPreset: state.colorPreset,