- 복제 자식을 g 하나로 감싸 공용 b_svg_zoom_pan(wheelZoom · 커서 고정 · 상한 16 · 1배 팬) · [+ − ⤢] · 칸 비율 = viewBox - 강조 띠 · 판정 선 · 십자 · 사토장 모두 --color-danger · 고른 칸 붉은 테두리 · 줄 표시 · hover 옅게 - 그림에 대상 없는 칸 data-empty 회색 · 누름 없음(presentKeys) · 이름 칸도 그 줄 첫 키로 눌림 - 시험 test_164_detail_zoom 새로 · test_150_detail_pick presentKeys 사례 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
118 lines
4.8 KiB
JavaScript
118 lines
4.8 KiB
JavaScript
/* PLAN 164-1 단위검증 헬퍼 — [상세] 그림 줌 · 팬(`B06_Section_UI_Cross_Detail.ts` `attachDetailZoom`).
|
|
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 가짜 svg 에 휠 · 가운데 단추 · 더블클릭을 쏨.
|
|
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_164_detail_zoom.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 B06 = path.join(ROOT, "B06_Section");
|
|
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
|
|
// 카드 여백은 원본에서 읽음(화면과 같은 값)
|
|
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
|
|
const STUBS = {
|
|
"@ui/ui_template_modal": { openModal: () => {} },
|
|
"../A00_Common/b_page_state": {},
|
|
"./B06_Section_Api_Fetch": {},
|
|
"./B06_Section_UI_Section_Common": { CROSS_PAD, L: (key) => key },
|
|
"./B06_Section_UI_Cross_Detail_Pick": { DETAIL_TARGETS: {} },
|
|
};
|
|
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
|
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) => {
|
|
if (request.endsWith(".css")) return {};
|
|
if (STUBS[request]) return STUBS[request];
|
|
return loadTs(resolve(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
function el(tag) {
|
|
const attrs = {};
|
|
const listeners = {};
|
|
const self = {
|
|
tag,
|
|
parent: null,
|
|
childNodes: [],
|
|
attrs,
|
|
style: { setProperty: (name, value) => (attrs[`style:${name}`] = value) },
|
|
classList: { add: () => {}, remove: () => {} },
|
|
setAttribute: (name, value) => (attrs[name] = String(value)),
|
|
getAttribute: (name) => attrs[name] ?? null,
|
|
// 진짜 DOM 처럼 옛 부모에서 옮겨 옴
|
|
append: (...nodes) =>
|
|
nodes.forEach((node) => {
|
|
if (node.parent) node.parent.childNodes.splice(node.parent.childNodes.indexOf(node), 1);
|
|
node.parent = self;
|
|
self.childNodes.push(node);
|
|
}),
|
|
addEventListener: (type, fn) => (listeners[type] ??= []).push(fn),
|
|
fire: (type, event) => {
|
|
const full = { preventDefault() {}, stopPropagation() {}, pointerId: 1, ...event };
|
|
(listeners[type] ?? []).forEach((fn) => fn(full));
|
|
return full;
|
|
},
|
|
setPointerCapture: () => {},
|
|
releasePointerCapture: () => {},
|
|
// 칸 = viewBox 의 2 배(비율 같음)
|
|
getBoundingClientRect: () => ({ left: 0, top: 0, width: 1200, height: 600 }),
|
|
};
|
|
return self;
|
|
}
|
|
globalThis.document = { createElementNS: (_ns, tag) => el(tag), createElement: el };
|
|
|
|
const { attachDetailZoom } = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail.ts"));
|
|
|
|
const svg = el("svg");
|
|
svg.setAttribute("viewBox", "0 0 600 300");
|
|
svg.append(el("rect"), el("g"), el("g"));
|
|
let saved;
|
|
const handle = attachDetailZoom(svg, undefined, (next) => (saved = next));
|
|
const layer = svg.childNodes[0];
|
|
const out = {
|
|
wrapped: svg.childNodes.length === 1 && layer.tag === "g" && layer.childNodes.length === 3,
|
|
ratio: svg.attrs["style:--ratio"],
|
|
before: layer.getAttribute("transform"),
|
|
};
|
|
// 휠 당김(deltaY > 0) = 확대 · 커서(화면 600,300 = svg 300,150) 고정
|
|
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
|
|
out.wheelIn = { ...saved };
|
|
out.cursorFixed = saved.tx + saved.scale * 300;
|
|
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: -100 });
|
|
out.wheelOut = saved.scale;
|
|
// 원배율에서 좌클릭 끌기 = 팬 아님
|
|
handle.reset();
|
|
svg.fire("pointerdown", { button: 0, clientX: 100, clientY: 100 });
|
|
svg.fire("pointermove", { clientX: 300, clientY: 100 });
|
|
svg.fire("pointerup", {});
|
|
out.leftDrag = { ...saved };
|
|
// 가운데 단추 끌기 = 팬(1배에서도 · 화면 200px = svg 100)
|
|
svg.fire("pointerdown", { button: 1, clientX: 100, clientY: 100 });
|
|
svg.fire("pointermove", { clientX: 300, clientY: 140 });
|
|
svg.fire("pointerup", {});
|
|
out.middlePan = { ...saved };
|
|
out.panClickStopped = svg.fire("click", {
|
|
stopPropagation() {
|
|
this.stopped = true;
|
|
},
|
|
}).stopped;
|
|
// 다시 떠도 배율 이어 감
|
|
const again = el("svg");
|
|
again.setAttribute("viewBox", "0 0 600 300");
|
|
attachDetailZoom(again, saved, () => {});
|
|
out.restored = again.childNodes[0].getAttribute("transform");
|
|
// 더블클릭 원복
|
|
svg.fire("dblclick", {});
|
|
out.reset = layer.getAttribute("transform");
|
|
out.max = (() => {
|
|
for (let i = 0; i < 60; i++) svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
|
|
return saved.scale;
|
|
})();
|
|
process.stdout.write(JSON.stringify(out));
|