"""294 물넘이 낮은 쪽 연장 · 도수로/돌붙임 끝단 · 띠 ↔ 성토 사면 고름(요구 188-2 · 188-3 · 189 · 그림_188). 못박는 것 - 294-3 물넘이 측점 낮은 쪽 노면 끝 원지반이 노면보다 높으면(절토) 노면을 같은 기울기로 늘여 원지반과 처음 만나는 자리까지(그 쪽 절토 사면 없음 · `tilt_reach`) · 성토 쪽 그대로 · 반폭 안에서 못 만나면 옛 길 · 서버 정본 ↔ TS 짝 같은 값 = 횡단 설계 거울(`test_b06_cross_design_mirror` 「물넘이포장 낮은 쪽 노면 연장」) · 그림 = 콘크리트(포장층)가 늘인 끝까지. - 294-4 도수로/돌붙임 띠 = 낮은 쪽 끝단(늘인 끝 · 아니면 유출 쪽 노견 끝)에서 성토 사면 따라 · 길이 비움 = 원지반 닿으면 끝 · 양측 절토 물넘이 = 띠 없음 · 길이 = 그 길이(사면보다 길면 원지반 따라) · 다른 구조물 (배관 · 박스 · 세월교) 같은 규칙(`chuteRun` · 원지반 `layout.ground`) + 같은 「길이 (m)」 칸 · 면적 = 늘 손 입력(301-1 · 길이 · 폭으로 안 셈) · 길이는 집계 · B08 수량에 안 실림. 진짜 TS 는 `helper_294_3_ford_extend_chute.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)) 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 CASES, _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, } pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) def _line(samples_fn) -> list[dict]: return [ {"offset_m": o / 2, "elevation_m": round(samples_fn(o / 2), 4), "valid": True} for o in range(-24, 25) ] @pytest.fixture(scope="module") def dip() -> dict: return compute_cross_design(_dip_ground(), 100.4, **KW) @pytest.fixture(scope="module") def ts(dip, tmp_path_factory) -> dict: source = tmp_path_factory.mktemp("s294") / "in.json" fill_samples = _line(lambda o: 100.0 + 0.35 * o) fill = compute_cross_design(fill_samples, 100.4, **KW) payload = { "design": dip, "dipSamples": _dip_ground(), "fillSamples": fill_samples, "fillDesign": fill, } source.write_text(json.dumps(payload), encoding="utf-8") proc = subprocess.run( ["node", str(HERE / "helper_294_3_ford_extend_chute.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_양측_절토_낮은_쪽_늘인_끝은_원지반_교점(dip): reach = dip["tilt_reach"] # 원지반 101.05 + 0.2·o(−4.5 까지) · 노면 100.4 + 0.05·o → −4.3333 에서 만남 assert reach["side"] == "right" assert reach["offset_m"] == pytest.approx(-4.3333, abs=1e-4) assert reach["elevation_m"] == pytest.approx(100.4 - 0.05 * 4.3333, abs=1e-4) edge = dip["road_edges"]["right"]["offset_m"] for p in dip["design_line"]: # 노견 끝 ~ 늘인 끝 = 노면 기울기 그대로(절토 사면 없음) if reach["offset_m"] - 1e-6 <= p["offset_m"] <= edge: assert p["elevation_m"] == pytest.approx(100.4 + 0.05 * p["offset_m"], abs=1e-4) assert dip["ditch_enabled"] is False def test_성토_쪽은_그대로(): got = compute_cross_design(_line(lambda o: 100.0 + 0.35 * o), 100.4, **KW) assert "tilt_reach" not in got # 낮은 쪽(우) 노면 끝 원지반이 노면 아래 = 성토 사면 def test_반폭_안에서_못_만나면_옛_길(): got = compute_cross_design(_line(lambda o: 101.0 + 0.02 * o), 100.4, **KW) assert "tilt_reach" not in got # 늘인 노면이 끝까지 원지반 아래 = 절토 사면 그대로 right = [ p for p in got["design_line"] if p["offset_m"] < got["road_edges"]["right"]["offset_m"] ] assert any(p["elevation_m"] > got["road_edges"]["right"]["elevation_m"] for p in right) def test_서버_TS_같은_값은_거울_시험(): assert any(case["name"] == "물넘이포장 낮은 쪽 노면 연장" for case in CASES) def test_콘크리트는_늘인_끝까지(dip, ts): assert ts["edges"]["right"] == { "offset_m": dip["tilt_reach"]["offset_m"], "elevation_m": dip["tilt_reach"]["elevation_m"], } first = ts["pavePoints"].split()[1] # 포장층 윗면 우 끝 assert float(first.split(",")[0]) == pytest.approx(-4.3333, abs=1e-4) def test_띠_길이_맞춤_공용(ts): assert ts["runNone"] == [[[0, 0], [3, -4]]] # 길이 없음 = 원지반 닿으면 끝(그대로) assert ts["runShort"] == [[[0, 0], [1.2, -1.6]]] # 길이 2 = 사면 따라 2 m assert ts["runLong"] == [[[0, 0], [3, -4]], [[3, -4], [5, -4]]] # 사면 5 + 원지반 2 assert ts["runPointOnly"] == [] # 끝단이 이미 원지반 · 길이 없음 = 안 그림 assert ts["runPointLength"] == [[[3, -4], [5, -4]]] def test_양측_절토_물넘이_띠_없음(ts): assert len(ts["dipLine"]) == 1 # 늘인 끝이 원지반 assert ts["dipLine"][0]["offset"] == pytest.approx(-4.3333, abs=1e-4) assert ts["dipChute"] == [] tail = ts["dipChuteLength"] # 길이를 넣으면 늘인 끝에서 원지반 따라 그 길이 assert tail[0]["offset"] == pytest.approx(-4.3333, abs=1e-4) and len(tail) >= 2 def test_성토_쪽_띠는_원지반_닿으면_끝(ts): line = ts["fillLine"] end = line[-1] assert end["elevation"] == pytest.approx(100.0 + 0.35 * end["offset"], abs=1e-3) # 원지반 위 assert len(ts["fillLineOld"]) > len(line) # 원지반을 모르면 내려가는 원지반까지 따라갔음 chute = ts["fillChuteLength"] start, stop = chute[0], chute[-1] assert (stop["offset"] - start["offset"]) ** 2 + ( stop["elevation"] - start["elevation"] ) ** 2 == pytest.approx(1.0, abs=1e-6) def test_다른_구조물_같은_규칙과_길이_칸(ts): assert ts["chuteItems"][-1] == "length_m" assert ts["fordPaveChuteSame"] is True hatch = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Wall_Hatch.ts").read_text( encoding="utf-8" ) assert "const lines = chuteRun(allLines, chute.length_m, ground);" in hatch for name in ("Culvert", "Box", "Ford"): overlay = (ROOT / "B06_Section" / f"B06_Section_UI_Cross_{name}.ts").read_text( encoding="utf-8" ) assert "layout.ground," in overlay form = (ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts").read_text( encoding="utf-8" ) assert "syncChuteArea" not in form # 면적 = 손 입력(301-1) def test_길이는_집계_수량에_안_실림(): from B06_Section.B06_Section_Engine_Culvert import _chute from B08_Quantity.B08_Quantity_Router_Summary import _chutes from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows options = { "chute_add_1_structure": "돌붙임", "chute_add_1_bond": "찰붙임", "chute_add_1_area": 12, } longer = {**options, "chute_add_1_length_m": 7} assert _chute(longer)["length_m"] == 7 and "length_m" not in _chute(options) 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, longer, {"gchuteadd1": "SS1"}, {}) assert sheet.rows == [("chute|찰붙임", "㎡", 12.0)] item = {"type_id": "pipe", "options": options, "복사본": "pipe@50.0", "흙막이단": 0} plain = container_rows(item, {"gchuteadd1": "SS1"}) long = container_rows({**item, "options": longer}, {"gchuteadd1": "SS1"}) 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)