"""PLAN 407 · 요구 252-2 — 곡선 측점(MC 에 측점이 없으면 하나 더함). 짝: 파이썬 `B05_Profile_Engine_Sections_Core.curve_stations` · `generate_sections` ↔ TS `logic/B05_Profile_UI_RouteEdit_Frame.ts` `curveStations` · `logic/B05_Profile_Sections_Stations.ts` `buildStations`(헬퍼 `helper_344_3_pipeline_stations.cjs`). 열쇠 `curve:<누가거리>` · 이름 뒤 「 (MC)」 · 값 = 스냅 간격 자릿수(0.1 = 소수 한 자리 · 그 밖 · 스냅 끔 = 정수) · 일반측점 값과 같으면 안 더함 · 스냅 켬도 일반측점을 옮기지 않음(280-4 · 끔과 같은 목록) · BC · EC 안 더함. """ from __future__ import annotations import json import subprocess import sys import tempfile from pathlib import Path import numpy as np import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B05_Profile import B05_Profile_Engine_Sections_Core as core # noqa: E402 from common_util.common_util_station_key import station_key # noqa: E402 LINE = [[0.0, 0.0], [1000.3, 0.0]] LONG = {"start": [616.1, 0.0], "apex": [631.3, 3.0], "end": [646.5, 0.0], "radius_m": 40.0} SHORT = {"start": [101.0, 0.0], "apex": [107.3, 2.0], "end": [113.6, 0.0], "radius_m": 30.0} ON_GRID = {"start": [195.0, 0.0], "apex": [200.1, 2.0], "end": [205.2, 0.0], "radius_m": 30.0} CURVES = [LONG, SHORT, ON_GRID] GRID = [float(value) for value in range(0, 1001, 20)] class _Flat: def sample_xy(self, xy): count = len(np.asarray(xy, dtype=np.float64)) return np.full(count, 100.0), np.ones(count, dtype=bool) def _server(snap: bool, step: float | None = None, extras: tuple = ()) -> list[dict]: data = core.generate_sections( LINE, _Flat(), core.SectionGenerationOptions(extra_stations=extras), design_curves=CURVES, station_snap=snap, station_snap_step=step, ) return data["longitudinal"]["stations"] def _curves(stations: list[dict]) -> list[float]: return [s["chainage_m"] for s in stations if s["kind"] == "curve"] # ── 파이썬 규칙 ──────────────────────────────────────────────────────────────── def test_정수_MC_하나() -> None: assert core.curve_stations(GRID, [(101.0, 113.6)], 1000.3) == [107.0] def test_스냅_간격_0_1_이면_소수_한_자리() -> None: assert core.curve_stations(GRID, [(101.0, 113.6)], 1000.3, 0.1) == [107.3] assert core.curve_stations(GRID, [(101.0, 113.6)], 1000.3, 10.0) == [107.0] def test_일반측점_값이면_안_더함() -> None: assert core.curve_stations(GRID, [(195.0, 205.2)], 1000.3) == [] def test_MC_옆_관_측점이면_안_더함() -> None: assert core.curve_stations([*GRID, 107.003], [(101.0, 113.6)], 1000.3) == [] def test_노선_밖_MC_는_뺌() -> None: assert core.curve_stations([0.0], [(995.0, 1010.0)], 1000.3) == [] # MC 1003 def test_스냅_끔_MC_마다_곡선_측점() -> None: stations = _server(False) assert _curves(stations) == [107.0, 631.0] # 200 = 일반측점 위 → 안 더함 by = {s["chainage_m"]: s for s in stations} assert by[640.0]["kind"] == "regular" and by[200.0]["kind"] == "regular" assert by[107.0]["label"] == "STA.0+107.000 (MC)" assert len(stations) == len(_server(False, extras=())) == 51 + 1 + 2 # 격자 · EP · MC 둘 assert 101.0 not in by and 113.6 not in by # BC · EC 안 더함 def test_스냅_켬도_끔과_같은_곡선_측점() -> None: stations = _server(True) by = {s["chainage_m"]: s for s in stations} assert stations == _server(False) # 280-4 — 일반측점 안 옮김 assert _curves(stations) == [107.0, 631.0] assert by[640.0]["kind"] == "regular" and by[620.0]["kind"] == "regular" def test_스냅_켬_0_1_간격() -> None: stations = _server(True, 0.1) by = {s["chainage_m"]: s for s in stations} assert _curves(stations) == [107.3, 200.1, 631.3] # 일반측점 그대로 + MC 소수 한 자리 assert by[200.0]["kind"] == "regular" and by[640.0]["kind"] == "regular" def test_MC_에_구조물이면_구조물_측점_하나() -> None: stations = _server(False, extras=((107.0, "관"),)) by = {s["chainage_m"]: s for s in stations} assert by[107.0]["kind"] == "irregular" and _curves(stations) == [631.0] def test_열쇠() -> None: assert station_key("curve", 107.0, 20.0) == "curve:107.00" assert station_key("curve", 107.3, 20.0) == "curve:107.30" # ── 파이썬 ↔ TS 같은 목록 ───────────────────────────────────────────────────── CASES = [ {"name": "끔", "snap": False, "step": None}, {"name": "켬", "snap": True, "step": None}, {"name": "켬0.1", "snap": True, "step": 0.1}, {"name": "끔0.1", "snap": False, "step": 0.1}, # 끔이면 간격 무시 = 정수 ] @pytest.fixture(scope="module") def ts_rows() -> list[list[dict]]: job = { "cases": [{"line": LINE, "curves": CURVES, **case} for case in CASES], "pipes": [], "structures": [], "types": [], "move": { "line": LINE, "curves": CURVES, "movedLine": LINE, "movedCurves": CURVES, "fromM": 0.0, }, } with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "job.json" path.write_text(json.dumps(job), encoding="utf-8") proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HERE / "helper_344_3_pipeline_stations.cjs"), str(path)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout)["cases"] @pytest.mark.parametrize("index", range(len(CASES)), ids=[c["name"] for c in CASES]) def test_파이썬과_TS_같은_목록(ts_rows, index) -> None: case = CASES[index] server = _server(case["snap"], case["step"]) ours = ts_rows[index] assert [s["chainage_m"] for s in server] == pytest.approx( [s["chainageM"] for s in ours], abs=1e-6 ) for py, ts in zip(server, ours, strict=True): assert (py["kind"] == "curve") == (ts["kind"] == "curve"), py["chainage_m"] if ts["kind"] == "curve": assert ts["key"] == station_key("curve", py["chainage_m"], 20.0) assert any(s["kind"] == "curve" for s in ours) # 곡선 측점은 격자 번호를 안 가져감 — 격자 열쇠는 그대로 1 ~ 50 하나씩(0 = BP) grid = sorted(int(s["key"][5:]) for s in ours if s["kind"] == "grid") assert grid == list(range(1, 51))