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 (
+