diff --git a/B04_PreProcess/B04_PreProcess_UI_Camera.ts b/B04_PreProcess/B04_PreProcess_UI_Camera.ts index c2161342..71f1489d 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Camera.ts +++ b/B04_PreProcess/B04_PreProcess_UI_Camera.ts @@ -56,6 +56,18 @@ export function niceScaleDistance(roughMeters: number): number { /** 극점을 넘어 화면이 뒤집히지 않도록 남기는 여유각(rad). */ const POLAR_EPSILON = 0.02; + +/** + * 탑뷰를 극점에서 살짝 띄우는 기울기(수평/수직 비). + * + * 카메라를 정확히 수직으로 세우면 `lookAt`이 화면의 가로·세로 방향을 정할 수 없다(시선과 + * up이 평행). 그 자리에서 조금만 끌어도 화면 축이 한 번에 180° 돌아간다 — 헤드리스 크롬으로 + * 실측했다: 2px 드래그 한 걸음에 화면축 반전, 그 뒤 같은 방향 드래그는 먹통(2026-08-08). + * + * 2°는 화면에서 위에서 내려다보는 그림과 사실상 구분되지 않으면서, 여유각(`POLAR_EPSILON` + * ≈ 1.15°)보다 바깥이라 첫 드래그부터 회전이 자연스럽게 이어진다. + */ +export const TOP_VIEW_TILT = Math.tan((2 * Math.PI) / 180); /** 포인트클라우드 클릭 허용 반경 — 카메라 거리에 비례(멀수록 점이 성기게 보인다). */ const POINT_PICK_RATIO = 0.01; /** 휠 한 칸당 배율. 휠을 위로 올리면 이 값의 역수만큼 멀어진다(2026-08-01 사용자 지시). */ diff --git a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts index 2e621c57..655b8157 100644 --- a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts +++ b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts @@ -11,6 +11,7 @@ import { bindCursorPivotControls, bindSurfaceViewerTheme, getTopFitDistance, + TOP_VIEW_TILT, niceScaleDistance, SURFACE_CAMERA_FOV, targetPlaneMetersPerPixel, @@ -305,7 +306,8 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { const aspect = viewerArea.clientWidth / Math.max(viewerArea.clientHeight, 1); const distance = referenceBounds ? getTopFitDistance(referenceBounds, aspect) : span * 1.2; controls.target.set(0, 0, 0); - camera.position.set(0, distance, 0.001); + // 정확히 수직이면 lookAt이 화면 방향을 못 정해 첫 드래그에 화면이 뒤집힌다. + camera.position.set(0, distance, distance * TOP_VIEW_TILT); camera.near = Math.max(distance / 10000, 0.01); camera.far = distance * 100; camera.updateProjectionMatrix(); diff --git a/B04_PreProcess/B04_PreProcess_UI_Viewer.ts b/B04_PreProcess/B04_PreProcess_UI_Viewer.ts index ccde0957..b54ca3a8 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Viewer.ts +++ b/B04_PreProcess/B04_PreProcess_UI_Viewer.ts @@ -9,6 +9,7 @@ import { bindSurfaceViewerTheme, getReferenceCenter, getTopFitDistance, + TOP_VIEW_TILT, niceScaleDistance, SURFACE_CAMERA_FOV, targetPlaneMetersPerPixel, @@ -195,7 +196,8 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer { function setCameraView(view: CameraView): void { const directions: Record = { iso: [120, 95, 135], - top: [0, 1, 0.00001], + // 정확히 수직이면 lookAt이 화면 방향을 못 정해 첫 드래그에 화면이 뒤집힌다. + top: [0, 1, TOP_VIEW_TILT], front: [0, 60, 240], side: [240, 60, 0], }; diff --git a/B05_Profile/B05_Profile_UI_Viewer.ts b/B05_Profile/B05_Profile_UI_Viewer.ts index b259a1ba..821aecd3 100644 --- a/B05_Profile/B05_Profile_UI_Viewer.ts +++ b/B05_Profile/B05_Profile_UI_Viewer.ts @@ -4,7 +4,7 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js"; import { API_BASE_URL } from "@config/config_frontend"; import { fetchCachedBytes, fetchCachedJson } from "../A00_Common/b_asset_cache"; -import { bindCursorPivotControls } from "../B04_PreProcess/B04_PreProcess_UI_Camera"; +import { bindCursorPivotControls, TOP_VIEW_TILT } from "../B04_PreProcess/B04_PreProcess_UI_Camera"; import { createRouteMarkers, modelToScene, @@ -165,7 +165,8 @@ export function createRouteViewer(): RouteViewer { controls.target.set(0, 0, 0); const positions = { iso: [distance, distance, distance], - top: [0, distance, 0.001], + // 정확히 수직이면 lookAt이 화면 방향을 못 정해 첫 드래그에 화면이 뒤집힌다. + top: [0, distance, distance * TOP_VIEW_TILT], front: [0, distance * 0.25, distance], side: [distance, distance * 0.25, 0], } as const;