공용 common_util_view_window(viewOf · frameOf) 신설. design_profile rebuild · 접기 · render, Heights 높이 맞춤, 표 · 유토곡선 크기 끌기, 유토곡선 세로창 미끄러짐, 끌기 뒤 그리기, ▲▼ · 방향키 길게 누르기, 배수유역도, 구조물 말풍선 — 메인 창 전역 틀 · 시계 대신 요소가 놓인 창 것. 새 시험 test_230_2_popout_frames(메인 창 틀 멈춤 흉내 · 새 창 패널 코드 훑기). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
163 lines
6.0 KiB
JavaScript
163 lines
6.0 KiB
JavaScript
/* 종단 새 창 + 최대화 휠(계획서 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));
|