fix(B04,B05): 3D 화면맞춤 노선 포함·계획선 출처 basis 분리

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-02 06:44:05 +09:00
co-authored by Claude Opus 5
parent 019a742948
commit a321f0f9a6
2 changed files with 38 additions and 5 deletions
@@ -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);