From a321f0f9a6306f315ed7a86c949b9c2b275550e9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 2 Sep 2026 06:44:05 +0900 Subject: [PATCH] =?UTF-8?q?fix(B04,B05):=203D=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4=20=EB=85=B8=EC=84=A0=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=C2=B7=EA=B3=84=ED=9A=8D=EC=84=A0=20=EC=B6=9C=EC=B2=98=20basis?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B04 지표면 3D 뷰어가 지표면 범위만으로 카메라 거리를 정해, 라이다가 노선의 일부만 덮으면 나머지가 화면 밖으로 잘렸음. 카메라 타깃이 늘 지표면 중심이라 한쪽만 넓혀서는 소용이 없어, 중심에서 가장 먼 노선 정점까지를 반폭으로 잡아 대칭으로 넓힌 `fitBounds()` 를 `fitCamera()` 가 씀. 노선이 없으면 종전 동작 유지. - 계획선 1차(배관 정착)와 2차(직선 분할 폴백)가 같은 `_profile_entry()` 를 써서 저장본 `basis` 가 둘 다 `station_alignment` 로 나갔음. `PIPE_ANCHORED_BASIS` 신규 + `_profile_entry(basis=)` 인자로 1차만 값을 가름. 2차는 종전 값 유지, 편집 재구성은 저장본 값 승계. 검증: 용화 프로젝트 공용 브라우저 실측 — 노선색 픽셀 x 범위 48~399(우측 끝에 닿음) → 145~386(양끝 캔버스 안). pytest 349 passed / 17 skipped / 0 failed, tsc 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B04_PreProcess_UI_TerrainViewer.ts | 30 +++++++++++++++++-- .../B05_Profile_Engine_Grade_Profile.py | 13 ++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts index 39819486..215afe8b 100644 --- a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts +++ b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts @@ -432,13 +432,37 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { return { center, span }; }; + /** 화면맞춤 기준 범위 — 지표면 범위에 계획노선까지 담는다. + * + * 카메라 타깃은 늘 지표면 중심(0,0,0)이라 범위를 한쪽으로만 늘려서는 소용이 없다. + * 중심에서 가장 먼 노선 정점까지를 반폭으로 잡아 **대칭으로** 넓힌다. 그러지 않으면 + * 라이다가 노선의 일부만 덮을 때 나머지가 화면 밖으로 잘린다(2026-09-02 용화 실측: + * 노선 2,136m 중 라이다는 1,400m만 덮어 오른쪽이 캔버스 밖으로 나갔다). */ + const fitBounds = (): SurfaceBounds | null => { + if (!referenceBounds || routePoints.length < 2) return referenceBounds; + const cx = (referenceBounds.x_min + referenceBounds.x_max) / 2; + const cy = (referenceBounds.y_min + referenceBounds.y_max) / 2; + let halfX = (referenceBounds.x_max - referenceBounds.x_min) / 2; + let halfY = (referenceBounds.y_max - referenceBounds.y_min) / 2; + for (const point of routePoints) { + halfX = Math.max(halfX, Math.abs(point.x - cx)); + halfY = Math.max(halfY, Math.abs(point.y - cy)); + } + return { + ...referenceBounds, + x_min: cx - halfX, + x_max: cx + halfX, + y_min: cy - halfY, + y_max: cy + halfY, + }; + }; + const fitCamera = (object: THREE.Object3D) => { const { span } = getFitParams(object); const aspect = viewerArea.clientWidth / Math.max(viewerArea.clientHeight, 1); - const distance = referenceBounds - ? getTopFitDistance(referenceBounds, aspect) - : span * 1.2; + const bounds = fitBounds(); + const distance = bounds ? getTopFitDistance(bounds, aspect) : span * 1.2; controls.target.set(0, 0, 0); // 정확히 수직이면 lookAt이 화면 방향을 못 정해 첫 드래그에 화면이 뒤집힌다. camera.position.set(0, distance, distance * TOP_VIEW_TILT); diff --git a/B05_Profile/B05_Profile_Engine_Grade_Profile.py b/B05_Profile/B05_Profile_Engine_Grade_Profile.py index b8db30eb..87cdae81 100644 --- a/B05_Profile/B05_Profile_Engine_Grade_Profile.py +++ b/B05_Profile/B05_Profile_Engine_Grade_Profile.py @@ -44,6 +44,10 @@ from B05_Profile.B05_Profile_Engine_Grade_Solver import ( from config.config_system import FOREST_ROAD_PROFILE_ALIGNMENT ALIGNMENT_PROFILE_ID = "design_grade_line" +# 계획선이 어느 진입점에서 나왔는지 저장본만 보고 가릴 수 있게 값을 나눈다. 1차·2차가 +# 같은 `_profile_entry()` 를 쓰다 보니 둘 다 `station_alignment` 로 나가, 사고 조사 때 +# 폴백으로 떨어진 것을 저장본에서 확인하지 못했다(2026-09-02). +PIPE_ANCHORED_BASIS = "pipe_anchored" ALIGNMENT_BASIS = "station_alignment" @@ -68,6 +72,7 @@ def _profile_entry( direction: str, balanced: bool, warnings: list[str], + basis: str = ALIGNMENT_BASIS, ) -> dict[str, Any]: """`design_profiles` 배열 계약(기존 스키마)에 맞춰 계획선 한 벌을 만든다. @@ -80,7 +85,7 @@ def _profile_entry( "schema_version": ALIGNMENT_SCHEMA_VERSION, "id": ALIGNMENT_PROFILE_ID, "name": "계획선", - "basis": ALIGNMENT_BASIS, + "basis": basis, "criteria": {**options.as_dict(), "resolved_main_direction": direction}, # 구 스키마 호환: 변화점 목록을 pvis 이름으로도 노출한다. "pvis": alignment["pvi"], @@ -254,7 +259,9 @@ def design_pipe_anchored_profile( ) warnings.extend(alignment["warnings"]) balanced = bool(alignment["balance"]["within_tolerance"]) - return alignment, _profile_entry(alignment, options, direction, balanced, warnings) + return alignment, _profile_entry( + alignment, options, direction, balanced, warnings, PIPE_ANCHORED_BASIS + ) def design_alignment_profile( @@ -389,4 +396,6 @@ def rebuild_alignment_profile( direction, alignment["balance"]["within_tolerance"], alignment["warnings"], + # 재구성은 저장된 자동 선형을 그대로 쓰므로 출처도 그대로 물려받는다. + str(previous.get("basis") or ALIGNMENT_BASIS), )