feat(B05): 계획 종단선이 횡단배수 최소고를 자동 확보

계획선의 변화점(PVI)은 배수유역이 확정한 배관 배치 측점이다. 그 자리 계획고를
지반고와 같게 두면 관·구체가 들어갈 자리가 없어, 시설 제원만큼 들어 올린다.

- common_util_drainage_pipes: facility_clearance_m()/pipe_anchor_clearances() 신설
  (정본 산식) — 배수관 관경+토피 0.5, BOX암거 구체높이+토피 0.5,
  물넘이·세월교는 좌측 패널이 설계유량으로 산출한 월류 높이(ford_height_m)
- Engine_Sections: 관 지점에서 시설 여유를 함께 읽어(_load_pipe_anchors) 전달
- Engine_Grade_Profile: 앵커 목표 표고 = 지반고 + 시설 여유
- Profile_MinCover(화면 경고): 세월교·물넘이를 월류 높이 기준으로 추가.
  백엔드가 정본이고 이쪽은 편집 중 즉시 경고용 사본 — 값 일치는 테스트로 잠금

검증: 재생성한 계획선의 배관 측점 실측 — Ø1000 +1.50 / 세월교 +0.30 /
BOX +2.50 / Ø800 +1.30 (전부 요구 여유와 일치), 화면 경고 소거 확인.
pytest 10/10, tsc·ruff 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 14:12:38 +09:00
co-authored by Claude Opus 5
parent af9d2b5530
commit e1d0d6dda3
4 changed files with 108 additions and 17 deletions
+21 -7
View File
@@ -20,7 +20,11 @@ from B05_Profile.B05_Profile_Engine_Sections_Core import (
SectionGenerationOptions,
generate_sections,
)
from common_util.common_util_drainage_pipes import parse_pipe_points, route_signature
from common_util.common_util_drainage_pipes import (
parse_pipe_points,
pipe_anchor_clearances,
route_signature,
)
from common_util.common_util_json import atomic_write_json
from common_util.common_util_route_geometry import RouteVertex
from common_util.common_util_surface_sampler import build_surface_sampler
@@ -89,7 +93,9 @@ def _cross_summary(cross_section: dict[str, Any]) -> dict[str, Any]:
}
def _load_pipe_chainages(project_root: Path, polyline: list[list[float]]) -> list[float]:
def _load_pipe_anchors(
project_root: Path, polyline: list[list[float]]
) -> tuple[list[float], dict[float, float]]:
"""배수유역도가 확정한 배관 배치 측점(누가거리)을 읽는다.
관 지점 파일에는 저장 당시 노선 지문이 함께 있다 — 노선이 바뀌었으면 버린다
@@ -103,12 +109,12 @@ def _load_pipe_chainages(project_root: Path, polyline: list[list[float]]) -> lis
/ DRAINAGE_PIPE_POINTS_FILENAME
)
if not path.is_file():
return []
return [], {}
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
logger.warning("B05 계획선: 관 지점 파일을 읽지 못했습니다 (%s)", path)
return []
return [], {}
vertices = [
RouteVertex(
x=float(p[0]), y=float(p[1]), z=float(p[2]) if len(p) > 2 else 0.0, chainage_m=0.0
@@ -117,8 +123,12 @@ def _load_pipe_chainages(project_root: Path, polyline: list[list[float]]) -> lis
]
if str(document.get("route_signature") or "") != route_signature(vertices):
logger.info("B05 계획선: 노선이 바뀌어 저장된 관 지점을 쓰지 않습니다.")
return []
return [pipe.chainage_m for pipe in parse_pipe_points(document.get("points"))]
return [], {}
points = parse_pipe_points(document.get("points"))
# 시설 제원이 요구하는 최소 여유(관경+토피 등)를 함께 넘긴다 — 계획선이 그만큼
# 들려야 관·구체가 들어갈 자리가 생긴다(2026-08-23 사용자 지시).
clearances = {chainage: clearance for chainage, clearance in pipe_anchor_clearances(points)}
return [pipe.chainage_m for pipe in points], clearances
def _append_design_profiles(
@@ -126,6 +136,7 @@ def _append_design_profiles(
grade_options: GradeDesignOptions | None,
station_interval_m: float | None = None,
pipe_chainages: list[float] | None = None,
pipe_clearances: dict[float, float] | None = None,
) -> dict[str, Any] | None:
"""종단 계획선을 산출해 longitudinal에 붙이고 요약을 반환한다.
@@ -147,6 +158,7 @@ def _append_design_profiles(
grade_options,
pipe_chainages,
station_interval_m=station_interval_m,
pipe_clearances=pipe_clearances,
)
except (ValueError, KeyError, ArithmeticError):
logger.exception("B05 배관 정착 계획선 산출 실패 — 직선 분할 선형으로 대체")
@@ -261,12 +273,14 @@ def run_section_generation(
cross_dir.mkdir(parents=True, exist_ok=True)
# 종단면 저장 (계획선은 저장 직전에 종단 데이터에 붙인다)
pipe_anchors = _load_pipe_anchors(project_root, polyline)
grade_summary = _append_design_profiles(
result["longitudinal"],
grade_options,
(result.get("options") or {}).get("station_interval_m"),
# 1차 계획선의 변화점 = 배수유역도가 확정한 배관 배치 측점.
pipe_chainages=_load_pipe_chainages(project_root, polyline),
pipe_chainages=pipe_anchors[0],
pipe_clearances=pipe_anchors[1],
)
# 계획선 경사 기반 포장 제안(법정 상한 초과 측점) — 비치명적.
try: