- 얹기: 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>
221 lines
9.9 KiB
Python
221 lines
9.9 KiB
Python
"""구조물 정본(C군 사면안정 벽)을 횡단 측점에 얹는다.
|
|
|
|
왜 필요한가(2026-09-06 사용자 확정) — 좌측 「구조물 배치」로 넣은 옹벽·돌쌓기 같은 벽이
|
|
횡단도에도 서고 **절·성토 면적에도 반영**돼야 한다. 지금까지 이 목록은 측점만 심고
|
|
(`B05_Profile_Engine_Sections.resolve_extra_stations`) 기하가 없어 면적이 그대로였다.
|
|
|
|
방법은 **이미 도는 길을 그대로 태우는 것**이다. 독립 기슭막이가 쓰는 `section.revetment`
|
|
제원과 같은 꼴로 얹으면 횡단 기하(`B06_Section_UI_Cross_Revetment`)·설계선 트림·폐회로
|
|
면적(`B06_Section_Structure_Layouts`)·3D 가 손대지 않고 따라온다. 옛 D군 기슭막이가
|
|
관 정본으로 이관되며 사라졌던 `attach_revetments`(2026-08-28)를 C군 벽으로 되살린 것이다.
|
|
|
|
치수는 여기서 정하지 않는다 — 높이·형태만 넘기고 도형은 화면·3D 가 같은 산식으로 그린다.
|
|
|
|
**짝**: `common_util/common_util_structure_walls.ts` — 브라우저는 아직 저장하지 않은
|
|
목록으로 같은 제원을 만들어야 해서 한 벌을 더 둔다. 거울 테스트
|
|
`tmp/tests/test_b06_structure_walls_mirror.py` 가 같은 값이 나오는지 대조한다.
|
|
남는 까닭(511-B B-6) — B07 단 추가(`tier_extras`) · 형상 얹기 · 소단 구간 Node 못 부를 때.
|
|
"""
|
|
|
|
import logging
|
|
import math
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
|
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
|
from common_util.common_util_structure_catalog import base_type, catalog_row
|
|
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 같은 자리 거리 한 곳(323-7)
|
|
_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
|
|
|
|
|
def _wall_form(type_id: str) -> str | None:
|
|
"""종류 → 횡단 기하가 아는 **형태** 이름 = 구조물 표 형상칸.form(322-1 · TS `formOf` 짝).
|
|
|
|
형태는 벽 안 무늬만 정하므로 가장 가까운 것으로 잇는다(폼에서 형태를 비웠을 때).
|
|
"""
|
|
form = ((catalog_row(type_id) or {}).get("shapeOpts") or {}).get("form")
|
|
return form if isinstance(form, str) else None
|
|
|
|
|
|
# 좌측 폼 「추가 단」 상자 저장 키 머리(178-3) — `tier_{n}_form` · `tier_{n}_height`.
|
|
_TIER_KEY = re.compile(r"^tier_(\d+)_")
|
|
|
|
|
|
def _num(value: Any) -> float | int | None:
|
|
"""유한한 수만(TS `num` 짝 — 글 · 참거짓은 None)."""
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
return None
|
|
return value if math.isfinite(value) else None
|
|
|
|
|
|
def tier_extras(type_id: str, options: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""「추가 단」 상자 → 추가 단 [{형태 · 높이}](n 차례) — 옹벽 = 콘크리트 · TS `tierExtras` 짝."""
|
|
ns = sorted({int(m.group(1)) for key in options if (m := _TIER_KEY.match(key))})
|
|
return [
|
|
{
|
|
"form": "콘크리트"
|
|
if base_type(type_id) == "retaining_wall" # 본보기 포함(323-7)
|
|
else (options.get(f"tier_{n}_form") or None),
|
|
"height_m": _num(options.get(f"tier_{n}_height")),
|
|
}
|
|
for n in ns
|
|
]
|
|
|
|
|
|
def wall_specs_from(structures: list[Any]) -> list[dict[str, Any]]:
|
|
"""구조물 목록 → 벽 제원(TS `wallSpecsFrom` 짝 · 형상 얹기 한 벌 323-4)."""
|
|
types = structure_type_map()
|
|
found: list[dict[str, Any]] = []
|
|
for structure in structures:
|
|
definition = types.get(structure.type_id)
|
|
if definition is None or (catalog_row(structure.type_id) or {}).get("shape") != "wall":
|
|
continue
|
|
start, end = structure.start_m, structure.end_m
|
|
if start is None or end is None:
|
|
continue
|
|
options = structure.options or {}
|
|
extras = tier_extras(structure.type_id, options)
|
|
found.append(
|
|
{
|
|
"structure_id": structure.structure_id,
|
|
"type_id": structure.type_id,
|
|
"name": definition.name,
|
|
"start_m": float(min(start, end)),
|
|
"end_m": float(max(start, end)),
|
|
"anchor_m": float(structure.anchor_m()),
|
|
"form": options.get("form") or _wall_form(structure.type_id),
|
|
"height_m": options.get("height_m"),
|
|
# C군 폼에는 설치 측 칸이 없다 — 비워 두면 화면이 **성토가 나는 쪽**으로
|
|
# 세운다(`computeRevetmentLayout`). 사용자가 정하고 싶어지면 그때 칸을 낸다.
|
|
"side": options.get("side"),
|
|
# 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다.
|
|
# 비어 있으면 화면이 터파기를 안 그린다(근거 없음).
|
|
"foundation": options.get("foundation"),
|
|
# 단 수 = 1 + 추가 단 상자 수 · 상자 없으면 옛 `tiers`(178-3)
|
|
"tiers": len(extras) + 1 if extras else options.get("tiers"),
|
|
"extras": extras,
|
|
"lift_m": options.get("lift_m"),
|
|
"shift_m": options.get("shift_m"),
|
|
# 전면 기울기를 B08 과 같은 칸으로 가르는 값 — 저장 원본(2026-09-14 B5 · TS 짝 같은 키).
|
|
"face_role": options.get("face_role"),
|
|
"face_slope_ratio": options.get("face_slope_ratio"),
|
|
}
|
|
)
|
|
return found
|
|
|
|
|
|
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).
|
|
"""
|
|
from common_util.common_util_cross_berm import (
|
|
BERM_DEFAULT_DEPTH_M,
|
|
BERM_DEFAULT_INTERVAL_M,
|
|
BERM_DEFAULT_SLOPE_DEG,
|
|
BERM_DEFAULT_WIDTH_M,
|
|
)
|
|
|
|
def number(options: dict[str, Any], key: str, fallback: float) -> float:
|
|
try:
|
|
value = float(options.get(key))
|
|
except (TypeError, ValueError):
|
|
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:
|
|
return berm_spans_from(load_structures(str(project_root))[1])
|
|
except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬
|
|
logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)")
|
|
return None
|
|
|
|
|
|
def berm_span_at(
|
|
spans: list[tuple[float, float, dict[str, float]]], chainage_m: float
|
|
) -> dict[str, float] | None:
|
|
"""그 측점을 덮는 소단 제원 — 겹치면 먼저 놓은 것(TS `bermSpecAt` 짝)."""
|
|
for start, end, spec in spans:
|
|
if start - 1e-6 <= chainage_m <= end + 1e-6:
|
|
return spec
|
|
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:
|
|
return 0
|
|
attached = 0
|
|
for section in cross_sections:
|
|
chainage = section.get("chainage_m")
|
|
if not isinstance(chainage, (int, float)):
|
|
continue
|
|
if section.get("culvert") or section.get("revetment"):
|
|
continue
|
|
for spec in walls:
|
|
if (
|
|
spec["start_m"] - _EDGE_TOLERANCE_M
|
|
<= float(chainage)
|
|
<= spec["end_m"] + _EDGE_TOLERANCE_M
|
|
):
|
|
section["revetment"] = spec
|
|
attached += 1
|
|
break
|
|
return attached
|