Files
Aislo/B05_Profile/B05_Profile_UI_Viewer_Camera.ts
T
eomsangdonandClaude Opus 5.5 d05a0de164 feat(B05): 3D 원근 · 직교 두 단추 · 기본 직교 · FRONT · SIDE 수평 보기
- 「원근으로/직교로」 토글을 「원근」 「직교」 두 단추로 · 켜진 쪽 강조
- 카메라 기본 = 직교
- FRONT = 남에서 북 수평 · SIDE = 동에서 서 수평 · 앞 가장자리 반폭만큼 물려 화면맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
2026-10-06 11:58:37 +09:00

81 lines
3.0 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Viewer_Camera.ts
* B05 뷰어의 **원근/직교 두 카메라**와 그 사이 갈아 끼우기.
*
* 기본은 직교 — 원근(시야각 45°)은 단추로 고른다. 갈아 끼울 때 위치·시선·근평면·
* 먼평면과 **보이는 크기**를 그대로 옮기므로 화면이 튀지 않는다.
*
* 카메라 객체가 바뀌므로 쓰는 쪽은 붙잡아 두지 말고 `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 = "ortho";
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<typeof createCameraRig>;