diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 08d5885..13db37a 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -88,8 +88,60 @@ function ViewCubeAnimator() { return null; } - -function LoadingFallback() { +/** Switches between perspective and orthographic projection while keeping + * the visual framing (position, target, on-screen size of the scene). */ +function CameraSwitcher() { + const mode = useModelStore((s) => s.cameraMode); + const { size } = useThree(); + // Snapshot current view from whichever camera is active right now, before swap. + const snap = useMemo(() => { + const c = mainCameraRef.current; + const ctrl = mainControlsRef.current; + const target = (ctrl?.target ?? new THREE.Vector3()).clone(); + const pos = c ? c.position.clone() : new THREE.Vector3(2, 2, 2); + const up = c ? c.up.clone() : new THREE.Vector3(0, 1, 0); + const dist = pos.distanceTo(target); + // Compute ortho zoom that matches current perspective framing. + let zoom = 100; + if (c && (c as THREE.PerspectiveCamera).isPerspectiveCamera) { + const fov = ((c as THREE.PerspectiveCamera).fov ?? 50) * (Math.PI / 180); + const worldHeight = 2 * Math.max(dist, 0.001) * Math.tan(fov / 2); + zoom = Math.max(1, size.height / Math.max(worldHeight, 1e-6)); + } else if (c && (c as THREE.OrthographicCamera).isOrthographicCamera) { + zoom = (c as THREE.OrthographicCamera).zoom; + } + return { target, pos, up, zoom }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode]); + // Sync OrbitControls target after camera swap. + useEffect(() => { + const ctrl = mainControlsRef.current; + if (ctrl?.target) ctrl.target.copy(snap.target); + ctrl?.update?.(); + }, [snap]); + if (mode === 'ortho') { + return ( + + ); + } + return ( + + ); +} return (