"""390-5 스냅 간격 = 조작 단위(요구 243) — 스냅 켬이면 방향키 한 번 = 고른 간격 · 끄는 동안 MC 가 간격 배수에 맞은 자리로 보임 · MC 측점 값 = 간격 자릿수(0.1 = 소수 한 자리 · 그 밖 = 정수) · 간격을 저장 노선(곡선표 `station_snap_step`)에 같이 → 서버 측점 셈(파이썬 짝)이 같은 값. TS 셈은 `helper_390_5_snap_step_ops.cjs`(Node) · 파이썬 짝 거울 · 저장 왕복 · 편집기 이음은 여기. ./venv/Scripts/python.exe -m pytest resources/tester/test_390_5_snap_step_ops.py -q """ import json import random 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_Engine_Sections_Core as core # noqa: E402 from B05_Profile.B05_Profile_Engine_Sections import load_station_snap_step # noqa: E402 from B05_Profile.B05_Profile_Route_Save import RouteDraftInput, RouteSaveRequest # noqa: E402 from B05_Profile.B05_Profile_Router_Replan import _write_planned_polyline # noqa: E402 from common_util.common_util_route_geometry import planned_route_working_path # noqa: E402 B05 = ROOT / "B05_Profile" HELPER = Path(__file__).with_name("helper_390_5_snap_step_ops.cjs") GRID = [float(v) for v in range(0, 1001, 20)] STEPS = {"0.1": 0.1, "1": 1.0, "10": 10.0, "station": None} def _spans() -> list[list[float]]: rng = random.Random(3905) spans, at = [], 15.0 while at < 950: start = round(at + rng.uniform(0, 30), 3) end = round(start + rng.uniform(18, 60), 3) spans.append([start, end]) at = end + rng.uniform(5, 25) return spans SPANS = _spans() @pytest.fixture(scope="module") def got() -> dict: run = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HELPER), json.dumps({"grid": GRID, "spans": SPANS})], capture_output=True, text=True, encoding="utf-8", timeout=120, ) assert run.returncode == 0, run.stderr return json.loads(run.stdout) # ── ① 방향키 ── def test_스냅_끔_방향키는_0_1_Shift_1(got) -> None: assert got["keys"]["off"] == pytest.approx(0.1) assert got["keys"]["offShift"] == pytest.approx(1.0) @pytest.mark.parametrize("step", ["0.1", "1", "10", "20"]) def test_스냅_켬_방향키_한_번_고른_간격_Shift_무시(got, step) -> None: key = got["keys"]["snap"][step] assert key["plain"] == pytest.approx(float(step)) assert key["shift"] == pytest.approx(float(step)) # ── ② 끌기 미리보기 ── @pytest.mark.parametrize("step", ["0.1", "1", "10", "20"]) def test_끄는_동안_MC_가_간격_배수(got, step) -> None: drag = got["drag"][step] assert drag["off"] == 0 assert max(drag["residues"]) <= drag["tol"] assert drag["dragMoved"] <= float(step) / 2 + 1e-6 # 끈 IP 는 접선 따라 ±간격/2 안만 @pytest.mark.parametrize("step", ["0.1", "1", "10", "20"]) def test_미리보기는_쌓이지_않고_놓을_때와_같음(got, step) -> None: drag = got["drag"][step] assert drag["stale"] == 0 assert drag["release"] == 0 assert drag["before"] == 0 # 끈 IP 앞앞 IP 는 그대로 def test_끌기_방향키_편집기_이음() -> None: pointer = (B05 / "B05_Profile_UI_RouteEdit_Pointer.ts").read_text(encoding="utf-8") keys = (B05 / "B05_Profile_UI_RouteEdit_Keys.ts").read_text(encoding="utf-8") edit = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8") assert "params.fitDrag(dragNode)" in pointer assert "params.fitDrag(now.index)" in keys and "params.snapStepM()" in keys assert "fit.fitDrag(snapshotNow(), base, index, fitLimits())" in edit # 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림) assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex))" in pointer assert "now.raw.forEach((vertex, index) => (state.planned[index] = vertex))" in keys assert edit.count("\n") <= 695 # ── ③ MC 측점 값 · 파이썬 짝 ── def test_자릿수_배수(got) -> None: assert got["scale"] == [10, 1, 1, 1] assert [core._mc_scale(v) for v in (0.1, 1.0, 10.0, None)] == [10, 1, 1, 1] @pytest.mark.parametrize("key", list(STEPS)) def test_MC_측점_값_자릿수(got, key) -> None: values = got["snapped"][key] moved = [v for v in values if v not in GRID] assert moved, "호 안 측점이 MC 로 옮겨져야 함" scale = 10 if key == "0.1" else 1 assert all(abs(v * scale - round(v * scale)) < 1e-9 for v in values) if key == "0.1": assert any(abs(v - round(v)) > 1e-9 for v in moved) # 소수 한 자리 MC 가 실제로 생김 @pytest.mark.parametrize("key", list(STEPS)) def test_파이썬_짝_거울(got, key) -> None: py = [float(v) for v in core.snap_curve_stations(GRID, SPANS, STEPS[key])] assert py == got["snapped"][key] def test_옛_부르기는_정수_그대로() -> None: old = [float(v) for v in core.snap_curve_stations(GRID, SPANS)] assert old == [float(v) for v in core.snap_curve_stations(GRID, SPANS, None)] assert all(v == round(v) for v in old) def test_눈금_글_0_1_때만_소수_한_자리(got) -> None: labels = got["labels"] assert labels["int"] == "5+20" and labels["oneDefault"] == "5+20" # 다른 화면 = 정수(09-29) assert labels["one"] == "5+19.8" and labels["oneSmall"] == "5+0.1" assert labels["oneWhole"] == "6+0" overlay = (ROOT / "B04_PreProcess" / "B04_PreProcess_UI_MapOverlays.ts").read_text( encoding="utf-8" ) assert "stationLabel(chainage, interval, options.chainages ? 1 : 0)" in overlay # ── 저장 왕복 ── def test_초안_간격이_요청으로_펴짐() -> None: nodes = [(0.0, 0.0), (50.0, 10.0), (100.0, 0.0)] draft = RouteDraftInput(nodes=nodes, stationSnap=True, stationSnapStep=0.1) resolved = RouteSaveRequest(draft=draft).resolved() assert resolved.station_snap is True and resolved.station_snap_step == 0.1 old = RouteSaveRequest(draft=RouteDraftInput(nodes=nodes, stationSnap=True)) assert old.resolved().station_snap_step is None @pytest.mark.parametrize( ("snap", "step", "want"), [ (True, 0.1, 0.1), (True, 1.0, 1.0), (True, 10.0, 10.0), (True, None, None), (False, 0.1, None), ], ) def test_곡선표_저장_왕복(tmp_path, snap, step, want) -> None: path = planned_route_working_path(tmp_path) path.parent.mkdir(parents=True, exist_ok=True) points = [(0.0, 0.0), (120.0, 15.0), (240.0, -10.0), (360.0, 30.0)] _write_planned_polyline(path, points, 30.0, station_snap=snap, station_snap_step=step) assert load_station_snap_step(tmp_path) == want def test_옛_곡선표는_측점_No(tmp_path) -> None: path = planned_route_working_path(tmp_path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text("x,y\n0,0\n10,0\n", encoding="utf-8") path.with_name(f"{path.stem}_curves.json").write_text( json.dumps({"curves": [], "station_snap": True}), encoding="utf-8" ) assert load_station_snap_step(tmp_path) is None def test_서버_측점_셈이_저장_간격을_씀() -> None: sections = (B05 / "B05_Profile_Engine_Sections.py").read_text(encoding="utf-8") assert sections.count("station_snap_step=load_station_snap_step(project_root)") == 2 design = ROOT / "A00_Common" / "design" page = (design / "design_pipeline_page.ts").read_text(encoding="utf-8") assert "stationSnapStep: routePlan.station_snap_step" in page stage = (design / "logic" / "design_pipeline_stations.ts").read_text(encoding="utf-8") assert "stationSnapStep: ctx.inputs.route?.stationSnapStep" in stage apply = (B05 / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8") assert "stationSnap: true, stationSnapStep: null" in apply # 추천 = 측점 No.