"""D03 다시 긋기 · 노드 기록(PLAN 599-2 · 599-3 · 요구 463-11 · 12) — TS `logic/D03_PlanAnalysis_Span.ts` · `_Plan.ts`. 두 꺾임점 사이를 새 점으로 바꿈 · 끝 두 점 그대로 · 뒤 갈래 차례 밈 · 속 점 갈래 = 막음 · 마크 = 바뀐 구간 안 길이 비율 · 뒤는 길이 차 · 조각 id 그대로 · 노드 기록 = 꼭짓점과 같은 길이로 따라감 (더함 · 끼움 · 뺌 · 지움 · 뒤집기 · 잇기 · 다시 긋기) · 옮긴 노드 = 기록 남고 어긋남. ./venv/Scripts/python.exe -m pytest resources/tester/test_599_3_plan_span.py -q """ from __future__ import annotations import json import math import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Span.ts" TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" _RUNNER = """ import { writeFileSync } from "node:fs"; import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js"; import * as span from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Span.js"; const Y = 2026; let n = plan.newLine(plan.emptyNetwork(), [0, 0]); for (const p of [[100, 0], [200, 0], [300, 0]]) n = plan.appendVertex(n, 0, p); n = plan.normalizePieces(n, Y); n = plan.normalizePieces(plan.splitPiece(n, "main", 150), Y); n = plan.normalizePieces(plan.splitPiece(n, "main", 250), Y); n = plan.addBranch(n, 0, 2); n = plan.normalizePieces(plan.appendVertex(n, 1, [200, 80]), Y); const done = span.replaceSpan(n, 0, 1, 2, [[150, 50]]); const wide = span.replaceSpan(n, 0, 0, 3, [[150, 50]]); const bad = span.replaceSpan(n, 0, 2, 2, []); const straight = span.replaceSpan(n, 0, 0, 2, []); // 599-2 노드 기록 const L = span.nodeRecord("length", 20, 9); const S = span.nodeRecord("slope", 20, 9); let r = plan.newLine(plan.emptyNetwork(), [0, 0]); r = plan.appendVertex(r, 0, [10, 0]); r = plan.appendVertex(r, 0, [30, 0], L); r = plan.appendVertex(r, 0, [30, 5], S); const recorded = r; const inserted = plan.insertVertex(r, 0, 0, [5, 0]); const dropped = plan.dropLastVertex(r, 0); const removed = plan.removeVertex(r, 0, 1); const reversed = plan.reverseLine(r, 0); let t = plan.appendVertex(plan.newLine(r, [30, 5]), 1, [40, 5], L); const joined = plan.attachHead(t, 1); const moved = plan.moveVertex(r, 0, 2, [33, 0]); const flat = () => 0; const off = [1, 2, 3].map((v) => span.nodeOff(moved.lines[0], v, flat)); const respanned = span.replaceSpan(r, 0, 1, 3, [[20, 9]], [L]); const plain = span.replaceSpan(n, 0, 1, 2, [[150, 50]]); writeFileSync( process.argv[2], JSON.stringify({ before: n, done, wide, bad, straight, recorded, inserted, dropped, removed, reversed, joined, moved, off, respanned, plain, }), ); """ 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") result = tmp_path / "out.json" subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(result)], cwd=str(ROOT), check=True, capture_output=True, ) return json.loads(result.read_text(encoding="utf-8")) def test_replace_span(tmp_path: Path) -> None: r = _run_ts(tmp_path) done = r["done"] main = done["lines"][0] assert main["vertices"] == [[0, 0], [100, 0], [150, 50], [200, 0], [300, 0]] assert done["lines"][1]["branch"] == {"line": "main", "vertex": 3} # 뒤 갈래 한 칸 밈 new_span = 2 * math.hypot(50, 50) pieces = [(p["id"], p["from_m"], p["to_m"]) for p in done["pieces"] if p["line"] == "main"] before = [p["id"] for p in r["before"]["pieces"] if p["line"] == "main"] assert [p[0] for p in pieces] == before # 조각 id 그대로 assert pieces[1][1] == round(100 + new_span / 2, 3) # 구간 속 마크 = 길이 비율 assert pieces[2][1] == round(250 - 200 + 100 + new_span, 3) # 뒤 마크 = 길이 차 assert pieces[2][2] == round(300 - 200 + 100 + new_span, 3) def test_blocked_and_straight(tmp_path: Path) -> None: r = _run_ts(tmp_path) assert "갈래" in r["wide"] # 속 점(2)에 갈래 assert "고르세요" in r["bad"] straight = r["straight"]["lines"][0]["vertices"] assert straight == [[0, 0], [200, 0], [300, 0]] # 속 점 없이 곧게 assert r["straight"]["lines"][1]["branch"]["vertex"] == 1 L = {"kind": "length", "length_m": 20, "slope_pct": None} S = {"kind": "slope", "length_m": None, "slope_pct": 9} F = {"kind": "free", "length_m": None, "slope_pct": None} def _nodes(network: dict, i: int = 0) -> list: line = network["lines"][i] assert len(line.get("nodes") or line["vertices"]) == len(line["vertices"]) # 서버 400 막음 return line.get("nodes") def test_node_records_follow_vertices(tmp_path: Path) -> None: r = _run_ts(tmp_path) assert _nodes(r["recorded"]) == [None, None, L, S] # 첫 기록 = 앞 노드 기록 없음 assert _nodes(r["inserted"]) == [None, None, None, L, S] assert _nodes(r["dropped"]) == [None, None, L] assert _nodes(r["removed"]) == [None, L, S] assert _nodes(r["reversed"]) == [None, S, L, None] # 토막 끝 노드 몫 = 한 칸 밀림 assert len(r["joined"]["lines"]) == 1 and _nodes(r["joined"]) == [None, None, L, S, L] assert _nodes(r["moved"]) == [None, None, L, S] # 옮겨도 기록 남음 # 2 = 길이 20 기록 · 지금 23 · 3 = 기울기 9 % 기록 · 평지 0 % → 둘 다 어긋남 · 1 = 기록 없음 assert r["off"] == [False, True, True] assert _nodes(r["respanned"]) == [None, None, L, F] # 속 점 = 그때 기록 · 뒤 노드 = 자유 assert "nodes" not in r["plain"]["lines"][0] # 기록 없는 선 = 그대로 없음