- StationFit reach = intervalM(반 간격이면 MC 18+9 ~ 19+1 못 맞춤) - 끌기 · 방향키 — 미리보기 측점 밖 곡선 수가 누를 때보다 늘면 그 걸음 되돌림 · dragMoved 는 막힘 검사 뒤(433-14) - 계산 줄 stations 단계 · follow 새 측점 — 초안 없으면 저장 노선 station_snap · station_snap_step(snapOptions) - 시험 — 390_5 끌기 훑기(놓은 뒤 측점 밖 0 · 자동 · 고정 R) · 344_3 저장 스냅 · 266 사면 이동 한 걸음 · 다시 그림 한 번 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
218 lines
9.1 KiB
Python
218 lines
9.1 KiB
Python
"""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) + 1e-6 # 끈 IP 는 접선 따라 ±간격 안만(441)
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["auto", "fixed"])
|
|
def test_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
|
|
"""441 — 측점 No. 끌기(IP 셋 × 12 방향 × 3 거리) · 놓은 뒤 MC 가 다 측점 위 · 맞춤이 늘 닿음
|
|
(반 간격 한도면 18+9 ~ 19+1 에서 못 맞춰 막힘 걸음이 생김)."""
|
|
swept = got["swept"][mode]
|
|
assert swept["drags"] == 108 and swept["released"] > 0
|
|
assert swept["offAfter"] == 0, swept["worst"]
|
|
assert swept["strayed"] == 0
|
|
|
|
|
|
@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
|
|
# 미리보기 측점 밖이 누를 때보다 늘면 그 걸음 되돌림 · 막힘 검사 뒤에 옮김 표시(441 · 433-14)
|
|
assert "params.offStation() > dragOff" in pointer and "params.offStation() > now.off" in keys
|
|
move = pointer[pointer.index("const previous = state.planned[dragNode];") :]
|
|
assert move.index("shortfallCrossed(") < move.index("params.offStation() > dragOff")
|
|
assert move.index("params.offStation() > dragOff") < move.index("dragMoved = true;")
|
|
assert "offStation: () => (stationSnap ? fit.offStationCount(" in edit
|
|
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 "...snapOptions(ctx.inputs.route, saved?.())" in stage # 초안 · 없으면 저장 노선(441)
|
|
assert "stationSnapStep: draft.stationSnapStep" in stage
|
|
assert "stationSnapStep: saved?.station_snap_step" in stage
|
|
assert "saved: () => plan" in page and "...snapOptions(inputs.route, plan)" in page
|
|
apply = (B05 / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
|
assert "stationSnap: true, stationSnapStep: null" in apply # 추천 = 측점 No.
|