refactor(B05·B06): 소단 = 옹벽처럼 측점에 제원 얹음 — 형상 얹기 한 벌 · 세션 사본 berm 걷음 (PLAN 577-2 · 요구 434 · 437)

- 얹기: attachShapes · attach_shapes 에 소단(section.berm · 덮는 소단 없음 = null) · SHAPE_KEYS 에 berm · TS attachBermSpecs ↔ 파이썬 attach_berm_specs · berm_spans_from 짝
- 읽기: 저장 노선 다시 셈(브라우저 · Node cross.refresh) = 얹힌 section.berm(칸 없음 = 저장분 design.berm) · 노선 초안 · 서버 Node 묶음 cross = crossWithShapes 의 얹힌 제원(bermAt · crossWithBerm 걷음)
- 계단은 계산 안쪽 그대로 · [저장] 길(구조물 정본 → cross.berm_spans) 그대로
- 지움: 시설 줄 syncBerm · putBerm(C4.copy) · writeBermSpans · 세션 칸 berm · bermRequest · design_shared bermAt
- 시험: test_272_berm_source 측점 얹기 거울로 · test_323_4 소단 얹음 · 477 · 511-A · 472 떠남 · 365 · 272 고름 헬퍼가 사본 대신 얹힌 소단 · design_pipeline_page 700 줄 → 699(주석 한 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 11:33:06 +09:00
co-authored by Claude Opus 5.5
parent 189474a507
commit 403722e687
33 changed files with 249 additions and 308 deletions
@@ -110,11 +110,10 @@ def wall_specs_from(structures: list[Any]) -> list[dict[str, Any]]:
return found
def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 `[(시작, 끝, 제원)]` — [저장] 서버 다시 셈의 소단 원천(194-1).
def berm_spans_from(structures: list[Any]) -> list[tuple[float, float, dict[str, float]]]:
"""구조물 목록 → 소단 구간 `[(시작, 끝, 제원)]`(형상 얹기 한 벌 577-2 · [저장] 194-1).
짝: TS `bermSpansFromStructures`(같은 규칙 — 폭 0 = 없음 · 깊이 빈 값 = 기본 1.0 · 0 미만 = 0).
못 읽으면 None(부르는 쪽이 저장분 `design.berm` 으로 물러섬).
"""
from common_util.common_util_cross_berm import (
BERM_DEFAULT_DEPTH_M,
@@ -130,35 +129,41 @@ def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, fl
return fallback
return value if math.isfinite(value) and value >= 0 else fallback
spans: list[tuple[float, float, dict[str, float]]] = []
for item in structures:
if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4)
continue
anchor = item.chainage_m if item.chainage_m is not None else item.start_m
start = item.start_m if item.start_m is not None else anchor
end = item.end_m if item.end_m is not None else anchor
if start is None or end is None:
continue
options = item.options or {}
width = number(options, "width_m", BERM_DEFAULT_WIDTH_M)
if width <= 0:
continue
raw = options.get("depth_m")
try:
depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0)
except (TypeError, ValueError):
depth = BERM_DEFAULT_DEPTH_M
if not math.isfinite(depth):
depth = BERM_DEFAULT_DEPTH_M
spec = {
"width_m": width,
"interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M),
"slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG),
"depth_m": depth,
}
spans.append((float(min(start, end)), float(max(start, end)), spec))
return spans
def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 — [저장] 서버 다시 셈의 소단 원천(194-1) · 못 읽으면 None(부르는 쪽이
저장분 `design.berm` 으로 물러섬)."""
try:
spans: list[tuple[float, float, dict[str, float]]] = []
for item in load_structures(str(project_root))[1]:
if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4)
continue
anchor = item.chainage_m if item.chainage_m is not None else item.start_m
start = item.start_m if item.start_m is not None else anchor
end = item.end_m if item.end_m is not None else anchor
if start is None or end is None:
continue
options = item.options or {}
width = number(options, "width_m", BERM_DEFAULT_WIDTH_M)
if width <= 0:
continue
raw = options.get("depth_m")
try:
depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0)
except (TypeError, ValueError):
depth = BERM_DEFAULT_DEPTH_M
if not math.isfinite(depth):
depth = BERM_DEFAULT_DEPTH_M
spec = {
"width_m": width,
"interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M),
"slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG),
"depth_m": depth,
}
spans.append((float(min(start, end)), float(max(start, end)), spec))
return spans
return berm_spans_from(load_structures(str(project_root))[1])
except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬
logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)")
return None
@@ -174,6 +179,24 @@ def berm_span_at(
return None
def attach_berm_specs(
cross_sections: list[dict[str, Any]], spans: list[tuple[float, float, dict[str, float]]]
) -> int:
"""측점마다 소단 제원을 얹음(TS `attachBermSpecs` 짝 · 577-2) — 덮는 소단 없음 = None
(얹었음 표시). 얹은 측점 수."""
attached = 0
for section in cross_sections:
chainage = section.get("chainage_m")
if isinstance(chainage, bool) or not isinstance(chainage, (int, float)):
continue
if not math.isfinite(chainage):
continue
spec = berm_span_at(spans, float(chainage))
section["berm"] = dict(spec) if spec else None
attached += 1 if spec else 0
return attached
def attach_wall_specs(cross_sections: list[dict[str, Any]], walls: list[dict[str, Any]]) -> int:
"""구간 안 측점에 벽 제원을 얹는다(TS `attachWallSpecs` 짝). 얹은 개수를 돌려준다."""
if not walls: