Files
Aislo/resources/tester/test_530_3_plan_logic.py
T
eomsangdonandClaude Opus 5.5 1ddb5cb1b2 feat(D03): 예정노선 그리기 — 시점 · 종점 여럿 · 들어올 때 자동 받기 · [긋기] 양끝 · 부분 수정 · 기울기 % · ° 미리보기 · 방향 바꾸기 · 선 고르기 · 지우기 · 연도 (PLAN 579-3 · 580-1~3 · 요구 440 · 441)
- 579-3: D02 시점 · 종점 목록 받음 · 점 그림 · 맞춤 · 등고선 범위 · 「범위 밖」 판정에 시점 · 종점 · 들어올 때 받은 범위가 못 덮으면 바로 받기(진행 막대 · 직전 실패면 안 함)
- 선 이름 = 갈래 없는 선 「본선」(둘 이상 「본선 N」) · 갈래 「갈래 N」 — TS lineNames ↔ D04 line_names 짝 · 거울 시험
- 580-1: [긋기] 하나 — 시점 · 종점(끝낼 때 뒤집어 No.0 = 반대쪽) · 선 꼬리 · 선 머리 · 선 위(갈래)에서 시작 · 머리가 다른 선 꼬리에 닿으면 한 선 · 꺾임점이면 갈래 · 꺾임점 끌기 · 더하기 · 지우기(마크는 구간 비율)
- 580-2: 미리보기 글 = 거리 + 가상 종단 기울기 % · ° · 기울기 고정 칸 옆 ° 환산 · 고른 선 [방향 바꾸기]
- 580-3: 지도에서 선 · 조각 고름(좌측 줄 밝힘) · Delete · [선 지움] · 선 통째 연도 · 긋는 해(긋는 중 바꾸면 다음 점부터) · [연도 구분] 뒤 연도 고름 바로 · 마크 끌기
- 새 파일: D03_PlanAnalysis_UI_Edit.ts(고르기 · 고치기) · D03_PlanAnalysis_UI_DrawPaint.ts(그림 · 집기 — UI_Draw 700 줄 넘어 나눔)
- UI_Map = 겹 꼴에 release · pointerup · 끌기 중 포인터 잡기 · [?] 안내 줄(그리기 본체 draw() 는 sub2 몫이라 손 안 댐)
- 시험: 530-3 새 꼴 함수 · 서버 derive 대조 · 선 이름 거울 / 553-4 그림 자리 옮김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:45:10 +09:00

