/* 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), }), );