Files
Aislo/resources/tester/helper_280_4_fit.cjs
T
eomsangdonandClaude Sonnet 5.5 156ce1cf52 test(508-1b 옛 헬퍼 공용 불러오기 3/7): 헬퍼 28 곳 loadTs 베낌 걷고 makeLoader 로
- 헬퍼 동작 그대로(풀이 함수 본문은 옮겨 담음) · 제품 코드 안 고침
- 확인: 이 묶음 헬퍼를 쓰는 시험 218 개 전 218 통과 → 후 218 통과 (시험별 junit 대조 · 어긋남 0)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 17:08:08 +09:00

127 lines
4.8 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 { 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 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),
}),
);