Files
Aislo/resources/tester/helper_390_5_snap_step_ops.cjs
T

152 lines
5.5 KiB
JavaScript

/* 390-5 스냅 간격 = 조작 단위 — 방향키 걸음(`KeyNudge.arrowStep`) · 끌기 미리보기(`StationFit.fitDrag`) ·
* MC 측점 값 자릿수(`Frame.snapCurveStations` · 눈금 글 `stationLabel`)를 Node 로 돌려 JSON 한 줄
* (`test_390_5_snap_step_ops.py` 가 판정 · 파이썬 짝 거울은 그쪽).
* node helper_390_5_snap_step_ops.cjs '<{"grid":[…],"spans":[[bc,ec]…]}>'
*/
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 curve = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Curve.ts"));
const frame = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Frame.ts"));
const nudge = loadTs(path.join(LOGIC, "B05_Profile_RouteEdit_KeyNudge.ts"));
const station = loadTs(path.join(ROOT, "common_util", "common_util_station.ts"));
const INTERVAL = 20;
const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: INTERVAL };
const ROUTE = [
[0, 0],
[93, 7],
[171, 61],
[262, 48],
[337, 112],
[431, 101],
[503, 160],
];
const STEPS = [0.1, 1, 10, INTERVAL];
function columns(planned) {
const n = planned.length;
return {
planned,
curveOn: planned.map(() => true),
curveRadius: planned.map(() => null),
curveLock: planned.map(() => null),
curveArc: planned.map(() => null),
apexLock: planned.map((_, i) => i === 0 || i === n - 1),
};
}
const build = (planned) =>
curve.buildEditedPolyline(
planned,
planned.map(() => true),
planned.map(() => null),
LIMITS.minRadiusM,
);
function mcs(planned) {
const built = build(planned);
return frame.curveSpans(built.vertices, built.curves).map(([a, b]) => (a + b) / 2);
}
const residue = (mc, step) => Math.abs(mc - Math.round(mc / step) * step);
const moved = (a, b) => Math.max(...a.map((v, i) => Math.hypot(v[0] - b[i][0], v[1] - b[i][1])));
// ① 방향키 — 화면 축이 30° 돌고 1 px = 0.5 m 인 지도
const turn = Math.PI / 6;
const metricAt = (x, y) => [
100 + 0.5 * (x * Math.cos(turn) - y * Math.sin(turn)),
200 + 0.5 * (x * Math.sin(turn) + y * Math.cos(turn)),
];
const len = ([x, y]) => Math.hypot(x, y);
const keys = {
off: len(nudge.arrowStep("ArrowUp", false, metricAt)),
offShift: len(nudge.arrowStep("ArrowUp", true, metricAt)),
snap: Object.fromEntries(
STEPS.map((step) => [
step,
{
plain: len(nudge.arrowStep("ArrowRight", false, metricAt, step)),
shift: len(nudge.arrowStep("ArrowRight", true, metricAt, step)),
},
]),
),
};
// ② 끌기 미리보기 — 측점 No. 로 맞춘 노선에서 IP 3 을 끌어 감(간격별)
const base = fit.fitCurveStations(columns(ROUTE), LIMITS, 1);
const DRAG = 3;
const mouse = [base[DRAG][0] + 7.3, base[DRAG][1] + 4.1];
const drag = Object.fromEntries(
STEPS.map((step) => {
const limits = { ...LIMITS, intervalM: step };
const now = base.map((v, i) => (i === DRAG ? mouse : v));
const preview = fit.fitDrag(columns(now), base, DRAG, limits);
// 앞 프레임이 다른 자리를 이미 밀어 둔 상태에서도 같은 값(쌓이지 않음)
const stale = preview.map((v, i) => (i === DRAG ? mouse : [v[0] + 0.7, v[1] - 0.4]));
const again = fit.fitDrag(columns(stale), base, DRAG, limits);
// 놓을 때 정본 맞춤(`applyEdit` = fitStart → fitCurveStations)이 미리보기를 안 바꿈
const from = fit.fitStart(columns(base), columns(preview));
const release = fit.fitCurveStations(columns(preview), limits, from);
const after = mcs(preview);
const built = build(preview);
return [
step,
{
residues: after.map((mc) => residue(mc, step)),
tol: fit.mcTolM(step),
off: fit.offStationCount(built.vertices, built.curves, step),
dragMoved: Math.hypot(preview[DRAG][0] - mouse[0], preview[DRAG][1] - mouse[1]),
stale: moved(preview, again),
release: moved(preview, release),
before: moved(base.slice(0, DRAG - 1), preview.slice(0, DRAG - 1)),
},
];
}),
);
// ③ MC 측점 값 · 눈금 글
const input = JSON.parse(process.argv[2] || '{"grid":[],"spans":[]}');
const snapped = Object.fromEntries(
[
["0.1", 0.1],
["1", 1],
["10", 10],
["station", null],
].map(([key, step]) => [key, frame.snapCurveStations(input.grid, input.spans, step)]),
);
const labels = {
int: station.stationLabel(119.8, 20),
one: station.stationLabel(119.8, 20, 1),
oneWhole: station.stationLabel(120, 20, 1),
oneSmall: station.stationLabel(100.1, 20, 1),
oneDefault: station.stationLabel(119.84, 20),
};
console.log(
JSON.stringify({ keys, drag, snapped, labels, scale: [0.1, 1, 10, null].map(frame.mcScale) }),
);