feat(D 예정노선): 고름 층 노드 → 선 → 구역 → 전체 · 판 하나 재활용 · 우클릭 메뉴 · 지점 ↔ 노드 붙음 (PLAN 598-1~3 · 요구 463)
- 새 logic/D03_PlanAnalysis_Select.ts — 토막 판정 · 끌어 고르기 토막 · 토막 지움(가운데 = 구역 둘 · 뒤쪽 새 구역) · 전체 지움 · 지점 ↔ 노드 같은 자리 - 새 UI_Select.ts — 고름 상태(다시 누름 = 한 단 넓힘 · 같은 해 = 왼쪽 해 줄로만) - UI_Edit.ts — 우클릭 즉시 지움 걷음 · 노드 끌면 지점 같이 · 지점 12 px 안 붙음 · 지움 창구(dropNode · dropRegion · dropAll · splitAt) - UI_Props.ts — 판 = 노드 · 선 · 구역 · 전체 · 구간 · 마커 몸 · [방향 바꾸기] · [갈래 시점 지정] 걷음 - 새 UI_Menu.ts — 우클릭 메뉴(Points 에서 옮김 · 노드 지움 · 지점으로 · 연도 구역 나눔) - UI_Points.ts — 핀 놓을 때 노드 12 px 안 붙음(nodeNear) - key_registry plan 줄 · 시험 585_2 갈래 시점 지정 줄 지움(요구 463-15 걷음) · 553_4 메뉴 자리 · 새 시험 598_1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,11 @@ def _no_hex(path: str) -> None:
|
||||
|
||||
|
||||
def test_d03_points_parts():
|
||||
"""591-2 — D02 걷음 · 지점 칸 = 목록 줄 · 아이콘 단추 · 우클릭 메뉴 부품."""
|
||||
"""591-2 — D02 걷음 · 지점 칸 = 목록 줄 · 아이콘 단추 · 우클릭 메뉴 부품(598-2 = `UI_Menu`)."""
|
||||
assert not (ROOT / "D02_PlanBase" / "D02_PlanBase_UI_Page.ts").exists()
|
||||
ts = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Points.ts")
|
||||
assert "createListRow(" in ts and "createIconButton(" in ts and "createMapContextMenu(" in ts
|
||||
assert "createListRow(" in ts and "createIconButton(" in ts
|
||||
assert "createMapContextMenu(" in _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Menu.ts")
|
||||
for raw in ('createElement("button")', 'createElement("input")', 'el("input"'):
|
||||
assert raw not in ts
|
||||
|
||||
@@ -78,7 +79,7 @@ def test_d03_parts():
|
||||
assert ".d03-plan__layers" in css and ".d03-plan__legend" in css
|
||||
assert "createSegment({" in draw and draw.count("createInputField(") == 2
|
||||
props = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Props.ts") # 585-2 조각 연도 = 속성 판
|
||||
assert "createSelectField(" in props and "createFloatPanel(" in props
|
||||
assert "createSelectField(" in props and "createFloatPanel(" in props and "createKv(" in props
|
||||
assert 'createElement("button")' not in props and 'createElement("select")' not in props
|
||||
assert 'createElement("select")' not in three and "createSelectField" in three
|
||||
assert "createCheckbox" not in three and "buildCorridor" not in three # 예상 코리도 걷음
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""585-2 · 585-3 예정노선 끌어 고르기 판정 · 여러 구간 연도 · 연도 구역 마커 나눔 · 합침 · 갈래 시점 지정
|
||||
(요구 445 A4~A6 · 446) — TS `logic/D03_PlanAnalysis_Plan.ts`.
|
||||
"""585-2 · 585-3 예정노선 끌어 고르기 판정 · 여러 구간 연도 · 연도 구역 마커 나눔 · 합침
|
||||
(요구 445 A4~A6 · 446) — TS `logic/D03_PlanAnalysis_Plan.ts`. 갈래 시점 지정은 걷음(요구 463-15 · PLAN 598-3).
|
||||
|
||||
창(오른쪽으로) = 구간 꺾은선 점이 모두 상자 안 · 걸침(왼쪽으로) = 점 하나라도 안 또는 토막이 상자 변과 만남 ·
|
||||
돌린 화면도 보이는 px 로 셈 · 여러 구간 연도 한 번에.
|
||||
@@ -46,7 +46,7 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
...marks(),
|
||||
}));
|
||||
|
||||
/** 585-3 — 마커 = 구역 나누기만(해 그대로) · 지움 = 앞 구간이 먹음 · 갈래 처음 해 · 갈래 시점 지정. */
|
||||
/** 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]);
|
||||
@@ -58,17 +58,9 @@ function marks() {
|
||||
// 갈래 처음 해 = 붙은 자리(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),
|
||||
};
|
||||
}
|
||||
"""
|
||||
@@ -118,14 +110,9 @@ def test_여러_구간_연도(tmp_path: Path) -> None:
|
||||
assert years["p1"] == years["p3"] == 2028 and years["p2"] == 2026
|
||||
|
||||
|
||||
def test_마커_구역_나눔_합침_갈래(tmp_path: Path) -> None:
|
||||
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("다른 선에 닿은 끝이 없습니다")
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""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]),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
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]]
|
||||
Reference in New Issue
Block a user