- 헬퍼 동작 그대로(풀이 함수 본문은 옮겨 담음) · 제품 코드 안 고침 - 확인: 이 묶음 헬퍼를 쓰는 시험 218 개 전 218 통과 → 후 218 통과 (시험별 junit 대조 · 어긋남 0) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
127 lines
4.8 KiB
JavaScript
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),
|
|
}),
|
|
);
|