/* 282 종단 도구줄 — 진짜 TS(도구 묶음 render · withToolLead · attachProfilePopout · 배수유역 판)를 * 가짜 DOM 위에서 돌려 JSON 으로 냄. * · [새 창] = 도구 묶음 맨 앞([초기화] 바로 왼쪽) · 다시 그려도 그 자리 * · 새 창 열림 · 닫힘 = 글 바뀜 · 제자리 돌림 * · 옛 값 drainage-visible=false 여도 판(손잡이) 보임 */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8"); const load = (code, scope) => { const js = ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const exports = {}; const names = Object.keys(scope); new Function("exports", ...names, js)(exports, ...names.map((n) => scope[n])); return exports; }; // ---- 가짜 DOM ---- function el(tag) { const node = { tagName: tag.toUpperCase(), className: "", textContent: "", title: "", hidden: false, disabled: false, attrs: {}, listeners: {}, children: [], parent: null, dataset: {}, style: {}, classList: { list: () => node.className.split(/\s+/).filter(Boolean), contains: (c) => node.classList.list().includes(c), add: (c) => !node.classList.contains(c) && (node.className = [...node.classList.list(), c].join(" ")), remove: (c) => (node.className = node.classList .list() .filter((x) => x !== c) .join(" ")), toggle: (c, on) => (on ?? !node.classList.contains(c)) ? node.classList.add(c) : node.classList.remove(c), }, detach() { if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node); node.parent = null; }, append(...kids) { kids.forEach((k) => (k.detach(), (k.parent = node), node.children.push(k))); }, prepend(...kids) { kids.reverse().forEach((k) => (k.detach(), (k.parent = node), node.children.unshift(k))); }, replaceChildren(...kids) { node.children.slice().forEach((c) => c.detach()); node.append(...kids); }, setAttribute: (k, v) => (node.attrs[k] = v), addEventListener: (type, fn) => (node.listeners[type] ??= []).push(fn), click: () => (node.listeners.click ?? []).forEach((fn) => fn({ stopPropagation() {} })), }; return node; } const fakeDoc = () => { const doc = { title: "" }; doc.createElement = el; doc.head = el("head"); doc.body = el("body"); doc.documentElement = Object.assign(el("html"), { attributes: [] }); doc.querySelectorAll = () => []; doc.importNode = (n) => n; return doc; }; const document = fakeDoc(); const frames = []; const window = { listeners: {}, addEventListener: (t, fn) => (window.listeners[t] ??= []).push(fn), removeEventListener: () => {}, open: () => popup, document, getComputedStyle: () => ({ backgroundColor: "" }), requestAnimationFrame: (fn) => frames.push(fn), }; const popupListeners = {}; const popup = { closed: false, document: fakeDoc(), close: () => (popup.closed = true), addEventListener: (t, fn) => (popupListeners[t] ??= []).push(fn), ResizeObserver: class { observe() {} }, requestAnimationFrame: () => 0, cancelAnimationFrame: () => {}, }; // ---- 진짜 도구 묶음 ---- const { createProfileTools } = load(read("B05_Profile", "B05_Profile_UI_Profile_Tools.ts"), { require: () => ({ sameRun: () => false }), document, }); const tools = createProfileTools({ onStraighten() {}, onShift() {}, onTilt() {}, onResetToSaved() {}, canResetToSaved: () => true, onUndo() {}, onRedo() {}, canUndo: () => false, canRedo: () => false, hasStraightRuns: () => false, onChanged() {}, onModeChange() {}, freeHint: () => "", mountOverlay() {}, }); // ---- 진짜 withToolLead(design_profile.ts) ---- const profileSrc = read("A00_Common", "design", "design_profile.ts"); const leadAt = profileSrc.indexOf("const withToolLead"); const leadSrc = profileSrc.slice(leadAt, profileSrc.indexOf("};", leadAt) + 2); const button = el("button"); const { withToolLead } = load(`${leadSrc}\nexports.withToolLead = withToolLead;`, { ctx: { toolLead: button }, }); // ---- 진짜 attachProfilePopout ---- const { attachProfilePopout } = load(read("A00_Common", "design", "design_profile_popout.ts"), { require: () => ({ showToast() {} }), document, window, getComputedStyle: () => ({ backgroundColor: "" }), requestAnimationFrame: (fn) => frames.push(fn), }); const slot = el("div"); const root = el("div"); slot.append(root); const balanceBar = el("div"); root.append(balanceBar); const profile = { root, redraw() {}, dispose() {} }; attachProfilePopout(profile, slot, button); const render = () => { const wrap = withToolLead(tools.render()); balanceBar.replaceChildren(wrap); return wrap.children.map((c) => c.textContent); }; const out = { first: render(), buttonClass: button.className }; out.again = render(); button.click(); out.opened = { text: button.textContent, inPopup: popup.document.body.children[0] === root }; out.openedRender = render(); button.click(); out.closed = { text: button.textContent, inSlot: root.parent === slot, popupClosed: popup.closed }; out.closedRender = render(); // ---- 옛 값 drainage-visible=false — 판 · 손잡이 보임 ---- const store = { "drainage-visible": "false", "drainage-collapsed": "false" }; const sideSrc = read("B05_Profile", "B05_Profile_UI_Drainage_Side.ts"); const panelRoot = Object.assign(el("aside"), { className: "b05-drainage" }); const handle = el("button"); panelRoot.append(handle); const fakePanel = { root: panelRoot, setView() {}, dispose() {} }; const side = load(sideSrc, { require: (id) => (id.endsWith(".css") ? {} : { createDrainagePanel: () => fakePanel }), }); const sideToolSrc = read("B05_Profile", "B05_Profile_UI_Profile_SideTool.ts"); const { drainageSide } = load(sideToolSrc, { require: (id) => id.includes("b_page_state") ? { readStateRaw: (k) => store[k] ?? null, writeStateRaw: (k, v) => (store[k] = v) } : side, }); const board = drainageSide({ projectId: "p", view: "profile" }); out.side = { rootHidden: board.root.hidden, handleHidden: handle.hidden, hasSetVisible: "setVisible" in board, }; process.stdout.write(JSON.stringify(out));