fix(483-1·2 B05 스냅): 일반측점을 MC 로 옮기지 않음 · MC 특별측점 더함 · 스냅 간격 1 m · 0.1 m 지움
- 서버 snap_curve_stations ↔ TS snapCurveStations · gridIndices 지움 — 스냅 켬 = 끔과 같은 측점(20 m 그대로 + 「No.N+10 (MC)」) - 고르기 = 10 m · 측점 No. 둘 · load_station_snap_step = 10 만 · 옛 0.1 · 1 = 측점 No. - 시험 = 새 test_280_4_stations(거울 다섯 경우) · 224_2 · 344_3 · 366_3 · 390_4 · 390_5 · 407 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/* PLAN 483(요구 280-4) 측점 목록 거울 헬퍼 — 진짜 `B05_Profile/logic/B05_Profile_Sections_Stations.ts`
|
||||
* `buildStations` 를 트랜스파일해 Node 로 돌림. 입력 argv[2] JSON(사례 목록) · 표준출력 JSON.
|
||||
* 짝 = `test_280_4_stations.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
function loadTs(full) {
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const logic = loadTs(path.join(ROOT, "B05_Profile", "logic", "B05_Profile_Sections_Stations.ts"));
|
||||
const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_station.ts"));
|
||||
station.setPlusStations(() => []);
|
||||
const cases = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const out = cases.map((c) =>
|
||||
logic
|
||||
.buildStations(c.line, c.curves, [], [], {
|
||||
types: [],
|
||||
stationSnap: c.snap,
|
||||
stationSnapStep: c.step,
|
||||
})
|
||||
.map((s) => ({ m: s.chainageM, kind: s.kind, key: s.key })),
|
||||
);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -1,5 +1,5 @@
|
||||
/* 390-5 스냅 간격 = 조작 단위 — 방향키 걸음(`KeyNudge.arrowStep`) · 끌기 미리보기(`StationFit.fitDrag`) ·
|
||||
* MC 측점 값 자릿수(`Frame.snapCurveStations` · 눈금 글 `stationLabel`)를 Node 로 돌려 JSON 한 줄
|
||||
* MC 곡선 측점(`Frame.curveStations` · 280-4) · 눈금 글 `stationLabel`)를 Node 로 돌려 JSON 한 줄
|
||||
* (`test_390_5_snap_step_ops.py` 가 판정 · 파이썬 짝 거울은 그쪽).
|
||||
* node helper_390_5_snap_step_ops.cjs '<{"grid":[…],"spans":[[bc,ec]…]}>'
|
||||
*/
|
||||
@@ -45,7 +45,7 @@ const ROUTE = [
|
||||
[431, 101],
|
||||
[503, 160],
|
||||
];
|
||||
const STEPS = [0.1, 1, 10, INTERVAL];
|
||||
const STEPS = [10, INTERVAL]; // 280-4 — 0.1 · 1 m 지움
|
||||
|
||||
function columns(planned) {
|
||||
const n = planned.length;
|
||||
@@ -310,13 +310,11 @@ const sweptHandle = {
|
||||
|
||||
// ③ MC 측점 값 · 눈금 글
|
||||
const input = JSON.parse(process.argv[2] || '{"grid":[],"spans":[]}');
|
||||
const snapped = Object.fromEntries(
|
||||
const curveAt = Object.fromEntries(
|
||||
[
|
||||
["0.1", 0.1],
|
||||
["1", 1],
|
||||
["10", 10],
|
||||
["station", null],
|
||||
].map(([key, step]) => [key, frame.snapCurveStations(input.grid, input.spans, step)]),
|
||||
].map(([key, step]) => [key, frame.curveStations(input.grid, input.spans, 1e9, step)]),
|
||||
);
|
||||
const labels = {
|
||||
int: station.stationLabel(119.8, 20),
|
||||
@@ -332,7 +330,7 @@ console.log(
|
||||
drag,
|
||||
swept,
|
||||
sweptHandle,
|
||||
snapped,
|
||||
curve: curveAt,
|
||||
labels,
|
||||
scale: [0.1, 1, 10, null].map(frame.mcScale),
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""요구 224-2 · PLAN 326 — 계획노선 편집 [측점 스냅].
|
||||
"""요구 224-2 · PLAN 326 · 483(요구 280-4) — 계획노선 편집 [측점 스냅].
|
||||
|
||||
짝: 파이썬 `B05_Profile_Engine_Sections_Core` (`_chainages` · `_design_curve_spans` ·
|
||||
`snap_curve_stations`) ↔ TS `B05_Profile_UI_RouteEdit_Frame.ts` (`stationsAlong` ·
|
||||
`curveSpans` · `snapCurveStations`). 켜면 호(BC~EC) 안 측점 중 곡선 중점 MC 에 가장 가까운
|
||||
하나를 floor(MC+0.5) 로 옮김 · 측점 없는 호는 안 심음 · 같은 m 하나로.
|
||||
`curve_stations`) ↔ TS `B05_Profile_UI_RouteEdit_Frame.ts` (`stationsAlong` ·
|
||||
`curveSpans` · `curveStations`). 스냅은 일반측점을 옮기지 않음(280-4) — MC(floor(MC+0.5))에
|
||||
측점이 없으면 곡선 측점 하나를 더함 · 0.005 m 안 측점 있으면 안 더함.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,44 +30,32 @@ TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
GRID = [float(value) for value in range(0, 1001, 20)]
|
||||
|
||||
|
||||
def _snap(spans: list[tuple[float, float]], stations: list[float] | None = None) -> list[float]:
|
||||
return [float(v) for v in core.snap_curve_stations(stations or GRID, spans)]
|
||||
def _curve(spans: list[tuple[float, float]], stations: list[float] | None = None) -> list[float]:
|
||||
return core.curve_stations(stations or GRID, spans, 1e9)
|
||||
|
||||
|
||||
# ── 파이썬 규칙 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_곡선_없음_그대로() -> None:
|
||||
assert _snap([]) == GRID
|
||||
def test_곡선_없음_곡선_측점_없음() -> None:
|
||||
assert _curve([]) == []
|
||||
|
||||
|
||||
def test_측점_없는_호는_안_심음() -> None:
|
||||
assert _snap([(621.0, 639.0)]) == GRID
|
||||
def test_MC_가_측점_밖이면_더함() -> None:
|
||||
assert _curve([(621.0, 639.0)]) == [630.0]
|
||||
assert _curve([(612.0, 634.0)]) == [623.0]
|
||||
|
||||
|
||||
def test_호_안_하나를_정수_MC_로() -> None:
|
||||
out = _snap([(612.0, 634.0)]) # MC 623
|
||||
assert 623.0 in out and 620.0 not in out and len(out) == len(GRID)
|
||||
|
||||
|
||||
def test_호_안_둘이면_MC_에_가까운_하나만() -> None:
|
||||
out = _snap([(616.1, 646.5)]) # MC 631.3 — 640(8.7) 이 620(11.3) 보다 가까움
|
||||
assert 631.0 in out and 640.0 not in out and 620.0 in out
|
||||
|
||||
|
||||
def test_BC_위_측점은_호_밖() -> None:
|
||||
assert _snap([(620.0, 630.0)]) == GRID
|
||||
assert _snap([(610.0, 620.0)]) == GRID # EC 위도 밖
|
||||
def test_MC_가_일반측점_위면_안_더함() -> None:
|
||||
assert _curve([(610.0, 630.0)]) == [] # MC 620
|
||||
|
||||
|
||||
def test_반_올림() -> None:
|
||||
out = _snap([(611.0, 640.0)]) # MC 625.5 → 626 · 640 = EC 위라 밖
|
||||
assert 626.0 in out and 620.0 not in out and 640.0 in out
|
||||
assert _curve([(611.0, 640.0)]) == [626.0] # MC 625.5 → 626
|
||||
|
||||
|
||||
def test_같은_m_는_하나로() -> None:
|
||||
# 20.3 이 MC 20.35 → 20 으로 옮겨 이웃 20 과 같은 m — 하나만 남음
|
||||
assert _snap([(20.1, 20.6)], [0.0, 20.0, 20.3, 40.0]) == [0.0, 20.0, 40.0]
|
||||
def test_0005_m_안_측점이면_안_더함() -> None:
|
||||
assert _curve([(20.1, 20.6)], [0.0, 20.0, 20.354, 40.0]) == [] # MC 20.35 → 20
|
||||
|
||||
|
||||
# ── 파이썬 ↔ TS 거울 ─────────────────────────────────────────────────────────
|
||||
@@ -125,7 +113,7 @@ const out = {
|
||||
rows: cases.map((item) => {
|
||||
const grid = frame.stationsAlong(item.line, item.interval, item.end);
|
||||
const spans = frame.curveSpans(item.line, item.curves);
|
||||
return { grid, spans, snapped: frame.snapCurveStations(grid, spans) };
|
||||
return { grid, spans, curve: frame.curveStations(grid, spans, 1e9) };
|
||||
}),
|
||||
};
|
||||
writeFileSync(process.argv[3], JSON.stringify(out));
|
||||
@@ -139,7 +127,7 @@ def _python_row(case: dict) -> dict:
|
||||
return {
|
||||
"grid": [float(v) for v in grid],
|
||||
"spans": [[span[0], span[1]] for span in spans],
|
||||
"snapped": [float(v) for v in core.snap_curve_stations(grid, spans)],
|
||||
"curve": core.curve_stations(grid, spans, 1e9),
|
||||
}
|
||||
|
||||
|
||||
@@ -183,11 +171,11 @@ def _ts_rows(tmp_path: Path) -> dict:
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_스냅_셈이_두_쪽에서_같다(tmp_path: Path) -> None:
|
||||
def test_측점_셈이_두_쪽에서_같다(tmp_path: Path) -> None:
|
||||
actual = _ts_rows(tmp_path)
|
||||
assert actual["tolerance"] == core.DESIGN_CURVE_MATCH_TOLERANCE_M
|
||||
assert actual["edge"] == core.CURVE_STATION_EDGE_M
|
||||
moved = 0
|
||||
added = 0
|
||||
for index, case in enumerate(CASES):
|
||||
expected = _python_row(case)
|
||||
row = actual["rows"][index]
|
||||
@@ -195,15 +183,16 @@ def test_스냅_셈이_두_쪽에서_같다(tmp_path: Path) -> None:
|
||||
assert len(row["spans"]) == len(expected["spans"]), f"사례 {index} 곡선 수"
|
||||
for got, want in zip(row["spans"], expected["spans"], strict=True):
|
||||
assert got == pytest.approx(want, abs=1e-9), f"사례 {index} 곡선"
|
||||
assert row["snapped"] == pytest.approx(expected["snapped"], abs=1e-9), f"사례 {index} 스냅"
|
||||
moved += row["snapped"] != row["grid"]
|
||||
assert moved >= 3 # 옮김이 실제로 일어난 사례를 견줌
|
||||
assert row["curve"] == pytest.approx(expected["curve"], abs=1e-9), f"사례 {index} 곡선 측점"
|
||||
added += bool(row["curve"])
|
||||
assert added >= 3 # 곡선 측점이 실제로 생긴 사례를 견줌
|
||||
|
||||
|
||||
def test_짝_머리글이_서로를_가리킨다() -> None:
|
||||
text = TS_FILE.read_text(encoding="utf-8")
|
||||
assert "B05_Profile_Engine_Sections_Core.py" in text and "snap_curve_stations" in text
|
||||
assert "snapCurveStations" in (core.__doc__ or "")
|
||||
assert "B05_Profile_Engine_Sections_Core.py" in text and "curve_stations" in text
|
||||
assert "curveStations" in (core.__doc__ or "")
|
||||
assert "snapCurveStations" not in text and not hasattr(core, "snap_curve_stations")
|
||||
|
||||
|
||||
# ── generate_sections ────────────────────────────────────────────────────────
|
||||
@@ -236,13 +225,13 @@ def test_끔은_지금과_같음() -> None:
|
||||
assert 640.0 in [s["chainage_m"] for s in _stations(False)]
|
||||
|
||||
|
||||
def test_켬은_640_없고_631_있음() -> None:
|
||||
def test_켬은_640_남고_631_곡선_측점() -> None:
|
||||
# 280-4 — 켬도 일반측점을 옮기지 않음 · 끔과 같은 목록(640 남고 MC 631 에 곡선 측점)
|
||||
assert _stations(True) == _stations(False)
|
||||
by_chainage = {s["chainage_m"]: s for s in _stations(True)}
|
||||
assert 640.0 not in by_chainage and 631.0 in by_chainage
|
||||
assert by_chainage[631.0]["kind"] == "regular"
|
||||
assert by_chainage[620.0]["kind"] == "regular" and by_chainage[640.0]["kind"] == "regular"
|
||||
assert by_chainage[631.0]["kind"] == "curve"
|
||||
assert by_chainage[631.0]["plan_radius_m"] == 40.0 # 호 안 = 곡선표 반경
|
||||
# 끔 = 640 남고 MC(631)에 곡선 측점 하나 더(252-2) · 켬 = 640 을 631 로 옮김(측점 수 그대로)
|
||||
assert len(by_chainage) == len(_stations(False)) - 1
|
||||
|
||||
|
||||
def test_끔은_MC_에_곡선_측점() -> None:
|
||||
@@ -252,13 +241,13 @@ def test_끔은_MC_에_곡선_측점() -> None:
|
||||
assert by_chainage[631.0]["label"].endswith(" (MC)")
|
||||
|
||||
|
||||
def test_631_에_관이면_grid() -> None:
|
||||
by_chainage = {s["chainage_m"]: s for s in _stations(True, ((631.0, "관"),))}
|
||||
assert by_chainage[631.0]["kind"] == "irregular"
|
||||
assert by_chainage[631.0]["grid"] is True
|
||||
# 640 에 놓인 관은 스냅 뒤 격자가 아니라 따로 선 측점
|
||||
off_grid = {s["chainage_m"]: s for s in _stations(True, ((640.0, "관"),))}
|
||||
assert off_grid[640.0]["kind"] == "irregular" and "grid" not in off_grid[640.0]
|
||||
def test_640_에_관이면_grid() -> None:
|
||||
on_grid = {s["chainage_m"]: s for s in _stations(True, ((640.0, "관"),))}
|
||||
assert on_grid[640.0]["kind"] == "irregular" and on_grid[640.0]["grid"] is True
|
||||
# 631(MC)에 놓인 관은 따로 선 측점 · 곡선 측점은 안 더함(0.005 m 안 측점)
|
||||
at_mc = {s["chainage_m"]: s for s in _stations(True, ((631.0, "관"),))}
|
||||
assert at_mc[631.0]["kind"] == "irregular" and "grid" not in at_mc[631.0]
|
||||
assert 640.0 in at_mc
|
||||
|
||||
|
||||
# ── 서버 저장 · 정본 (326-2) ─────────────────────────────────────────────────
|
||||
@@ -349,11 +338,13 @@ def test_측점_셈_둘_다_스냅을_읽음(tmp_path: Path, monkeypatch) -> Non
|
||||
)
|
||||
monkeypatch.setattr(sections, "_load_route_polyline", lambda *_args: _LINE)
|
||||
monkeypatch.setattr(sections, "build_surface_sampler", lambda *_args, **_kw: _Flat())
|
||||
# [저장] 때 구조물 측점 셈 — 631 은 스냅한 격자 자리
|
||||
# [저장] 때 구조물 측점 셈 — 스냅 켬도 격자 그대로(280-4) · 640 은 격자 · 631 은 따로 선 측점
|
||||
stations, _ = sections.sample_structure_sections(
|
||||
tmp_path, "route.geojson", "f", "m", False, extra_stations=((631.0, "관"),)
|
||||
tmp_path, "route.geojson", "f", "m", False, extra_stations=((631.0, "관"), (640.0, "관"))
|
||||
)
|
||||
assert [s["chainage_m"] for s in stations] == [631.0] and stations[0]["grid"] is True
|
||||
by_chainage = {s["chainage_m"]: s for s in stations}
|
||||
assert sorted(by_chainage) == [631.0, 640.0]
|
||||
assert by_chainage[640.0]["grid"] is True and "grid" not in by_chainage[631.0]
|
||||
|
||||
seen: dict = {}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""PLAN 483-1(요구 280-4) — 스냅이 일반측점을 MC 로 옮기지 않음 · MC 특별측점 더함.
|
||||
|
||||
서버 `_station_layout`(`generate_sections`) ↔ TS `buildStations` 거울 — 분석 D 재현 1 다섯 경우
|
||||
(BP–EP 200 m 직선 · 곡선 하나 · 측점 간격 20 m). 짝 = `helper_280_4_stations.cjs`.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
LINE = [[0.0, 0.0], [200.0, 0.0]]
|
||||
|
||||
|
||||
def _curve(bc: float, ec: float, radius: float = 30.0) -> list[dict]:
|
||||
return [
|
||||
{"start": [bc, 0.0], "apex": [(bc + ec) / 2, 3.0], "end": [ec, 0.0], "radius_m": radius}
|
||||
]
|
||||
|
||||
|
||||
CASES = {
|
||||
"A_10m_긴호_MC50": {"curves": _curve(34.3, 65.7), "snap": True, "step": 10.0},
|
||||
"B_10m_짧은호_MC50": {"curves": _curve(45.6, 54.4, 8.5), "snap": True, "step": 10.0},
|
||||
"C_10m_MC60": {"curves": _curve(44.3, 75.7), "snap": True, "step": 10.0},
|
||||
"D_측점No_MC47": {"curves": _curve(31.3, 62.7), "snap": True, "step": None},
|
||||
"E_스냅끔_MC50": {"curves": _curve(34.3, 65.7), "snap": False, "step": None},
|
||||
}
|
||||
GRID = [float(v) for v in range(0, 201, 20)]
|
||||
|
||||
|
||||
class _Flat:
|
||||
def sample_xy(self, xy):
|
||||
count = len(np.asarray(xy))
|
||||
return np.zeros(count), np.ones(count, dtype=bool)
|
||||
|
||||
|
||||
def _server(case: dict) -> list[dict]:
|
||||
data = core.generate_sections(
|
||||
[[x, y, 0.0] for x, y in LINE],
|
||||
_Flat(),
|
||||
core.SectionGenerationOptions(),
|
||||
design_curves=case["curves"],
|
||||
station_snap=case["snap"],
|
||||
station_snap_step=case["step"],
|
||||
)
|
||||
return data["longitudinal"]["stations"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts() -> dict:
|
||||
jobs = [{"line": LINE, **case} for case in CASES.values()]
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "job.json"
|
||||
path.write_text(json.dumps(jobs), encoding="utf-8")
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_280_4_stations.cjs"), str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return dict(zip(CASES, json.loads(proc.stdout), strict=True))
|
||||
|
||||
|
||||
_KIND = {"bp": "end", "ep": "end", "regular": "grid", "curve": "curve"}
|
||||
|
||||
EXPECT_CURVE = {
|
||||
"A_10m_긴호_MC50": 50.0,
|
||||
"B_10m_짧은호_MC50": 50.0,
|
||||
"C_10m_MC60": None,
|
||||
"D_측점No_MC47": 47.0,
|
||||
"E_스냅끔_MC50": 50.0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(CASES))
|
||||
def test_일반측점_그대로_MC_특별측점(name, ts):
|
||||
server = _server(CASES[name])
|
||||
regular = [s["chainage_m"] for s in server if s["kind"] in ("bp", "regular", "ep")]
|
||||
assert regular == GRID # 20 m 일반측점 하나도 안 사라짐
|
||||
curves = [s for s in server if s["kind"] == "curve"]
|
||||
mc = EXPECT_CURVE[name]
|
||||
if mc is None: # N+0 = 일반측점 위 MC → 특별측점 없음
|
||||
assert curves == []
|
||||
else:
|
||||
assert [s["chainage_m"] for s in curves] == [mc]
|
||||
assert curves[0]["label"].endswith(" (MC)")
|
||||
# 거울 — 누가거리 · 종류
|
||||
ours = ts[name]
|
||||
assert [s["m"] for s in ours] == pytest.approx([s["chainage_m"] for s in server], abs=1e-6)
|
||||
assert [s["kind"] for s in ours] == [_KIND[s["kind"]] for s in server]
|
||||
|
||||
|
||||
def test_A_열쇠(ts):
|
||||
keys = {s["key"]: s["m"] for s in ts["A_10m_긴호_MC50"]}
|
||||
assert keys["grid:2"] == 40.0 and keys["curve:50.00"] == 50.0
|
||||
for s in ts["A_10m_긴호_MC50"]:
|
||||
if s["kind"] == "grid":
|
||||
assert s["key"] == f"grid:{round(s['m'] / 20)}"
|
||||
|
||||
|
||||
def test_스냅_켬_끔_같은_목록():
|
||||
on = _server(CASES["A_10m_긴호_MC50"])
|
||||
off = _server(CASES["E_스냅끔_MC50"])
|
||||
assert on == off
|
||||
@@ -256,19 +256,19 @@ def test_추가_측점_열쇠(r):
|
||||
assert len(ours) == len(r["cases"][0]) + 1
|
||||
|
||||
|
||||
def test_스냅_측점은_원래_격자_번호(r):
|
||||
plain = {s["key"]: s["chainageM"] for s in r["cases"][0] if s["kind"] == "grid"}
|
||||
snapped = {s["key"]: s["chainageM"] for s in r["cases"][1] if s["kind"] == "grid"}
|
||||
assert plain.keys() == snapped.keys() # 옮겨도 번호 그대로 · 겹침 없음
|
||||
moved = {k: (plain[k], snapped[k]) for k in plain if plain[k] != snapped[k]}
|
||||
assert moved and all(abs(a - b) <= 10 for a, b in moved.values())
|
||||
def test_스냅은_측점을_옮기지_않음(r):
|
||||
"""280-4 — 스냅 켬 = 끔과 같은 측점 · 열쇠(일반측점 그대로 + MC 곡선 측점)."""
|
||||
plain = [(s["key"], s["chainageM"]) for s in r["cases"][0]]
|
||||
assert [(s["key"], s["chainageM"]) for s in r["cases"][1]] == plain
|
||||
assert any(s["kind"] == "curve" for s in r["cases"][1])
|
||||
|
||||
|
||||
def test_초안_없으면_저장_스냅_따름(r):
|
||||
"""441 — 초안 없는 저장 노선 측점 단계 = 저장 station_snap · 초안이 있으면 초안 값."""
|
||||
"""441 — 초안 없는 저장 노선 측점 단계 = 저장 station_snap · 초안이 있으면 초안 값.
|
||||
280-4 뒤 스냅은 측점을 옮기지 않아 켬 · 끔 목록이 같음."""
|
||||
snapped = [s["chainageM"] for s in r["cases"][1]]
|
||||
plain = [s["chainageM"] for s in r["cases"][0]]
|
||||
assert snapped != plain
|
||||
assert snapped == plain
|
||||
got = r["savedSnap"]
|
||||
assert got["saved"] == snapped
|
||||
assert got["savedOff"] == plain and got["noPlan"] == plain
|
||||
|
||||
@@ -97,4 +97,4 @@ def test_편집기_이음() -> None:
|
||||
edits = (B05 / "logic" / "B05_Profile_UI_RouteEdit_Edits.ts").read_text("utf-8")
|
||||
assert "측점 밖 곡선 " in edits
|
||||
chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text("utf-8")
|
||||
assert "곡선 가운데(MC)를 가까운 일반측점에" in chrome
|
||||
assert "고친 곡선의 가운데(MC)를 고른 간격 배수에" in chrome # 280-4
|
||||
|
||||
@@ -14,7 +14,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
SNAP = B05 / "B05_Profile_UI_RouteEdit_SnapStep.ts"
|
||||
TS = ROOT / "config" / "node_modules" / "typescript"
|
||||
STEPS = ["0.1", "1", "10", "station"]
|
||||
STEPS = ["10", "station"] # 280-4 — 0.1 · 1 m 지움
|
||||
|
||||
NODE = """
|
||||
const fs = require("fs");
|
||||
@@ -46,8 +46,6 @@ def texts() -> dict:
|
||||
("step", "want"),
|
||||
[
|
||||
("station", "곡선 가운데(MC)를 일반측점에 맞췄습니다."),
|
||||
("0.1", "곡선 가운데(MC)를 0.1 m 배수에 맞췄습니다."),
|
||||
("1", "곡선 가운데(MC)를 1 m 배수에 맞췄습니다."),
|
||||
("10", "곡선 가운데(MC)를 10 m 배수에 맞췄습니다."),
|
||||
],
|
||||
)
|
||||
@@ -62,3 +60,14 @@ def test_편집기는_고른_간격_글을_씀() -> None:
|
||||
assert 'applyEdit(stationSnap ? snap.text() : "옮겼습니다.")' in edit # 놓을 때(430)
|
||||
snap = SNAP.read_text(encoding="utf-8")
|
||||
assert "text: () => snapFitText(select.value)" in snap
|
||||
|
||||
|
||||
def test_고르기는_10_m_측점_No_둘() -> None:
|
||||
"""280-4 — 고르기 option 둘 · STEPS 둘(옛 pref 0.1 · 1 = 측점 No.)."""
|
||||
import re
|
||||
|
||||
chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8")
|
||||
block = chrome[chrome.index('data-act="snap-step"') :]
|
||||
block = block[: block.index("</select>")]
|
||||
assert re.findall(r'<option value="([^"]+)"', block) == ["10", "station"]
|
||||
assert 'const STEPS = ["10", "station"];' in SNAP.read_text(encoding="utf-8")
|
||||
|
||||
@@ -27,7 +27,7 @@ from common_util.common_util_route_geometry import planned_route_working_path #
|
||||
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}
|
||||
STEPS = {"10": 10.0, "station": None} # 280-4 — 0.1 · 1 m 지움
|
||||
|
||||
|
||||
def _spans() -> list[list[float]]:
|
||||
@@ -65,7 +65,7 @@ def test_스냅_끔_방향키는_0_1_Shift_1(got) -> None:
|
||||
assert got["keys"]["offShift"] == pytest.approx(1.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
|
||||
@pytest.mark.parametrize("step", ["10", "20"])
|
||||
def test_스냅_켬_방향키_한_번_고른_간격_Shift_무시(got, step) -> None:
|
||||
key = got["keys"]["snap"][step]
|
||||
assert key["plain"] == pytest.approx(float(step))
|
||||
@@ -75,7 +75,7 @@ def test_스냅_켬_방향키_한_번_고른_간격_Shift_무시(got, step) -> N
|
||||
# ── ② 끌기 미리보기 ──
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
|
||||
@pytest.mark.parametrize("step", ["10", "20"])
|
||||
def test_끄는_동안_MC_가_간격_배수(got, step) -> None:
|
||||
drag = got["drag"][step]
|
||||
assert drag["off"] == 0
|
||||
@@ -103,7 +103,7 @@ def test_손잡이_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
|
||||
assert got["sweptHandle"]["unguarded"][mode]["offAfter"] > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
|
||||
@pytest.mark.parametrize("step", ["10", "20"])
|
||||
def test_미리보기는_쌓이지_않고_놓을_때와_같음(got, step) -> None:
|
||||
drag = got["drag"][step]
|
||||
assert drag["stale"] == 0
|
||||
@@ -145,25 +145,20 @@ def test_자릿수_배수(got) -> None:
|
||||
|
||||
|
||||
@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 가 실제로 생김
|
||||
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:
|
||||
py = [float(v) for v in core.snap_curve_stations(GRID, SPANS, STEPS[key])]
|
||||
assert py == got["snapped"][key]
|
||||
assert core.curve_stations(GRID, SPANS, 1e9, STEPS[key]) == got["curve"][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)]
|
||||
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)
|
||||
|
||||
|
||||
@@ -183,9 +178,9 @@ def test_눈금_글_0_1_때만_소수_한_자리(got) -> None:
|
||||
|
||||
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)
|
||||
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 == 0.1
|
||||
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
|
||||
|
||||
@@ -193,8 +188,8 @@ def test_초안_간격이_요청으로_펴짐() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("snap", "step", "want"),
|
||||
[
|
||||
(True, 0.1, 0.1),
|
||||
(True, 1.0, 1.0),
|
||||
(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),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
`buildStations`(헬퍼 `helper_344_3_pipeline_stations.cjs`). 열쇠 `curve:<누가거리>` ·
|
||||
이름 뒤 「 (MC)」 ·
|
||||
값 = 스냅 간격 자릿수(0.1 = 소수 한 자리 · 그 밖 · 스냅 끔 = 정수) · 일반측점 값과 같으면 안 더함 ·
|
||||
스냅 켬 · 호 안 일반측점 = 224-2 대로 옮김(곡선 측점 없음) · BC · EC 안 더함.
|
||||
스냅 켬도 일반측점을 옮기지 않음(280-4 · 끔과 같은 목록) · BC · EC 안 더함.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -91,18 +91,19 @@ def test_스냅_끔_MC_마다_곡선_측점() -> None:
|
||||
assert 101.0 not in by and 113.6 not in by # BC · EC 안 더함
|
||||
|
||||
|
||||
def test_스냅_켬_짧은_호만_곡선_측점() -> None:
|
||||
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 옮김 그대로
|
||||
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]
|
||||
assert 631.3 in by and by[200.1]["kind"] == "regular" # 옮긴 일반측점
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user