사용자 지시(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>
82 lines
3.1 KiB
TypeScript
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>;
|