"""315 물넘이포장 횡단 — 도수로 · 돌붙임 길이(성토) · 월류 높이선 성토 쪽 연장(요구 211-1). 못박는 것 - 315-1 길이 없음 = 성토는 원지반까지(그대로) · 길이 > 성토 사면 = 사면 끝에서 원지반 따라 이어 그 길이 (이은 조각까지 한 선 · 옛 흠 = 첫 조각만 그림) · 길이 < 사면 = 사면 따라 그 길이 · 절토(늘인 노면 끝이 원지반) = 그대로(길이 없음 = 안 그림 · 길이 = 원지반 따라). - 315-2 월류 높이선 — 절토 쪽 = 노면 끝 위 월류 높이(그대로) · 성토 쪽 = 성토 사면선을 노면 끝 너머 위 · 안쪽으로 점선 연장해 월류 높이선과 만나는 자리에서 끝(같은 점선 양식 한 벌). 진짜 TS 는 `helper_315_ford_chute_overflow.cjs` 가 Node 로 돌림. 화면은 사용자 확인. """ from __future__ import annotations import json import math 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)) if str(HERE) not in sys.path: sys.path.insert(0, str(HERE)) from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from config.config_system import STANDARD_CROSS_SECTION # noqa: E402 from test_b06_cross_design_mirror import _dip_ground # noqa: E402 KW = { "ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left", "paved": True, "tilted": True, "tilt_pct": 5.0, "standard": STANDARD_CROSS_SECTION, } DEPTH = 0.3 pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) def _fill_samples() -> list[dict]: """좌 = 절토(원지반이 노면 위) · 우(낮은 쪽) = 성토 — 원지반 100 + 0.35·o.""" return [ {"offset_m": o / 2, "elevation_m": round(100.0 + 0.35 * o / 2, 4), "valid": True} for o in range(-40, 41) ] @pytest.fixture(scope="module") def ts(tmp_path_factory) -> dict: source = tmp_path_factory.mktemp("s315") / "in.json" payload = { "fillSamples": _fill_samples(), "fillDesign": compute_cross_design(_fill_samples(), 100.4, **KW), "dipSamples": _dip_ground(), "dipDesign": compute_cross_design(_dip_ground(), 100.4, **KW), } source.write_text(json.dumps(payload), encoding="utf-8") proc = subprocess.run( ["node", str(HERE / "helper_315_ford_chute_overflow.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 _length(line: list[dict]) -> float: return sum( math.hypot(b["offset"] - a["offset"], b["elevation"] - a["elevation"]) for a, b in zip(line, line[1:]) ) def _on_ground(p: dict) -> bool: return p["elevation"] == pytest.approx(100.0 + 0.35 * p["offset"], abs=1e-3) def test_길이_없음_성토는_원지반까지(ts): line = ts["fillNone"] assert line[0]["offset"] == pytest.approx(-2.0) # 낮은 쪽(우) 노견 끝 assert _on_ground(line[-1]) and not _on_ground(line[0]) assert all(b["elevation"] < a["elevation"] for a, b in zip(line, line[1:])) def test_성토보다_긴_길이는_원지반_따라_이어_그_길이(ts): slope = _length(ts["fillNone"]) line = ts["fillLong"] assert slope < 6 assert _length(line) == pytest.approx(6.0, abs=1e-6) # 옛 흠 = 사면 길이에서 멈춤 toe = ts["fillNone"][-1] beyond = [p for p in line if p["offset"] < toe["offset"] - 1e-6] assert beyond and all(_on_ground(p) for p in beyond) # 사면 끝 너머 = 원지반 위 assert line[: len(ts["fillNone"])] == ts["fillNone"] # 앞은 사면 그대로 · 한 선 def test_짧은_길이는_사면_따라_그_길이(ts): line = ts["fillShort"] assert _length(line) == pytest.approx(1.0, abs=1e-6) assert not any(_on_ground(p) for p in line) def test_절토_쪽_그대로(ts): assert ts["dipNone"] == [] # 늘인 노면 끝이 원지반 · 길이 없음 = 안 그림 line = ts["dipLong"] # 길이 = 늘인 끝에서 원지반 따라 그 길이 assert line[0]["offset"] == pytest.approx(ts["dipEdges"]["right"]["offset_m"], abs=1e-6) assert _length(line) == pytest.approx(2.0, abs=1e-6) def test_그린_띠도_긴_길이(ts): nums = [float(v) for pair in ts["bandPoints"].split() for v in pair.split(",")] offsets = nums[0::2] assert min(offsets) < ts["fillNone"][-1]["offset"] - 1 # 띠가 사면 끝 너머 원지반까지 def test_월류선_절토_쪽은_노면_끝_위(ts): edges = ts["fillEdges"] first = ts["fillOverflow"][0] # 좌 = 절토 assert first["offset"] == pytest.approx(edges["left"]["offset_m"]) assert first["elevation"] == pytest.approx(edges["left"]["elevation_m"] + DEPTH) dip = ts["dipOverflow"] # 양측 절토 = 두 점 그대로 assert len(dip) == 2 assert dip[-1]["offset"] == pytest.approx(ts["dipEdges"]["right"]["offset_m"]) assert dip[-1]["elevation"] == pytest.approx(ts["dipEdges"]["right"]["elevation_m"] + DEPTH) def test_월류선_성토_쪽은_성토선_점선_연장과_만남(ts): edges = ts["fillEdges"] left, right = edges["left"], edges["right"] *_, meet, end = ts["fillOverflow"] # 끝 = 우 노면 끝(성토 사면 시작) · 그 앞 교점 = 노면 안쪽 assert end == pytest.approx({"offset": right["offset_m"], "elevation": right["elevation_m"]}) assert right["offset_m"] < meet["offset"] < left["offset_m"] # 교점은 월류 높이선 위 road = (left["elevation_m"] - right["elevation_m"]) / (left["offset_m"] - right["offset_m"]) over = right["elevation_m"] + DEPTH + road * (meet["offset"] - right["offset_m"]) assert meet["elevation"] == pytest.approx(over, abs=1e-9) # 교점 → 노면 끝 = 성토 사면과 같은 물매(사면선의 연장) slope = ts["fillNone"] fill = (slope[0]["elevation"] - slope[1]["elevation"]) / ( slope[0]["offset"] - slope[1]["offset"] ) ext = (meet["elevation"] - end["elevation"]) / (meet["offset"] - end["offset"]) assert ext == pytest.approx(fill, abs=1e-9) # 그린 점선 = 같은 점(같은 양식 한 벌) assert ts["deckPoints"].split()[-2:] == [ f"{meet['offset']},{meet['elevation']}", f"{end['offset']},{end['elevation']}", ]