Files
Aislo/B06_Section/B06_Section_Engine_Culvert.py
T
eomsangdonandClaude Fable 5 0d5fba4400 feat(B06): 배수관 횡단도 세트(배관·기슭막이/집수정·보호공) 추가
B05에서 배치한 배수관 측점의 B06 횡단도에 배관·기슭막이(또는 집수정)·보호공을
분리 도형으로 그린다. 기존 도형(지반선·면적 밴드·포장·암 경계)은 손대지 않고,
계획선 표시 규칙만 세트와 정합을 맞춰 함께 정비했다.

- 백엔드: pipe_points.json 정본(facility=="pipe")에 레지스트리 기본값을 채워
  chainage ±0.02m로 맞는 횡단에 culvert 키를 얹는다(Engine_Culvert 신규, Router의
  상세조회·재생성 두 경로에서 호출). 횡단 dict는 자유형이라 Pydantic 변경 없음.
- 배관: 유입 invert = min(노견 아래 원지반, 노면고 − 관경 − 토피 0.5), 유출 앵커는
  성토사면이 지반과 만나는 사면 끝(성토사면 길이 5m 한계 — 별표2 Ⅰ.2.차.(3)).
  역경사는 수평 클램프, 길이는 m 단위 올림. 단면은 축 법선 방향 직사각형이고
  외경선(실선)+내경선(점선) 2겹으로 관벽 두께를 보인다(파형강관 2.7T — KS D 3590).
  끝단면은 구조물의 계류측 변을 복사해 자른다.
- 기슭막이: 배면 수직 사다리꼴 + 전면 평행사변형 띠(1:0.3, t=0.45 — 실무 L3=45)의
  합성 단면. 높이는 이음선 상단점이 성토 사면선과 만나는 접점까지, 하한 관경+0.45,
  상한은 형태별 한계(찰 3.0/메 2.0 — 교본 7-3). 근입 0.5m(실무 기초콘크리트 H=0.5).
  돌 해칭은 벽 폴리곤 clipPath 안에만 그려 어떤 높이에서도 벽 밖으로 새지 않는다.
- 집수정: 절토측 유입이거나 원지반이 관 단면의 30% 이상을 막으면 자동 전환.
  I형(벽)·ㄴ형(+바닥)·ㄷ형(+반대측 막음), 기본 ㄴ형·내공 1.0m. 구조물이 원지반
  안쪽에 박히면 절토선을 공사 계획선(보라 실선)으로 긋는다. 라벨은 I형 벽 하단.
- 보호공: 시작 윗점 = 관 하단 꼭짓점, 시작 변은 기슭막이 전면과 같은 경사.
  성토경사를 따라 원지반까지 내려간 뒤, 최소 길이(낙차고×2)를 채울 때까지 원지반을
  따라 더 간다.
- 계획선 표시: 원지반과 차이 없는 구간은 그리지 않되(보라 점선 폐지), 노면·노견·
  측구부터 사면 1차 접점까지는 항상 그린다. 측구는 노견 바깥에 붙으므로 보호 범위를
  측구 폭만큼 넓히고 접점 탐색도 그 바깥에서 시작한다. 배수관 측점은 벽 접점에서 트림.
- B05: 배수관 측점 계획고를 지반고 + 관경 + 토피 0.5m 아래로 내리지 못하게 막고
  경고 토스트를 띄운다.
- 700줄 제한: 오버레이 파일이 919줄이 되어 기하(_Geom 678줄)와 그리기(247줄)로
  분리했다. import 경로는 재수출로 유지해 B05·Cross_View는 건드리지 않았다.

근거 미확정(코드 주석·툴팁에 명시): 토피 0.5m(별표2 교량·암거 복토 50㎝ 교차 참조),
기슭막이 근입 0.5m(실무값), 흄관·VR관 관벽 두께(관경÷12 — KS F 4403 미확보).

검증: tsc, ruff, pytest 148 passed(신규 test_b06_culvert_set.py 6건), 헤드 브라우저 —
4+4.3·13+15.7 세트 표시, 보호공 시작점과 관 하단 꼭짓점 간격 0.6px 이내·시작 변
기울기 실측 0.223(= 0.3·H/(H+0.5) 이론값과 일치)·최소 길이 5.0m 확보, 계획선
세그먼트 연결 틈 0개(8·9·10·12·4+4.3). ⚠ 13+15.7 성토사면 5.03m로 법정 5m를
0.03m 초과 — 툴팁에 표기하고 접점 보간 여부는 사용자 판단 대기.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 19:43:51 +09:00

