/* PLAN 397 단위검증 헬퍼 — B06 횡단 카드 Ctrl+휠 줌. * 실제 TS(`B06_Section_UI_Cross_View_Zoom.attachZoomPan` → 공용 `b_svg_zoom_pan`)를 Node 로 돌림. * 결과 = 표준출력 JSON 한 벌. 짝 = `test_397_cross_ctrl_wheel.py`. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const common = fs.readFileSync( path.join(ROOT, "B06_Section", "B06_Section_UI_Section_Common.ts"), "utf8", ); const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)(); const STUBS = { "./B06_Section_UI_Section_Common": { CROSS_PAD, L: (k) => k } }; function loadTs(full) { const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; const localRequire = (request) => STUBS[request] ?? {}; new Function("exports", "module", "require", js)(box.exports, box, localRequire); return box.exports; } globalThis.localStorage = { getItem: () => null }; // 휠 방향 기본(밂 = 확대) STUBS["./input_prefs"] = loadTs(path.join(ROOT, "A00_Common", "input_prefs.ts")); STUBS["../A00_Common/b_svg_zoom_pan"] = loadTs(path.join(ROOT, "A00_Common", "b_svg_zoom_pan.ts")); const zoomMod = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_View_Zoom.ts")); const W = 600; const H = 300; function fakeSvg() { const listeners = {}; const style = {}; return { style: { setProperty: (k, v) => (style[k] = v) }, styles: style, setAttribute() {}, classList: { add() {}, remove() {} }, addEventListener: (type, fn) => (listeners[type] ??= []).push(fn), /** 사건을 쏘고 preventDefault 가 불렸는지 돌려줌. */ fire: (type, event) => { let prevented = false; (listeners[type] ?? []).forEach((fn) => fn({ preventDefault: () => (prevented = true), stopPropagation() {}, pointerId: 1, ctrlKey: false, ...event, }), ); return prevented; }, setPointerCapture() {}, releasePointerCapture() {}, getBoundingClientRect: () => ({ left: 0, top: 0, width: W, height: H }), }; } const round = (v) => Math.round(v * 1e6) / 1e6; const svg = fakeSvg(); let state = { scale: 1, tx: 0, ty: 0 }; const handle = zoomMod.attachZoomPan(svg, fakeSvg(), W, H, undefined, (s) => (state = s)); /** 화면 (x, y) 아래 도형 층 좌표 — 마우스 중심 판정. */ const under = (x, y) => [round((x - state.tx) / state.scale), round((y - state.ty) / state.scale)]; const out = { touchAction: svg.styles["touch-action"] }; const at = [400, 120]; out.plain = { prevented: svg.fire("wheel", { clientX: 400, clientY: 120, deltaY: -100 }) }; out.plain.scale = state.scale; const before = under(...at); out.ctrl = { prevented: svg.fire("wheel", { clientX: 400, clientY: 120, deltaY: -100, ctrlKey: true }), }; out.ctrl.scale = round(state.scale); out.ctrl.anchorKept = JSON.stringify(under(...at)) === JSON.stringify(before); svg.fire("wheel", { clientX: 400, clientY: 120, deltaY: 100, ctrlKey: true }); out.ctrlOut = round(state.scale); svg.fire("wheel", { clientX: 200, clientY: 150, deltaY: -8, ctrlKey: true }); // 트랙패드 핀치 벌림 out.pinch = round(state.scale); handle.zoom(1 / 0.85); // [+] 단추 — 같은 상태에 이어 붙음 out.button = round(state.scale); for (let i = 0; i < 40; i++) svg.fire("wheel", { clientX: 1, clientY: 1, deltaY: -100, ctrlKey: true }); out.max = round(state.scale); handle.reset(); // [⤢] out.fit = state; // 휠 = 줌 화면(B06 상세 · B05 판 · B08)은 그대로 — 맨 휠 줌 const plainSvg = fakeSvg(); let plainState = null; STUBS["../A00_Common/b_svg_zoom_pan"].attachSvgZoomPan({ svg: plainSvg, layer: fakeSvg(), widthPx: W, heightPx: H, pad: CROSS_PAD, wheelZoom: true, onChange: (s) => (plainState = s), }); plainSvg.fire("wheel", { clientX: 300, clientY: 150, deltaY: -100 }); out.wheelTrue = { scale: round(plainState.scale), touchAction: plainSvg.styles["touch-action"] }; console.log(JSON.stringify(out));