- 하한 검사 = 모든 평면 조작이 applyEdit 에서 지난 걸음과 견줌 · 새로 깨면 그 걸음으로 되돌림 · 까닭 상태줄(끼우기 · 지우기 · 놓기 · 방향키 · 곡선 칸) · 손잡이 끌기도 걸음마다 검사 - 곡선 삭제 = 내각 155° 미만이면 막지 않고 경고(상태줄 · 알림) - 끌기 · 손잡이 놓을 때 스냅 켬이면 「함께 밀린 교점 · 못 맞춘 곡선」 보고 - 스냅 끔 · 간격 바꿈 = 계산 줄에 초안 바로(tellDraft) - R · 길이 고정 · 넣은 R = 곡선표 edits 칸(짝 B05_Profile_Route_NodeEdits.py ↔ flattenServerPlan) · 다시 열면 넣은 값으로 - 상태줄 = 겹친 노드(0.5 m 안) · 못 그린 곡선 표시 - 시험 test_430_route_edit_rules · 배선 바뀐 시험 둘(352-3 · 390-4) · 398-2 도우미 알림 가짜 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
186 lines
7.9 KiB
Python
186 lines
7.9 KiB
Python
"""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
|