"""364-3 절/성토선 비탈 끝 모서리 둥글림 — 요구 239 브레인 검증 세션 자료로 전후 꺾임. 자료 `fixtures/b05_364_3_footprint_req239.json` = 세션 측점 줄 70 + 노선. 고치기 전 세션 선: 비탈 끝 30° 넘는 꺾임 왼 14 · 오른 3 · 최대 140.7°(접힘 버린 자리 · S 자 촘촘히 빠짐 · 잔 갈지자). TS(`B05_Profile_PlanFootprint.ts`) 도 같은 자료로 같은 점인지 본다. ./venv/Scripts/python.exe -m pytest resources/tester/test_364_3_slope_end_round.py -q """ from __future__ import annotations import json import subprocess import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope as plan # noqa: E402 DATA = json.loads( (ROOT / "resources/tester/fixtures/b05_364_3_footprint_req239.json").read_text(encoding="utf-8") ) ROUTE = [tuple(point) for point in DATA["route"]] RECORDS = DATA["records"] END = ROUTE[-1][0] TS_FILE = ROOT / "B05_Profile" / "logic" / "B05_Profile_PlanFootprint.ts" TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" def _turns(lines: list) -> list[float]: """조각 안 꺾임(°) — 조각 끝(깎기↔쌓기 바뀜 · 양 끝)은 뺌.""" return [ plan._turn_deg(line[i - 1], line[i], line[i + 1]) for line in lines for i in range(1, len(line) - 1) ] def _pieces() -> tuple: return plan.slope_pieces(ROUTE, RECORDS, 0.0, END, 2.0) def test_비탈_끝_30도_넘는_꺾임() -> None: edge, fill, cut, _ticks = _pieces() fill_turns, cut_turns = _turns(fill["lines"]), _turns(cut["lines"]) over = sum(t > 30 for t in fill_turns + cut_turns) # 남는 하나 = 측점 860 · 861 사이 1 m 에 비탈 폭 4.4 → 6.0 m(측점 자리라 그대로) assert over <= 1, over assert max(fill_turns + cut_turns) < 40.0 assert sum(t > 15 for t in fill_turns + cut_turns) <= 25 assert max(_turns(edge["lines"])) < 10.0 # 도로 끝은 손대지 않음 def test_둥글림을_끄면_꺾임이_남음() -> None: """기준이 바뀐 길을 재는지 — 둥글림만 끄면 30° 넘는 꺾임이 그대로 나옴.""" keep = plan.ROUND_TURN_DEG plan.ROUND_TURN_DEG = 1e9 try: _edge, fill, cut, _ticks = _pieces() finally: plan.ROUND_TURN_DEG = keep turns = _turns(fill["lines"]) + _turns(cut["lines"]) assert sum(t > 30 for t in turns) >= 5 and max(turns) > 90.0 def test_측점_비탈_끝은_그대로() -> None: """남은 측점 비탈 끝 점은 둥글림 뒤에도 같은 자리 점으로 남음.""" keep = plan.ROUND_TURN_DEG plan.ROUND_TURN_DEG = 1e9 try: _e, fill0, cut0, _t = _pieces() finally: plan.ROUND_TURN_DEG = keep _e, fill, cut, _t = _pieces() before = {tuple(p) for line in fill0["lines"] + cut0["lines"] for p in line} after = {tuple(p) for line in fill["lines"] + cut["lines"] for p in line} for record in RECORDS: x, y, nx, ny = plan.route_normal(ROUTE, record["chainage_m"]) for side in ("left", "right"): offset = record[f"{side}_end_m"] want = (round(x + nx * offset, 4), round(y + ny * offset, 4)) if want in before: assert want in after, (record["chainage_m"], side) DRIVER = """ import { readFileSync, writeFileSync } from "node:fs"; import { slopePieces } from "./B05_Profile_PlanFootprint.js"; const c = JSON.parse(readFileSync(process.argv[2], "utf-8")); writeFileSync(process.argv[3], JSON.stringify(slopePieces(c.route, c.records, 0, c.end, 2))); """ @pytest.fixture(scope="module") def ts_pieces(tmp_path_factory: pytest.TempPathFactory) -> dict: out = tmp_path_factory.mktemp("ts_round") subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(TSC), str(TS_FILE), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", "--noCheck", "--noResolve"], cwd=str(ROOT), check=True, capture_output=True, ) # fmt: skip (out / "driver.mjs").write_text(DRIVER, encoding="utf-8") payload = {"route": ROUTE, "records": RECORDS, "end": END} (out / "in.json").write_text(json.dumps(payload), encoding="utf-8") run = subprocess.run( # noqa: S603 ["node", str(out / "driver.mjs"), str(out / "in.json"), str(out / "out.json")], capture_output=True, text=True, cwd=str(out), ) # fmt: skip assert run.returncode == 0, run.stderr return json.loads((out / "out.json").read_text(encoding="utf-8")) def test_TS_도_같은_점(ts_pieces) -> None: edge, fill, cut, ticks = _pieces() for name, want, got in ( ("도로 끝", edge["lines"], ts_pieces["edge"]), ("쌓기", fill["lines"], ts_pieces["fill"]), ("깎기", cut["lines"], ts_pieces["cut"]), ("눈금", ticks["ticks"], ts_pieces["ticks"]), ): assert len(want) == len(got), f"{name} 선 수" for line_w, line_g in zip(want, got): assert len(line_w) == len(line_g), f"{name} 점 수" for pw, pg in zip(line_w, line_g): assert abs(pw[0] - pg[0]) < 2e-4 and abs(pw[1] - pg[1]) < 2e-4, (name, pw, pg)