Files
Aislo/resources/tester/test_364_3_slope_end_round.py
T
eomsangdonandClaude Opus 5.5 3e3215826e fix(B05·B07 평면): 364-3 절/성토선 비탈 끝 모서리 둥글림 · S 자 촘촘히 (TS ↔ 파이썬 짝)
- 까닭 셋: 접힘 버린 자리 건너뜀 모서리(898.6 → 908 m · 139°) · 촘촘히가 가운데 점만 봐 S 자 빠짐(860 → 861 · 70°) · 잔 갈지자
- 촘촘히 = 4분 점도 봄 · 비탈 끝 = 가장 많이 꺾인 점부터 앞뒤 3 m 를 두 접선에 붙는 2차 곡선으로(15° 넘음) · 측점 · 깎기↔쌓기 바뀜 점은 그대로 · 둥글린 자리 눈금 뺌
- 요구 239 세션 자료: 30° 넘는 꺾임 왼 14 · 오른 3 · 최대 140.7° → 0 · 1 · 34.8°(측점 860 · 861 사이 1 m 에 폭 4.4 → 6.0 m)
- 시험 test_364_3_slope_end_round.py(전후 · 측점 그대로 · TS 같은 점) · 자료 fixtures/b05_364_3_footprint_req239.json

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
2026-10-16 23:53:08 +09:00

132 lines
5.2 KiB
Python

"""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)