- logic/Select placeNode — 지점 자리 노드면 지점 같이 · 첫 노드 = 까닭 글 - UI_Props 노드 판 = 599-1 방식 칸(createDrawKindFields) · 방식 고름 = 바로 · 수 칸 = Enter · 칸 떠날 때 - 판 · 메뉴 [다시 긋기] = draw.respan(599-3) - 시험 598_1 placeNode 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
7.6 KiB
Python
151 lines
7.6 KiB
Python
"""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]],
|
||
] # 노드 기록 같이 · 뒤쪽 첫 = 없음
|