feat(D 예정노선): 거리재기 첫 점이 노선 위면 노선 따라 길이 (PLAN 600-2 · 요구 463-23)
- 새 순수 셈 logic/D03_PlanAnalysis_Measure — 같은 선 누가 차 · 갈래 고리 따라 올라가 만남 · 이어지지 않으면 곧은 거리 - 재기 겹 — 첫 누름 선 8 px · 꺾임점 12 px 안이면 노선 따라 · 굵게 그림 · 글 「노선 따라 L m · 측점 · Δh」 - Page.ts 한 줄(노선 넘김) · 새 시험 test_600_2_plan_measure Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""D03 거리재기 「노선 따라」 셈(PLAN 600-2 · 요구 463-23) — `logic/D03_PlanAnalysis_Measure.ts`.
|
||||
|
||||
같은 선 = 누가 차 · 갈래 ↔ 본선 = 갈래 고리를 따라 올라가 만나는 점까지 · 이어지지 않은 본선끼리 = null.
|
||||
TS 를 tsc 로 옮겨 node 로 돌림(서버 · 화면 없이).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_600_2_plan_measure.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Measure.ts"
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
# main 200 m (0,0)→(100,0)→(100,100) · b1 = main 꼭짓점 1 에서 60 m · b2 = b1 끝에서 40 m · other = 이어지지 않은 본선
|
||||
NETWORK = {
|
||||
"pieces": [],
|
||||
"lines": [
|
||||
{"id": "main", "vertices": [[0, 0], [100, 0], [100, 100]], "branch": None},
|
||||
{"id": "b1", "vertices": [[100, 0], [160, 0]], "branch": {"line": "main", "vertex": 1}},
|
||||
{"id": "b2", "vertices": [[160, 0], [160, 40]], "branch": {"line": "b1", "vertex": 1}},
|
||||
{"id": "other", "vertices": [[0, 500], [50, 500]], "branch": None},
|
||||
],
|
||||
}
|
||||
|
||||
CASES = {
|
||||
"same": ["routeSpan", NETWORK, {"line": "main", "atM": 40}, {"line": "main", "atM": 150}],
|
||||
"same_back": ["routeSpan", NETWORK, {"line": "main", "atM": 150}, {"line": "main", "atM": 40}],
|
||||
"branch_main_after": [
|
||||
"routeSpan",
|
||||
NETWORK,
|
||||
{"line": "b1", "atM": 30},
|
||||
{"line": "main", "atM": 150},
|
||||
],
|
||||
"branch_main_before": [
|
||||
"routeSpan",
|
||||
NETWORK,
|
||||
{"line": "b1", "atM": 30},
|
||||
{"line": "main", "atM": 60},
|
||||
],
|
||||
"twig_root": ["routeSpan", NETWORK, {"line": "b2", "atM": 40}, {"line": "main", "atM": 0}],
|
||||
"twig_twig": ["routeSpan", NETWORK, {"line": "b2", "atM": 10}, {"line": "b1", "atM": 20}],
|
||||
"apart": ["routeSpan", NETWORK, {"line": "main", "atM": 10}, {"line": "other", "atM": 10}],
|
||||
"near_branch": ["nearestOnRoute", NETWORK, [130, 5], {"line": "main", "atM": 10}],
|
||||
"near_other_excluded": ["nearestOnRoute", NETWORK, [20, 490], {"line": "main", "atM": 10}],
|
||||
}
|
||||
|
||||
_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import * as measure from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Measure.js";
|
||||
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
const out = {};
|
||||
for (const [key, [name, ...args]] of Object.entries(cases)) out[key] = measure[name](...args);
|
||||
writeFileSync(process.argv[3], JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
tmp = tmp_path_factory.mktemp("measure")
|
||||
out = tmp / "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, result = tmp / "job.json", tmp / "out.json"
|
||||
job.write_text(json.dumps(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_같은_선은_누가_차(r):
|
||||
assert r["same"]["lengthM"] == pytest.approx(110)
|
||||
assert r["same"]["stations"] == [40, 150]
|
||||
assert r["same"]["path"][0] == [40, 0]
|
||||
assert r["same"]["path"][-1] == [100, 50]
|
||||
assert r["same_back"]["lengthM"] == pytest.approx(110)
|
||||
assert r["same_back"]["path"][0] == [100, 50]
|
||||
|
||||
|
||||
def test_갈래에서_본선으로(r):
|
||||
"""갈래 30 m 지점 → 갈래 첫 점(30) → 본선 100 m → 150 m(50) = 80 · 본선 60 m 쪽 = 30 + 40 = 70."""
|
||||
assert r["branch_main_after"]["lengthM"] == pytest.approx(80)
|
||||
assert r["branch_main_after"]["stations"] is None
|
||||
assert r["branch_main_after"]["path"][0] == [130, 0]
|
||||
assert r["branch_main_before"]["lengthM"] == pytest.approx(70)
|
||||
|
||||
|
||||
def test_갈래의_갈래는_고리를_따라_올라감(r):
|
||||
"""b2 끝(40) → b1 끝(160,0)까지 → b1 첫 점(60) → 본선 0 m(100) = 200 m."""
|
||||
assert r["twig_root"]["lengthM"] == pytest.approx(200)
|
||||
# b2 10 m(160,10) → b2 첫 점 10 → b1 20 m(120,0) 까지 40 = 50
|
||||
assert r["twig_twig"]["lengthM"] == pytest.approx(50)
|
||||
|
||||
|
||||
def test_이어지지_않은_본선은_길_없음(r):
|
||||
assert r["apart"] is None
|
||||
|
||||
|
||||
def test_가까운_자리는_이어진_선_안에서만(r):
|
||||
assert r["near_branch"]["line"] == "b1"
|
||||
assert r["near_branch"]["atM"] == pytest.approx(30)
|
||||
assert r["near_other_excluded"]["line"] == "main"
|
||||
Reference in New Issue
Block a user