2026-08-28 사용자 지시(자동 배치는 만들지 않는다). - 레지스트리에 tiers(단 수, 기본 1 = 단일 벽) 추가. 서버가 스펙에 실어 보낸다. - 자리 규칙: **성토선 하단(성토면 끝)이 1단**, 단을 늘리면 설계 성토면을 따라 위로 올라가며 그 아래에 단이 들어온다(배관 세트와 다른 점은 관이 없고 시작 자리가 성토선 하단이라는 것). 자리는 성토끝 → 노견 끝을 단 수로 등분해 잡는다. - 3D는 단마다 솔리드를 스윕한다. BUILD_VERSION 87, 구조물 해시에 단 수 포함. 폐기한 시도: 배관 세트처럼 아래로 쌓기. 벽 상단이 이미 지반선이라 다음 단 시작점이 곧바로 원지반 아래가 되어 "묻힘" 가드에 걸렸다 — 3단 요청에 1단만 생성되는 것을 실측으로 확인하고 방향을 위로 바꿨다. 검증: pytest 241 passed / 7 skipped. 공용 브라우저 실측(285~315m·우측·높이 2.0· 3단) — 카드 3장 × 벽 3개 = 9개, 자리가 성토끝에서 노견 쪽으로 등간격 (315m: -5.42/538.10 → -4.28/538.94 → -3.14/539.77), 벽 하단은 상단 -2.5m (높이 2.0 + 근입 0.5). 3D revet 솔리드 9개. 임시 데이터는 원래 정본으로 복구 (구조물 0 · 측점 23 · 관 4). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
3.5 KiB
Python
81 lines
3.5 KiB
Python
"""독립 기슭막이(구조물 정본 D군) 제원을 횡단 측점에 얹는다.
|
|
|
|
배관 유입·유출에 딸린 기슭막이는 관 정본이 관리하지만(`B06_Section_Engine_Culvert`),
|
|
배관과 무관한 **독립 기슭막이**는 구조물 정본(`structures.json`)이 정본이다. 여기서는
|
|
구간(시작~종료) 안의 측점에 형태·높이·설치 측을 붙이기만 한다 — 치수 결정·도형은
|
|
화면(`B06_Section_UI_Cross_Revetment`)과 3D가 같은 산식으로 그린다(2026-08-28 사용자 확정).
|
|
|
|
측점 자체는 `B05_Profile_Engine_Sections.resolve_extra_stations`가 시작·기준·종료에 심는다.
|
|
"""
|
|
|
|
import logging
|
|
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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 정본이 누가거리를 0.01m로 끊어 쓴다.
|
|
_EDGE_TOLERANCE_M = 0.02
|
|
|
|
|
|
def load_revetments(project_root: Path) -> list[dict[str, Any]]:
|
|
"""구조물 정본에서 독립 기슭막이(D군 구간형) 목록을 읽는다. 실패하면 빈 목록."""
|
|
try:
|
|
types = structure_type_map()
|
|
found: list[dict[str, Any]] = []
|
|
for structure in load_structures(str(project_root))[1]:
|
|
definition = types.get(structure.type_id)
|
|
if definition is None or definition.group != "D" or definition.placement != "interval":
|
|
continue
|
|
start, end = structure.start_m, structure.end_m
|
|
if start is None or end is None:
|
|
continue
|
|
options = structure.options or {}
|
|
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"),
|
|
"height_m": options.get("height_m"),
|
|
"side": options.get("side"),
|
|
# 다단 — 1이면 단일 벽. 전개 규칙은 화면·3D가 같은 산식으로 푼다.
|
|
"tiers": options.get("tiers"),
|
|
}
|
|
)
|
|
return found
|
|
except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다
|
|
logger.exception("B06 독립 기슭막이 정본을 읽지 못했습니다 (없는 것으로 본다)")
|
|
return []
|
|
|
|
|
|
def attach_revetments(project_root: Path, cross_sections: list[dict[str, Any]]) -> int:
|
|
"""구간 안 측점의 횡단 dict에 `revetment` 키를 얹는다. 얹은 개수를 돌려준다.
|
|
|
|
한 측점에 여러 개가 겹치면 **먼저 시작한 것**을 쓴다 — 겹침 정리는 사용자 몫이다.
|
|
"""
|
|
revetments = load_revetments(project_root)
|
|
if not revetments:
|
|
return 0
|
|
attached = 0
|
|
for section in cross_sections:
|
|
chainage = section.get("chainage_m")
|
|
if not isinstance(chainage, (int, float)):
|
|
continue
|
|
for spec in revetments:
|
|
if (
|
|
spec["start_m"] - _EDGE_TOLERANCE_M
|
|
<= float(chainage)
|
|
<= spec["end_m"] + _EDGE_TOLERANCE_M
|
|
):
|
|
section["revetment"] = spec
|
|
attached += 1
|
|
break
|
|
return attached
|