190 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B06 배수관 세트(배관·기슭막이·보호공) 횡단도 정보.
B05 배수유역도가 찍은 관 지점(`pipe_points.json` = 배관 정본)을 읽어, 같은 누가거리의
횡단면에 그림을 그릴 수 있을 만큼의 제원을 `culvert` 키로 얹는다. 값을 새로 만들지 않고
**정본 + 레지스트리 기본값**만 조합한다 — 상수 사본을 두면 B05 폼과 갈라진다.
근거(지식DB):
· 관은 **지반선상 매설** — 뜨게 매설은 부적합 (교본 8장, 그림 8-3·8-9)
· 관 지름 1,000㎜ 이상(현지여건 800㎜ 이상) — 별표2 .2.사.(2)
· 유입구 = 계곡부 기슭막이 / 계통적 옆도랑은 집수정, 유출구 = 기슭막이 (교본 8장 3·4절)
· 기슭막이 계획비탈 1:0.3~1:0.5 (사방기술교본), 돌쌓기 전면 1:0.3 (교본 7-3)
· 유출구부터 원지반까지 도수로·물받이 설치 의무 — 별표2 Ⅰ.2.사.(2)
⚠ 아래 세 상수는 **임도 확정값이 아니다**. 지식DB에 임도 기준이 없어 교차 참조로
채택했고(2026-08-19 사용자 확정), 임도 기준이 확보되면 그 값으로 교체한다.
"""
from __future__ import annotations
import json
import logging
from functools import lru_cache
from pathlib import Path
from typing import Any
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from common_util.common_util_drainage_pipes import PIPE_FACILITY_PIPE, parse_pipe_points
from config.config_system import (
DRAINAGE_CACHE_DIRNAME,
DRAINAGE_EDITS_DIRNAME,
DRAINAGE_PIPE_POINTS_FILENAME,
)
logger = logging.getLogger(__name__)
# 관 지점과 횡단 측점을 같은 자리로 볼 허용 오차(m). 정본이 누가거리를 0.01m로 끊어 쓴다.
_CHAINAGE_TOLERANCE_M = 0.02
# ⚠ 교차 참조 ① 관 위 최소 토피(m). 임도 배수관 토피 규정이 지식DB에 없어 별표2
# 교량·암거 "복토 시 흙 두께 50㎝ 이상"을 끌어왔다. B05 계획고 하향 차단 기준이다.
MIN_PIPE_COVER_M = 0.5
# ⚠ 교차 참조 ② 보호공(물받이) 길이 = 낙차고(기슭막이 높이) × 배수.
# 사방기술교본 바닥막이 물받이 "길이 = 낙차고의 2~3배" 중 2배 채택.
APRON_LENGTH_FACTOR = 2.0
# 보호공 두께(m) — 사용자 확정 0.45 (2026-08-20, 실무 돌붙임 L3=45와 정합).
# 사방교본 물받이 "두께 1m 내외"는 바닥막이용 교차 참조라 채택하지 않았다.
APRON_THICKNESS_M = 0.45
# 기슭막이 전면 기울기(1:n). 돌쌓기 전면 1:0.3(교본 7-3) = 사방교본 계획비탈
# 1:0.3~0.5의 급한 쪽. 지식DB 근거가 있는 값이라 교차 참조 표시가 붙지 않는다.
REVET_FACE_SLOPE = 0.3
# (2026-08-20 사용자 확정) 배관은 **수평도 가능** — 그림에서 경사를 강제로 만들지
# 않는다. config `DRAINAGE_PIPE_SLOPE_DEG`(10도)는 유효직경 수리계산 전용으로 남는다.
# 유입구 구조 선택지 — 레지스트리 `pipe.inlet_type` choices와 같은 문자열이다.
INLET_STRUCTURE_BASIN = "집수정"
def pipe_points_file(project_root: Path) -> Path:
"""프로젝트 저장소 안의 관 지점 정본 경로."""
return (
project_root
/ "B04_PreProcess"
/ DRAINAGE_CACHE_DIRNAME
/ DRAINAGE_EDITS_DIRNAME
/ DRAINAGE_PIPE_POINTS_FILENAME
)
@lru_cache(maxsize=1)
def _registry_pipe_defaults() -> dict[str, Any]:
"""레지스트리 `pipe` 타입 옵션의 기본값 묶음.
저장분에 없는 옵션을 채우는 유일한 출처다 — 여기서 꺼내 쓰면 B05 폼이 보여 주는
기본값과 항상 같은 값이 그림에 들어간다.
"""
pipe_type = structure_type_map().get("pipe")
if pipe_type is None:
return {}
return {option.key: option.default for option in pipe_type.options}
def _number(value: Any, fallback: float | None) -> float | None:
"""숫자 옵션 하나를 float로 정리한다. 문자열 저장분(관경 "1000")도 받는다."""
if isinstance(value, bool):
return fallback
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
try:
return float(value.strip())
except ValueError:
return fallback
return fallback
def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) -> dict[str, Any]:
"""유입("inlet")·유출("outlet") 한쪽의 부속 제원.
구조가 집수정이면 기슭막이·보호공을 만들지 않는다 — 집수정 단면은 후속 작업이라
화면은 라벨만 쓴다(2026-08-19 사용자 확정).
"""
structure = options.get(f"{side}_type") or defaults.get(f"{side}_type") or "기슭막이"
spec: dict[str, Any] = {"role": side, "structure": str(structure)}
if spec["structure"] == INLET_STRUCTURE_BASIN:
return spec
height = _number(
options.get(f"{side}_revet_height_m"),
_number(defaults.get(f"{side}_revet_height_m"), None),
)
length = _number(
options.get(f"{side}_revet_length_m"),
_number(defaults.get(f"{side}_revet_length_m"), None),
)
form = options.get(f"{side}_revet_form") or defaults.get(f"{side}_revet_form")
spec.update(
{
"revet_form": str(form) if form else None,
"revet_height_m": height,
"revet_length_m": length,
"face_slope": REVET_FACE_SLOPE,
}
)
# 보호공은 기슭막이 바닥의 세굴 방지 구조 — 낙차고(기슭막이 높이)에 종속한다.
if height is not None and height > 0:
spec["apron_length_m"] = round(height * APRON_LENGTH_FACTOR, 3)
spec["apron_thickness_m"] = APRON_THICKNESS_M
return spec
def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
"""관 1개소의 세트 제원(관 + 유입·유출 기슭막이 + 보호공)."""
defaults = _registry_pipe_defaults()
values = dict(options or {})
diameter_mm = _number(
values.get("pipe_diameter_mm"), _number(defaults.get("pipe_diameter_mm"), 1000.0)
)
kind = values.get("pipe_kind") or defaults.get("pipe_kind")
return {
"type": "pipe",
"pipe_kind": str(kind) if kind else None,
"diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3),
"min_cover_m": MIN_PIPE_COVER_M,
"inlet": _side_spec(values, defaults, "inlet"),
"outlet": _side_spec(values, defaults, "outlet"),
}
def load_culvert_sets(project_root: Path) -> dict[float, dict[str, Any]]:
"""관 지점 정본을 읽어 누가거리별 세트 제원을 돌려준다.
노선 지문은 보지 않는다 — 횡단면 자체가 현 노선에서 만들어진 것이라, 지문이
어긋난 옛 관 지점은 매칭될 측점이 없어 자연히 걸러진다. 파일이 없거나 깨졌으면 빈 값.
"""
path = pipe_points_file(project_root)
if not path.is_file():
return {}
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
logger.warning("B06 배수관 세트: 관 지점 파일을 읽지 못했습니다 (%s)", path)
return {}
sets: dict[float, dict[str, Any]] = {}
for point in parse_pipe_points(document.get("points")):
# BOX암거·물넘이포장·세월교는 그림이 다르다 — 이번 범위는 배관뿐이다.
if point.facility != PIPE_FACILITY_PIPE:
continue
sets[round(float(point.chainage_m), 2)] = _culvert_set(point.options)
return sets
def attach_culvert_sets(project_root: Path, cross_sections: list[dict[str, Any]]) -> int:
"""배관이 놓인 측점의 횡단 dict에 `culvert` 키를 얹는다. 얹은 개수를 돌려준다."""
sets = load_culvert_sets(project_root)
if not sets:
return 0
attached = 0
for section in cross_sections:
chainage = _number(section.get("chainage_m"), None)
if chainage is None:
continue
for pipe_chainage, spec in sets.items():
if abs(chainage - pipe_chainage) <= _CHAINAGE_TOLERANCE_M:
section["culvert"] = spec
attached += 1
break
return attached