/* 408-1 R 바꿈 뒤 뒤쪽 측점 맞춤(요구 252-3) — `_CurveBar` 배선을 Node 로 돌려 JSON 한 줄 * (`test_408_radius_downstream.py` 가 판정). 창(`_Label`)은 손잡이만 잡는 가짜 · 편집 창 `applyEdit` 는 * 같은 길(스냅 켬 = `fitStart` → `fitCurveStations` → 길이 고정 R · 하한 → `buildEditedPolyline`)로 흉내. * node helper_408_radius_downstream.cjs */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const B05 = path.join(ROOT, "B05_Profile"); const cache = new Map(); let handlers = null; const STUBS = { "./B05_Profile_UI_RouteEdit_Label": { createCurveLabel: (given) => { handlers = given; return { show: () => {}, hide: () => {}, destroy: () => {} }; }, centerDirectionOf: () => null, }, }; 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) => STUBS[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 curve = loadTs(path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Curve.ts")); const edits = loadTs(path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Edits.ts")); const frame = loadTs(path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Frame.ts")); const fit = loadTs(path.join(B05, "logic", "B05_Profile_RouteEdit_StationFit.ts")); const bar = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_CurveBar.ts")); // 곡선 넷(IP1~4) · 직선이 길어 맞춤이 접선 절반에 안 막힘 const ROUTE = [ [0, 0], [110, 0], [210, 60], [320, 60], [420, -10], [540, -10], ]; const LIMITS = { minRadiusM: 12, limitRadiusM: 12, limitArcM: 5, intervalM: 20 }; const KEYS = ["planned", "curveOn", "curveRadius", "curveLock", "curveArc", "apexLock"]; const copy = (state) => Object.fromEntries( KEYS.map((key) => [key, state[key].map((v) => (Array.isArray(v) ? [...v] : v))]), ); function editor(snap, apexLock = []) { const n = ROUTE.length; const state = { picked: -1, planned: ROUTE.map((v) => [v[0], v[1]]), nodeInfo: [], curveInfo: [], curveOn: Array(n).fill(true), curveRadius: Array(n).fill(30), curveLock: Array(n).fill(null), curveArc: Array(n).fill(null), apexLock: Array.from({ length: n }, (_, i) => apexLock.includes(i)), limitRadiusM: LIMITS.limitRadiusM, limitArcM: LIMITS.limitArcM, line: [], snap: snap ? { stepM: LIMITS.intervalM, stationM: 20 } : null, }; const status = { textContent: "" }; const markEdited = () => { edits.applyArcLocks(state.planned, state.curveLock, state.curveArc, state.curveRadius); edits.applyCurveLimits( state.planned, state.curveOn, state.curveRadius, LIMITS.limitRadiusM, LIMITS.limitArcM, ); const built = curve.buildEditedPolyline( state.planned, state.curveOn, state.curveRadius, LIMITS.minRadiusM, ); state.line = built.vertices; state.curveInfo = built.curves; state.nodeInfo = built.nodes; }; let committed = null; const applyEdit = (message) => { const from = fit.fitStart(committed, state); if (snap) state.planned = fit.fitCurveStations(state, LIMITS, from); markEdited(); status.textContent = `예상노선 — ${message} 꼬리`; committed = copy(state); }; bar.createCurveBar({ canvas: { getBoundingClientRect: () => ({ left: 0, top: 0, right: 800, bottom: 600 }) }, container: { querySelector: (sel) => (sel === ".b05-routeedit__status" ? status : null) }, state: () => state, toScreen: (v) => [v[0], v[1]], applyEdit, onUnselect: () => {}, }); // 처음 = 스냅 켤 때처럼 전체 맞춤 한 걸음 markEdited(); applyEdit("측점 스냅을 켰습니다."); const mcs = () => frame .curveSpans(state.line, state.curveInfo) .map(([a, b]) => Math.round(((a + b) / 2) * 1000) / 1000); const off = () => fit.offStationCount(state.line, state.curveInfo, LIMITS.intervalM); const pick = (k) => { state.picked = k; }; return { state, status, h: () => handlers, mcs, off, pick }; } const out = {}; const row = (ed) => ({ mcs: ed.mcs(), off: ed.off(), status: ed.status.textContent, planned: ed.state.planned.map((v) => [...v]), }); for (const snap of [true, false]) { const key = snap ? "on" : "off"; const ed = editor(snap); out[`${key}Start`] = row(ed); ed.pick(1); ed.h().onRadius(60); // 앞쪽 곡선 R 키움 out[`${key}R60`] = row(ed); ed.h().onArcLength(50); // 곡선 길이 입력도 out[`${key}L50`] = row(ed); ed.h().onRadius(null); // 빈 값 = 자동 out[`${key}Auto`] = row(ed); } // IP3 고정 = 그 곡선은 못 맞춤 → 상태줄 「못 맞춘 곡선 No.…」 { const ed = editor(true, [3]); out.lockStart = row(ed); ed.pick(1); ed.h().onRadius(60); out.lockR60 = row(ed); } process.stdout.write(JSON.stringify(out));