'use client' import { emitter, sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { CameraControls, CameraControlsImpl } from '@react-three/drei' import { useCallback, useEffect, useMemo, useRef } from 'react' import { Box3, Vector3 } from 'three' const tempBox = new Box3() const tempCenter = new Vector3() const tempSize = new Vector3() export const ViewerCameraControls = () => { const controls = useRef(null!) const selection = useViewer((s) => s.selection) const nodes = useScene((s) => s.nodes) const cameraMode = useViewer((s) => s.cameraMode) const firstLoad = useRef(true) // Get the deepest selected node ID (excluding selectedIds) const targetNodeId = selection.zoneId ?? selection.levelId ?? selection.buildingId // Configure mouse buttons - same as editor const mouseButtons = useMemo(() => { const wheelAction = cameraMode === 'orthographic' ? CameraControlsImpl.ACTION.ZOOM : CameraControlsImpl.ACTION.DOLLY return { left: CameraControlsImpl.ACTION.SCREEN_PAN, middle: CameraControlsImpl.ACTION.SCREEN_PAN, right: CameraControlsImpl.ACTION.ROTATE, wheel: wheelAction, } }, [cameraMode]) useEffect(() => { if (!controls.current) return // On first load, set a default camera position if (firstLoad.current) { firstLoad.current = false controls.current.setLookAt(30, 30, 30, 0, 0, 0, false) } let node = targetNodeId ? nodes[targetNodeId] : null; if (!targetNodeId) { 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(() => { controls.current.setLookAt( position[0], position[1], position[2], target[0], target[1], target[2], true, ) }) return } if (!targetNodeId) { // No selection and no site - do nothing return } // Calculate camera position based on the node's 3D object const object3D = sceneRegistry.nodes.get(targetNodeId) if (!object3D) return // Compute bounding box tempBox.setFromObject(object3D) tempBox.getCenter(tempCenter) tempBox.getSize(tempSize) // Calculate a good viewing distance based on the object size const maxDim = Math.max(tempSize.x, tempSize.y, tempSize.z) const distance = Math.max(maxDim * 2, 15) // Position camera at an angle looking at the center const cameraPos = new Vector3( tempCenter.x + distance * 0.7, tempCenter.y + distance * 0.5, tempCenter.z + distance * 0.7, ) controls.current.setLookAt( cameraPos.x, cameraPos.y, cameraPos.z, tempCenter.x, tempCenter.y, tempCenter.z, true, ) }, [targetNodeId, nodes]) useEffect(() => { const handleTopView = () => { if (!controls.current) return const currentPolarAngle = controls.current.polarAngle // Toggle: if already near top view (< 0.1 radians ≈ 5.7°), go back to 45° // Otherwise, go to top view (0°) const targetAngle = currentPolarAngle < 0.1 ? Math.PI / 4 : 0 controls.current.rotatePolarTo(targetAngle, true) } const handleOrbitCW = () => { if (!controls.current) return const currentAzimuth = controls.current.azimuthAngle const currentPolar = controls.current.polarAngle // Round to nearest 90° increment, then rotate 90° clockwise const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2) const target = rounded - Math.PI / 2 controls.current.rotateTo(target, currentPolar, true) } const handleOrbitCCW = () => { if (!controls.current) return const currentAzimuth = controls.current.azimuthAngle const currentPolar = controls.current.polarAngle // Round to nearest 90° increment, then rotate 90° counter-clockwise const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2) const target = rounded + Math.PI / 2 controls.current.rotateTo(target, currentPolar, true) } emitter.on('camera-controls:top-view', handleTopView) emitter.on('camera-controls:orbit-cw', handleOrbitCW) emitter.on('camera-controls:orbit-ccw', handleOrbitCCW) return () => { emitter.off('camera-controls:top-view', handleTopView) emitter.off('camera-controls:orbit-cw', handleOrbitCW) emitter.off('camera-controls:orbit-ccw', handleOrbitCCW) } }, []) const onTransitionStart = useCallback(() => { useViewer.getState().setCameraDragging(true) }, []) const onRest = useCallback(() => { useViewer.getState().setCameraDragging(false) }, []) return ( ) }