Files
Aislo/resources/tester/helper_280_4_fit.cjs
T
eomsangdonandClaude Opus 5.5 75f990c97e fix(483-3 B05 스냅): 손대지 않은 곡선 MC 꼴(N+0 · N+10) 유지 — 고친 곡선만 고른 간격
- StationFit fitAfter · fitDrag(before) — 고치기 전 노선의 곡선 꼴을 IP 자리 + 곡선 칸 열쇠로 기억 · 밀 거리 = 측점 간격
- 10 m 배수(No.N+10)도 측점 위(offStationMcs · 맞춤) · 서로 다른 측점 = 누가거리
- RouteEdit = fitAfter 한 줄 · fitLimits stationM · 694줄
- 시험 = 새 test_280_4_fit(고치기 전 동작 = 190 → 180 대조) · 366_3 · 390_5 이음 글

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:08:09 +09:00

137 lines
5.3 KiB
JavaScript

/* PLAN 483-3(요구 280-4) 「유지」 — `logic/B05_Profile_RouteEdit_StationFit.ts` 를 Node 로 돌려 JSON 한 줄
* (`test_280_4_fit.py` 가 판정). 분석 D 재현 2 노선(helper_366_3 와 같은 지그재그).
*/
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 frame = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Frame.ts"));
const STATION = 20;
const BASE = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, stationM: STATION };
const STEP10 = { ...BASE, intervalM: 10 };
const STEPNO = { ...BASE, intervalM: STATION };
const ROUTE = [
[0, 0],
[93, 7],
[171, 61],
[262, 48],
[337, 112],
[431, 101],
[503, 160],
];
function columns(planned) {
const n = planned.length;
return {
planned: planned.map(([x, y]) => [x, y]),
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 withPlanned = (cols, planned) => ({ ...cols, planned: planned.map(([x, y]) => [x, y]) });
const round2 = (v) => Math.round(v * 100) / 100;
function mcs(cols) {
const built = edits.rebuildLine({ ...cols, curveRadius: [...cols.curveRadius] }, STEPNO);
return frame.curveSpans(built.vertices, built.curves).map(([a, b]) => round2((a + b) / 2));
}
function off(cols, step) {
const built = edits.rebuildLine({ ...cols, curveRadius: [...cols.curveRadius] }, STEPNO);
return fit.offStationCount(built.vertices, built.curves, step);
}
/** IP `k` 를 들어오는 접선 따라 d 밀기 — 손 편집 흉내. */
function nudge(cols, k, d) {
const p = cols.planned;
const len = Math.hypot(p[k][0] - p[k - 1][0], p[k][1] - p[k - 1][1]);
const ux = (p[k][0] - p[k - 1][0]) / len;
const uy = (p[k][1] - p[k - 1][1]) / len;
const next = p.map(([x, y]) => [x, y]);
next[k] = [p[k][0] + ux * d, p[k][1] + uy * d];
return withPlanned(cols, next);
}
/** IP `k` 를 화면 좌표로 (dx, dy) 옮김 — 앞 교점 손 편집(접선 밖 · 맞춤이 되밀어도 제자리로 안 돌아감). */
function shift(cols, k, dx, dy) {
const next = cols.planned.map(([x, y]) => [x, y]);
next[k] = [next[k][0] + dx, next[k][1] + dy];
return withPlanned(cols, next);
}
/** 손 편집 한 걸음 = 편집기 applyEdit(`fitAfter(before, now, limits)`). */
const step = (before, now, limits) => withPlanned(now, fit.fitAfter(before, now, limits));
const FRONT = [0, 3]; // 앞 IP1 을 y +3 m
// ① 측점 No. 로 맞춘 노선
const s1 = withPlanned(columns(ROUTE), fit.fitCurveStations(columns(ROUTE), STEPNO, 1));
// ② 10 m · IP2 를 손으로 밀어 그 곡선 MC = N+10(밀 거리 훑어 첫 N+10)
let s2 = null;
let pushed = null;
for (let d = 1; d <= 12 && !s2; d += 0.5) {
const now = step(s1, nudge(s1, 2, d), STEP10);
const mc = mcs(now)[1];
if (Math.abs(mc / 10 - Math.round(mc / 10)) < 0.005 && Math.round(mc / 10) % 2 === 1)
((s2 = now), (pushed = d));
}
// ③ 간격만 측점 No. 로(편집기 = 간격 바꿈 → applyEdit · 노선 그대로)
const s3 = step(s2, s2, STEPNO);
// ④ 측점 No. 로 돌린 뒤 앞 IP1 +3 m
const s4 = step(s3, shift(s3, 1, FRONT[0], FRONT[1]), STEPNO);
// ⑤ 10 m 그대로 앞 IP1 +3 m
const s5 = step(s2, shift(s2, 1, FRONT[0], FRONT[1]), STEP10);
// ⑥ 그 곡선 IP2 를 손으로 다시 밀기(측점 No.) → N+0
const s6 = step(s3, nudge(s3, 2, 4), STEPNO);
// ⑦ before 없이(옛 동작) 측점 No. 로 다시 맞춤 = N+10 이 흔들림(기준이 고침을 재는지)
const s7 = withPlanned(s4, fit.fitCurveStations(shift(s3, 1, FRONT[0], FRONT[1]), STEPNO, 1));
// ⑧ 끄는 중 미리보기(fitDrag · before) = 놓을 때(fitAfter)
const dragNow = shift(s3, 1, FRONT[0], FRONT[1]);
const preview = withPlanned(dragNow, fit.fitDrag(dragNow, s3.planned, 1, STEPNO, s3));
const release = step(s3, preview, STEPNO);
const gap = Math.max(
...preview.planned.map((v, i) =>
Math.hypot(v[0] - release.planned[i][0], v[1] - release.planned[i][1]),
),
);
console.log(
JSON.stringify({
pushed,
mc: {
s1: mcs(s1),
s2: mcs(s2),
s3: mcs(s3),
s4: mcs(s4),
s5: mcs(s5),
s6: mcs(s6),
s7: mcs(s7),
},
off: { s2no: off(s2, STATION), s2ten: off(s2, 10), s4: off(s4, STATION) },
dragGap: gap,
previewMc: mcs(preview),
}),
);