- B05_Profile/logic/B05_Profile_Footprint_Stage.ts — footprintStage(ctx) · 측점 지반(우 = 음수 오프셋 · 빈칸 무효) + cross.byKey[key].detail(road_edges · design_line) → 측점 줄 · 바뀐 범위 밖은 ctx.prev.records 같은 열쇠 · 같은 누가거리 줄 그대로 - PlanFootprint.ts — footprintLines(계약 roadLeft · roadRight · slopeLeft · slopeRight + 점마다 깎기 여부 · 눈금 · 측점 줄) · 옛 꼴 이름 SlopePieces · 몸통 sampleLines 하나 - 시험 test_346_2_footprint_stage.py · 거울 시험 tsc 에 --noCheck --noResolve Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
186 lines
6.8 KiB
Python
186 lines
6.8 KiB
Python
"""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"]
|