/* 398-2 꺾임점 창 반지름 · 곡선 길이 넣기(요구 249) — `_CurveBar` 배선을 Node 로 돌려 JSON 한 줄 * (`test_398_2_curve_input.py` 가 판정). 창(`_Label`)은 손잡이만 잡는 가짜 · 편집 창 `applyEdit` 는 * 같은 길(스냅 켬 = `fitCurveStations` → 길이 고정 R · 하한 → `buildEditedPolyline`)로 흉내. * node helper_398_2_curve_input.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 shows = []; const STUBS = { "./B05_Profile_UI_RouteEdit_Label": { createCurveLabel: (given) => { handlers = given; return { show: (state) => shows.push(state), 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 fit = loadTs(path.join(B05, "logic", "B05_Profile_RouteEdit_StationFit.ts")); const bar = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_CurveBar.ts")); // IP1 = 앞뒤 직선 길어 R 60 들어감 · IP2 = 뒤 직선 16 m 라 접선 8 m 까지(큰 R 눌림) const ROUTE = [ [0, 0], [120, 0], [220, 70], [236, 70], [340, 0], ]; const LIMITS = { minRadiusM: 12, limitRadiusM: 12, limitArcM: 5, intervalM: 20 }; function editor(snap) { 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(null), curveLock: Array(n).fill(null), curveArc: Array(n).fill(null), apexLock: Array(n).fill(false), limitRadiusM: LIMITS.limitRadiusM, limitArcM: LIMITS.limitArcM, }; 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 made = null; const applyEdit = (message) => { if (snap) state.planned = fit.fitCurveStations(state, LIMITS, 1); markEdited(); made.sync(); status.textContent = `예상노선 — ${message} 꼬리`; }; made = 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(); const drawn = (k) => state.curveInfo.find((c) => c.node_first === k) ?? null; const arcOf = (k) => { const c = drawn(k); return c ? c.radius_m * edits.deflectionRad(c.inner_angle_deg) : null; }; const mc = (k) => { const c = drawn(k); if (!c) return null; const [[a, b]] = loadTs( path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Frame.ts"), ).curveSpans(state.line, [c]); return (a + b) / 2; }; const pick = (k) => { state.picked = k; made.sync(); }; const lastShow = () => shows[shows.length - 1]; return { state, status, handlers: () => handlers, drawn, arcOf, mc, pick, lastShow, markEdited }; } const round = (v) => (v === null ? null : Math.round(v * 1000) / 1000); const out = {}; for (const snap of [false, true]) { const key = snap ? "snapOn" : "snapOff"; const ed = editor(snap); const h = ed.handlers(); const row = {}; // IP1 — 반지름 40 → 60 · 곡선 길이 18 → 25 ed.pick(1); h.onRadius(40); row.r40 = round(ed.drawn(1).radius_m); h.onRadius(60); row.r60 = round(ed.drawn(1).radius_m); row.r60Shown = round(ed.lastShow().radiusShown); row.r60Status = ed.status.textContent; row.r60Mc = round(ed.mc(1)); h.onArcLength(18); row.l18 = round(ed.arcOf(1)); h.onArcLength(25); row.l25 = round(ed.arcOf(1)); row.l25Shown = round(ed.lastShow().arcLengthShown); row.l25Status = ed.status.textContent; // IP2 — 접선 자리 모자람(넣은 값 · 그린 값 · 까닭) ed.pick(2); h.onRadius(60); row.capR = round(ed.drawn(2).radius_m); row.capShown = round(ed.lastShow().radiusShown); row.capStatus = ed.status.textContent; h.onArcLength(40); row.capArcStatus = ed.status.textContent; h.onRadius(null); row.autoStatus = ed.status.textContent; out[key] = row; } // 「고정」 — 길이 고정 켬 = 교각이 바뀌어도 그 길이 · 끔 = 반지름이 남고 길이가 따라 바뀜 for (const lock of [true, false]) { const ed = editor(false); const h = ed.handlers(); ed.pick(1); h.onArcLength(25); if (lock) h.onLock("arc"); ed.state.planned[2] = [220, 50]; // IP1 교각이 바뀜 ed.markEdited(); out[lock ? "arcLocked" : "arcFree"] = { arc: round(ed.arcOf(1)), radius: round(ed.drawn(1).radius_m), }; } { const ed = editor(false); const h = ed.handlers(); ed.pick(1); h.onRadius(60); h.onLock("radius"); ed.state.planned[2] = [220, 50]; ed.markEdited(); out.radiusLocked = { radius: round(ed.drawn(1).radius_m), lock: ed.state.curveLock[1] }; } process.stdout.write(JSON.stringify(out));