From 1e905c257f73dfef369a6ccc22666df5469ee19d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 8 Aug 2026 18:45:42 +0900 Subject: [PATCH] =?UTF-8?q?fix(B04,B05):=20=ED=83=91=EB=B7=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B7=B9=EC=A0=90=EC=97=90=EC=84=9C=202=C2=B0=20=EB=9D=84?= =?UTF-8?q?=EC=9B=8C=20=EC=B2=AB=20=EB=93=9C=EB=9E=98=EA=B7=B8=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=92=A4=EC=A7=91=ED=9E=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앞선 두 커밋(c3f643b, 7d50ded)으로도 증상이 남아 헤드리스 크롬으로 실제 앱에 로그인해 B05 3D를 끌어 보며 측정했다. 원인은 회전 계산이 아니라 **탑뷰 카메라 자리**였다. 카메라를 정확히 수직으로 세우면(기존 top = [0, distance, 0.001], 극각 1e-6도) lookAt이 화면의 가로·세로 방향을 정하지 못한다. 그 자리에서 벗어나는 순간 화면 축이 180° 돌아간다. 실측(2px 드래그 6걸음): before 극각 0.00° -> 0.86°, 1걸음째 화면축 반전, 이후 같은 방향 드래그 먹통 after 극각 2.00° -> 1.15°, 반전 0회, 반대 방향 20걸음도 반전 0회(1.15° -> 130°) - TOP_VIEW_TILT(2°) 신설 — 화면상 위에서 내려다보는 그림과 구분되지 않으면서 여유각 (POLAR_EPSILON, 약 1.15°)보다 바깥이라 첫 드래그부터 회전이 이어진다. - B05 노선 뷰어, B04 지형·포인트클라우드 뷰어의 탑뷰 세 곳에 모두 적용. typecheck·prettier 통과, 정적 번들 재빌드. Co-Authored-By: Claude Opus 5 (1M context) --- B04_PreProcess/B04_PreProcess_UI_Camera.ts | 12 ++++++++++++ B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts | 4 +++- B04_PreProcess/B04_PreProcess_UI_Viewer.ts | 4 +++- B05_Profile/B05_Profile_UI_Viewer.ts | 5 +++-- 4 files changed, 21 insertions(+), 4 deletions(-) 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;