feat(B05 측점): 곡선 측점 — MC 에 측점이 없으면 곡선 측점 하나 더함 (407-1 · 요구 252-2)
- 서버 generate_sections ↔ TS buildStations 짝 · curve_stations ↔ curveStations 짝 · 종류 curve · 열쇠 curve:<누가거리> · 이름 뒤 (MC) - 값 = 스냅 간격 자릿수(0.1 = 소수 한 자리 · 그 밖 · 스냅 끔 = 정수) · 0.005 m 안 측점(일반 · 옮긴 · 관 · 구조물) 있으면 안 더함 · BC · EC 안 더함 · 224-2 옮김 그대로 - 격자 번호 안 가져감 · 노선 저장 이월 열쇠 · B06 종류(curve) · 종단 선 색 · 카드 · 종단 이름 (MC) · 평면 눈금 (MC) · 오른쪽 횡단 일반측점 갈래 - 시험: test_407_curve_station 새로 · 344_3 둘 · 224_2 · 346_1 헬퍼 저장 노선 곡선표 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
This commit is contained in:
@@ -46,6 +46,7 @@ const out = {
|
||||
return logic.buildStations(c.line, c.curves, job.pipes, job.structures, {
|
||||
types: job.types,
|
||||
stationSnap: c.snap,
|
||||
stationSnapStep: c.step ?? null, // 390-5 · 252-2 곡선 측점 값 자릿수
|
||||
});
|
||||
}),
|
||||
text: [0, 12.345, 40, 699.996].map(logic.stationText),
|
||||
|
||||
@@ -116,7 +116,11 @@ function realStages(savedBase, remote = null) {
|
||||
return {
|
||||
route: routeGround.createRouteStage({
|
||||
minRadiusM: () => job.minRadius,
|
||||
savedLine: () => ({ line: job.line, curves: [] }),
|
||||
// 저장 노선 곡선표 = 같은 노드의 편집 곡선(407 곡선 측점 · 빈 표면 옮긴 뒤에만 MC 측점이 생김)
|
||||
savedLine: () => ({
|
||||
line: job.line,
|
||||
curves: routeGround.draftLine(draftOf(job.nodes), job.minRadius).curves,
|
||||
}),
|
||||
}),
|
||||
follow: grade.followStage,
|
||||
stations: stationsMod.createStationsStage({ types: () => job.types }),
|
||||
|
||||
@@ -241,7 +241,15 @@ def test_켬은_640_없고_631_있음() -> None:
|
||||
assert 640.0 not in by_chainage and 631.0 in by_chainage
|
||||
assert by_chainage[631.0]["kind"] == "regular"
|
||||
assert by_chainage[631.0]["plan_radius_m"] == 40.0 # 호 안 = 곡선표 반경
|
||||
assert len(by_chainage) == len(_stations(False))
|
||||
# 끔 = 640 남고 MC(631)에 곡선 측점 하나 더(252-2) · 켬 = 640 을 631 로 옮김(측점 수 그대로)
|
||||
assert len(by_chainage) == len(_stations(False)) - 1
|
||||
|
||||
|
||||
def test_끔은_MC_에_곡선_측점() -> None:
|
||||
by_chainage = {s["chainage_m"]: s for s in _stations(False)}
|
||||
assert by_chainage[640.0]["kind"] == "regular"
|
||||
assert by_chainage[631.0]["kind"] == "curve"
|
||||
assert by_chainage[631.0]["label"].endswith(" (MC)")
|
||||
|
||||
|
||||
def test_631_에_관이면_grid() -> None:
|
||||
|
||||
@@ -192,7 +192,13 @@ def _close(a, b) -> bool:
|
||||
return abs(float(a) - float(b)) <= _TOL
|
||||
|
||||
|
||||
_KIND = {"bp": {"end"}, "ep": {"end"}, "regular": {"grid"}, "irregular": {"pipe", "structure"}}
|
||||
_KIND = {
|
||||
"bp": {"end"},
|
||||
"ep": {"end"},
|
||||
"regular": {"grid"},
|
||||
"irregular": {"pipe", "structure"},
|
||||
"curve": {"curve"}, # 252-2 곡선 측점(MC)
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index", range(len(CASES)), ids=[c["name"] for c in CASES])
|
||||
|
||||
@@ -47,6 +47,9 @@ CASES = [
|
||||
("irregular", 1.005, 20.0, None, "structure"),
|
||||
("pipe", 12.344, 20.0, "pipe-s1", "structure"),
|
||||
("structure", 12.345, 20.0, None, "structure"),
|
||||
# 252-2 곡선 측점(MC) — curve:<누가거리 0.01 m>
|
||||
("curve", 107.0, 20.0, None, "structure"),
|
||||
("curve", 107.35, 20.0, None, "structure"),
|
||||
]
|
||||
DEDUPE = (["grid:1", "structure:st-1", "grid:1", "structure:st-1"], [20.0, 25.0, 20.004, 25.006])
|
||||
LOOSE = ["85.05", "at:85.05", "grid:4", "pipe:pipe-s3", "-1.5", "end:BP"]
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""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 = 소수 한 자리 · 그 밖 · 스냅 끔 = 정수) · 일반측점 값과 같으면 안 더함 ·
|
||||
스냅 켬 · 호 안 일반측점 = 224-2 대로 옮김(곡선 측점 없음) · 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 _curves(stations) == [107.0]
|
||||
assert by[631.0]["kind"] == "regular" and 640.0 not in by # 224-2 옮김 그대로
|
||||
|
||||
|
||||
def test_스냅_켬_0_1_간격() -> None:
|
||||
stations = _server(True, 0.1)
|
||||
by = {s["chainage_m"]: s for s in stations}
|
||||
assert _curves(stations) == [107.3]
|
||||
assert 631.3 in by and by[200.1]["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))
|
||||
Reference in New Issue
Block a user