import { OrbitControls, PerspectiveCamera } from '@react-three/drei'; import { useFrame, useThree } from '@react-three/fiber'; import gsap from 'gsap'; import { useEffect, useRef } from 'react'; import { PerspectiveCamera as PerspectiveCameraImpl, Vector3 } from 'three'; import { useReducedMotionPreference } from '../hooks/useReducedMotionPreference'; import { useViewerStore } from '../store/viewerStore'; import type { CameraSnapshot } from '../types/viewer'; import { DEFAULT_CAMERA_SNAPSHOT, createCameraSnapshotForPreset } from './cameraPresets'; export function CameraController() { const camera = useThree((state) => state.camera) as PerspectiveCameraImpl; const controlsRef = useRef(null); const lastRequestIdRef = useRef(null); const lastSnapshotWriteRef = useRef(0); const reducedMotion = useReducedMotionPreference(); const sceneBounds = useViewerStore((state) => state.sceneBounds); const cameraRequest = useViewerStore((state) => state.cameraRequest); const cameraSnapshot = useViewerStore((state) => state.camera); const setCameraSnapshot = useViewerStore((state) => state.setCameraSnapshot); useEffect(() => { applyCameraSnapshot(camera, controlsRef.current, cameraSnapshot, true); }, []); useEffect(() => { if (!cameraRequest || lastRequestIdRef.current === cameraRequest.id) { return; } lastRequestIdRef.current = cameraRequest.id; const snapshot = cameraRequest.type === 'preset' ? createCameraSnapshotForPreset(cameraRequest.preset, sceneBounds) : cameraRequest.type === 'reset' ? createCameraSnapshotForPreset('isometric', sceneBounds) : cameraRequest.snapshot; animateCameraToSnapshot(camera, controlsRef.current, snapshot, reducedMotion); }, [camera, cameraRequest, reducedMotion, sceneBounds]); useFrame(({ clock }) => { const elapsed = clock.getElapsedTime(); if (elapsed - lastSnapshotWriteRef.current < 0.16) { return; } lastSnapshotWriteRef.current = elapsed; const target = controlsRef.current?.target ?? new Vector3(...DEFAULT_CAMERA_SNAPSHOT.target); setCameraSnapshot({ position: [camera.position.x, camera.position.y, camera.position.z], target: [target.x, target.y, target.z], up: [camera.up.x, camera.up.y, camera.up.z], fov: camera.fov }); }); return ( <> ); } function animateCameraToSnapshot( camera: PerspectiveCameraImpl, controls: any, snapshot: CameraSnapshot, reducedMotion: boolean ): void { if (reducedMotion) { applyCameraSnapshot(camera, controls, snapshot, true); return; } const target = controls?.target ?? new Vector3(...snapshot.target); gsap.killTweensOf(camera.position); gsap.killTweensOf(target); gsap.killTweensOf(camera.up); gsap.to(camera.position, { x: snapshot.position[0], y: snapshot.position[1], z: snapshot.position[2], duration: 0.85, ease: 'power3.out', onUpdate: () => controls?.update?.() }); gsap.to(target, { x: snapshot.target[0], y: snapshot.target[1], z: snapshot.target[2], duration: 0.85, ease: 'power3.out', onUpdate: () => controls?.update?.() }); if (snapshot.up) { gsap.to(camera.up, { x: snapshot.up[0], y: snapshot.up[1], z: snapshot.up[2], duration: 0.85, ease: 'power3.out', onUpdate: () => camera.updateProjectionMatrix() }); } if (snapshot.fov) { gsap.to(camera, { fov: snapshot.fov, duration: 0.85, ease: 'power3.out', onUpdate: () => camera.updateProjectionMatrix() }); } } function applyCameraSnapshot( camera: PerspectiveCameraImpl, controls: any, snapshot: CameraSnapshot, updateProjection: boolean ): void { camera.position.set(...snapshot.position); if (snapshot.up) { camera.up.set(...snapshot.up); } if (snapshot.fov) { camera.fov = snapshot.fov; } if (controls?.target) { controls.target.set(...snapshot.target); controls.update?.(); } if (updateProjection) { camera.updateProjectionMatrix(); } }