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:
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user