"""430 평면 형상 제어 규칙 한 곳 — 하한 검사(`applyEdit` 한 곳 · 끼우기 · 지우기 · 손잡이) · 곡선 삭제 155° 경고 · 놓을 때 맞춤 보고 · 스냅 끔 · 간격 즉시 계산 줄 · R · 길이 고정 · 넣은 R 곡선표 저장(짝) · 겹친 노드 · 못 그린 곡선 표시. TS 셈은 Node(`helper_430_route_edit_rules.cjs`) · 짝 = 파이썬 `_write_planned_polyline` 이 쓴 곡선표를 TS `flattenServerPlan` 이 펴서 넣은 값이 돌아오는가. ./venv/Scripts/python.exe -m pytest resources/tester/test_430_route_edit_rules.py -q """ import csv import json import subprocess import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B05_Profile import B05_Profile_Router_Replan as replan # noqa: E402 from B05_Profile.B05_Profile_Route_NodeEdits import node_edits # noqa: E402 from common_util.common_util_route_polyline import build_planned_polyline # noqa: E402 B05 = ROOT / "B05_Profile" HELPER = Path(__file__).with_name("helper_430_route_edit_rules.cjs") # IP2 뒤 직선 16 m — 넣은 R 200 은 접선 자리에 눌려 그려짐 · IP3 바로 뒤 0.3 m 겹친 점 NODES = [(0.0, 0.0), (120.0, 0.0), (220.0, 70.0), (236.0, 70.0), (236.2, 70.2), (340.0, 0.0)] FLAGS = [True, True, True, False, True, True] RADII = [None, 40.0, 200.0, None, None, None] LOCKS = [None, "radius", "arc", None, "radius", "bogus"] ARCS = [None, None, 33.5, None, None, None] @pytest.fixture(scope="module") def saved(tmp_path_factory) -> dict: path = tmp_path_factory.mktemp("route") / "planned_route.csv" replan._write_planned_polyline( path, NODES, 30.0, simplify=False, curve_flags=FLAGS, radii=RADII, locks=LOCKS, arcs=ARCS, ) with replan._nodes_path(path).open(encoding="utf-8") as handle: nodes = [(float(row["x"]), float(row["y"])) for row in csv.DictReader(handle)] outline = build_planned_polyline(nodes, min_radius_m=30.0, simplify=False) return { "path": path, "nodes": [node.as_dict() for node in outline.nodes], "curves": replan._read_curves(path), "edits": replan._read_curves(path, "edits"), } @pytest.fixture(scope="module") def got(saved, tmp_path_factory) -> dict: server = tmp_path_factory.mktemp("server") / "plan.json" payload = {key: saved[key] for key in ("nodes", "curves", "edits")} server.write_text(json.dumps(payload), encoding="utf-8") run = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HELPER), str(server)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert run.returncode == 0, run.stderr return json.loads(run.stdout) # ── 하한 검사 한 곳 ────────────────────────────────────────────────────────── def test_옮겨_새로_깨면_까닭(got) -> None: assert got["moveBreak"].startswith("하한에 걸려 되돌렸습니다 — IP1") assert "(곡선반지름 20m)" in got["moveBreak"] assert got["moveOk"] == "" def test_끼우기_새로_깨면_까닭_지우기_넉넉하면_통과(got) -> None: assert "IP1" in got["insertBreak"] and "IP2" in got["insertBreak"] assert got["deleteOk"] == "" assert got["manyGap"] == "" # 둘 넘게 다르면 안 견줌 def test_이미_밑돈_자리는_새로가_아님(got) -> None: assert got["alreadyBroken"] == "" # ── 겹친 노드 · 못 그린 곡선 ───────────────────────────────────────────────── def test_겹친_노드_못_그린_곡선(got) -> None: assert got["flags"] == {"overlap": [2], "undrawn": [1, 3]} assert got["flagsOff"] == {"overlap": [], "undrawn": []} # 곡선 끈 자리는 「못 그림」 아님 assert "겹친 노드 IP2(0.5 m 안 · 선에서 빠짐)" in got["summary"] assert "못 그린 곡선 IP1 · IP3" in got["summary"] def test_놓을_때_손으로_옮긴_IP_는_밀림에_안_셈(got) -> None: assert got["noteHand"] == " · 함께 밀린 교점 1곳" assert got["noteInput"] == " · 함께 밀린 교점 2곳" # ── 넣은 R · 고정 곡선표 저장(짝) ──────────────────────────────────────────── def test_곡선표_edits_는_저장_노드와_자리가_같음(saved) -> None: assert len(saved["edits"]) == len(saved["nodes"]) == len(NODES) - 1 # 겹친 점 하나 빠짐 assert saved["edits"][2] == {"curve": True, "radius_m": 200.0, "lock": "arc", "arc_m": 33.5} assert saved["edits"][1]["lock"] == "radius" assert saved["edits"][3]["curve"] is False assert saved["edits"][4]["lock"] is None # 모르는 고정 값은 버림 def test_다시_열면_넣은_R_고정이_돌아옴(got, saved) -> None: flat = got["flat"] drawn = next(c["radius_m"] for c in saved["curves"] if c["node_first"] == 2) assert drawn < 200 - 1 # 그린 R 은 눌림 assert flat["curveRadius"][1:3] == [40, 200] # 넣은 값 assert flat["curveLock"][1:3] == ["radius", "arc"] assert flat["curveArc"][2] == 33.5 assert flat["curveOn"][3] is False assert got["flatOld"]["curveRadius"][2] == pytest.approx(drawn, abs=0.01) # 옛 곡선표 = 그린 R def test_node_edits_짧은_칸은_기본값() -> None: edits = node_edits([(0, 0), (10, 0), (20, 5)], None, [None, 15.0]) assert edits[1] == {"curve": True, "radius_m": 15.0, "lock": None, "arc_m": None} assert edits[2] == {"curve": True, "radius_m": None, "lock": None, "arc_m": None} def test_편집값_없이_쓴_곡선표는_edits_없음(tmp_path) -> None: path = tmp_path / "planned_route.csv" replan._write_planned_polyline(path, NODES, 30.0, simplify=False) assert replan._read_curves(path, "edits") == [] # ── 배선 ──────────────────────────────────────────────────────────────────── def _read(name: str) -> str: return (B05 / name).read_text(encoding="utf-8") def test_편집기_배선() -> None: main = _read("B05_Profile_UI_RouteEdit.ts") apply_edit = main.split("function applyEdit(", 1)[1][:700] assert "edits.floorStop(before, planned, nodeInfo, fitLimits())" in apply_edit assert "restoreSnapshot(before, stop, false)" in apply_edit assert "curveBar.noteFit(snap.text(), before, index)" in main # 놓을 때 맞춤 보고 snap = main.split("bindStationSnap(chrome", 1)[1][:400] assert "tellDraft();" in snap # 끔 · 간격 바꿈 즉시 계산 줄 assert "snap.changed()" in main.split("function tellDraft()", 1)[1][:400] assert "plan.node_edits" in main assert "nodes: () => ({ planned, curveOn, curveRadius, curveLock, curveArc })" in main assert len(main.splitlines()) <= 695 def test_손잡이_끌기_하한_검사() -> None: pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts") handle = pointer.split("// 곡선 시작·끝점을 끈다", 1)[1][:2200] assert "shortfallCrossed(" in handle assert "params.commit(dragHandle?.node ?? dragNode)" in pointer def test_곡선_삭제_155_경고() -> None: bar = _read("B05_Profile_UI_RouteEdit_CurveBar.ts") on = bar.split("onCurveOn:", 1)[1][:900] assert "!curveOptionalAt(inner)" in on and '"warning"' in on def test_저장에_고정_실음_읽기에_node_edits() -> None: apply = _read("B05_Profile_UI_RouteEdit_Apply.ts") assert "nodes.curveLock?.[index] ?? null" in apply assert "nodes.curveArc?.[index] ?? null" in apply router = _read("B05_Profile_Router_Replan.py") assert '"node_edits":' in router assert "locks=list(draft.curveLock) if draft else None" in router