/* ============================================================================= * B05_Profile_UI_Viewer_Camera.ts * B05 뷰어의 **원근/직교 두 카메라**와 그 사이 갈아 끼우기. * * 기본은 원근(시야각 45°) — B04 지표면 화면과 같은 조작감이다(2026-09-04 사용자 확정). * 탑뷰에서 크기를 정밀하게 대조할 때만 직교로 바꾼다. 갈아 끼울 때 위치·시선·근평면· * 먼평면과 **보이는 크기**를 그대로 옮기므로 화면이 튀지 않는다. * * 카메라 객체가 바뀌므로 쓰는 쪽은 붙잡아 두지 말고 `camera()`로 그때그때 읽을 것. * ========================================================================== */ import * as THREE from "three"; import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; export type ProjectionKind = "perspective" | "ortho"; const FOV = 45; /** 원근 45°의 반각 tan — 직교 반높이를 같은 크기감으로 맞출 때 쓴다. */ const HALF_TAN = Math.tan((FOV * Math.PI) / 360); export function createCameraRig() { const perspective = new THREE.PerspectiveCamera(FOV, 1, 0.1, 100000); perspective.position.set(100, 120, 100); const ortho = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 100000); let kind: ProjectionKind = "perspective"; let aspect = 1; let halfHeight = 100; const active = (): THREE.PerspectiveCamera | THREE.OrthographicCamera => kind === "perspective" ? perspective : ortho; function apply(): void { perspective.aspect = aspect; perspective.updateProjectionMatrix(); ortho.left = -halfHeight * aspect; ortho.right = halfHeight * aspect; ortho.top = halfHeight; ortho.bottom = -halfHeight; ortho.updateProjectionMatrix(); } return { camera: active, kind: () => kind, /** 뷰포트 종횡비(리사이즈 시). */ setAspect(value: number): void { aspect = value; apply(); }, /** 화면맞춤 — 시점까지 거리로 직교 반높이를 잡는다(원근과 같은 크기감). */ setFit(distance: number): void { halfHeight = distance * HALF_TAN; ortho.zoom = 1; apply(); }, /** 투영 전환. 보이는 크기를 유지하며 OrbitControls의 대상 카메라도 갈아 끼운다. */ setKind(next: ProjectionKind, controls: OrbitControls): void { if (next === kind) return; const from = active(); kind = next; const to = active(); to.quaternion.copy(from.quaternion); to.near = from.near; to.far = from.far; const offset = from.position.clone().sub(controls.target); if (next === "ortho") { halfHeight = offset.length() * HALF_TAN; ortho.zoom = 1; to.position.copy(from.position); } else { // 직교는 배율(zoom)로도 커지므로, 같은 크기로 보이는 거리까지 카메라를 물린다. to.position.copy(controls.target).add(offset.setLength(halfHeight / ortho.zoom / HALF_TAN)); } apply(); controls.object = to; controls.update(); }, }; } export type CameraRig = ReturnType;