Files
Aislo/B05_Profile/B05_Profile_UI_Viewer_Camera.ts
eomsangdonandClaude Opus 5 5203191751 feat(B05): 진입 로딩 3D 후순위 · 도넛 회전 분리 · 직교/원근 전환 단추
사용자 지시(2026-09-04) — 「3D 는 보조라 후순위. 조용히 뒤에서 뜨고 다 되면
팝업으로 알릴 것. 로딩 도넛은 진행률과 무관하게 계속 돌고 숫자만 갱신」,
그리고 「직교/원근 전환 단추는 넣음」.

진입 로딩
- 단계 5 → 4. ④ 종·횡단 자료까지 끝나면 로딩 표시를 걷어 화면을 바로 쓰게 함.
- 3D 지형은 finally 뒤 배경 작업으로 돌리고 끝나면 토스트 「3D 지형 준비 완료」.
  실패해도 나머지 화면은 그대로 씀. [초기화]는 재진입이라 같은 규칙이 걸림.
- 「확정 지표면 없음」 판정을 ③ 뒤로 앞당김.

진행 서클(공용)
- 회전 껍데기(`__spin`)를 한 겹 두고 CSS 회전을 거기에만 검. 껍데기는 늘 돌고
  안쪽 svg 는 12시 고정이라 호·숫자가 제자리에서 갱신됨. `is-indeterminate` 제거.

직교/원근 전환
- 카메라 두 벌을 `B05_Profile_UI_Viewer_Camera.ts` 가 쥐고 갈아 끼움. 위치·시선·
  근평면·먼평면과 보이는 크기를 옮겨 화면이 튀지 않음.
- 마커 입력·구조물 클릭·커서 피벗 유틸이 카메라를 함수로 받게 바꿈(공용 유틸은
  값도 그대로 받아 B04 호출부는 무변경).

자체검증(공용 브라우저 5173, 용화_LAS) — 진행률 25→50→75%, 회전 애니메이션
running 유지(변환행렬 표본 전부 다름), 로딩 걷힌 순간 그래프선 2·좌측단추 61·
3D 메쉬 2, 1.8초 뒤 메쉬 489 + 토스트. 초기화 재진입도 같음. 전환 단추는
Ortho ↔ Perspective 왕복 후 값 완전 일치, B04 페이지 오류 0건.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 19:13:46 +09:00

82 lines
3.1 KiB
TypeScript

/* =============================================================================
* 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<typeof createCameraRig>;