"""585-2 · 585-3 예정노선 끌어 고르기 판정 · 여러 구간 연도 · 연도 구역 마커 나눔 · 합침 · 갈래 시점 지정 (요구 445 A4~A6 · 446) — TS `logic/D03_PlanAnalysis_Plan.ts`. 창(오른쪽으로) = 구간 꺾은선 점이 모두 상자 안 · 걸침(왼쪽으로) = 점 하나라도 안 또는 토막이 상자 변과 만남 · 돌린 화면도 보이는 px 로 셈 · 여러 구간 연도 한 번에. ./venv/Scripts/python.exe -m pytest resources/tester/test_585_2_plan_select.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_Plan.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"; const Y = 2026; // 본선 (0,0)→(100,0)→(100,100) · 마크 50 · 150 → p1 [0,50] p2 [50,150] p3 [150,200] · 둘째 선 (200,0)→(300,0) let n = plan.newLine(plan.emptyNetwork(), [0, 0]); n = plan.appendVertex(plan.appendVertex(n, 0, [100, 0]), 0, [100, 100]); n = plan.normalizePieces(n, Y); n = plan.normalizePieces(plan.splitPiece(n, "main", 50), Y); n = plan.normalizePieces(plan.splitPiece(n, "main", 150), Y); n = plan.normalizePieces(plan.appendVertex(plan.newLine(n, [200, 0]), 1, [300, 0]), Y); const view = ([x, y]) => [x, -y]; // 화면 y 아래로 const turned = ([x, y]) => [-y, -x]; // 90° 돌린 화면 const box = (x0, y0, x1, y1) => ({ x0, y0, x1, y1 }); const pick = (b, crossing, to = view) => plan.piecesInBox(n, to, b, crossing); writeFileSync(process.argv[2], JSON.stringify({ pieces: n.pieces.map((p) => [p.id, p.line, p.from_m, p.to_m]), window: pick(box(-10, -10, 60, 10), false), crossing: pick(box(60, 10, -10, -10), true), through: [pick(box(70, -5, 80, 5), true), pick(box(70, -5, 80, 5), false)], wholeLine: pick(box(190, -5, 310, 5), false), partLine: [pick(box(190, -5, 250, 5), false), pick(box(250, 5, 190, -5), true)], all: pick(box(-10, -110, 310, 10), false), rotated: pick(box(-110, -110, -40, -90), false, turned), years: plan.setPiecesYear(n, ["p1", "p3"], 2028).pieces.map((p) => [p.id, p.year]), ...marks(), })); /** 585-3 — 마커 = 구역 나누기만(해 그대로) · 지움 = 앞 구간이 먹음 · 갈래 처음 해 · 갈래 시점 지정. */ function marks() { const span = (net) => net.pieces.filter((p) => p.line === "main").map((p) => [p.id, p.from_m, p.to_m, p.year]); let m = plan.newLine(plan.emptyNetwork(), [0, 0]); m = plan.normalizePieces(plan.appendVertex(m, 0, [100, 0]), Y); const cut = plan.normalizePieces(plan.splitPiece(m, "main", 40), Y); const edge = plan.splitPiece(cut, "main", 40.5); // 다른 마커 1 m 안 = 그대로 const yeared = plan.setPiecesYear(cut, ["p2"], 2028); const merged = plan.normalizePieces(plan.removeMark(yeared, "main", 40, 1e-3), Y); // 갈래 처음 해 = 붙은 자리(60 m · p2 = 2028) 구간 해 let b = plan.normalizePieces(plan.insertVertex(yeared, 0, 0, [60, 0]), Y); b = plan.normalizePieces(plan.appendVertex(plan.addBranch(b, 0, 1), 1, [60, 50]), Y); // 갈래 시점 지정 — (60,90) 에서 그어 (60,0) 꺾임점에 닿은 본선 → 닿은 끝이 시점인 갈래 let t = plan.normalizePieces(plan.insertVertex(m, 0, 0, [60, 0]), Y); t = plan.normalizePieces(plan.appendVertex(plan.newLine(t, [60, 90]), 1, [60, 0]), Y); const branched = plan.branchFromEnd(t, 1); const lone = plan.normalizePieces(plan.appendVertex(plan.newLine(m, [0, 50]), 1, [30, 80]), Y); return { cut: span(cut), edgeSame: edge === cut, merged: span(merged), branchYear: b.pieces.filter((p) => p.line !== "main").map((p) => p.year), branched: typeof branched === "string" ? branched : branched.lines.map((l) => [l.id, l.vertices[0], l.branch]), again: typeof branched === "string" ? null : plan.branchFromEnd(branched, 1), lone: plan.branchFromEnd(lone, 1), }; } """ 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) ids = {p[0]: p[1:] for p in got["pieces"]} assert ids["p1"] == ["main", 0, 50] and ids["p2"] == ["main", 50, 150] other = next(k for k, v in ids.items() if v[0] != "main") assert got["window"] == ["p1"] # p2 는 (100, 100) 쪽이 상자 밖 assert got["crossing"] == ["p1", "p2"] # 왼쪽으로 = p2 앞 끝이 상자 안 assert got["through"] == [["p2"], []] # 점 없이 토막만 지남 = 걸침만 assert got["wholeLine"] == [other] # 마커 없는 선 = 구간 하나 = 선 통째 assert got["partLine"] == [[], [other]] # 선 반만 = 창 없음 · 걸침 있음 assert sorted(got["all"]) == sorted(ids) assert got["rotated"] == ["p3"] # 돌린 화면 = 보이는 px 로 셈 def test_여러_구간_연도(tmp_path: Path) -> None: got = _run_ts(tmp_path) years = dict(got["years"]) assert years["p1"] == years["p3"] == 2028 and years["p2"] == 2026 def test_마커_구역_나눔_합침_갈래(tmp_path: Path) -> None: got = _run_ts(tmp_path) assert got["cut"] == [["p1", 0, 40, 2026], ["p2", 40, 100, 2026]] # 구역만 나눔 · 해 그대로 assert got["edgeSame"] # 다른 마커 1 m 안 = 안 놓음 assert got["merged"] == [["p1", 0, 100, 2026]] # 지움 = 앞 구간 해 · id 가 먹음 assert got["branchYear"] == [2028] # 갈래 처음 해 = 붙은 자리 구간 해 lines = {line_id: (head, branch) for line_id, head, branch in got["branched"]} head, branch = lines[next(k for k in lines if k != "main")] assert head == [60, 0] and branch == {"line": "main", "vertex": 1} # 닿은 끝 = 시점 assert got["again"] == "이미 갈래입니다 — 갈라지는 점이 시점입니다." assert got["lone"].startswith("다른 선에 닿은 끝이 없습니다")