fix(B04): 도엽 서피스를 라이다와 같은 원점에 놓고, 유역 저장본에 좌표계를 남긴다
① 3D 서피스 겹쳐보기 어긋남 — `write_glb()`는 넘겨받은 상자의 중심을 원점으로 삼는데 모델마다 자기 상자를 넘겨 도엽 서피스와 라이다 지표면이 다른 원점에 섰다 (2f940d8a 실측: 수평 7.85m·높이 1.28m). 도엽 서피스가 라이다 포인트 상자 (`structured.npz`, 화면 `setReferenceBounds`와 같은 값)를 화면 원점으로 쓰게 했다. 표고 격자 상자(`bounds`)는 절취 범위 그대로 두고 `scene_bounds`를 npz에 따로 남긴다 — 등고선 API도 이 값을 먼저 써서 등고선이 메시 위에 얹힌다. 라이다 없는 사업지는 기준이 자기뿐이라 종전대로 자기 상자를 쓴다. ② 세부유역 저장본 좌표계 — `04_detailed_basins.geojson`에 변환에 쓴 좌표계를 `crs_input`으로 남기고, B07 유역도가 그 값으로 되돌린다. 기록이 없는 옛 저장본은 노선 CSV의 EPSG 라벨로 쓰였으므로 그 라벨로 되돌린다(경고 로그 + 재확정 안내). 검증: tmp/tests 42 passed (신규 test_scene_origin_and_basin_crs.py 5건). 패치 코드로 도엽 서피스를 다시 만들어 GLB 정점 상자를 대조 — sheet [-426.58, -68.95, -386.41]~[411.42, 71.52, 379.59], csf [-172.12, -44.06, -199.69]~[184.11, 3.22, 165.72] 로 같은 원점. 수정 전 sheet는 [-419.5, -70.24, -383.0]~[418.5, 70.24, 383.0] (자기 중심)였다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ from B07_DesignDetail.B07_DesignDetail_Schema import (
|
||||
DesignDrawingItem,
|
||||
)
|
||||
from common_util.common_util_drainage_pipes import detail_basins_path
|
||||
from common_util.common_util_route_geometry import find_planned_route_file, read_planned_route
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from config.config_system import DRAWING_SCALE_BASIN
|
||||
|
||||
@@ -107,19 +108,46 @@ def _write_manifest(project_root: Path, manifest: dict[str, Any]) -> None:
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def _geojson_features(path: Path) -> list[dict[str, Any]]:
|
||||
"""GeoJSON 피처 목록만 읽는다. 파일이 없거나 깨졌으면 빈 목록."""
|
||||
def _geojson_payload(path: Path) -> dict[str, Any]:
|
||||
"""GeoJSON 전체를 읽는다. 파일이 없거나 깨졌으면 빈 dict."""
|
||||
if not path.is_file():
|
||||
return []
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
logger.warning("B07 유역도: GeoJSON을 읽지 못했습니다 — %s", path)
|
||||
return []
|
||||
features = payload.get("features") if isinstance(payload, dict) else None
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def _geojson_features(path: Path) -> list[dict[str, Any]]:
|
||||
"""GeoJSON 피처 목록만 읽는다. 파일이 없거나 깨졌으면 빈 목록."""
|
||||
features = _geojson_payload(path).get("features")
|
||||
return features if isinstance(features, list) else []
|
||||
|
||||
|
||||
def _basins_crs(context: Any, payload: dict[str, Any]) -> str:
|
||||
"""저장본을 미터로 되돌릴 좌표계.
|
||||
|
||||
저장할 때 쓴 좌표계를 파일이 갖고 있으면 그 값이다. 없으면 좌표계 기록 이전에 저장된
|
||||
파일이라 노선 CSV의 EPSG 라벨로 쓰였다 — 그 라벨로 되돌려야 왕복이 맞는다
|
||||
(2026-09-01). 라벨을 못 읽으면 현재 사업지 좌표계로 둔다.
|
||||
"""
|
||||
stored = payload.get("crs_input")
|
||||
if isinstance(stored, str) and stored:
|
||||
return stored
|
||||
route_file = find_planned_route_file(context.project_root / "B03_FileInput" / "input")
|
||||
planned = read_planned_route(route_file) if route_file else None
|
||||
if planned is not None and planned.epsg:
|
||||
logger.warning(
|
||||
"B07 유역도: 좌표계 기록이 없는 옛 저장본 — 노선 CSV 라벨 EPSG:%s로 되돌립니다. "
|
||||
"B04에서 유역을 다시 확정하면 현재 좌표계로 새로 남습니다.",
|
||||
planned.epsg,
|
||||
)
|
||||
return f"EPSG:{planned.epsg}"
|
||||
return context.crs
|
||||
|
||||
|
||||
def _geometry_lines(geometry: Any) -> list[list[tuple[float, float]]]:
|
||||
"""LineString·MultiLineString·Polygon을 점열 목록으로 편다."""
|
||||
if not isinstance(geometry, dict):
|
||||
@@ -206,7 +234,10 @@ def watershed_source(context: Any) -> dict[str, Any]:
|
||||
|
||||
저장본은 전부 WGS84라 여기서 미터 좌표로 되돌린다(B04가 저장할 때와 반대 방향).
|
||||
"""
|
||||
to_metric = Transformer.from_crs("EPSG:4326", context.crs, always_xy=True)
|
||||
basins_payload = _geojson_payload(detail_basins_path(context.stored_path))
|
||||
to_metric = Transformer.from_crs(
|
||||
"EPSG:4326", _basins_crs(context, basins_payload), always_xy=True
|
||||
)
|
||||
|
||||
def metric(point: tuple[float, float]) -> tuple[float, float]:
|
||||
x, y = to_metric.transform(point[0], point[1])
|
||||
@@ -214,7 +245,7 @@ def watershed_source(context: Any) -> dict[str, Any]:
|
||||
|
||||
route_xy = [(vertex.x, vertex.y) for vertex in context.vertices]
|
||||
basins: list[dict[str, Any]] = []
|
||||
for feature in _geojson_features(detail_basins_path(context.stored_path)):
|
||||
for feature in basins_payload.get("features") or []:
|
||||
properties = feature.get("properties") or {}
|
||||
if properties.get("kind") != "detail_basin":
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user