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>
190 lines
8.1 KiB
Python
190 lines
8.1 KiB
Python
"""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
|