Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -66,7 +66,7 @@ function measure(planned, interval = INTERVAL) {
|
||||
mcs,
|
||||
residues: mcs.map((mc) => Math.abs(mc - Math.round(mc / interval) * interval)),
|
||||
innerStations: total.filter((s) => s > 0 && s < length).length,
|
||||
off: fit.offStationCount(built.vertices, built.curves, interval),
|
||||
off: fit.offStationCount(built.vertices, built.curves, interval, INTERVAL),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,7 +89,11 @@ console.log(
|
||||
// 390-2 스냅 간격 — 0.1 · 1 · 10 m 배수 · 측점 밖 개수도 같은 간격
|
||||
steps: Object.fromEntries(
|
||||
[0.1, 1, 10].map((step) => {
|
||||
const fitted = fit.fitCurveStations(columns(ROUTE), { ...LIMITS, intervalM: step }, 1);
|
||||
const fitted = fit.fitCurveStations(
|
||||
columns(ROUTE),
|
||||
{ ...LIMITS, intervalM: step, stationM: INTERVAL },
|
||||
1,
|
||||
);
|
||||
const offBefore = measure(ROUTE, step).off;
|
||||
return [step, { offBefore, ...measure(fitted, step), moved: moved(ROUTE, fitted) }];
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/* 432 MC 를 일반측점 위에 두면 그 뒤 스냅대로 — 측점 No.(20 m) 노선 곡선 하나를 손으로 측점 + 0.3 m 에 두고
|
||||
* 「위」 판정 폭 · 맞춤 · 밀어내는 걸음 막힘(`pushedOff`)을 JSON 한 줄로(`test_432_mc_on_station.py` 가 판정).
|
||||
*/
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
const { loadTs } = makeLoader({
|
||||
root: ROOT,
|
||||
requireFor: (full) => (request) =>
|
||||
request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request),
|
||||
});
|
||||
|
||||
const LOGIC = path.join(ROOT, "B05_Profile", "logic");
|
||||
const fit = loadTs(path.join(LOGIC, "B05_Profile_RouteEdit_StationFit.ts"));
|
||||
const edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts"));
|
||||
const frame = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Frame.ts"));
|
||||
|
||||
const STATION = 20;
|
||||
const LIMITS = {
|
||||
minRadiusM: 30,
|
||||
limitRadiusM: 12,
|
||||
limitArcM: 0,
|
||||
intervalM: STATION,
|
||||
stationM: STATION,
|
||||
};
|
||||
const ROUTE = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[180, 60],
|
||||
[280, 60],
|
||||
[360, 0],
|
||||
[460, 0],
|
||||
];
|
||||
const K = 2; // 손으로 측점에 둘 곡선의 꺾임점
|
||||
|
||||
const columns = (p, lock = []) => ({
|
||||
planned: p.map(([x, y]) => [x, y]),
|
||||
curveOn: p.map(() => true),
|
||||
curveRadius: p.map(() => null),
|
||||
curveLock: p.map(() => null),
|
||||
curveArc: p.map(() => null),
|
||||
apexLock: p.map((_, i) => i === 0 || i === p.length - 1 || lock.includes(i)),
|
||||
});
|
||||
const built = (cols, limits = LIMITS) =>
|
||||
edits.rebuildLine({ ...cols, curveRadius: [...cols.curveRadius] }, limits);
|
||||
const mcs = (cols, limits) => {
|
||||
const b = built(cols, limits);
|
||||
return frame.curveSpans(b.vertices, b.curves).map(([s, e]) => (s + e) / 2);
|
||||
};
|
||||
const off = (cols, limits = LIMITS) => {
|
||||
const b = built(cols, limits);
|
||||
return fit.offStationCount(b.vertices, b.curves, limits.intervalM, limits.stationM);
|
||||
};
|
||||
/** IP `k` 를 들어오는 직선 따라 밀어 곡선 `k` MC = target(손으로 끈 자리) — 할선 몇 번. */
|
||||
function place(cols, k, target) {
|
||||
const base = cols.planned[k];
|
||||
const prev = cols.planned[k - 1];
|
||||
const len = Math.hypot(base[0] - prev[0], base[1] - prev[1]);
|
||||
const ux = (base[0] - prev[0]) / len;
|
||||
const uy = (base[1] - prev[1]) / len;
|
||||
const at = (d) => {
|
||||
const c = {
|
||||
...cols,
|
||||
planned: cols.planned.map((v, i) => (i === k ? [base[0] + ux * d, base[1] + uy * d] : v)),
|
||||
};
|
||||
return { c, mc: mcs(c, LIMITS)[k - 1] };
|
||||
};
|
||||
let d = 0;
|
||||
let got = at(0);
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
d += target - got.mc;
|
||||
got = at(d);
|
||||
}
|
||||
return got;
|
||||
}
|
||||
|
||||
const fitted = columns(fit.fitCurveStations(columns(ROUTE), LIMITS, 1));
|
||||
const fittedMcs = mcs(fitted, LIMITS);
|
||||
const station = Math.round(fittedMcs[K - 1] / STATION) * STATION;
|
||||
// 손으로 측점 + 0.3 m(폭 안) · + 0.7 m(폭 밖) — 맞춤이 못 옮기게 그 IP 고정(못 맞춰도 「위」)
|
||||
const inside = place(fitted, K, station + 0.3);
|
||||
const outside = place(fitted, K, station + 0.7);
|
||||
const lockIn = { ...inside.c, apexLock: inside.c.apexLock.map((v, i) => v || i === K) };
|
||||
const lockInFit = { ...lockIn, planned: fit.fitCurveStations(lockIn, LIMITS, 1) };
|
||||
// 고정 없이 폭 안 = 같은 측점에 정확히 맞춤
|
||||
const freeFit = { ...inside.c, planned: fit.fitCurveStations(inside.c, LIMITS, 1) };
|
||||
// 그 뒤 걸음 — 앞 교점(K-1)을 끌어 그 곡선 MC 를 밀어냄 · R 바꿈 · 폭 안 작은 걸음
|
||||
const nudge = (cols, k, dx, dy) => {
|
||||
const now = {
|
||||
...cols,
|
||||
planned: cols.planned.map((v, i) => (i === k ? [v[0] + dx, v[1] + dy] : v)),
|
||||
};
|
||||
return { ...now, planned: fit.fitAfter(cols, now, LIMITS) };
|
||||
};
|
||||
const drag = nudge(lockInFit, K - 1, 0, 4);
|
||||
const small = nudge(lockInFit, K - 1, 0, 0.1);
|
||||
const withRadius = (cols, r) => {
|
||||
const now = { ...cols, curveRadius: cols.curveRadius.map((x, i) => (i === K ? r : x)) };
|
||||
return { ...now, planned: fit.fitAfter(cols, now, LIMITS) };
|
||||
};
|
||||
const radius = withRadius(lockInFit, 100);
|
||||
const radiusIn = withRadius(lockInFit, 60);
|
||||
const freeRadius = withRadius(freeFit, 100);
|
||||
// 1 m 스냅 · 측점 No. 20 m — 측점 밖 정수 m 자리는 옛 판정(0.05 m) 그대로
|
||||
const one = { ...LIMITS, intervalM: 1 };
|
||||
const odd = place(fitted, K, station + 3.3);
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
station,
|
||||
fittedOff: off(fitted),
|
||||
inside: { mc: inside.mc, off: off(inside.c) },
|
||||
outside: { mc: outside.mc, off: off(outside.c) },
|
||||
lockInFit: { mc: mcs(lockInFit, LIMITS)[K - 1], off: off(lockInFit) },
|
||||
freeFit: { mc: mcs(freeFit, LIMITS)[K - 1], off: off(freeFit) },
|
||||
drag: { mc: mcs(drag, LIMITS)[K - 1], pushed: fit.pushedOff(lockInFit, drag, LIMITS) },
|
||||
small: { mc: mcs(small, LIMITS)[K - 1], pushed: fit.pushedOff(lockInFit, small, LIMITS) },
|
||||
radius: { mc: mcs(radius, LIMITS)[K - 1], pushed: fit.pushedOff(lockInFit, radius, LIMITS) },
|
||||
radiusIn: {
|
||||
mc: mcs(radiusIn, LIMITS)[K - 1],
|
||||
pushed: fit.pushedOff(lockInFit, radiusIn, LIMITS),
|
||||
},
|
||||
freeRadius: {
|
||||
mc: mcs(freeRadius, LIMITS)[K - 1],
|
||||
pushed: fit.pushedOff(freeFit, freeRadius, LIMITS),
|
||||
},
|
||||
oneStep: {
|
||||
insideOff: off(inside.c, one),
|
||||
oddMc: odd.mc,
|
||||
oddOff: (() => {
|
||||
const b = built(odd.c, one);
|
||||
return fit.offStationMcs(b.vertices, b.curves, 1, STATION);
|
||||
})(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -133,7 +133,7 @@ def test_끌기_방향키_편집기_이음() -> None:
|
||||
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 "stationSnap ? fit.offStationCount(" in edit and " offStation,\n" in edit
|
||||
assert edit.count("\n") <= 695
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""432 MC 를 일반측점 위에 두면 그 뒤 스냅대로.
|
||||
|
||||
「위」 판정 폭 = 측점 목록 「(MC)」 규칙(`curveStations` 반올림 · 정수 m = ±0.5 m) ·
|
||||
폭 안이면 정확히 맞춰 보고 못 맞춰도 「위」 · 그 곡선을 폭 밖으로 미는 걸음(끌기 · R 입력)은
|
||||
`pushedOff` 로 막음 · 측점 밖 정수 m 자리(1 m 스냅)는 옛 판정 그대로.
|
||||
|
||||
TS 셈은 `helper_432_mc_on_station.cjs`(Node · `StationFit` 그대로).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_432_mc_on_station.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_432_mc_on_station.cjs")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=120
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_폭_안은_위_폭_밖은_측점_밖(got) -> None:
|
||||
station = got["station"]
|
||||
assert got["fittedOff"] == 0
|
||||
assert got["inside"]["mc"] == pytest.approx(station + 0.3, abs=1e-6)
|
||||
assert got["inside"]["off"] == 0
|
||||
assert got["outside"]["mc"] == pytest.approx(station + 0.7, abs=1e-6)
|
||||
assert got["outside"]["off"] == 1
|
||||
|
||||
|
||||
def test_못_맞춰도_위_맞출_수_있으면_정확히(got) -> None:
|
||||
station = got["station"]
|
||||
assert got["lockInFit"]["off"] == 0 # 고정 IP · 그 자리 그대로 「위」
|
||||
assert got["lockInFit"]["mc"] == pytest.approx(station + 0.3, abs=1e-3)
|
||||
assert got["freeFit"]["off"] == 0 # 같은 측점에 정확히(다른 측점으로 안 옮김)
|
||||
assert abs(got["freeFit"]["mc"] - station) <= 0.05
|
||||
|
||||
|
||||
def test_폭_밖으로_미는_걸음_막음(got) -> None:
|
||||
station = got["station"]
|
||||
for case in ("drag", "radius"):
|
||||
assert abs(got[case]["mc"] - station) > 0.5 and got[case]["pushed"] is True
|
||||
for case in ("small", "radiusIn"):
|
||||
assert abs(got[case]["mc"] - station) < 0.5 and got[case]["pushed"] is False
|
||||
assert abs(got["freeRadius"]["mc"] - station) <= 0.05 and got["freeRadius"]["pushed"] is False
|
||||
|
||||
|
||||
def test_1m_스냅_측점_밖은_옛_판정(got) -> None:
|
||||
one = got["oneStep"]
|
||||
assert one["insideOff"] == 0 # 일반측점 + 0.3 = 「(MC)」 폭
|
||||
assert one["oddOff"][0] == pytest.approx(one["oddMc"], abs=1e-6) # 정수 m + 0.3 = 측점 밖
|
||||
|
||||
|
||||
def test_편집기_배선() -> None:
|
||||
edit = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||||
apply_edit = edit[edit.index("function applyEdit(") :]
|
||||
apply_edit = apply_edit[: apply_edit.index("writeRouteLocks(")]
|
||||
assert (
|
||||
"stationSnap && fitting && before && fit.pushedOff(before, snapshotNow(), fitLimits())"
|
||||
in apply_edit
|
||||
)
|
||||
assert "fit.offStationCount(plannedLine, curveInfo, snap(), stationIntervalM)" in edit
|
||||
bar = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_CurveBar.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "offStationMcs(line, curveInfo, snap.stepM, snap.stationM)" in bar
|
||||
Reference in New Issue
Block a user