/* 453 평면 집기 · 스냅만 바꾼 초안 헬퍼 — 진짜 `_RouteEdit_Input`(pressWinner · handleAtScreen) · * `_RouteEdit_History` · `_RouteEdit_Draft`(다리 감싸기)를 트랜스파일해 돌림. 결과 JSON 한 벌(표준출력). */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); const data = new Map(); globalThis.sessionStorage = { get length() { return data.size; }, key: (i) => [...data.keys()][i] ?? null, getItem: (k) => (data.has(k) ? data.get(k) : null), setItem: (k, v) => data.set(k, String(v)), removeItem: (k) => data.delete(k), }; const win = new EventTarget(); win.setTimeout = setTimeout; win.clearTimeout = clearTimeout; win.sessionStorage = globalThis.sessionStorage; win.localStorage = { getItem: () => null }; globalThis.window = win; const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback }, "@util/common_util_svg": { stationLabel: (c, i) => `No.${Math.round(c / i)}` }, }; const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (request) => { if (STUBS[request]) return STUBS[request]; if (request.startsWith(".")) return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); return require(request); }, }); const page = path.join(ROOT, "B05_Profile"); const input = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Input.ts")); const edit = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_History.ts")); const draftLink = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Draft.ts")); // ① 누름 판정 — `_Pointer.chooseAt` 와 같은 셈(노드 · 손잡이는 화면 8 px 떨어짐 · 433-14 ③ 낮은 확대 R 12) const toScreen = ([x, y]) => [x, y]; const node = [100, 100]; const curves = [{ node_first: 1, start: [108, 100], end: [100, 108] }]; const choose = (px, py, markDistance = Infinity) => { const pick = input.pickAtScreen([[0, 0], node, [200, 200]], toScreen, px, py, 12, 6, null); const at = pick && "node" in pick ? toScreen(node) : null; const handle = input.handleAtScreen(curves, toScreen, px, py, 14); return input.pressWinner( at ? Math.hypot(at[0] - px, at[1] - py) : Infinity, 6, markDistance, handle?.distance ?? Infinity, ); }; const pick = { handleSide: choose(107, 100), // 노드 7 px(가운데 밖) · 손잡이 1 px — 손잡이 handleCenter: choose(108, 100), // 손잡이 위 · 노드 8 px — 손잡이 coreEdge: choose(106, 100), // 노드 6 px(가운데 끝) · 손잡이 2 px — 노드 nodeCore: choose(102, 100), // 노드 2 px(가운데 안) — 노드 nodeCloser: choose(93, 100), // 노드 7 px · 손잡이 15 px(문턱 밖) — 노드 handleOnly: choose(116, 100), // 노드 16 px(문턱 밖) · 손잡이 8 px — 손잡이 nothing: choose(150, 150), // 빈 곳 — pick(null 그대로) markOnCore: choose(103, 100, 1), // 마커 1 px 이어도 노드 가운데 3 px — 노드(433-21 ③) markCloser: choose(93, 100, 3), // 노드 7 px · 마커 3 px — 마커 nodeBeatsMark: choose(93, 100, 9), // 노드 7 px · 마커 9 px — 노드 tie: input.pressWinner(8, 6, 8, 8), // 같으면 마커 → 노드 → 손잡이 handleDistance: input.handleAtScreen(curves, toScreen, 111, 100, 14)?.distance ?? null, }; // ② 스냅만 바꾼 뒤 노선 처음으로 — 다리 초안(433-14 ②) const snap = (x) => ({ planned: [ [0, 0], [x, 0], [100, 0], ], curveOn: [true, true, true], curveRadius: [null, null, null], curveLock: [null, null, null], curveArc: [null, null, null], apexLock: [false, false, false], picked: -1, }); const draft = (snapChanged) => { const told = []; const bridge = { edited: (d) => told.push(d ? d.nodes[1][0] : null), saved: () => null }; const history = draftLink.watch( edit.createRouteEditHistory(snap(50)), bridge, () => ({ stationSnap: true, snapStepM: 1 }), () => snapChanged, ); history.commit(snap(60)); history.undo(); history.redo(); history.undo(); return told; }; const old = (() => { const told = []; const bridge = { edited: (d) => told.push(d ? d.nodes[1][0] : null), saved: () => null }; const history = draftLink.watch(edit.createRouteEditHistory(snap(50)), bridge, () => ({})); history.commit(snap(60)); history.undo(); return told; })(); // ③ 맞추지 않은 기준에서 막힌 걸음만 하고 놓음(433-14 ① · req269 idem 재현) — 막힌 걸음 미리보기 // (`fitPreview` = `fitDrag`)가 노선을 바꿈 → `endDrag` 가 맞추기 전 자리(`dragRaw`)로 되돌림. const fit = loadTs(path.join(page, "logic", "B05_Profile_RouteEdit_StationFit.ts")); const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: 20 }; const ROUTE = [[0, 0]]; { let seed = 1; const rnd = () => ((seed = (seed * 16807) % 2147483647), seed / 2147483647); let a = 0; for (let i = 0; i < 14; i += 1) { a += (rnd() - 0.5) * 1.6; const L = 35 + rnd() * 60; const q = ROUTE[ROUTE.length - 1]; ROUTE.push([q[0] + Math.cos(a) * L, q[1] + Math.sin(a) * L]); } } const cols = (planned) => ({ planned, curveOn: planned.map(() => true), curveRadius: planned.map(() => null), curveLock: planned.map(() => null), curveArc: planned.map(() => null), apexLock: planned.map((_, i) => i === 0 || i === planned.length - 1), }); const gap = (a, b) => Math.max(...a.map((v, i) => Math.hypot(v[0] - b[i][0], v[1] - b[i][1]))); const blocked = { drags: 0, previewMoved: 0, leftAfterRelease: 0 }; for (let K = 1; K < ROUTE.length - 1; K += 1) { const base = ROUTE.map(([x, y]) => [x, y]); const dragRaw = base.map(([x, y]) => [x, y]); // 막힌 걸음 = 누르기 전 자리 그대로 맞춤 let planned = fit.fitDrag(cols(base), base, K, LIMITS); blocked.drags += 1; if (gap(planned, base) > 1e-6) blocked.previewMoved += 1; dragRaw.forEach((vertex, index) => (planned[index] = vertex)); // endDrag(!dragMoved && dragRaw) if (gap(planned, base) > 1e-6) blocked.leftAfterRelease += 1; } process.stdout.write( JSON.stringify({ pick, blocked, draft: { snapChanged: draft(true), same: draft(false), old } }), );