feat(B06): 구조물 목록의 C군 벽을 횡단 면적에 반영
좌측 「구조물 배치」로 넣은 옹벽·돌쌓기 같은 벽이 지금까지 측점만 심고 기하가 없어 절·성토 면적이 그대로였음(2026-09-06 사용자 확정: 반영해야 함). - 독립 기슭막이가 쓰던 `section.revetment` 제원 자리에 그대로 얹음 — 기하·설계선 트림·폐회로 면적·3D 가 손대지 않고 따라옴. - 서버 `B06_Section_Engine_Structures_Wall.py`(저장분) ↔ 브라우저 `common_util_structure_walls.ts`(미저장 초안) 짝 + 거울 테스트. - 설치 측 칸이 없는 C군은 성토가 나는 쪽으로 자동 배치, 양측 절토면 세우지 않음. - 구조물 목록이 바뀌면 B06 이 초안을 얹어 다시 그림. 검증: 40m 돌쌓기(찰) 추가 시 그 측점 성토 5.81 → 5.02㎡. 테스트 390 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""구조물 정본(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
|
||||
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
|
||||
|
||||
# 구조물 종류 → 횡단 기하가 아는 **형태** 이름. 형태가 벽 높이 한계·두께를 정하므로
|
||||
# (`B06_Section_UI_Cross_Revetment.revetHeightLimit`) 가장 가까운 것으로 잇는다.
|
||||
_FORM_BY_TYPE = {
|
||||
"masonry_wet": "돌쌓기(찰)",
|
||||
"masonry_dry": "돌쌓기(메)",
|
||||
"boulder_masonry": "돌쌓기(메)",
|
||||
"retaining_wall": "콘크리트",
|
||||
"soil_guard": "통나무·목재틀",
|
||||
}
|
||||
|
||||
|
||||
def load_wall_structures(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""구조물 정본에서 C군 벽(구간형) 목록을 읽는다. 실패하면 빈 목록."""
|
||||
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 != "C" 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") or _FORM_BY_TYPE.get(structure.type_id),
|
||||
"height_m": options.get("height_m"),
|
||||
# C군 폼에는 설치 측 칸이 없다 — 비워 두면 화면이 **성토가 나는 쪽**으로
|
||||
# 세운다(`computeRevetmentLayout`). 사용자가 정하고 싶어지면 그때 칸을 낸다.
|
||||
"side": options.get("side"),
|
||||
"tiers": options.get("tiers"),
|
||||
"lift_m": options.get("lift_m"),
|
||||
"shift_m": options.get("shift_m"),
|
||||
}
|
||||
)
|
||||
return found
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다
|
||||
logger.exception("B06 구조물 벽 정본을 읽지 못했습니다 (없는 것으로 본다)")
|
||||
return []
|
||||
|
||||
|
||||
def attach_wall_structures(project_root: Path, cross_sections: list[dict[str, Any]]) -> int:
|
||||
"""구간 안 측점의 횡단 dict에 `revetment` 키를 얹는다. 얹은 개수를 돌려준다.
|
||||
|
||||
이미 관 정본이 얹은 세트(`culvert`·`revetment`)가 있는 측점은 **건드리지 않는다** —
|
||||
관 유입·유출 벽과 구조물 벽이 한 자리에 겹치면 어느 쪽 그림인지 읽히지 않는다.
|
||||
한 측점에 여러 개가 겹치면 먼저 시작한 것을 쓴다(겹침 정리는 사용자 몫).
|
||||
"""
|
||||
walls = load_wall_structures(project_root)
|
||||
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
|
||||
if attached:
|
||||
logger.info("B06 구조물 벽 %d개 측점에 얹음 (구조물 %d건)", attached, len(walls))
|
||||
return attached
|
||||
Reference in New Issue
Block a user