/* 계획노선 편집 창 **가운데 단추 팬 튐**(401 · 요구 250-3) 단위검증 헬퍼 — * `_RouteEdit_Input.ts` 의 `bindRouteEditNavigation` 에 가짜 캔버스를 붙이고 포인터 · 휠 이벤트를 * 흘려 보낸다. 회전은 진짜 `_RouteEdit_Rotate.ts` · 화면 셈은 진짜 B04 `metricToScreen`. * 판정 = 「잡은 땅 점이 손(커서)을 따라가나」 — 보이는 자리 = rerotate(metricToScreen(...)). */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); function memoryStorage() { const map = new Map(); return { getItem: (key) => (map.has(key) ? map.get(key) : null), setItem: (key, value) => map.set(key, String(value)), removeItem: (key) => map.delete(key), }; } globalThis.window = { localStorage: memoryStorage(), sessionStorage: memoryStorage() }; globalThis.localStorage = globalThis.window.localStorage; const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback } }; const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (request) => { if (STUBS[request]) return STUBS[request]; if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); return require(request); }, }); const input = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Input.ts")); const rotateMod = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Rotate.ts")); const render = loadTs(path.join(ROOT, "B04_PreProcess", "B04_PreProcess_UI_MapRender.ts")); const RECT = { left: 10, top: 20, width: 800, height: 600 }; const meta = { x_min: 0, y_min: 0, width_meters: 800, height_meters: 600 }; const W = [300, 250]; // 잡을 땅 점(m) /** 창 하나 — 가짜 캔버스 · 진짜 회전 · 진짜 팬/휠. */ function rig(rotationDeg = 0) { const listeners = {}; const canvas = { addEventListener: (type, fn) => (listeners[type] ??= []).push(fn), getBoundingClientRect: () => RECT, style: { cursor: "", removeProperty() {} }, setPointerCapture() {}, }; let view = { width: RECT.width, height: RECT.height, scale: 1, offsetX: 0, offsetY: 0, mapRect: { x: 0, y: 0, width: RECT.width, height: RECT.height }, }; const rotation = rotateMod.createMapRotation({ overlay: { querySelector: () => null }, size: () => ({ width: RECT.width, height: RECT.height }), onChange: () => {}, }); rotation.set((rotationDeg * Math.PI) / 180); input.bindRouteEditNavigation({ canvas, getView: () => view, setView: (next) => (view = next), getMeta: () => meta, unrotateDelta: rotation.unrotateDelta, rotateWheel: rotation.wheel, draw: () => {}, }); const fire = (type, event) => (listeners[type] ?? []).forEach((fn) => fn({ preventDefault() {}, ...event })); /** 땅 점이 화면(client)에 보이는 자리. */ const seen = ([x, y]) => { const [px, py] = rotation.rerotate(...render.metricToScreen(meta, view, x, y)); return [px + RECT.left, py + RECT.top]; }; return { fire, seen, view: () => view }; } const mouse = (x, y, extra = {}) => ({ pointerId: 1, pointerType: "mouse", clientX: x, clientY: y, button: 1, buttons: 4, ...extra, }); const gap = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]); /** 가운데 끌기 여러 걸음 — 걸음마다 잡은 점과 손의 거리 중 가장 큰 값. */ function dragFollows(rotationDeg) { const r = rig(rotationDeg); let [x, y] = r.seen(W); r.fire("pointerdown", mouse(x, y)); let worst = 0; for (const [dx, dy] of [ [40, 0], [0, -35], [-120, 80], [7, 3], [300, -200], ]) { x += dx; y += dy; r.fire("pointermove", mouse(x, y, { button: -1 })); worst = Math.max(worst, gap(r.seen(W), [x, y])); } r.fire("pointerup", mouse(x, y)); return worst; } /** 끄는 중 휠 확대(커서 자리) 뒤 다시 끌기 — 잡은 점이 손 밑에 남나. */ function wheelMidDrag(rotationDeg) { const r = rig(rotationDeg); let [x, y] = r.seen(W); r.fire("pointerdown", mouse(x, y)); x += 50; y += 20; r.fire("pointermove", mouse(x, y, { button: -1 })); r.fire("wheel", { clientX: x, clientY: y, deltaY: 100, deltaMode: 0, ctrlKey: false }); r.fire("wheel", { clientX: x, clientY: y, deltaY: 100, deltaMode: 0, ctrlKey: false }); const afterWheel = gap(r.seen(W), [x, y]); x += 30; y -= 60; r.fire("pointermove", mouse(x, y, { button: -1 })); return { afterWheel, afterMove: gap(r.seen(W), [x, y]), scale: r.view().scale }; } /** 끄는 중 Ctrl+휠 회전 뒤 다시 끌기 — 땅 점이 손이 민 만큼(같은 방향 · 같은 길이) 움직이나. */ function rotateMidDrag() { const r = rig(15); let [x, y] = r.seen(W); r.fire("pointerdown", mouse(x, y)); x += 20; r.fire("pointermove", mouse(x, y, { button: -1 })); r.fire("wheel", { clientX: x, clientY: y, deltaY: 100, deltaMode: 0, ctrlKey: true }); const before = r.seen(W); x += 60; y += 45; r.fire("pointermove", mouse(x, y, { button: -1 })); const after = r.seen(W); return gap([after[0] - before[0], after[1] - before[1]], [60, 45]); } /** 창 밖에서 가운데 단추를 놓고 들어옴 — buttons 0 move 는 팬 끝 · 그 뒤 move 는 화면 그대로. */ function releasedOutside() { const r = rig(15); const [x, y] = r.seen(W); r.fire("pointerdown", mouse(x, y)); r.fire("pointermove", mouse(x + 30, y, { button: -1 })); const held = { ...r.view() }; r.fire("pointermove", mouse(x + 400, y + 300, { button: -1, buttons: 0 })); r.fire("pointermove", mouse(x + 10, y + 10, { button: -1, buttons: 0 })); const lost = rig(0); lost.fire("pointerdown", mouse(x, y)); lost.fire("lostpointercapture", mouse(x, y)); const lostBefore = { ...lost.view() }; lost.fire("pointermove", mouse(x + 90, y, { button: -1 })); return { moved: Math.hypot(r.view().offsetX - held.offsetX, r.view().offsetY - held.offsetY), lostMoved: Math.hypot( lost.view().offsetX - lostBefore.offsetX, lost.view().offsetY - lostBefore.offsetY, ), }; } /** 손가락 둘 — 둘째 손가락이 팬을 넘겨받지 않음 · 첫 손가락은 계속 따라감. */ function secondTouch() { const r = rig(0); let [x, y] = r.seen(W); const touch = (id, px, py) => ({ pointerId: id, pointerType: "touch", clientX: px, clientY: py, button: 0, buttons: 1, }); r.fire("pointerdown", touch(1, x, y)); r.fire("pointerdown", touch(2, x + 200, y + 100)); r.fire("pointermove", touch(2, x + 260, y + 130)); x += 25; y += 15; r.fire("pointermove", touch(1, x, y)); return gap(r.seen(W), [x, y]); } /** 왼쪽 단추(편집 창) — 팬 안 함. */ function leftDoesNotPan() { const r = rig(0); const [x, y] = r.seen(W); r.fire("pointerdown", mouse(x, y, { button: 0, buttons: 1 })); r.fire("pointermove", mouse(x + 80, y + 40, { button: -1, buttons: 1 })); return Math.hypot(r.view().offsetX, r.view().offsetY); } process.stdout.write( JSON.stringify({ follow0: dragFollows(0), follow15: dragFollows(15), wheel0: wheelMidDrag(0), wheel15: wheelMidDrag(15), rotateMid: rotateMidDrag(), outside: releasedOutside(), secondTouch: secondTouch(), leftPan: leftDoesNotPan(), }), );