"""598-1 예정노선 고름 층 노드 → 선(토막) → 구역 → 전체 · 토막 지움(요구 463 9 · 12 · 13·16 · 15) — TS `logic/D03_PlanAnalysis_Select.ts`. 끝 토막 지움 = 끝 점 뺌 · 가운데 토막 지움 = 구역 둘(뒤쪽 = 새 구역 · 갈래 고리 · 조각 따라감) · 사라지는 노드에 갈래 = 까닭 글 · 끌어 고르기 토막 판정 · 지점 ↔ 노드 같은 자리. ./venv/Scripts/python.exe -m pytest resources/tester/test_598_1_plan_select_layers.py -q """ from __future__ import annotations import json import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Select.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 sel from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Select.js"; const Y = 2026; // 본선 (0,0)→(100,0)→(200,0)→(300,0) · 마크 150 → p1 [0,150] p2 [150,300] · 갈래 at 노드 2 (200,0)→(200,50) let n = plan.newLine(plan.emptyNetwork(), [0, 0]); for (const x of [100, 200, 300]) n = plan.appendVertex(n, 0, [x, 0]); n = plan.normalizePieces(plan.splitPiece(plan.normalizePieces(n, Y), "main", 150), Y); n = plan.normalizePieces(plan.setPiecesYear(n, ["p2"], 2028), Y); n = plan.normalizePieces(plan.appendVertex(plan.addBranch(n, 0, 2), 1, [200, 50]), Y); const show = (net) => typeof net === "string" ? net : ({ lines: net.lines.map((l) => [l.id, l.vertices, l.branch]), pieces: plan.normalizePieces(net, Y).pieces.map((p) => [p.id, p.line, p.from_m, p.to_m, p.year]), }); const seg = (line, index) => ({ line, index }); const view = ([x, y]) => [x, -y]; writeFileSync(process.argv[2], JSON.stringify({ segs: sel.allSegments(n), range: sel.segmentRange(n, seg("main", 1)), at: [sel.segmentAt(n.lines[0].vertices, 0), sel.segmentAt(n.lines[0].vertices, 100), sel.segmentAt(n.lines[0].vertices, 300)], nodeSeg: [sel.nodeSegment(n, { line: "main", vertex: 1 }), sel.nodeSegment(n, { line: "main", vertex: 3 })], pieces: sel.segmentPieces(n, [seg("main", 1)]).sort(), middle: show(sel.removeSegment(n, seg("main", 0 + 1))), tail: show(sel.removeSegment(n, seg("main", 2))), head: show(sel.removeSegment(n, seg("main", 0))), only: show(sel.removeSegment(n, seg("branch-1", 0))), parentOnly: sel.removeSegments(n, [seg("main", 0), seg("main", 1), seg("main", 2)]), many: show(sel.removeSegments(n, [seg("main", 0), seg("main", 2)])), box: sel.segmentsInBox(n, view, { x0: 90, y0: -10, x1: 210, y1: 10 }, false), cross: sel.segmentsInBox(n, view, { x0: 150, y0: 10, x1: 120, y1: -10 }, true), all: show(sel.removeAll(n)), nodes: sel.nodesAt(n, [200, 0]), moved: sel.movePointsAt([[200, 0], [5, 5]], [200, 0], [210, 5]), placed: (() => { const out = sel.placeNode(n, [[100, 0], [9, 9]], 0, 1, { kind: "length", length_m: 50, slope_pct: null }, (x) => x * 0.1); return [out.network.lines[0].vertices, out.network.lines[0].nodes, out.points]; })(), placeFirst: sel.placeNode(n, [], 0, 0, { kind: "free", length_m: null, slope_pct: null }, () => 0), nodes594: (() => { const r = { kind: "length", length_m: 100, slope_pct: null }; const withNodes = { ...n, lines: n.lines.map((l, i) => (i ? l : { ...l, nodes: [null, r, r, r] })) }; const out = sel.removeSegment(withNodes, seg("main", 1)); return out.lines.filter((l) => l.id !== "branch-1").map((l) => [l.id, l.nodes]); })(), })); """ 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, ) # fmt: skip 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_토막_자리(tmp_path: Path) -> None: got = _run_ts(tmp_path) assert [s["line"] for s in got["segs"]] == ["main"] * 3 + ["branch-1"] assert got["range"] == [100, 200] assert got["at"] == [0, 1, 2] # 꺾임점 위 = 뒤 토막 · 끝 = 마지막 assert got["nodeSeg"] == [{"line": "main", "index": 1}, {"line": "main", "index": 2}] assert got["pieces"] == ["p1", "p2"] # 100~200 토막에 마크 150 = 두 구간 def test_가운데_토막_지움_구역_둘(tmp_path: Path) -> None: got = _run_ts(tmp_path) lines = {i: (v, b) for i, v, b in got["middle"]["lines"]} assert lines["main"] == ([[0, 0], [100, 0]], None) # 앞쪽 = 같은 id back = next(k for k in lines if k not in ("main", "branch-1")) assert lines[back] == ([[200, 0], [300, 0]], None) # 뒤쪽 = 새 구역(본선) assert lines["branch-1"][1] == {"line": back, "vertex": 0} # 갈래 고리가 따라감 pieces = {(p[1], p[2], p[3]): (p[0], p[4]) for p in got["middle"]["pieces"]} assert pieces[("main", 0, 100)] == ("p1", 2026) # 앞 조각 id 지님 assert pieces[(back, 0, 100)][1] == 2028 # 뒤 조각 해 그대로 def test_끝_토막_지움_끝_점_뺌(tmp_path: Path) -> None: got = _run_ts(tmp_path) tail = {i: v for i, v, _ in got["tail"]["lines"]} assert tail["main"] == [[0, 0], [100, 0], [200, 0]] head = {i: (v, b) for i, v, b in got["head"]["lines"]} assert head["main"][0] == [[100, 0], [200, 0], [300, 0]] assert head["branch-1"][1] == {"line": "main", "vertex": 1} # 갈래 차례 −1 assert [p[2:4] for p in got["head"]["pieces"] if p[1] == "main"] == [[0, 50], [50, 200]] def test_갈래_붙은_노드_막음_하나뿐_구역_지움(tmp_path: Path) -> None: got = _run_ts(tmp_path) only = got["only"]["lines"] assert [i for i, _, _ in only] == ["main"] # 토막 하나뿐 = 구역 지움 assert got["parentOnly"] == "갈래가 붙은 점이 사라집니다 — 갈래를 먼저 지우세요." many = {i: v for i, v, _ in got["many"]["lines"]} assert many["main"] == [[100, 0], [200, 0]] # 뒤부터 지움 = 차례 안 꼬임 def test_끌어_고르기_전체_지점_노드(tmp_path: Path) -> None: got = _run_ts(tmp_path) assert got["box"] == [{"line": "main", "index": 1}] # 창 = 두 끝 모두 안 assert got["cross"] == [{"line": "main", "index": 1}] # 걸침 = 상자와 만남 assert got["all"] == {"lines": [], "pieces": []} assert got["nodes"] == [{"line": "main", "vertex": 2}] # 갈래 첫 점 뺌 assert got["moved"] == [[210, 5], [5, 5]] vertices, nodes, points = got["placed"] assert vertices == [[0, 0], [50, 0], [200, 0], [300, 0]] # 앞 노드 기준 50 m · 뒤 노드 그대로 assert nodes[1] == {"kind": "length", "length_m": 50, "slope_pct": None} and nodes[0] is None assert points == [[50, 0], [9, 9]] # 그 자리 지점 같이 assert got["placeFirst"] == "첫 노드는 방식이 없습니다." rec = {"kind": "length", "length_m": 100, "slope_pct": None} assert got["nodes594"] == [ ["main", [None, rec]], ["line-2", [None, rec]], ] # 노드 기록 같이 · 뒤쪽 첫 = 없음