- 공용 chuteRun(Wall_Hatch) — 길이 비움 = 원지반 닿으면 끝 · 넣음 = 사면 따라 그 길이 · 사면보다 길면 원지반 따라 이어 그 길이 · 끝단이 이미 원지반이면 안 그림
- 물넘이 — 띠 시작 = 늘인 노면 끝(294-3) · 아니면 유출 쪽 노견 끝 · 원지반 닿으면 끝(양측 절토 = 띠 없음)
- 배관 · 박스 · 세월교 — 띠 계산이 원지반(layout.ground)을 받아 같은 규칙 · 3D 띠도 같음 · 코리도 해시에 길이
- 명세 CHUTE_ITEMS 맨 아래 「길이 (m)」(chute_add_{n}_length_m · 그림 전용) · 물넘이는 공용 칸 그대로
- 면적 자동 = 물넘이 · 세월교만 월류 폭 × 길이(배관 · 박스 = 폭 정의 없어 손 입력 그대로 · 브레인 정함)
- 길이는 집계 열 · B08 수량(면적만)에 안 실림 · 마스터 정의 이름 표는 안 건드림(이름 표 대조 시험에서 이 칸만 밖)
- 시험 test_294_3 · 286_1 · 52 · 65 · 152_3 기대값
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
202 lines
8.4 KiB
Python
202 lines
8.4 KiB
Python
"""286 물넘이포장 종횡단 설계(요구 180-2).
|
|
|
|
못박는 것
|
|
- 286-1 물넘이포장도 기준 측점 하나 — TS `pipeStationMarks` · `plantedChainages` ↔ 서버
|
|
`resolve_extra_stations` 같은 값 · 라벨 「물넘이포장」 · B05 · B06 거름 걷음.
|
|
- 286-2 물넘이 자리 = 측구 없음(서버 정본 · 화면 짝 · 거울 = `test_b06_cross_design_mirror`) ·
|
|
노면 = 경사각도로 표고 낮은 쪽(설계선 양 끝 표고 · 같으면 단면유형).
|
|
- 286-3 도수로/돌붙임 = 다른 구조물과 같은 칸 + 맨 아래 「길이 (m)」(`ford_chute_{n}_length_m`) ·
|
|
비면 원지반까지 · 있으면 사면 따라 그 길이 · 서버 · TS 같은 값 · 집계 · B08 수량에 안 실림.
|
|
|
|
진짜 TS 는 `helper_286_1_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
ROOT = HERE.parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B05_Profile.B05_Profile_Engine_Sections import resolve_extra_stations # noqa: E402
|
|
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
|
|
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
|
from B06_Section.B06_Section_Router_Design import ford_pavement_ranges, paved_at # noqa: E402
|
|
from B06_Section.B06_Section_Engine_Culvert import _chute # noqa: E402
|
|
from B08_Quantity.B08_Quantity_Router_Summary import _chutes # noqa: E402
|
|
from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402
|
|
from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows # noqa: E402
|
|
|
|
PIPES = [
|
|
{"chainage_m": 95.0, "source": "user"},
|
|
{"chainage_m": 230.0, "source": "user", "facility": "ford_pavement"},
|
|
{"chainage_m": 300.0, "source": "user", "facility": "ford_bridge"},
|
|
]
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
|
reason="프론트엔드 의존성 미설치",
|
|
)
|
|
|
|
|
|
def _read(*parts: str) -> str:
|
|
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def ts(tmp_path_factory) -> dict:
|
|
types = [
|
|
{
|
|
"type_id": item.type_id,
|
|
"group": item.group,
|
|
"placement": item.placement,
|
|
"managed_by": item.managed_by,
|
|
}
|
|
for item in structure_type_map().values()
|
|
]
|
|
source = tmp_path_factory.mktemp("s286") / "in.json"
|
|
source.write_text(
|
|
json.dumps({"pipes": PIPES, "structures": [], "types": types}), encoding="utf-8"
|
|
)
|
|
proc = subprocess.run(
|
|
["node", str(HERE / "helper_286_1_ford_pavement.cjs"), str(source)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=60,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_물넘이포장_측점_TS_파이썬_같은_값(ts, tmp_path):
|
|
assert ts["fordMarks"] == [{"chainage_m": 230, "role": "anchor"}]
|
|
server = resolve_extra_stations(tmp_path, parse_pipe_points(PIPES))
|
|
assert [mark for mark, _label in server] == ts["planted"] == [95, 230, 300]
|
|
assert dict(server)[230.0] == "물넘이포장"
|
|
|
|
|
|
def test_B05_B06_물넘이_거름_걷음():
|
|
page = _read("B05_Profile", "B05_Profile_UI_Page_Structures.ts")
|
|
assert "withoutFordPavement" not in page
|
|
assert "FACILITY_NAMES.ford_pavement" not in page
|
|
view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts")
|
|
assert '"ford_pavement"' not in view
|
|
drafts = _read("B06_Section", "B06_Section_Section_Drafts.ts")
|
|
assert 'if (facility === "ford_pavement") return null;' not in drafts
|
|
|
|
|
|
def _ground(slope: float) -> list[dict]:
|
|
return [
|
|
{"offset_m": o / 2, "elevation_m": round(100.0 + slope * o / 2, 4), "valid": True}
|
|
for o in range(-24, 25)
|
|
]
|
|
|
|
|
|
def test_물넘이_자리_측구_없음():
|
|
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
|
|
plain = compute_cross_design(_ground(0.35), 100.4, **kwargs)
|
|
ford = compute_cross_design(_ground(0.35), 100.4, tilted=True, **kwargs)
|
|
assert plain["ditch_enabled"] is True and "tilted" not in plain
|
|
assert ford["ditch_enabled"] is False and ford["tilted"] is True
|
|
assert ford["ditch_area_m2"] == 0
|
|
# 사용자가 측구를 켜도 물넘이 자리는 안 섬
|
|
forced = compute_cross_design(_ground(0.35), 100.4, tilted=True, ditch_choice=True, **kwargs)
|
|
assert forced["ditch_enabled"] is False
|
|
|
|
|
|
def test_물넘이_범위_서버_판정(tmp_path):
|
|
edits = tmp_path / "B04_PreProcess" / "drainage" / "edits"
|
|
edits.mkdir(parents=True)
|
|
(edits / "pipe_points.json").write_text(
|
|
json.dumps({"points": [{**PIPES[1], "options": {"ford_width_m": 8}}]}), encoding="utf-8"
|
|
)
|
|
ranges = ford_pavement_ranges(tmp_path)
|
|
assert ranges == [(226.0, 234.0)]
|
|
assert paved_at(230.0, ranges) and paved_at(226.0, ranges) and not paved_at(240.0, ranges)
|
|
|
|
|
|
def test_노면은_표고_낮은_쪽으로():
|
|
# 287-4 — 설계 노면 = 경사각도(%)로 지대 낮은 쪽 한 경사 · 중심 = 계획고(콘크리트 윗면)
|
|
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
|
|
for slope, high in ((0.35, "left"), (-0.35, "right")):
|
|
got = compute_cross_design(_ground(slope), 100.4, tilted=True, tilt_pct=5.0, **kwargs)
|
|
left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right"))
|
|
assert (left > right) == (high == "left")
|
|
assert got["design_elevation_m"] == 100.4 and got["cross_slope_pct"] == 5.0
|
|
assert got["tilt_pct"] == 5.0
|
|
|
|
|
|
def test_저장_뒤_서버가_물넘이_측구를_맞춤():
|
|
router = _read("B06_Section", "B06_Section_Router_Design.py")
|
|
assert "stored_tilt(design) == tilt" in router
|
|
refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts")
|
|
assert "tilted: Boolean(section.ford_pavement || section.underdrain)," in refresh
|
|
|
|
|
|
CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30}
|
|
|
|
|
|
def test_도수로_길이_서버_TS_같은_값(ts):
|
|
assert (
|
|
_chute(CHUTE, "ford_chute")
|
|
== ts["chuteNoLength"]
|
|
== {
|
|
"structure": "돌붙임",
|
|
"thickness_m": 0.3,
|
|
}
|
|
)
|
|
long = {**CHUTE, "ford_chute_1_length_m": 3}
|
|
assert _chute(long, "ford_chute") == ts["chuteLength"]
|
|
assert ts["chuteLength"]["length_m"] == 3
|
|
|
|
|
|
def test_도수로_길이_없음_원지반까지_있음_그_길이(ts):
|
|
# 유출 = 좌(우가 표고 높음) · 노견(2.5, 100) → 원지반(10, 95)
|
|
assert ts["lineNoLength"] == [
|
|
{"offset": 2.5, "elevation": 100},
|
|
{"offset": 10, "elevation": 95},
|
|
]
|
|
start, end = ts["lineLength"]
|
|
assert start == {"offset": 2.5, "elevation": 100}
|
|
assert (end["offset"] - 2.5) ** 2 + (100 - end["elevation"]) ** 2 == pytest.approx(9.0)
|
|
assert end["offset"] == pytest.approx(2.5 + 3 * 7.5 / 9.0139, abs=1e-3)
|
|
assert ts["lineTooLong"] == ts["lineNoLength"] # 사면보다 길면 원지반에서 멈춤
|
|
|
|
|
|
def test_컨테이너_같은_칸_맨_아래_길이():
|
|
spec = _read("A00_Common", "design", "design_structure_ford_pavement.ts")
|
|
assert (
|
|
"items: CHUTE_ITEMS," in spec # 공용 칸 맨 아래 길이(294-4 · CHUTE_ITEMS 로 옮김)
|
|
)
|
|
|
|
|
|
def test_길이는_집계_수량에_안_실림():
|
|
options = {**CHUTE, "ford_chute_1_bond": "찰붙임", "ford_chute_1_area": 12}
|
|
item = {"type_id": "ford_pavement", "options": options, "복사본": "pipe@50.0"}
|
|
plain = container_rows(item, {"gfordpavechute1": "SS1"})
|
|
long = container_rows(
|
|
{**item, "options": {**options, "ford_chute_1_length_m": 7}}, {"gfordpavechute1": "SS1"}
|
|
)
|
|
# 집계 열이 읽는 칸(length_m · 쪽별 revet_length_m)은 그대로 — 길이는 안 읽히는 이름으로만
|
|
for key in ("length_m", "inlet_revet_length_m", "outlet_revet_length_m", "area_m"):
|
|
assert plain[0]["options"].get(key) == long[0]["options"].get(key)
|
|
|
|
class Sheet:
|
|
def __init__(self) -> None:
|
|
self.rows: list[tuple] = []
|
|
|
|
def add(self, at, key, sort, group, label, value, combos) -> None:
|
|
self.rows.append((key, label, value))
|
|
|
|
sheet = Sheet()
|
|
_chutes(sheet, 50.0, {**options, "ford_chute_1_length_m": 7}, {"gfordpavechute1": "SS1"}, {})
|
|
assert sheet.rows == [("chute|찰붙임", "㎡", 12.0)] # 돌붙임 수량 = 면적 · 길이 안 탐
|