266 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D03 손 긋기 꼴 다루기(PLAN 562-3 · 563-1 · 580) — TS `logic/D03_PlanAnalysis_Plan.ts`.
조각이 선을 빈틈없이 덮음 · 마크 놓기 · 지우기 · 갈래 차례 밀기 · 갈래 붙은 점 못 뺌 ·
서버 꼴 읽기 · 화면 꼴(선 · 마크 · 조각)을 서버 셈(`derive`)이 그대로 받음 ·
580 = 종점에서 그은 선 뒤집어 잇기 · 갈래로 붙음 · 꺾임점 옮김 · 지움(마크 비율) ·
선 지움 · 방향 바꿈 · 선 통째 연도 · 마크 옮김 · 선 이름 거울(D04 `line_names`).
./venv/Scripts/python.exe -m pytest resources/tester/test_530_3_plan_logic.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from D03_PlanAnalysis import D03_PlanAnalysis_Network as server # noqa: E402
from D04_PlanConfirm.D04_PlanConfirm_Repository import line_names # noqa: E402
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;
const empty = plan.emptyNetwork();
let n = plan.newLine(empty, [0, 0]);
n = plan.appendVertex(n, 0, [100, 0]);
n = plan.appendVertex(n, 0, [100, 100]);
n = plan.normalizePieces(n, Y);
const one = n;
n = plan.normalizePieces(plan.splitPiece(n, "main", 50), Y);
const split = n;
const tooClose = plan.splitPiece(n, "main", 50.5);
n = plan.normalizePieces(plan.setPieceYear(n, 1, 2027), Y);
n = plan.addBranch(n, 0, 1);
n = plan.appendVertex(n, 1, [160, 0]);
n = plan.normalizePieces(n, Y);
const branched = n;
const inserted = plan.insertVertex(n, 0, 0, [40, 0]);
const blocked = plan.dropLastVertex(inserted, 0);
const dropped = plan.dropLastVertex(n, 1);
const guard = plan.dropLastVertex(plan.dropLastVertex(n, 0), 0);
const removed = plan.normalizePieces(plan.removeMark(n, "main", 50.4, 1), Y);
const grown = plan.normalizePieces(plan.appendVertex(n, 0, [100, 150]), Y);
const read = plan.fromServer(
n.lines,
[{ line: "main", at_m: 50 }, { line: "main", at_m: 150 }],
[{ id: "p9", line: "main", from_m: 0, to_m: 50, year: 2028 }],
Y,
);
// 580 — 본선(0,0)→(100,0) 반쯤 · 종점(200,0)에서 거꾸로 그은 선이 본선 꼬리에 닿음 → 한 선
let j = plan.normalizePieces(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), Y);
j = plan.newLine(j, [200, 0]);
j = plan.appendVertex(j, 1, [150, 50]);
j = plan.appendVertex(j, 1, [100, 0]);
j = plan.normalizePieces(j, 2027);
const joined = plan.normalizePieces(plan.attachHead(plan.reverseLine(j, 1), 1), Y);
const sameYear = plan.normalizePieces(
plan.attachHead(plan.reverseLine(plan.setLineYear(j, j.lines[1].id, Y), 1), 1),
Y,
);
// 종점에서 그은 선이 본선 꺾임점에 닿음 → 그 점의 갈래(선 차례 = 부모 뒤)
let b = plan.normalizePieces(
plan.appendVertex(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), 0, [200, 0]),
Y,
);
b = plan.newLine(b, [100, 80]);
b = plan.appendVertex(b, 1, [100, 0]);
const asBranch = plan.attachHead(plan.reverseLine(b, 1), 1);
// 꺾임점 옮김 · 지움 — 마크 비율 · 붙은 갈래 첫 점
let m = plan.normalizePieces(
plan.appendVertex(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), 0, [100, 100]),
Y,
);
m = plan.normalizePieces(plan.splitPiece(m, "main", 150), Y);
m = plan.normalizePieces(plan.appendVertex(plan.addBranch(m, 0, 1), 1, [160, 0]), Y);
const movedV = plan.normalizePieces(plan.moveVertex(m, 0, 1, [100, -100]), Y);
let r = plan.normalizePieces(
plan.appendVertex(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), 0, [100, 100]),
Y,
);
r = plan.normalizePieces(plan.splitPiece(r, "main", 150), Y);
const removedV = plan.normalizePieces(plan.removeVertex(r, 0, 1), Y);
const removeBlocked = plan.removeVertex(m, 0, 1);
const lineBlocked = plan.removeLine(m, 0);
const lineGone = plan.normalizePieces(plan.removeLine(m, 1), Y);
const reversed = plan.normalizePieces(plan.reverseLine(m, 0), Y);
const branchFlip = plan.reverseLine(m, 1);
const lineYear = plan.setLineYear(m, "main", 2029);
const markMoved = plan.normalizePieces(plan.moveMark(m, 1, 120), Y);
const markClamp = plan.normalizePieces(plan.moveMark(m, 1, 500), Y);
const names = [
plan.lineNames([{ branch: null }]),
plan.lineNames([{ branch: null }, { branch: null }, { branch: { line: "main", vertex: 1 } }]),
plan.lineNames([{ branch: null }, { branch: { line: "main", vertex: 1 } }]),
];
writeFileSync(process.argv[2], JSON.stringify({
one, split, tooClose, branched, inserted, blocked, dropped, guard, removed, grown, read,
marks: plan.marksOf(branched),
empty, joined, sameYear, asBranch, m, movedV, removedV, removeBlocked, lineBlocked, lineGone,
reversed, branchFlip, lineYear, markMoved, markClamp, names,
}));
"""
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 _spans(network: dict, line: str = "main") -> list[tuple[float, float, int]]:
return [(p["from_m"], p["to_m"], p["year"]) for p in network["pieces"] if p["line"] == line]
def test_pieces_cover_and_split(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
assert _spans(got["one"]) == [(0, 200, 2026)]
assert _spans(got["split"]) == [(0, 50, 2026), (50, 200, 2026)]
assert [p["id"] for p in got["split"]["pieces"]] == ["p1", "p2"] # 앞 조각이 id 를 지님
assert got["tooClose"] == got["split"] # 마크 1 m 안 = 안 놓음
assert _spans(got["removed"]) == [(0, 200, 2026)] # 마크 지움 = 앞 조각 연도
assert _spans(got["grown"]) == [(0, 50, 2026), (50, 250, 2027)] # 선이 늘면 끝 조각이 늚
assert got["empty"] == {"lines": [], "pieces": []} # 빈 꼴 = 선 없음(시점 · 종점에서 시작)
def test_branch_order_and_guard(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
branch = got["branched"]["lines"][1]
assert branch["branch"] == {"line": "main", "vertex": 1} and branch["vertices"][0] == [100, 0]
assert _spans(got["branched"], branch["id"]) == [(0, 60, 2026)]
# 본선 앞 구간에 꺾임점을 끼우면 갈래가 붙은 차례가 한 칸 밀림
assert got["inserted"]["lines"][0]["vertices"][1] == [40, 0]
assert got["inserted"]["lines"][1]["branch"]["vertex"] == 2
assert got["blocked"]["lines"][0]["vertices"][-1] == [100, 0] # 끝 점은 갈래 없음 → 빠짐
assert "갈래가 붙은 점" in got["guard"] # 갈래가 붙은 점은 못 뺌
assert got["dropped"]["lines"][1]["vertices"] == [[100, 0]]
def test_from_server_fills_marks(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
# 조각 없는 구역(마크만) = 처음 연도 · 새 id
assert _spans(got["read"]) == [(0, 50, 2028), (50, 150, 2026), (150, 200, 2026)]
assert got["read"]["pieces"][0]["id"] == "p9"
assert len({p["id"] for p in got["read"]["pieces"]}) == 4 # 갈래 조각 포함 겹침 없음
def test_server_accepts_screen_body(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
body = {**got["branched"], "marks": got["marks"], "epsg": 5186}
derived = server.derive(body, 2026)
assert [(p["line"], p["from_m"], p["to_m"], p["year"]) for p in derived["pieces"]] == [
("main", 0.0, 50.0, 2026),
("main", 50.0, 200.0, 2027),
("branch-1", 0.0, 60.0, 2026),
]
def test_reversed_stroke_joins_or_branches(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
joined = got["joined"]
# 종점에서 그은 선 = 뒤집혀 본선 꼬리에 이어 한 선 · No.0 = 시점 쪽 · 다른 해면 이음 자리 마크
assert len(joined["lines"]) == 1
assert joined["lines"][0]["vertices"] == [[0, 0], [100, 0], [150, 50], [200, 0]]
total = 100 + 2 * (50**2 * 2) ** 0.5
spans = _spans(joined)
assert spans[0] == (0, 100, 2026) and spans[1][0] == 100 and spans[1][2] == 2027
assert abs(spans[1][1] - total) < 1e-3
assert len(got["sameYear"]["pieces"]) == 1 # 같은 해면 이음 자리 마크 없음
branch = got["asBranch"]["lines"][1]
assert branch["branch"] == {"line": "main", "vertex": 1}
assert branch["vertices"] == [[100, 0], [100, 80]]
body = {**got["asBranch"], "marks": [], "pieces": [], "epsg": 5186}
server.derive(body, 2026) # 서버가 그대로 받음(갈래 첫 점 = 부모 점)
body = {**joined, "marks": [], "epsg": 5186}
body["marks"] = [
{"line": p["line"], "at_m": p["from_m"]} for p in joined["pieces"] if p["from_m"] > 0
]
assert len(server.derive(body, 2026)["pieces"]) == 2
def test_vertex_move_remove_keeps_marks(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
# 본선 (0,0)-(100,0)-(100,100) · 마크 150 m(둘째 구간 반) · 갈래 = 꺾임점 1 에서 (160,0)
moved = got["movedV"]
assert moved["lines"][0]["vertices"][1] == [100, -100]
assert moved["lines"][1]["vertices"][0] == [100, -100] # 붙은 갈래 첫 점 같이
first = (100**2 + 100**2) ** 0.5
mark = [p for p in moved["pieces"] if p["line"] == "main"][1]["from_m"]
assert abs(mark - (first + 100)) < 1e-3 # 둘째 구간(길이 200) 반
removed = got["removedV"]
assert removed["lines"][0]["vertices"] == [[0, 0], [100, 100]]
share = 0.75 * (2 * 100**2) ** 0.5 # 합친 구간(200 m) 안 150 m = 3/4
assert abs(_spans(removed)[1][0] - share) < 1e-3
assert "갈래가 붙은 점" in got["removeBlocked"]
assert "갈래가 붙은 선" in got["lineBlocked"]
assert [line["id"] for line in got["lineGone"]["lines"]] == ["main"]
assert all(p["line"] == "main" for p in got["lineGone"]["pieces"])
assert "갈래는 방향을" in got["branchFlip"]
def test_reverse_year_mark_move(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
rev = got["reversed"]
assert rev["lines"][0]["vertices"] == [[100, 100], [100, 0], [0, 0]]
assert rev["lines"][1]["branch"] == {"line": "main", "vertex": 1} # 3 − 1 − 1
assert _spans(rev) == [(0, 50, 2026), (50, 200, 2026)]
assert {p["year"] for p in got["lineYear"]["pieces"] if p["line"] == "main"} == {2029}
assert _spans(got["markMoved"]) == [(0, 120, 2026), (120, 200, 2026)]
assert _spans(got["markClamp"]) == [(0, 199, 2026), (199, 200, 2026)] # 끝에서 1 m 남김
body = {**rev, "marks": [{"line": "main", "at_m": 50}], "epsg": 5186}
server.derive(body, 2026)
def test_line_names_mirror(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
cases = [
[{"branch": None}],
[{"branch": None}, {"branch": None}, {"branch": {"line": "main", "vertex": 1}}],
[{"branch": None}, {"branch": {"line": "main", "vertex": 1}}],
]
assert got["names"] == [line_names(c) for c in cases]
assert got["names"][1] == ["본선 1", "본선 2", "갈래 1"] # 시점 둘 = 둘째도 본선
assert got["names"][2] == ["본선", "갈래 1"] # 옛 꼴 이름 그대로