Files
Aislo/resources/tester/test_390_5_snap_step_ops.py
T
eomsangdonandClaude Opus 5.5 75f990c97e fix(483-3 B05 스냅): 손대지 않은 곡선 MC 꼴(N+0 · N+10) 유지 — 고친 곡선만 고른 간격
- StationFit fitAfter · fitDrag(before) — 고치기 전 노선의 곡선 꼴을 IP 자리 + 곡선 칸 열쇠로 기억 · 밀 거리 = 측점 간격
- 10 m 배수(No.N+10)도 측점 위(offStationMcs · 맞춤) · 서로 다른 측점 = 누가거리
- RouteEdit = fitAfter 한 줄 · fitLimits stationM · 694줄
- 시험 = 새 test_280_4_fit(고치기 전 동작 = 190 → 180 대조) · 366_3 · 390_5 이음 글

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:08:09 +09:00

229 lines
9.9 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 = {"10": 10.0, "station": None} # 280-4 — 0.1 · 1 m 지움
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", ["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", ["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("mode", ["auto", "fixed"])
def test_손잡이_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
"""433-21 ① — 손잡이(BC · EC) 끌기도 노드 갈래와 같게: 미리보기 측점 밖이 늘면 그 걸음 되돌림 →
놓은 뒤 MC 가 다 측점 위(되돌림 없으면 216 중 20 남음 = 기준이 고침을 잼)."""
swept = got["sweptHandle"][mode]
assert swept["drags"] == 216 and swept["released"] > 0
assert swept["offAfter"] == 0, swept["worst"]
assert got["sweptHandle"]["unguarded"][mode]["offAfter"] > 0
@pytest.mark.parametrize("step", ["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(index)" in pointer and "fitPreview(after.planned, node);" in pointer
assert "params.fitDrag(now.index)" in keys and "params.snapStepM()" in keys
assert "fit.fitDrag(snapshotNow(), base.planned, index, fitLimits(), base)" 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;")
# 손잡이 갈래도 같은 차례(433-21 ①) — 하한 · 측점 밖 막힘 뒤에 옮김 표시
handle = pointer[pointer.index("const node = dragHandle.node;") :]
handle = handle[: handle.index("if (dragNode >= 0) {")]
assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex));" in handle
assert handle.index("shortfallCrossed(") < handle.index("params.offStation() > dragOff")
assert handle.index("params.offStation() > dragOff") < handle.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["curve"][key]
assert values, "MC 곡선 측점이 생겨야 함"
assert all(v == round(v) for v in values)
@pytest.mark.parametrize("key", list(STEPS))
def test_파이썬_짝_거울(got, key) -> None:
assert core.curve_stations(GRID, SPANS, 1e9, STEPS[key]) == got["curve"][key]
def test_옛_부르기는_정수_그대로() -> None:
old = core.curve_stations(GRID, SPANS, 1e9)
assert old == core.curve_stations(GRID, SPANS, 1e9, 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=10.0)
resolved = RouteSaveRequest(draft=draft).resolved()
assert resolved.station_snap is True and resolved.station_snap_step == 10.0
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, None), # 280-4 — 옛 0.1 · 1 m = 측점 No.
(True, 1.0, None),
(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.