fix(454 B05 방향키 측점 걸음): 곡선 맞춤 후보 측점 둘 · 뒤 곡선 앞보기 (433-21 ②)
- 곡선마다 내림 · 올림 측점 후보 · 뒤 곡선이 측점 위거나 맞춰지는 쪽 고름 - 무작위 노선 30 개 20 m 걸음 측점 밖 막힘 141 → 25 · 놓은 뒤 측점 밖 43 → 0 - 시험 test_454_arrow_station_step 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/* 454(433-21 ②) 방향키 측점 No. 걸음 — 무작위 짧은 다리 노선(14 IP · 고정 R) 교점마다 네 방향 한 걸음
|
||||
* 누르고 측점 밖 막힘 수를 JSON 한 줄로(`test_454_arrow_station_step.py` 가 판정).
|
||||
* 누름 판정은 편집기 방향키 길과 같음 — 하한 깸 = 막힘 · 미리보기(`fitDrag`) 측점 밖이 늘면 막힘.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
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) =>
|
||||
request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
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 INTERVAL = 20;
|
||||
const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: INTERVAL };
|
||||
const columns = (p) => ({
|
||||
planned: p,
|
||||
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),
|
||||
});
|
||||
const copy = (c) => ({
|
||||
...c,
|
||||
planned: c.planned.map(([x, y]) => [x, y]),
|
||||
curveRadius: [...c.curveRadius],
|
||||
});
|
||||
const off = (b) => fit.offStationCount(b.vertices, b.curves, INTERVAL);
|
||||
const floor = (b) => edits.curveShortfalls(b.nodes, LIMITS.limitRadiusM, LIMITS.limitArcM);
|
||||
|
||||
function route(seed) {
|
||||
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
||||
const line = [[0, 0]];
|
||||
let a = 0;
|
||||
for (let i = 0; i < 14; i += 1) {
|
||||
a += (rnd() - 0.5) * 1.6;
|
||||
const length = 35 + rnd() * 60;
|
||||
const p = line[line.length - 1];
|
||||
line.push([p[0] + Math.cos(a) * length, p[1] + Math.sin(a) * length]);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
const out = { press: 0, floorBlock: 0, offBlock: 0, offStart: 0, offAfterRelease: 0 };
|
||||
for (let seed = 1; seed <= 10; seed += 1) {
|
||||
const hist = columns(fit.fitCurveStations(columns(route(seed)), LIMITS, 1));
|
||||
hist.curveRadius = edits
|
||||
.rebuildLine(copy(hist), LIMITS)
|
||||
.nodes.map((n) => (n.radius_m === null ? null : Math.round(n.radius_m * 100) / 100));
|
||||
out.offStart += off(edits.rebuildLine(copy(hist), LIMITS));
|
||||
for (let k = 1; k < hist.planned.length - 1; k += 1)
|
||||
for (const [dx, dy] of [
|
||||
[1, 0],
|
||||
[-1, 0],
|
||||
[0, 1],
|
||||
[0, -1],
|
||||
]) {
|
||||
const st = copy(hist);
|
||||
const b0 = edits.rebuildLine(st, LIMITS);
|
||||
const node = st.planned[k];
|
||||
st.planned[k] = [node[0] + dx * INTERVAL, node[1] + dy * INTERVAL];
|
||||
out.press += 1;
|
||||
if (edits.shortfallCrossed(floor(b0), floor(edits.rebuildLine(st, LIMITS)))) {
|
||||
out.floorBlock += 1;
|
||||
continue;
|
||||
}
|
||||
const pv = { ...st, planned: fit.fitDrag(st, hist.planned, k, LIMITS) };
|
||||
if (off(edits.rebuildLine(copy(pv), LIMITS)) > off(b0)) {
|
||||
out.offBlock += 1;
|
||||
continue;
|
||||
}
|
||||
const rel = { ...pv, planned: fit.fitCurveStations(pv, LIMITS, fit.fitStart(hist, pv)) };
|
||||
if (off(edits.rebuildLine(copy(rel), LIMITS))) out.offAfterRelease += 1;
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,35 @@
|
||||
"""454(433-21 ②) 방향키 측점 No. 20 m 걸음 막힘 — 곡선 맞춤이 후보 측점 둘(내림 · 올림) + 뒤 곡선
|
||||
앞보기로 고름. 무작위 노선 열 개 · 교점마다 네 방향 = 520 누름(`helper_454_arrow_station_step.cjs`).
|
||||
고치기 전 측점 밖 막힘 48 · 처음 맞춤 측점 밖 2 · 놓은 뒤 측점 밖 43 → 6 · 0 · 0.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_454_arrow_station_step.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HELPER = Path(__file__).with_name("helper_454_arrow_station_step.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:
|
||||
assert got["press"] == 520
|
||||
|
||||
|
||||
def test_측점_밖_막힘_줄어듦(got) -> None:
|
||||
assert got["offBlock"] <= 10
|
||||
|
||||
|
||||
def test_처음_맞춤_놓은_뒤_측점_밖_없음(got) -> None:
|
||||
assert got["offStart"] == 0 and got["offAfterRelease"] == 0
|
||||
Reference in New Issue
Block a user