"""계획선 출처(`basis`) 구분과 B04 3D 화면맞춤 범위 (2026-09-02 결함 2건). ① 1차(배관 정착)와 2차(직선 분할)가 같은 `_profile_entry()` 를 쓰다 보니 저장본 `basis` 가 둘 다 `station_alignment` 로 나갔다. 사고 조사 때 "폴백으로 떨어졌다" 를 저장본에서 확인하지 못한 원인이다. 값을 갈랐는지 잠근다. ② B04 지표면 3D 뷰어가 `referenceBounds`(지표면 범위)만으로 카메라 거리를 정해, 라이다가 노선의 일부만 덮으면 나머지가 화면 밖으로 잘렸다(용화 실측 — 노선 2,136m 중 라이다 1,400m). 노선까지 담는 대칭 확장이 들어갔는지 잠근다. """ import sys from pathlib import Path import numpy as np PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from B05_Profile.B05_Profile_Engine_Grade import resolve_grade_options # noqa: E402 from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402 ALIGNMENT_BASIS, PIPE_ANCHORED_BASIS, design_alignment_profile, design_pipe_anchored_profile, rebuild_alignment_profile, ) def _longitudinal(length_m: float = 400.0, step: float = 20.0) -> dict: """지반고가 완만하게 내려가는 종단 — 두 진입점 모두 계획선을 만들 수 있다.""" chainages = np.arange(0.0, length_m + step, step) return { "length_m": float(length_m), "samples": [ {"chainage_m": float(c), "elevation_m": 100.0 - 0.02 * float(c), "valid": True} for c in chainages ], "stations": [ {"chainage_m": float(c), "kind": "regular", "elevation_m": 100.0 - 0.02 * float(c)} for c in chainages ], } # ── ① 계획선 출처가 저장본에서 갈린다 ──────────────────────────────────────── def test_two_bases_are_distinct_values(): """두 값이 같으면 저장본으로 진입점을 못 가른다.""" assert PIPE_ANCHORED_BASIS != ALIGNMENT_BASIS def test_pipe_anchored_profile_reports_its_own_basis(): """1차(배관 정착)는 `pipe_anchored` 로 남는다.""" _, entry = design_pipe_anchored_profile( _longitudinal(), resolve_grade_options("trunk"), [200.0], station_interval_m=20.0, ) assert entry["basis"] == PIPE_ANCHORED_BASIS def test_fallback_profile_keeps_the_station_basis(): """2차(직선 분할 폴백)는 종전 값 그대로 — 옛 저장본과 뜻이 어긋나지 않는다.""" _, entry = design_alignment_profile( _longitudinal(), resolve_grade_options("trunk"), station_interval_m=20.0, ) assert entry["basis"] == ALIGNMENT_BASIS def test_rebuild_inherits_the_stored_basis(): """사용자 편집 재구성은 저장된 자동 선형을 그대로 쓰므로 출처도 물려받는다.""" longitudinal = _longitudinal() alignment, entry = design_pipe_anchored_profile( longitudinal, resolve_grade_options("trunk"), [200.0], station_interval_m=20.0, ) # 저장 경로(`B05_Profile_Engine_Sections.py:252`)와 같은 자리에 선형을 얹는다. longitudinal["design_profiles"] = [entry] longitudinal["profile_alignment"] = alignment _, rebuilt = rebuild_alignment_profile(longitudinal, None) assert rebuilt["basis"] == PIPE_ANCHORED_BASIS # ── ② B04 3D 화면맞춤이 노선까지 담는다 ────────────────────────────────────── VIEWER_SOURCE = (PROJECT_ROOT / "B04_PreProcess" / "B04_PreProcess_UI_TerrainViewer.ts").read_text( encoding="utf-8" ) def test_fit_camera_uses_route_expanded_bounds(): """`fitCamera` 가 지표면 범위 대신 노선을 담은 범위를 쓴다.""" assert "const bounds = fitBounds();" in VIEWER_SOURCE assert "getTopFitDistance(bounds, aspect)" in VIEWER_SOURCE assert "getTopFitDistance(referenceBounds, aspect)" not in VIEWER_SOURCE def test_fit_bounds_expands_symmetrically_around_the_reference_center(): """카메라 타깃이 지표면 중심이라 한쪽만 넓히면 소용없다 — 반폭을 대칭으로 잡는다.""" assert "halfX = Math.max(halfX, Math.abs(point.x - cx))" in VIEWER_SOURCE assert "halfY = Math.max(halfY, Math.abs(point.y - cy))" in VIEWER_SOURCE assert "x_min: cx - halfX" in VIEWER_SOURCE assert "x_max: cx + halfX" in VIEWER_SOURCE