"""346-2 평면 발자국 — TS(`B05_Profile/logic/B05_Profile_PlanFootprint.ts`) ↔ 파이썬 (`B07_DesignDetail_Engine_Cad_PlanSlope.py` · `slope_records` 한 줄) 거울 시험. ./venv/Scripts/python.exe -m pytest resources/tester/test_346_2_plan_footprint_mirror.py -q """ from __future__ import annotations import json import math 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)) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import ( # noqa: E402 route_frame, side_is_cut, slope_end, slope_pieces, ) TS_FILE = ROOT / "B05_Profile" / "logic" / "B05_Profile_PlanFootprint.ts" TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" TOL = 2e-4 # 점 좌표 round(·, 4) — 반올림 경계만 1e-4 갈릴 수 있음 VERTICES = [(0.0, 0.0), (100.0, 0.0), (160.0, 60.0), (170.0, 140.0)] def _route() -> list[tuple[float, float, float]]: route, chain = [], 0.0 for index, (x, y) in enumerate(VERTICES): if index: chain += math.hypot(x - VERTICES[index - 1][0], y - VERTICES[index - 1][1]) route.append((chain, x, y)) return route def _section(chainage: float) -> tuple[dict, list[dict]]: """측점 횡단 — 지반 기울기가 측점마다 바뀌어 깎기 · 쌓기가 뒤집힘 · 노면 ±2.5 m.""" tilt = 0.4 * math.sin(chainage / 37.0) road = 100.0 + 0.6 * math.cos(chainage / 23.0) offsets = [-20.0 + 0.5 * i for i in range(81)] def ground(o: float) -> float: return 100.0 + tilt * o + 0.3 * math.sin(o / 3.0) samples = [ {"offset_m": o, "elevation_m": ground(o), "valid": abs(o) < 18.5 or chainage < 100} for o in offsets ] line = [] for o in offsets: if abs(o) <= 2.5: line.append({"offset_m": o, "elevation_m": road}) continue run = abs(o) - 2.5 g = ground(o) edge_g = ground(2.5 if o > 0 else -2.5) z = min(g, road + run) if edge_g > road else max(g, road - run / 1.5) line.append({"offset_m": o, "elevation_m": z}) design = { "road_edges": { "left": {"offset_m": 2.5, "elevation_m": road}, "right": {"offset_m": -2.5, "elevation_m": road}, }, "design_line": line, } return design, samples def _stations() -> list[dict]: route = _route() total = route[-1][0] stations = [] chainages = [20.0 * i for i in range(int(total // 20) + 1)] + [total, 60.0] # 겹친 측점 하나 for chainage in chainages: x, y, nx, ny = route_frame(route, chainage) design, samples = _section(chainage) stations.append( { "chainage_m": chainage, "x": x, "y": y, "tx": ny, "ty": -nx, "design": design, "samples": samples, } ) stations.append({**stations[3], "chainage_m": 70.0, "design": {"road_edges": None}}) stations.append({**stations[4], "chainage_m": 90.0, "samples": []}) # 지반 없음 return stations def _py_record(station: dict) -> dict | None: """`B07_DesignDetail_Router_Support_Plan.slope_records` 의 측점 한 줄(파일 읽기 뺌).""" design = station["design"] or {} edges = design.get("road_edges") or {} try: left_edge = float(edges["left"]["offset_m"]) right_edge = float(edges["right"]["offset_m"]) except (KeyError, TypeError, ValueError): return None line, samples = design.get("design_line") or [], station["samples"] or [] record = { "chainage_m": float(station["chainage_m"]), "left_edge_m": left_edge, "left_end_m": slope_end(line, samples, left_edge, "left"), "right_edge_m": right_edge, "right_end_m": slope_end(line, samples, right_edge, "right"), } for side in ("left", "right"): elevation = edges[side].get("elevation_m") if isinstance(elevation, (int, float)): record[f"{side}_cut"] = side_is_cut(samples, record[f"{side}_edge_m"], elevation) length = math.hypot(station["tx"], station["ty"]) or 1.0 record.update( x=station["x"], y=station["y"], nx=-station["ty"] / length, ny=station["tx"] / length ) return record DRIVER = """ import { readFileSync, writeFileSync } from "node:fs"; import { planFootprint } from "./B05_Profile_PlanFootprint.js"; const cases = JSON.parse(readFileSync(process.argv[2], "utf-8")); writeFileSync(process.argv[3], JSON.stringify(cases.map((c) => planFootprint(c.route, c.stations, c.start, c.end, c.step)))); """ CASES = [(0.0, 400.0, 2.0), (35.0, 130.0, 2.0), (0.0, 400.0, 10.0)] @pytest.fixture(scope="module") def ts_result(tmp_path_factory: pytest.TempPathFactory) -> list[dict]: out = tmp_path_factory.mktemp("ts_footprint") 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(), "stations": _stations(), "start": s, "end": e, "step": t} for s, e, t in CASES ] (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 _same_lines(want: list, got: list, name: str) -> None: 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]) < TOL and abs(pw[1] - pg[1]) < TOL, f"{name} {pw} {pg}" @pytest.mark.parametrize("index", range(len(CASES))) def test_발자국이_두_쪽에서_같다(ts_result, index: int) -> None: start, end, step = CASES[index] records = [r for r in map(_py_record, _stations()) if r is not None] edge, fill, cut, ticks = slope_pieces(_route(), records, start, end, step) got = ts_result[index] _same_lines(edge["lines"], got["edge"], "도로 끝") _same_lines(fill["lines"], got["fill"], "쌓기") _same_lines(cut["lines"], got["cut"], "깎기") _same_lines(ticks["ticks"], got["ticks"], "눈금") def test_깎기_쌓기가_함께_나옴(ts_result) -> None: got = ts_result[0] assert len(got["edge"]) == 2 and got["fill"] and got["cut"] and got["ticks"]