Files
Aislo/B06_Section/B06_Section_Engine_Structures_Wall.py
T
eomsangdonandClaude Opus 5.5 81f7d98e6f feat(구조물표): 323-7 새 종류 · 정리 — M02 종류 더하기(본보기 복사 · 일곱 갈래 본보기 그대로) · 손코딩 동작 · 명세 = 본보기 키 · 특수 동작 = 동작 이름 · 정본 목록 = 표 · drawing_views 걷음 · 옛 이름 되짚기 걷음(자리로 가름) · 허용 거리 파이썬 한 곳 · 벽 툴팁 = 종류 이름
- M02 종류별 탭 [종류 더하기] = 본보기(구조물 정본 원본) 복사 — 키 자동 · 이름 「본보기 2」 · 옵션 · 표시 · 그룹 · 배치 · 규칙 · 종류 덧칸 · 반영 그대로 · 종류.{새}.본보기 · 관 지점 종류 · 복사본은 본보기 밖
- 구조물 표 = 본보기 처음값 이어받음(resolveCatalog · resolve_catalog 짝 · 줄 칸 base) · baseType/base_type · specFor(명세) · 덧단 · 길이 전후 풀기 · 옹벽 추가 단 콘크리트
- 검사(화면 = 서버 짝) — 본보기 = 원본 · 같은 군 · 자리꼴 · 정본 · 만든 뒤 고정 · 복사본은 본보기가 숨긴 칸 이어받음 · 서버 덧칸 본보기 받음
- 특수 동작 = 동작 이름 — 바로 안 놓기 · 포장 나누기 = 포장_물넘이_겹침 · 월류 높이 기다림 = 월류단면
- 정본 목록 = 표 — 파이썬 관 지점 종류 = catalog owner · B05 시설 서브폼 = 표 형상 · 안 쓰는 손 목록 걷음
- drawing_views — 스키마에서 빼고 읽을 때 버림 · M02 [저장] 때 빠짐(코드는 마스터 안 고침)
- 이관 = 지난 판 이름 대신 측점 계획 자리(planted)로 가름 · all_type_names 걷음
- 허용 거리 = common_util_structure_station.py 한 곳(SAME_STATION_M · EDGE_TOLERANCE_M · PIPE_MATCH_M) · TS 표는 station.ts 머리
- 시험: test_323_7_new_type(템플릿 · 일곱 갈래 · 그림 · 검사 짝 · 서버 읽기) · 322-0 유령 시험 = 자리 · 옛 도움 파일 동작 · 표 길 · 기대 글 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-14 18:59:13 +09:00

222 lines
10 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` 가 같은 값이 나오는지 대조한다.
"""
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 load_wall_structures(project_root: Path) -> list[dict[str, Any]]:
"""구조물 정본에서 벽(표 형상 wall · 322-1) 목록을 읽는다. 실패하면 빈 목록.
소단은 형상 berm — 사면을 계단으로 끊는 시설이라 벽 제원 자리에 안 얹음(계획서 3-9).
"""
try:
return wall_specs_from(load_structures(str(project_root))[1])
except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다
logger.exception("B06 구조물 벽 정본을 읽지 못했습니다 (없는 것으로 본다)")
return []
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 load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 `[(시작, 끝, 제원)]` — [저장] 서버 다시 셈의 소단 원천(194-1).
짝: TS `bermSpansFromStructures`(같은 규칙 — 폭 0 = 없음 · 깊이 빈 값 = 기본 1.0 · 0 미만 = 0).
못 읽으면 None(부르는 쪽이 저장분 `design.berm` 으로 물러섬).
"""
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
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
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_wall_structures(project_root: Path, cross_sections: list[dict[str, Any]]) -> int:
"""구간 안 측점의 횡단 dict에 `revetment` 키를 얹는다. 얹은 개수를 돌려준다.
이미 관 정본이 얹은 세트(`culvert`·`revetment`)가 있는 측점은 **건드리지 않는다** —
관 유입·유출 벽과 구조물 벽이 한 자리에 겹치면 어느 쪽 그림인지 읽히지 않는다.
한 측점에 여러 개가 겹치면 먼저 시작한 것을 쓴다(겹침 정리는 사용자 몫).
"""
attached = attach_wall_specs(cross_sections, load_wall_structures(project_root))
if attached:
logger.info("B06 구조물 벽 %d개 측점에 얹음", attached)
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