import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; /* ═══════════════════════════════════════════════════════════════════ * Image Compare (RMBG) — Frontend * ─────────────────────────────────────────────────────────────────── * ComfyUI v2 compatible — follows official DOM widget patterns: * • V1 (LiteGraph): onDrawForeground + LiteGraph mouse hooks * • V2 (Vue Nodes): addDOMWidget with + ResizeObserver * * Key v2 rules: * 1. Never call setDirtyCanvas in V2 mode * 2. Use requestAnimationFrame to throttle DOM renders * 3. Only render on actual state changes, not every pointermove * 4. Clean up event listeners on node removal * ═══════════════════════════════════════════════════════════════════ */ // ── Mode string → internal numeric ID ──────────────────────────────── const MODE_MAP = { left_right: 0, up_down: 1, overlay: 2, difference: 3, side_by_side: 4, highlight_diff: 5, }; const STATE_KEY = "compareState"; const CANVAS_BACKING_CAP = 6000; const MIN_W = 320; const MIN_H = 280; const INIT_W = 440; const INIT_H = 360; // ── V2 detection ───────────────────────────────────────────────────── function isVueNodes() { return !!window.LiteGraph?.vueNodesMode; } // ── API helpers ────────────────────────────────────────────────────── function pixApiUrl(route) { try { if (typeof api?.apiURL === "function") return api.apiURL(route); } catch (_) { } return route; } function buildCmpUrl(d) { return pixApiUrl( `/view?filename=${encodeURIComponent(d.filename)}` + `&type=${encodeURIComponent(d.type)}` + `&subfolder=${encodeURIComponent(d.subfolder || "")}` + `&t=${Date.now()}` ); } function loadCmpImage(node, meta, idx) { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { if (idx === 0) node._cmpImg1 = img; else node._cmpImg2 = img; node.imgs = null; // Invalidate diff cache when images change node._cmpDiffCacheKey = null; node._cmpDiffCanvas = null; repaint(node); }; img.src = buildCmpUrl(meta); } // ── State persistence (debounced) ──────────────────────────────────── let _saveTimer = null; function saveCompareState(node) { if (_saveTimer) clearTimeout(_saveTimer); _saveTimer = setTimeout(() => { node.properties = node.properties || {}; const prev = node.properties[STATE_KEY] || {}; node.properties[STATE_KEY] = { opacity: node._cmpOpacity ?? 0.5, splitX: node._cmpSplitX ?? 0.5, splitY: node._cmpSplitY ?? 0.5, sideRatio: node._cmpSideRatio ?? 0.5, diffThreshold: node._cmpDiffThreshold ?? 38, images: prev.images || [], }; _saveTimer = null; }, 300); } function saveCompareImagesToProps(node, d1, d2) { node.properties = node.properties || {}; const clean = (d) => d ? { filename: d.filename, subfolder: d.subfolder || "", type: d.type || "temp" } : null; node.properties[STATE_KEY] = { ...node.properties[STATE_KEY], images: [clean(d1), clean(d2)], }; } function restoreCompareFromProperties(node) { if (node._cmpImg1 || node._cmpImg2) return; const s = node.properties?.[STATE_KEY]; if (!s) return; if (typeof s.opacity === "number") node._cmpOpacity = s.opacity; if (typeof s.splitX === "number") node._cmpSplitX = s.splitX; if (typeof s.splitY === "number") node._cmpSplitY = s.splitY; if (typeof s.sideRatio === "number") node._cmpSideRatio = s.sideRatio; if (typeof s.diffThreshold === "number") node._cmpDiffThreshold = s.diffThreshold; if (Array.isArray(s.images)) { if (s.images[0]) loadCmpImage(node, s.images[0], 0); if (s.images[1]) loadCmpImage(node, s.images[1], 1); } repaint(node); } // ── Repaint dispatch ────────────────────────────────────────────────── function repaint(node) { if (!node) return; if (isVueNodes()) { // V2: schedule a render via rAF — never touch setDirtyCanvas if (node._cmpRafPending) return; node._cmpRafPending = true; requestAnimationFrame(() => { node._cmpRafPending = false; node._cmpDomRender?.(); }); } else { node.setDirtyCanvas?.(true, false); } } // ── Image-fit helper ────────────────────────────────────────────────── function fitImage(img, areaX, areaY, areaW, areaH) { if (!img || !img.naturalWidth) { return { x: areaX, y: areaY, w: areaW, h: areaH }; } const ar = img.naturalWidth / img.naturalHeight; const fw = areaW; const fh = fw / ar; if (fh <= areaH) { return { x: areaX, y: areaY + (areaH - fh) / 2, w: areaW, h: fh }; } const nh = areaH; const nw = nh * ar; return { x: areaX + (areaW - nw) / 2, y: areaY, w: nw, h: nh }; } // ── Status bar helper ────────────────────────────────────────────────── function drawStatusBar(ctx, rect, fill, color = "rgba(255,200,0,0.95)") { if (!rect || rect.w <= 0) return; const barH = 4; const barY = rect.y; ctx.fillStyle = "rgba(0,0,0,0.45)"; ctx.fillRect(rect.x, barY, rect.w, barH); ctx.fillStyle = color; ctx.fillRect(rect.x, barY, Math.max(0, Math.min(1, fill)) * rect.w, barH); } // ════════════════════════════════════════════════════════════════════ // Paint core — draws the interactive image area // ════════════════════════════════════════════════════════════════════ function paintCompare(ctx, node, W, H, imgTop) { const imgH = H - imgTop; if (imgH <= 0) return; // ── Empty state ───────────────────────────────────────────── if (!node._cmpImg1 && !node._cmpImg2) { ctx.save(); ctx.fillStyle = "#555"; ctx.font = "12px 'Segoe UI',sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("Connect images & run to compare", W / 2, imgTop + imgH / 2); ctx.restore(); return; } const m = node._cmpMode; const img1 = node._cmpImg1; const img2 = node._cmpImg2; ctx.save(); ctx.beginPath(); ctx.rect(0, imgTop, W, imgH); ctx.clip(); // ── difference (static) ──────────────────────────────────── if (m === 3) { const r1 = fitImage(img1 || img2, 0, imgTop, W, imgH); if (img1) ctx.drawImage(img1, r1.x, r1.y, r1.w, r1.h); if (img2) { const r2 = fitImage(img2, 0, imgTop, W, imgH); ctx.globalCompositeOperation = "difference"; ctx.drawImage(img2, r2.x, r2.y, r2.w, r2.h); ctx.globalCompositeOperation = "source-over"; } ctx.restore(); return; } // ── overlay (opacity) ───────────────────────────────────── if (m === 2) { const r1 = fitImage(img1 || img2, 0, imgTop, W, imgH); if (img1) ctx.drawImage(img1, r1.x, r1.y, r1.w, r1.h); if (img2) { const r2 = fitImage(img2, 0, imgTop, W, imgH); ctx.globalAlpha = node._cmpOpacity; ctx.drawImage(img2, r2.x, r2.y, r2.w, r2.h); ctx.globalAlpha = 1; } if (img1 || img2) drawStatusBar(ctx, r1, node._cmpOpacity); ctx.restore(); return; } // ── side_by_side (draggable divider) ────────────────────── if (m === 4) { const ratio = node._cmpSideRatio; const halfW = W * ratio; if (img1) { const r1 = fitImage(img1, 0, imgTop, halfW, imgH); ctx.save(); ctx.beginPath(); ctx.rect(0, imgTop, halfW, imgH); ctx.clip(); ctx.drawImage(img1, r1.x, r1.y, r1.w, r1.h); ctx.restore(); } if (img2) { const r2 = fitImage(img2, halfW, imgTop, W - halfW, imgH); ctx.save(); ctx.beginPath(); ctx.rect(halfW, imgTop, W - halfW, imgH); ctx.clip(); ctx.drawImage(img2, r2.x, r2.y, r2.w, r2.h); ctx.restore(); } if (ratio > 0.01 && ratio < 0.99) { ctx.strokeStyle = "rgba(255,255,255,0.7)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(halfW, imgTop); ctx.lineTo(halfW, imgTop + imgH); ctx.stroke(); } ctx.restore(); return; } // ── left_right (left/right wipe) ──────────────────── if (m === 0) { const sx = W * node._cmpSplitX; if (img2) { const r2 = fitImage(img2, 0, imgTop, W, imgH); ctx.save(); ctx.beginPath(); ctx.rect(sx, imgTop, W - sx, imgH); ctx.clip(); ctx.drawImage(img2, r2.x, r2.y, r2.w, r2.h); ctx.restore(); } if (img1) { const r1 = fitImage(img1, 0, imgTop, W, imgH); ctx.save(); ctx.beginPath(); ctx.rect(0, imgTop, sx, imgH); ctx.clip(); ctx.drawImage(img1, r1.x, r1.y, r1.w, r1.h); ctx.restore(); } if (node._cmpSplitX > 0.01 && node._cmpSplitX < 0.99) { ctx.strokeStyle = "rgba(255,255,255,0.7)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(sx, imgTop); ctx.lineTo(sx, imgTop + imgH); ctx.stroke(); } ctx.restore(); return; } // ── up_down (up/down wipe) ────────────────────── if (m === 1) { const sy = imgTop + imgH * node._cmpSplitY; if (img2) { const r2 = fitImage(img2, 0, imgTop, W, imgH); ctx.save(); ctx.beginPath(); ctx.rect(0, sy, W, imgTop + imgH - sy); ctx.clip(); ctx.drawImage(img2, r2.x, r2.y, r2.w, r2.h); ctx.restore(); } if (img1) { const r1 = fitImage(img1, 0, imgTop, W, imgH); ctx.save(); ctx.beginPath(); ctx.rect(0, imgTop, W, sy - imgTop); ctx.clip(); ctx.drawImage(img1, r1.x, r1.y, r1.w, r1.h); ctx.restore(); } if (node._cmpSplitY > 0.01 && node._cmpSplitY < 0.99) { ctx.strokeStyle = "rgba(255,255,255,0.7)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(0, sy); ctx.lineTo(W, sy); ctx.stroke(); } ctx.restore(); return; } // ── highlight_diff (grey base, red on difference) ───────── if (m === 5) { const r1 = fitImage(img1 || img2, 0, imgTop, W, imgH); if (img1) { ctx.drawImage(img1, r1.x, r1.y, r1.w, r1.h); ctx.save(); ctx.globalCompositeOperation = "saturation"; ctx.fillStyle = "hsl(0,0%,50%)"; ctx.fillRect(r1.x, r1.y, r1.w, r1.h); ctx.restore(); } if (img2 && img1) { const threshold = node._cmpDiffThreshold ?? 38; const cw = Math.min(Math.round(r1.w), 512); const ch = Math.min(Math.round(r1.h), 512); const cacheKey = img1.src + "|" + img2.src + "|" + threshold + "|" + cw + "x" + ch; if (node._cmpDiffCacheKey !== cacheKey) { node._cmpDiffCacheKey = cacheKey; const tmpC = document.createElement("canvas"); tmpC.width = cw; tmpC.height = ch; const tctx = tmpC.getContext("2d"); tctx.drawImage(img1, 0, 0, cw, ch); const d1 = tctx.getImageData(0, 0, cw, ch); tctx.clearRect(0, 0, cw, ch); tctx.drawImage(img2, 0, 0, cw, ch); const d2 = tctx.getImageData(0, 0, cw, ch); const out = tctx.createImageData(cw, ch); for (let i = 0; i < d1.data.length; i += 4) { const dr = Math.abs(d1.data[i] - d2.data[i]); const dg = Math.abs(d1.data[i + 1] - d2.data[i + 1]); const db = Math.abs(d1.data[i + 2] - d2.data[i + 2]); const avg = (dr + dg + db) / 3; if (avg > threshold) { out.data[i] = 255; out.data[i + 1] = 0; out.data[i + 2] = 0; out.data[i + 3] = 255; } else { out.data[i + 3] = 0; } } tctx.putImageData(out, 0, 0); node._cmpDiffCanvas = tmpC; } if (node._cmpDiffCanvas) { ctx.drawImage(node._cmpDiffCanvas, r1.x, r1.y, r1.w, r1.h); } } ctx.restore(); return; } ctx.restore(); } // ════════════════════════════════════════════════════════════════════ // Mouse event handlers (shared logic) // ════════════════════════════════════════════════════════════════════ function cmpMove(node, lx, ly, W, H, imgTop) { const m = node._cmpMode; const imgH = H - imgTop; const img1 = node._cmpImg1; const img2 = node._cmpImg2; const ir = fitImage(img1 || img2, 0, imgTop, W, imgH); const inImg = img1 || img2 ? lx >= ir.x && lx <= ir.x + ir.w && ly >= ir.y && ly <= ir.y + ir.h : false; if (m === 4 && node._cmpDragging && inImg) { node._cmpSideRatio = Math.max(0.02, Math.min(0.98, lx / W)); return true; } if (m === 2 && node._cmpDragging && inImg) { node._cmpOpacity = Math.max(0, Math.min(1, (lx - ir.x) / ir.w)); return true; } if (m === 0 && inImg) { node._cmpSplitX = Math.max(0, Math.min(1, lx / W)); return true; } if (m === 1 && inImg) { node._cmpSplitY = Math.max(0, Math.min(1, (ly - imgTop) / imgH)); return true; } return false; } function cmpDown(node, lx, ly, W, H, imgTop) { const m = node._cmpMode; const imgH = H - imgTop; const img1 = node._cmpImg1; const img2 = node._cmpImg2; const ir = fitImage(img1 || img2, 0, imgTop, W, imgH); const inImg = img1 || img2 ? lx >= ir.x && lx <= ir.x + ir.w && ly >= ir.y && ly <= ir.y + ir.h : false; if (m === 4 && inImg) { const halfW = W * node._cmpSideRatio; if (Math.abs(lx - halfW) < 12) { node._cmpDragging = "divider"; node._cmpSideRatio = Math.max(0.02, Math.min(0.98, lx / W)); return true; } } if ((m === 0 || m === 1) && inImg) { node._cmpDragging = "split"; return cmpMove(node, lx, ly, W, H, imgTop); } if (m === 2 && inImg) { node._cmpDragging = "opacity"; node._cmpOpacity = Math.max(0, Math.min(1, (lx - ir.x) / ir.w)); return true; } return false; } function cmpUp(node) { if (node._cmpDragging) saveCompareState(node); node._cmpDragging = false; } function cmpWheel(node, ly, deltaY, imgTop) { const m = node._cmpMode; if (m === 2 && ly >= imgTop) { node._cmpOpacity = Math.max(0, Math.min(1, node._cmpOpacity + (deltaY > 0 ? -0.05 : 0.05))); saveCompareState(node); return true; } if (m === 5 && ly >= imgTop) { node._cmpDiffThreshold = Math.max(5, Math.min(120, (node._cmpDiffThreshold ?? 38) + (deltaY > 0 ? -3 : 3))); saveCompareState(node); return true; } return false; } function cmpLeave(node) { if (node._cmpDragging) saveCompareState(node); node._cmpDragging = false; if (node._cmpMode === 0) node._cmpSplitX = 0.5; else if (node._cmpMode === 1) node._cmpSplitY = 0.5; } // ════════════════════════════════════════════════════════════════════ // V2 (Vue Nodes 2.0) DOM Widget // ─ Canvas inside a DOM container; events are local. // ─ Uses rAF-throttled render, only triggered by real state changes. // ─ NEVER calls setDirtyCanvas. // ════════════════════════════════════════════════════════════════════ function canvasBackingScale(cssW, cssH) { const dpr = window.devicePixelRatio || 1; const zoom = Math.max(1, app.canvas?.ds?.scale || 1); let s = dpr * zoom; const longCss = Math.max(cssW || 0, cssH || 0); if (longCss > 0 && longCss * s > CANVAS_BACKING_CAP) { s = CANVAS_BACKING_CAP / longCss; } return s; } function createCompareDOMWidget(node) { const root = document.createElement("div"); root.className = "ailab-cmp-root"; root.style.cssText = "position:relative;width:100%;flex:1 1 0;min-height:0;box-sizing:border-box;cursor:default;"; root._cmpNode = node; const canvas = document.createElement("canvas"); canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;"; root.appendChild(canvas); const widget = node.addDOMWidget("compare_viewer", "compare_viewer", root, { serialize: false, hideOnZoom: false, getMinHeight: () => MIN_H, }); widget.computeLayoutSize = () => ({ minHeight: MIN_H, minWidth: 1 }); // In V1, make widget canvas-only so LiteGraph doesn't render as HTML try { Object.defineProperty(widget.options, "canvasOnly", { configurable: true, enumerable: true, get() { return !window.LiteGraph?.vueNodesMode; }, }); } catch (_) { widget.options.canvasOnly = !window.LiteGraph?.vueNodesMode; } // ── rAF-throttled render — the ONLY render path for V2 ──── // Scheduled via repaint(); never called directly from events. const render = () => { const cssW = root.clientWidth; const cssH = root.clientHeight; if (cssW <= 0 || cssH <= 0) return; const s = canvasBackingScale(cssW, cssH); const bw = Math.round(cssW * s); const bh = Math.round(cssH * s); if (canvas.width !== bw) canvas.width = bw; if (canvas.height !== bh) canvas.height = bh; const ctx = canvas.getContext("2d"); ctx.setTransform(s, 0, 0, s, 0, 0); ctx.clearRect(0, 0, cssW, cssH); node._cmpDomW = cssW; node._cmpDomH = cssH; paintCompare(ctx, node, cssW, cssH, 0); }; node._cmpDomRender = render; // ── Local coordinate mapping ────────────────────────────── const localPos = (e) => { const r = root.getBoundingClientRect(); const sx = r.width ? root.clientWidth / r.width : 1; const sy = r.height ? root.clientHeight / r.height : 1; return [(e.clientX - r.left) * sx, (e.clientY - r.top) * sy]; }; const W = () => node._cmpDomW || root.clientWidth; const H = () => node._cmpDomH || root.clientHeight; // ── Pointer events — only repaint when state actually changes ── root.addEventListener("pointerdown", (e) => { const [lx, ly] = localPos(e); if (cmpDown(node, lx, ly, W(), H(), 0)) { e.stopPropagation(); if (node._cmpDragging) { try { root.setPointerCapture(e.pointerId); } catch (_) { } } repaint(node); } }); root.addEventListener("pointermove", (e) => { const [lx, ly] = localPos(e); const changed = cmpMove(node, lx, ly, W(), H(), 0); if (changed) { e.stopPropagation(); repaint(node); } // Update cursor — cheap, no canvas repaint needed const m = node._cmpMode; const ir = fitImage(node._cmpImg1 || node._cmpImg2, 0, 0, W(), H()); const inImg = (node._cmpImg1 || node._cmpImg2) && lx >= ir.x && lx <= ir.x + ir.w && ly >= ir.y && ly <= ir.y + ir.h; const prevCursor = root.style.cursor; let nextCursor = "default"; if (inImg && (m === 0 || m === 2 || m === 4)) nextCursor = "ew-resize"; else if (inImg && m === 1) nextCursor = "ns-resize"; if (prevCursor !== nextCursor) root.style.cursor = nextCursor; }); root.addEventListener("pointerup", (e) => { cmpUp(node); try { root.releasePointerCapture(e.pointerId); } catch (_) { } repaint(node); }); root.addEventListener("pointerleave", () => { cmpLeave(node); repaint(node); }); // ── Wheel: only intercept in overlay/highlight_diff modes ─── // In other modes, let the event bubble so graph zoom works. root.addEventListener("wheel", (e) => { const [, ly] = localPos(e); if (cmpWheel(node, ly, e.deltaY, 0)) { e.preventDefault(); e.stopPropagation(); repaint(node); } }, { passive: false }); // ── ResizeObserver: repaint when container size changes ───── const ro = new ResizeObserver(() => repaint(node)); ro.observe(root); node._cmpDomRO = ro; // Initial render repaint(node); return widget; } // ════════════════════════════════════════════════════════════════════ // V1 (LiteGraph) helpers // ════════════════════════════════════════════════════════════════════ function getV1WidgetBottomY(node) { const widgets = node.widgets || []; if (widgets.length > 0) { const last = widgets[widgets.length - 1]; if (last && typeof last.last_y === "number") { return Math.ceil(last.last_y + (last.computedHeight || 20) + 6); } } const titleH = 30; const inputH = (node.inputs?.length || 0) * 26; const widgetH = widgets.length * 26; return Math.max(titleH + inputH + widgetH + 4, 60); } function getV1ImgTop(node) { return node._cmpImgTop ?? 60; } // ════════════════════════════════════════════════════════════════════ // Register Extension // ════════════════════════════════════════════════════════════════════ app.registerExtension({ name: "AILab.ImageCompareView", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "AILab_ImageCompareView") return; // ── onNodeCreated ────────────────────────────────────────── const _origCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { _origCreated?.apply(this, arguments); this._cmpMode = 0; this._cmpSplitX = 0.5; this._cmpSplitY = 0.5; this._cmpOpacity = 0.5; this._cmpSideRatio = 0.5; this._cmpDiffThreshold = 38; this._cmpDragging = false; this._cmpImg1 = null; this._cmpImg2 = null; this._cmpImgTop = 54; this.hideOutputImages = true; this.size = [INIT_W, INIT_H]; // V2: create DOM widget if (isVueNodes()) { createCompareDOMWidget(this); } // Hook the native "mode" widget const syncMode = () => { const w = this.widgets?.find((w) => w.name === "mode"); if (!w) return; const val = w.value || w.options?.default || "left_right"; this._cmpMode = MODE_MAP[val] ?? 0; const origCb = w.callback; w.callback = (...args) => { const v = args[0] || w.value; this._cmpMode = MODE_MAP[v] ?? 0; repaint(this); if (typeof origCb === "function") return origCb.apply(w, args); }; }; queueMicrotask(syncMode); // Restore from saved state (only if not already restored by onConfigure) queueMicrotask(() => { if (!this._cmpRestored) { this._cmpRestored = true; restoreCompareFromProperties(this); } }); }; // ── onConfigure (workflow load) ──────────────────────────── const _origConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function () { const r = _origConfigure ? _origConfigure.apply(this, arguments) : undefined; this._cmpRestored = true; restoreCompareFromProperties(this); return r; }; // ── onExecuted ──────────────────────────────────────────── nodeType.prototype.onExecuted = function (output) { const imgs = output?.images || []; const d1 = imgs.find((i) => i.slot === 1) || null; const d2 = imgs.find((i) => i.slot === 2) || null; saveCompareImagesToProps(this, d1, d2); if (d1) loadCmpImage(this, d1, 0); else this._cmpImg1 = null; if (d2) loadCmpImage(this, d2, 1); else this._cmpImg2 = null; repaint(this); }; // ── onDrawBackground: suppress default preview ──────────── nodeType.prototype.onDrawBackground = function () { if (this.flags?.collapsed) return; if (this.imgs) this.imgs = null; }; // ── onDrawForeground (V1 only) ───────────────────────────── const _origDraw = nodeType.prototype.onDrawForeground; nodeType.prototype.onDrawForeground = function (ctx) { if (_origDraw) _origDraw.call(this, ctx); if (this.flags?.collapsed) return; if (isVueNodes()) return; if (this.size[0] < MIN_W) this.size[0] = MIN_W; if (this.size[1] < MIN_H) this.size[1] = MIN_H; this._cmpImgTop = getV1WidgetBottomY(this); paintCompare(ctx, this, this.size[0], this.size[1], this._cmpImgTop); }; // ── V1 Mouse hooks ──────────────────────────────────────── const _origDown = nodeType.prototype.onMouseDown; nodeType.prototype.onMouseDown = function (e, pos) { if (isVueNodes()) return _origDown ? _origDown.call(this, e, pos) : undefined; const imgTop = getV1ImgTop(this); if (cmpDown(this, pos[0], pos[1], this.size[0], this.size[1], imgTop)) { this.setDirtyCanvas(true, false); return true; } if (_origDown) return _origDown.call(this, e, pos); }; const _origMove = nodeType.prototype.onMouseMove; nodeType.prototype.onMouseMove = function (e, pos) { if (isVueNodes()) return _origMove ? _origMove.call(this, e, pos) : undefined; const imgTop = getV1ImgTop(this); if (cmpMove(this, pos[0], pos[1], this.size[0], this.size[1], imgTop)) { this.setDirtyCanvas(true, false); } if (_origMove) return _origMove.call(this, e, pos); }; const _origUp = nodeType.prototype.onMouseUp; nodeType.prototype.onMouseUp = function (e, pos) { if (isVueNodes()) return _origUp ? _origUp.call(this, e, pos) : undefined; cmpUp(this); if (_origUp) return _origUp.call(this, e, pos); }; const _origWheel = nodeType.prototype.onMouseWheel; nodeType.prototype.onMouseWheel = function (e, pos) { if (isVueNodes()) return _origWheel ? _origWheel.call(this, e, pos) : undefined; const imgTop = getV1ImgTop(this); if (cmpWheel(this, pos[1], e.deltaY, imgTop)) { this.setDirtyCanvas(true, false); return true; } if (_origWheel) return _origWheel.call(this, e, pos); }; const _origLeave = nodeType.prototype.onMouseLeave; nodeType.prototype.onMouseLeave = function (e) { if (isVueNodes()) return _origLeave ? _origLeave.call(this, e) : undefined; cmpLeave(this); this.setDirtyCanvas(true, false); if (_origLeave) return _origLeave.call(this, e); }; // ── onResize ────────────────────────────────────────────── const _origResize = nodeType.prototype.onResize; nodeType.prototype.onResize = function (e) { if (_origResize) _origResize.call(this, e); if (isVueNodes()) return; this.size[0] = Math.max(this.size[0], MIN_W); this.size[1] = Math.max(this.size[1], MIN_H); }; // ── onRemoved: cleanup ──────────────────────────────────── const _origRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onRemoved = function () { try { this._cmpDomRO?.disconnect(); } catch (_) { } this._cmpDomRO = null; this._cmpDomRender = null; this._cmpRafPending = false; this._cmpDiffCanvas = null; this._cmpDiffCacheKey = null; return _origRemoved ? _origRemoved.apply(this, arguments) : undefined; }; }, });