diff --git a/apps/editor/app/viewer/[id]/viewer-overlay.tsx b/apps/editor/app/viewer/[id]/viewer-overlay.tsx index 03ff3721..d626c39d 100644 --- a/apps/editor/app/viewer/[id]/viewer-overlay.tsx +++ b/apps/editor/app/viewer/[id]/viewer-overlay.tsx @@ -68,6 +68,8 @@ interface ViewerOverlayProps { owner?: ProjectOwner | null canShowScans?: boolean canShowGuides?: boolean + onBack?: () => void + hideCollections?: boolean } export const ViewerOverlay = ({ @@ -75,6 +77,8 @@ export const ViewerOverlay = ({ owner, canShowScans = true, canShowGuides = true, + onBack, + hideCollections, }: ViewerOverlayProps) => { const selection = useViewer((s) => s.selection) const nodes = useScene((s) => s.nodes) @@ -130,12 +134,21 @@ export const ViewerOverlay = ({
{/* Project info + back */}
- - - + {onBack ? ( + + ) : ( + + + + )}
{projectName || 'Untitled'} @@ -248,9 +261,11 @@ export const ViewerOverlay = ({
{/* Collections Panel - Top Right */} -
- -
+ {!hideCollections && ( +
+ +
+ )} {/* Controls Panel - Bottom Center */}
diff --git a/apps/editor/components/editor/custom-camera-controls.tsx b/apps/editor/components/editor/custom-camera-controls.tsx index 21dfb4da..2c3314cd 100644 --- a/apps/editor/components/editor/custom-camera-controls.tsx +++ b/apps/editor/components/editor/custom-camera-controls.tsx @@ -5,14 +5,20 @@ import { useViewer } from '@pascal-app/viewer' import { CameraControls, CameraControlsImpl } from '@react-three/drei' import { useThree } from '@react-three/fiber' import { useCallback, useEffect, useMemo, useRef } from 'react' -import { Vector3 } from 'three' +import { Box3, Vector3 } from 'three' import { EDITOR_LAYER } from '@/lib/constants' +import useEditor from '@/store/use-editor' const currentTarget = new Vector3() +const tempBox = new Box3() +const tempCenter = new Vector3() +const tempSize = new Vector3() export const CustomCameraControls = () => { const controls = useRef(null!) - const currentLevelId = useViewer((state) => state.selection.levelId) + const isPreviewMode = useEditor((s) => s.isPreviewMode) + const selection = useViewer((s) => s.selection) + const currentLevelId = selection.levelId const firstLoad = useRef(true) const camera = useThree((state) => state.camera) @@ -23,6 +29,7 @@ export const CustomCameraControls = () => { }, [camera, raycaster]) useEffect(() => { + if (isPreviewMode) return // Preview mode uses auto-navigate instead let targetY = 0 if (currentLevelId) { const levelMesh = sceneRegistry.nodes.get(currentLevelId) @@ -41,7 +48,7 @@ export const CustomCameraControls = () => { currentTarget.z, true, ) - }, [currentLevelId]) + }, [currentLevelId, isPreviewMode]) // Configure mouse buttons based on control mode and camera mode const cameraMode = useViewer((state) => state.cameraMode) @@ -53,12 +60,14 @@ export const CustomCameraControls = () => { : CameraControlsImpl.ACTION.DOLLY return { - left: CameraControlsImpl.ACTION.NONE, + left: isPreviewMode + ? CameraControlsImpl.ACTION.SCREEN_PAN + : CameraControlsImpl.ACTION.NONE, middle: CameraControlsImpl.ACTION.SCREEN_PAN, right: CameraControlsImpl.ACTION.ROTATE, wheel: wheelAction, } - }, [cameraMode]) + }, [cameraMode, isPreviewMode]) useEffect(() => { const keyState = { @@ -81,11 +90,15 @@ export const CustomCameraControls = () => { ? CameraControlsImpl.ACTION.ZOOM : CameraControlsImpl.ACTION.DOLLY controls.current.mouseButtons.wheel = wheelAction - controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE - if (space) { + if (isPreviewMode) { + // In preview mode, left-click is always pan (viewer-style) controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN + } else if (space) { + controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN + } else { + controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE } } @@ -137,7 +150,62 @@ export const CustomCameraControls = () => { document.removeEventListener('keydown', onKeyDown) document.removeEventListener('keyup', onKeyUp) } - }, [cameraMode]) + }, [cameraMode, isPreviewMode]) + + // Preview mode: auto-navigate camera to selected node (viewer behavior) + const previewTargetNodeId = isPreviewMode + ? (selection.zoneId ?? selection.levelId ?? selection.buildingId) + : null + + useEffect(() => { + if (!isPreviewMode || !controls.current) return + + const nodes = useScene.getState().nodes + let node = previewTargetNodeId ? nodes[previewTargetNodeId] : null + + if (!previewTargetNodeId) { + const site = Object.values(nodes).find((n) => n.type === 'site') + node = site || null + } + if (!node) return + + // Check if node has a saved camera + if (node.camera) { + const { position, target } = node.camera + requestAnimationFrame(() => { + if (!controls.current) return + controls.current.setLookAt( + position[0], position[1], position[2], + target[0], target[1], target[2], + true, + ) + }) + return + } + + if (!previewTargetNodeId) return + + // Calculate camera position from bounding box + const object3D = sceneRegistry.nodes.get(previewTargetNodeId) + if (!object3D) return + + tempBox.setFromObject(object3D) + tempBox.getCenter(tempCenter) + tempBox.getSize(tempSize) + + const maxDim = Math.max(tempSize.x, tempSize.y, tempSize.z) + const distance = Math.max(maxDim * 2, 15) + + controls.current.setLookAt( + tempCenter.x + distance * 0.7, + tempCenter.y + distance * 0.5, + tempCenter.z + distance * 0.7, + tempCenter.x, + tempCenter.y, + tempCenter.z, + true, + ) + }, [isPreviewMode, previewTargetNodeId]) useEffect(() => { const handleNodeCapture = ({ nodeId }: CameraControlEvent) => { diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index ee41bdd3..e3a329e7 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -1,15 +1,18 @@ 'use client' import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core' -import { useViewer, Viewer } from '@pascal-app/viewer' +import { InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer' import { useEffect } from 'react' import { useProjectScene } from '@/features/community/lib/models/hooks' import { useProjectStore } from '@/features/community/lib/projects/store' import { useKeyboard } from '@/hooks/use-keyboard' import { initSFXBus } from '@/lib/sfx-bus' import useEditor from '@/store/use-editor' +import { ViewerOverlay } from '@/app/viewer/[id]/viewer-overlay' +import { ViewerZoneSystem } from '@/app/viewer/[id]/viewer-zone-system' import { FeedbackDialog } from '../feedback-dialog' import { PascalRadio } from '../pascal-radio' +import { PreviewButton } from '../preview-button' import { CeilingSystem } from '../systems/ceiling/ceiling-system' import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system' import { ZoneSystem } from '../systems/zone/zone-system' @@ -102,6 +105,8 @@ export default function Editor({ projectId }: EditorProps) { const isProjectLoading = useProjectStore((state) => state.isLoading) const isSceneLoading = useProjectStore((state) => state.isSceneLoading) const isLoading = isProjectLoading || isSceneLoading + const isPreviewMode = useEditor((s) => s.isPreviewMode) + const activeProject = useProjectStore((s) => s.activeProject) useEffect(() => { if (projectId) { @@ -121,40 +126,56 @@ export default function Editor({ projectId }: EditorProps) { return (
{isLoading && } - - - - {/* Top-right controls */} -
-
- -
-
- -
-
+ {isPreviewMode ? ( + useEditor.getState().setPreviewMode(false)} + /> + ) : ( + <> + + + + + {/* Top-right controls */} +
+
+ +
+
+ +
+
+ +
+
+ + + + + + )} - - - }> - - - + + {!isPreviewMode && } + {!isPreviewMode && } - {/* Editor only system to toggle zone visibility */} - + {/* Swap zone systems: viewer drill-down vs editor layer toggle */} + {isPreviewMode ? : } - {/* */} - - + {!isPreviewMode && ( + + )} + {!isPreviewMode && } - + {!isPreviewMode && } + {isPreviewMode && } - + {!isPreviewMode && }
) diff --git a/apps/editor/components/preview-button.tsx b/apps/editor/components/preview-button.tsx new file mode 100644 index 00000000..97af7158 --- /dev/null +++ b/apps/editor/components/preview-button.tsx @@ -0,0 +1,16 @@ +'use client' + +import { Eye } from 'lucide-react' +import useEditor from '@/store/use-editor' + +export function PreviewButton() { + return ( + + ) +} diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 247a729f..4766e6aa 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -19,6 +19,7 @@ import { Sidebar, SidebarContent, SidebarHeader, + useSidebarStore, } from "@/components/ui/primitives/sidebar"; import { Popover, @@ -108,6 +109,11 @@ export function AppSidebar() { useEffect(() => { setMounted(true); + // Widen default sidebar (288px → 432px) for better project title visibility + const store = useSidebarStore.getState(); + if (store.width <= 288) { + store.setWidth(432); + } }, []); useEffect(() => { diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx index 61191140..1d97d24f 100644 --- a/apps/editor/store/use-editor.tsx +++ b/apps/editor/store/use-editor.tsx @@ -76,6 +76,9 @@ type EditorState = { // Generic hole editing (works for slabs, ceilings, and any future polygon nodes) editingHole: { nodeId: string; holeIndex: number } | null setEditingHole: (hole: { nodeId: string; holeIndex: number } | null) => void + // Preview mode (viewer-like experience inside the editor) + isPreviewMode: boolean + setPreviewMode: (preview: boolean) => void } const useEditor = create()((set, get) => ({ @@ -219,6 +222,16 @@ const useEditor = create()((set, get) => ({ setSpaces: (spaces) => set({ spaces }), editingHole: null, setEditingHole: (hole) => set({ editingHole: hole }), + isPreviewMode: false, + setPreviewMode: (preview) => { + if (preview) { + set({ isPreviewMode: true, mode: 'select', tool: null, catalogCategory: null }) + // Clear zone/item selection for clean viewer drill-down hierarchy + useViewer.getState().setSelection({ selectedIds: [], zoneId: null }) + } else { + set({ isPreviewMode: false }) + } + }, })) export default useEditor