Files
Aislo/resources/tester/test_530_3_plan_logic.py
T
eomsangdonandClaude Opus 5.5 50e9c1b015 feat(530-3): D03 예정노선 분석 화면 — 평면 · 지점 끌기 · 더 받기 · 갈래 카드 · 측점 · 지반 횡단 · 3D
- 평면 = 위성 + 도엽 등고선 + 다른 갈래(옅게) + 고른 노선 + 측점(누가 = 등록 시작부터) + 지점 끌기 겹
- 끈 지점 = 캐시 · 받기 · 분석 · 보기는 points 로 실어 보냄 · [저장] 때만 D02 규칙으로 정본
- 연도별 갈래 넷 카드(연장 · 최대 기울기 · 위반 수) · 고름 = 캐시 · [저장] = 서버 저장본으로 다시 셈
- 측점 누름 = 하 패널 지반선 횡단(sectionsGround · buildCrossSvg 지반만 인자 · 기본 그대로)
- 3D 모달 = 연도 DEM + 노선 + 측점 막대(코리도 · 구조물 안 부름)
- 화면 계산 logic 짝(pickMode · chainRows · joinedLine ↔ 파이썬) 거울 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:11:23 +09:00

143 lines
5.0 KiB
Python

"""D03 화면 계산 거울(PLAN 530-3) — 파이썬 `D03_PlanAnalysis_Analyze` ↔ TS `logic/D03_PlanAnalysis_Plan.ts`.
짝: `pick_mode` · `chain` · `joined_line` ↔ `pickMode` · `chainRows` · `joinedLine`.
TS 만: 측점 누가(경계 = 측점 · 간격 배수) · 앞붙임 선(선 위 누가 = 등록 시작 누가) · 받은 지형 판정.
./venv/Scripts/python.exe -m pytest resources/tester/test_530_3_plan_logic.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from D03_PlanAnalysis import D03_PlanAnalysis_Analyze as analyze # noqa: E402
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Plan.ts"
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js";
const job = JSON.parse(readFileSync(process.argv[2], "utf8"));
const out = job.cases.map((c) => {
const rows = plan.chainRows(job.results, c.choices, c.start);
return {
picks: job.results.map((r) => plan.pickMode(r, c.choices[r.year])),
rows,
line: plan.joinedLine(job.results, rows),
stations: plan.stationChainages(rows, 20),
};
});
const lead = plan.leadInLine([[0, 0], [3, 4], [10, 4]], 1234.5);
const reach = plan.nodesInReach(
[[500, 500], [900, 500], [1700, 500]],
[{ x0: 0, y0: 0, x1: 1300, y1: 1000 }, null],
);
writeFileSync(process.argv[3], JSON.stringify({ out, lead, reach }));
"""
_P0, _P1, _P2 = [0.0, 0.0], [300.0, 400.0], [900.0, 400.0]
_RESULTS = [
{
"year": 2026,
"start": _P0,
"end": _P1,
"routes": {
"gradeline": {"planned": [_P0, [100.0, 200.0], _P1], "metrics": {"length_m": 530.0}},
"earthwork": {"error": "길 없음"},
"gentle": {"planned": [_P0, _P1], "metrics": {"length_m": 500.004}},
},
},
{
"year": 2027,
"start": _P1,
"end": _P2,
"routes": {
"gradeline": {"error": "길 없음"},
"gentle": {"planned": [_P1, [600.0, 500.0], _P2], "metrics": {"length_m": 640.333}},
"weighted": {"planned": [_P1, _P2], "metrics": {"length_m": 600.0}},
},
},
]
_CASES = [
{"choices": {}, "start": 1000.0},
{"choices": {"2026": "gentle", "2027": "weighted"}, "start": 17.25},
{"choices": {"2026": "earthwork", "2027": "follow"}, "start": 0.0},
]
def _run_ts(tmp_path: Path) -> dict:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(TS_FILE),
"--outDir",
str(out),
"--rootDir",
str(ROOT),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
],
cwd=str(ROOT),
check=True,
capture_output=True,
)
for emitted in out.rglob("*.js"):
emitted.write_text(
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
job = tmp_path / "job.json"
result = tmp_path / "out.json"
job.write_text(json.dumps({"results": _RESULTS, "cases": _CASES}), encoding="utf-8")
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(job), str(result)],
cwd=str(ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
def test_ts_matches_python_chain(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
for case, ts in zip(_CASES, got["out"]):
choices = {int(k): v for k, v in case["choices"].items()}
rows = analyze.chain(_RESULTS, choices, case["start"])
assert ts["picks"] == [analyze.pick_mode(r, choices.get(r["year"])) for r in _RESULTS]
assert ts["rows"] == rows
assert ts["line"] == analyze.joined_line(_RESULTS, rows)
# 측점 = 경계(1530) · 20 배수 · 끝 — 첫 연도 시작 1000 · 끝 2170.333
stations = got["out"][0]["stations"]
assert stations[:3] == [1000.0, 1020.0, 1040.0]
assert 1530.0 in stations and 1520.0 in stations and 1540.0 in stations
assert stations[-2:] == [2160.0, 2170.333]
# 시작 17.25 → 첫 규칙 측점 20
assert got["out"][1]["stations"][:2] == [17.25, 20.0]
def test_lead_in_and_reach(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
back = got["lead"][0]
assert abs(back[0] + 1234.5 * 0.6) < 1e-9 and abs(back[1] + 1234.5 * 0.8) < 1e-9
assert got["lead"][1:] == [[0, 0], [3, 4], [10, 4]]
# 마디 0 = 첫 연도 안 · 마디 1 = 첫 연도 안 + 둘째 연도 DEM 없음 = 밖 · 마디 2 = 밖
assert got["reach"] == [True, False, False]