'use client' import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core' import type { Object3D } from 'three' import { create } from 'zustand' import { persist } from 'zustand/middleware' import type { EdgeMode } from '../lib/edge-style' import type { ColorPreset, RenderShading } from '../lib/materials' export type RenderContext = 'editor' | 'viewer' type SelectionPath = { buildingId: BuildingNode['id'] | null levelId: LevelNode['id'] | null zoneId: ZoneNode['id'] | null selectedIds: BaseNode['id'][] // For items/assets (multi-select) } type Outliner = { selectedObjects: Object3D[] hoveredObjects: Object3D[] } type ViewerState = { selection: SelectionPath previewSelectedIds: BaseNode['id'][] setPreviewSelectedIds: (ids: BaseNode['id'][]) => void hoverHighlightMode: string setHoverHighlightMode: (mode: string) => void hoveredId: AnyNode['id'] | ZoneNode['id'] | null setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void cameraMode: 'perspective' | 'orthographic' setCameraMode: (mode: 'perspective' | 'orthographic') => void sceneTheme: string setSceneTheme: (id: string) => void renderContext: RenderContext setRenderContext: (context: RenderContext) => void shading: RenderShading shadingByContext: Partial> setShading: (shading: RenderShading) => void textures: boolean setTextures: (textures: boolean) => void colorPreset: ColorPreset setColorPreset: (preset: ColorPreset) => void edges: EdgeMode setEdges: (edges: EdgeMode) => void shadows: boolean setShadows: (shadows: boolean) => void unit: 'metric' | 'imperial' setUnit: (unit: 'metric' | 'imperial') => void levelMode: 'stacked' | 'exploded' | 'solo' | 'manual' setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void wallMode: 'up' | 'cutaway' | 'down' setWallMode: (mode: 'up' | 'cutaway' | 'down') => void showScans: boolean setShowScans: (show: boolean) => void showGuides: boolean setShowGuides: (show: boolean) => void showGrid: boolean setShowGrid: (show: boolean) => void projectId: string | null setProjectId: (id: string | null) => void projectPreferences: Record< string, { showScans?: boolean; showGuides?: boolean; showGrid?: boolean } > // Smart selection update setSelection: (updates: Partial) => void resetSelection: () => void outliner: Outliner // No setter as we will manipulate directly the arrays // Export functionality exportScene: ((format?: 'glb' | 'stl' | 'obj') => Promise) | null setExportScene: (fn: ((format?: 'glb' | 'stl' | 'obj') => Promise) | null) => void debugColors: boolean setDebugColors: (enabled: boolean) => void walkthroughMode: boolean setWalkthroughMode: (mode: boolean) => void cameraDragging: boolean setCameraDragging: (dragging: boolean) => void /** * True while a host-driven drag is in progress (editor handles — * height arrow, width arrow, etc.). Suppresses node pointer event * routing so the synthetic click on pointerup doesn't reroute * selection to whatever mesh the cursor lands on at release. * Conceptually a sibling of `cameraDragging` — both mean "user is * dragging; don't treat the next pointerup as a click on the * scene." Set by the host (e.g. `NodeArrowHandles` in the editor); * the viewer only reads it. */ inputDragging: boolean setInputDragging: (dragging: boolean) => void } const useViewer = create()( persist( (set) => ({ selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] }, previewSelectedIds: [], setPreviewSelectedIds: (ids) => set({ previewSelectedIds: ids }), hoverHighlightMode: 'default', setHoverHighlightMode: (mode) => set((state) => (state.hoverHighlightMode === mode ? state : { hoverHighlightMode: mode })), hoveredId: null, setHoveredId: (id) => set((state) => (state.hoveredId === id ? state : { hoveredId: id })), cameraMode: 'perspective', setCameraMode: (mode) => set({ cameraMode: mode }), sceneTheme: 'studio', setSceneTheme: (id) => set({ sceneTheme: id }), renderContext: 'editor', setRenderContext: (context) => set({ renderContext: context }), shading: 'rendered', shadingByContext: {}, setShading: (shading) => set((state) => ({ shading, shadingByContext: { ...state.shadingByContext, [state.renderContext]: shading }, })), textures: true, setTextures: (textures) => set({ textures }), colorPreset: 'clay', setColorPreset: (preset) => set({ colorPreset: preset }), edges: 'soft', setEdges: (edges) => set({ edges }), shadows: true, setShadows: (shadows) => set({ shadows }), unit: 'metric', setUnit: (unit) => set({ unit }), levelMode: 'stacked', setLevelMode: (mode) => set({ levelMode: mode }), wallMode: 'up', setWallMode: (mode) => set({ wallMode: mode }), showScans: true, setShowScans: (show) => set((state) => { const projectPreferences = { ...(state.projectPreferences || {}) } if (state.projectId) { projectPreferences[state.projectId] = { ...(projectPreferences[state.projectId] || {}), showScans: show, } } return { showScans: show, projectPreferences } }), showGuides: true, setShowGuides: (show) => set((state) => { const projectPreferences = { ...(state.projectPreferences || {}) } if (state.projectId) { projectPreferences[state.projectId] = { ...(projectPreferences[state.projectId] || {}), showGuides: show, } } return { showGuides: show, projectPreferences } }), showGrid: true, setShowGrid: (show) => set((state) => { const projectPreferences = { ...(state.projectPreferences || {}) } if (state.projectId) { projectPreferences[state.projectId] = { ...(projectPreferences[state.projectId] || {}), showGrid: show, } } return { showGrid: show, projectPreferences } }), projectId: null, setProjectId: (id) => set((state) => { if (!id) return { projectId: id } const prefs = state.projectPreferences?.[id] || {} return { projectId: id, showScans: prefs.showScans ?? true, showGuides: prefs.showGuides ?? true, showGrid: prefs.showGrid ?? true, } }), projectPreferences: {}, setSelection: (updates) => set((state) => { const newSelection = { ...state.selection, ...updates } // Hierarchy Guard: If we change a high-level parent, reset the children unless explicitly provided if (updates.buildingId !== undefined) { if (updates.levelId === undefined) newSelection.levelId = null if (updates.zoneId === undefined) newSelection.zoneId = null if (updates.selectedIds === undefined) newSelection.selectedIds = [] } if (updates.levelId !== undefined) { if (updates.zoneId === undefined) newSelection.zoneId = null if (updates.selectedIds === undefined) newSelection.selectedIds = [] } if (updates.zoneId !== undefined) { if (updates.selectedIds === undefined) newSelection.selectedIds = [] } return { selection: newSelection, previewSelectedIds: [] } }), resetSelection: () => set({ selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [], }, previewSelectedIds: [], }), outliner: { selectedObjects: [], hoveredObjects: [] }, exportScene: null, setExportScene: (fn) => set({ exportScene: fn }), debugColors: false, setDebugColors: (enabled) => set({ debugColors: enabled }), walkthroughMode: false, setWalkthroughMode: (mode) => set({ walkthroughMode: mode }), cameraDragging: false, setCameraDragging: (dragging) => set({ cameraDragging: dragging }), inputDragging: false, setInputDragging: (dragging) => set({ inputDragging: dragging }), }), { name: 'viewer-preferences', partialize: (state) => ({ cameraMode: state.cameraMode, sceneTheme: state.sceneTheme, shadingByContext: state.shadingByContext, textures: state.textures, colorPreset: state.colorPreset, edges: state.edges, shadows: state.shadows, unit: state.unit, levelMode: state.levelMode, wallMode: state.wallMode, projectPreferences: state.projectPreferences, }), }, ), ) export default useViewer