/* 종단 새 창 + 최대화 휠(계획서 230-1) 단위검증 헬퍼 — 휠 이동(`B05_Profile_UI_Profile_Wheel.ts`) · * 스크롤 뒤 다시 그리기 시계(`design_profile_viewport.ts`)를 그 자리에서 트랜스파일해 돌리고 JSON 으로 냄. * 메인 창은 「가려짐」 — 화면 틀 · 시계가 돌지 않음(새 창 최대화가 메인 창을 덮은 꼴). */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); function load(file, mods = {}) { const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, (p) => mods[p] ?? require(p)); return box.exports; } // ---- 가짜 창 — frozen 이면 화면 틀 · 시계가 안 돎 ---- function fakeWindow(name, frozen) { const win = { name, frozen, now: 0, timers: [], seq: 0, frames: 0 }; win.setTimeout = (fn, ms) => (win.timers.push({ id: ++win.seq, at: win.now + ms, fn }), win.seq); win.clearTimeout = (id) => (win.timers = win.timers.filter((t) => t.id !== id)); win.requestAnimationFrame = (fn) => win.setTimeout(() => (win.frames++, fn(win.now)), 16); win.cancelAnimationFrame = win.clearTimeout; win.getSelection = () => ({ removeAllRanges: () => (win.cleared = true) }); win.tick = (ms) => { if (win.frozen) return; const end = win.now + ms; for (;;) { const next = win.timers.filter((t) => t.at <= end).sort((a, b) => a.at - b.at)[0]; if (!next) break; win.now = next.at; win.clearTimeout(next.id); next.fn(); } win.now = end; }; return win; } function fakeScroller(view, { width, scrollWidth }) { const calls = {}; let left = 0; const el = { ownerDocument: { defaultView: view }, clientWidth: width, clientHeight: 400, scrollWidth, scrollTop: 0, style: {}, addEventListener: (type, fn) => (calls[type] ??= []).push(fn), removeEventListener: () => {}, hasPointerCapture: () => false, setPointerCapture: () => {}, releasePointerCapture: () => {}, fire: (type, ev = {}) => { const e = { prevented: false, preventDefault: () => (e.prevented = true), ...ev }; (calls[type] ?? []).forEach((fn) => fn(e)); return e; }, }; Object.defineProperty(el, "scrollLeft", { get: () => left, set: (v) => (left = Math.min(Math.max(0, v), el.scrollWidth - el.clientWidth)), }); return el; } const main = fakeWindow("main", true); globalThis.window = main; globalThis.requestAnimationFrame = main.requestAnimationFrame; // 옛 코드가 쓰던 전역 = 메인 창 globalThis.cancelAnimationFrame = main.cancelAnimationFrame; const roCallbacks = []; globalThis.ResizeObserver = class { constructor(fn) { roCallbacks.push(fn); } observe() {} disconnect() {} }; const viewWindow = load(path.join(ROOT, "common_util", "common_util_view_window.ts")); const wheelMod = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"), { "../common_util/common_util_view_window": viewWindow, }); const viewport = load(path.join(ROOT, "A00_Common", "design", "design_profile_viewport.ts"), { "../../B05_Profile/B05_Profile_UI_Profile_Wheel": wheelMod, "@util/common_util_view_window": viewWindow, "@util/common_util_chart_ywindow": { needsFullRedraw: () => true }, }); function wheelDown(scroller, view, notches) { const prevented = []; for (let i = 0; i < notches; i++) prevented.push(scroller.fire("wheel", { deltaY: 120 }).prevented); view.tick(1000); return { left: Math.round(scroller.scrollLeft), prevented }; } const out = {}; // ① 새 창 보통 크기(1400) — 메인 창은 가려짐 const popup = fakeWindow("popup", false); const s1 = fakeScroller(popup, { width: 1400, scrollWidth: 6000 }); wheelMod.attachWheelHorizontalScroll(s1); out.popupNormal = { ...wheelDown(s1, popup, 3), popupFrames: popup.frames, mainTimers: main.timers.length, }; // ② 새 창 최대화(1400 → 1920) 뒤 휠 — 크기 바뀜 듣개가 돈 뒤에도 이동 const popup2 = fakeWindow("popup2", false); const s2 = fakeScroller(popup2, { width: 1400, scrollWidth: 6000 }); let draws = 0; let lightSyncs = 0; const last = { width: 1400, height: 400 }; const unbind = viewport.bindProfileViewport({ body: s2, editing: () => false, fitElevation: () => ({}), draw: () => draws++, lastSize: () => last, scheduleLightSync: () => lightSyncs++, scheduleDraw: () => {}, }); s2.clientWidth = 1920; s2.clientHeight = 1000; roCallbacks.at(-1)(); const before = main.timers.length; const moved = wheelDown(s2, popup2, 2); s2.fire("scroll"); popup2.tick(200); out.popupMax = { ...moved, limit: s2.scrollWidth - s2.clientWidth, lightSyncs, draws, mainTimersAdded: main.timers.length - before, }; unbind(); // ③ 끝까지 굴림 — 한도에서 멈추고 더는 막지 않음(기본 스크롤로 넘김) const s3 = fakeScroller(popup, { width: 1920, scrollWidth: 2200 }); wheelMod.attachWheelHorizontalScroll(s3); const atEnd = wheelDown(s3, popup, 5); out.popupEnd = { ...atEnd, after: s3.fire("wheel", { deltaY: 120 }).prevented }; // ④ 원래 창(가려지지 않음) — 자기 창 화면 틀로 그대로 이동 main.frozen = false; const s4 = fakeScroller(main, { width: 1400, scrollWidth: 6000 }); wheelMod.attachWheelHorizontalScroll(s4); out.mainWindow = wheelDown(s4, main, 3); // ⑤ 끌기 — 글자 선택은 스크롤러가 놓인 창에서 풂 const s5 = fakeScroller(popup, { width: 1400, scrollWidth: 6000 }); wheelMod.attachWheelHorizontalScroll(s5); s5.fire("pointerdown", { button: 0, pointerId: 1, clientX: 500, clientY: 100, target: null }); s5.fire("pointermove", { pointerId: 1, clientX: 400, clientY: 100 }); out.drag = { left: s5.scrollLeft, popupCleared: !!popup.cleared, mainCleared: !!main.cleared }; process.stdout.write(JSON.stringify(out));