- 평면 = 위성 + 도엽 등고선 + 다른 갈래(옅게) + 고른 노선 + 측점(누가 = 등록 시작부터) + 지점 끌기 겹 - 끈 지점 = 캐시 · 받기 · 분석 · 보기는 points 로 실어 보냄 · [저장] 때만 D02 규칙으로 정본 - 연도별 갈래 넷 카드(연장 · 최대 기울기 · 위반 수) · 고름 = 캐시 · [저장] = 서버 저장본으로 다시 셈 - 측점 누름 = 하 패널 지반선 횡단(sectionsGround · buildCrossSvg 지반만 인자 · 기본 그대로) - 3D 모달 = 연도 DEM + 노선 + 측점 막대(코리도 · 구조물 안 부름) - 화면 계산 logic 짝(pickMode · chainRows · joinedLine ↔ 파이썬) 거울 시험 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
5.0 KiB
Python
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]
|