Add files via upload
This commit is contained in:
@@ -52,6 +52,7 @@ const NODE_COLORS = {
|
||||
"AILab_MaskToList": "utility",
|
||||
"AILab_ImageMaskToList": "utility",
|
||||
"AILab_ColorToMask": "utility",
|
||||
"AILab_Resolution_Selector": "utility",
|
||||
};
|
||||
|
||||
function setNodeColors(node, theme) {
|
||||
|
||||
+325
-21
@@ -22,13 +22,327 @@ const getContrastTextColor = (hexColor) => {
|
||||
return luminance > 0.5 ? '#333333' : '#cccccc';
|
||||
};
|
||||
|
||||
// ── Color Conversion Helpers ──────────────────────────────────────────
|
||||
|
||||
function hexToRGB(hex) {
|
||||
hex = hex.replace('#', '');
|
||||
return {
|
||||
r: parseInt(hex.substr(0, 2), 16),
|
||||
g: parseInt(hex.substr(2, 2), 16),
|
||||
b: parseInt(hex.substr(4, 2), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function rgbToHSV(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
const d = max - min;
|
||||
let h = 0, s = max === 0 ? 0 : d / max, v = max;
|
||||
if (d !== 0) {
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + 6) % 6; break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h *= 60;
|
||||
}
|
||||
return { h, s, v };
|
||||
}
|
||||
|
||||
function hsvToRGB(h, s, v) {
|
||||
h = ((h % 360) + 360) % 360;
|
||||
const c = v * s;
|
||||
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
||||
const m = v - c;
|
||||
let r, g, b;
|
||||
if (h < 60) { r = c; g = x; b = 0; }
|
||||
else if (h < 120) { r = x; g = c; b = 0; }
|
||||
else if (h < 180) { r = 0; g = c; b = x; }
|
||||
else if (h < 240) { r = 0; g = x; b = c; }
|
||||
else if (h < 300) { r = x; g = 0; b = c; }
|
||||
else { r = c; g = 0; b = x; }
|
||||
return {
|
||||
r: Math.round((r + m) * 255),
|
||||
g: Math.round((g + m) * 255),
|
||||
b: Math.round((b + m) * 255),
|
||||
};
|
||||
}
|
||||
|
||||
function hexToHSV(hex) {
|
||||
const { r, g, b } = hexToRGB(hex);
|
||||
return rgbToHSV(r, g, b);
|
||||
}
|
||||
|
||||
function hsvToHex(h, s, v) {
|
||||
const { r, g, b } = hsvToRGB(h, s, v);
|
||||
return rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// ── Custom Color Picker ───────────────────────────────────────────────
|
||||
|
||||
function openColorPicker(initialColor, clientX, clientY, onApply) {
|
||||
// Close any existing picker
|
||||
const existing = document.querySelector('.ailab-cp-backdrop');
|
||||
if (existing) existing.remove();
|
||||
const existingPanel = document.querySelector('.ailab-cp-panel');
|
||||
if (existingPanel) existingPanel.remove();
|
||||
|
||||
const SV_SIZE = 180;
|
||||
const HUE_H = 14;
|
||||
const PAD = 12;
|
||||
const GAP = 8;
|
||||
|
||||
let hsv = hexToHSV(initialColor);
|
||||
|
||||
// ── Create DOM elements ──
|
||||
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'ailab-cp-backdrop';
|
||||
Object.assign(backdrop.style, {
|
||||
position: 'fixed', top: '0', left: '0',
|
||||
width: '100%', height: '100%', zIndex: '99998',
|
||||
});
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'ailab-cp-panel';
|
||||
Object.assign(panel.style, {
|
||||
position: 'fixed', zIndex: '99999',
|
||||
background: '#1e1e1e', border: '1px solid #444',
|
||||
borderRadius: '8px', padding: `${PAD}px`,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
|
||||
fontFamily: 'sans-serif', fontSize: '12px', color: '#ccc',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
// SV (saturation-value) canvas
|
||||
const svCanvas = document.createElement('canvas');
|
||||
svCanvas.width = SV_SIZE;
|
||||
svCanvas.height = SV_SIZE;
|
||||
Object.assign(svCanvas.style, {
|
||||
display: 'block', borderRadius: '4px', cursor: 'crosshair',
|
||||
marginBottom: `${GAP}px`,
|
||||
});
|
||||
|
||||
// Hue bar canvas
|
||||
const hueCanvas = document.createElement('canvas');
|
||||
hueCanvas.width = SV_SIZE;
|
||||
hueCanvas.height = HUE_H;
|
||||
Object.assign(hueCanvas.style, {
|
||||
display: 'block', borderRadius: '8px', cursor: 'pointer',
|
||||
marginBottom: `${GAP}px`,
|
||||
});
|
||||
|
||||
// Bottom row: preview swatch + hex input + OK button
|
||||
const row = document.createElement('div');
|
||||
Object.assign(row.style, {
|
||||
display: 'flex', alignItems: 'center', gap: `${GAP}px`,
|
||||
});
|
||||
|
||||
const preview = document.createElement('div');
|
||||
Object.assign(preview.style, {
|
||||
width: '28px', height: '28px', borderRadius: '4px',
|
||||
border: '1px solid #555', flexShrink: '0',
|
||||
});
|
||||
|
||||
const hexInput = document.createElement('input');
|
||||
hexInput.type = 'text';
|
||||
Object.assign(hexInput.style, {
|
||||
flex: '1', background: '#333', border: '1px solid #555',
|
||||
borderRadius: '4px', color: '#eee', padding: '4px 8px',
|
||||
fontFamily: 'monospace', fontSize: '13px', outline: 'none',
|
||||
width: '80px', minWidth: '0',
|
||||
});
|
||||
|
||||
const okBtn = document.createElement('button');
|
||||
okBtn.textContent = 'OK';
|
||||
Object.assign(okBtn.style, {
|
||||
background: '#4a9eff', color: '#fff', border: 'none',
|
||||
borderRadius: '4px', padding: '5px 14px', cursor: 'pointer',
|
||||
fontSize: '12px', fontWeight: 'bold',
|
||||
});
|
||||
|
||||
row.append(preview, hexInput, okBtn);
|
||||
panel.append(svCanvas, hueCanvas, row);
|
||||
|
||||
// ── Position panel below click point, clamped to viewport ──
|
||||
|
||||
const panelW = SV_SIZE + PAD * 2;
|
||||
const panelH = SV_SIZE + HUE_H + 28 + GAP * 2 + PAD * 2 + 4;
|
||||
|
||||
let px = clientX - panelW / 2;
|
||||
let py = clientY + 10;
|
||||
|
||||
if (px + panelW > window.innerWidth - 10) px = window.innerWidth - panelW - 10;
|
||||
if (px < 10) px = 10;
|
||||
if (py + panelH > window.innerHeight - 10) py = clientY - panelH - 10;
|
||||
if (py < 10) py = 10;
|
||||
|
||||
panel.style.left = `${px}px`;
|
||||
panel.style.top = `${py}px`;
|
||||
|
||||
// ── Drawing functions ──
|
||||
|
||||
function drawSV() {
|
||||
const ctx = svCanvas.getContext('2d');
|
||||
const { r, g, b } = hsvToRGB(hsv.h, 1, 1);
|
||||
|
||||
// Base hue fill
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.fillRect(0, 0, SV_SIZE, SV_SIZE);
|
||||
|
||||
// White gradient (left → right = low → high saturation)
|
||||
const whiteGrad = ctx.createLinearGradient(0, 0, SV_SIZE, 0);
|
||||
whiteGrad.addColorStop(0, '#ffffff');
|
||||
whiteGrad.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.fillStyle = whiteGrad;
|
||||
ctx.fillRect(0, 0, SV_SIZE, SV_SIZE);
|
||||
|
||||
// Black gradient (top → bottom = high → low value)
|
||||
const blackGrad = ctx.createLinearGradient(0, 0, 0, SV_SIZE);
|
||||
blackGrad.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
blackGrad.addColorStop(1, '#000000');
|
||||
ctx.fillStyle = blackGrad;
|
||||
ctx.fillRect(0, 0, SV_SIZE, SV_SIZE);
|
||||
|
||||
// Crosshair cursor
|
||||
const cx = hsv.s * SV_SIZE;
|
||||
const cy = (1 - hsv.v) * SV_SIZE;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 6, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawHue() {
|
||||
const ctx = hueCanvas.getContext('2d');
|
||||
const grad = ctx.createLinearGradient(0, 0, SV_SIZE, 0);
|
||||
[0, 60, 120, 180, 240, 300, 360].forEach(deg => {
|
||||
const { r, g, b } = hsvToRGB(deg, 1, 1);
|
||||
grad.addColorStop(deg / 360, `rgb(${r},${g},${b})`);
|
||||
});
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, SV_SIZE, HUE_H);
|
||||
|
||||
// Indicator thumb
|
||||
const ix = (hsv.h / 360) * SV_SIZE;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(ix - 2, 0, 4, HUE_H);
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(ix - 2, 0, 4, HUE_H);
|
||||
}
|
||||
|
||||
function updateAll() {
|
||||
const hex = hsvToHex(hsv.h, hsv.s, hsv.v);
|
||||
preview.style.backgroundColor = hex;
|
||||
hexInput.value = hex;
|
||||
drawSV();
|
||||
drawHue();
|
||||
}
|
||||
|
||||
// ── Interaction handlers ──
|
||||
|
||||
let svDrag = false, hueDrag = false;
|
||||
|
||||
function onSV(e) {
|
||||
const rect = svCanvas.getBoundingClientRect();
|
||||
hsv.s = Math.max(0, Math.min(1, (e.clientX - rect.left) / SV_SIZE));
|
||||
hsv.v = Math.max(0, Math.min(1, 1 - (e.clientY - rect.top) / SV_SIZE));
|
||||
updateAll();
|
||||
}
|
||||
|
||||
function onHue(e) {
|
||||
const rect = hueCanvas.getBoundingClientRect();
|
||||
hsv.h = Math.max(0, Math.min(360, ((e.clientX - rect.left) / SV_SIZE) * 360));
|
||||
updateAll();
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
if (svDrag) onSV(e);
|
||||
if (hueDrag) onHue(e);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
svDrag = false;
|
||||
hueDrag = false;
|
||||
}
|
||||
|
||||
svCanvas.addEventListener('pointerdown', (e) => {
|
||||
svDrag = true; onSV(e);
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
});
|
||||
hueCanvas.addEventListener('pointerdown', (e) => {
|
||||
hueDrag = true; onHue(e);
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
});
|
||||
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
|
||||
// Hex input: live update on valid input
|
||||
hexInput.addEventListener('input', () => {
|
||||
const val = hexInput.value.trim();
|
||||
if (/^#?[0-9a-fA-F]{6}$/.test(val)) {
|
||||
const hex = val.startsWith('#') ? val : '#' + val;
|
||||
hsv = hexToHSV(hex);
|
||||
preview.style.backgroundColor = hex;
|
||||
drawSV();
|
||||
drawHue();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent keystrokes from reaching ComfyUI canvas
|
||||
hexInput.addEventListener('keydown', (e) => e.stopPropagation());
|
||||
|
||||
// Prevent panel clicks from reaching backdrop
|
||||
panel.addEventListener('pointerdown', (e) => e.stopPropagation());
|
||||
|
||||
// ── Close / Apply ──
|
||||
|
||||
function close() {
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
backdrop.remove();
|
||||
panel.remove();
|
||||
}
|
||||
|
||||
backdrop.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
onApply(hsvToHex(hsv.h, hsv.s, hsv.v));
|
||||
close();
|
||||
});
|
||||
|
||||
okBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
onApply(hsvToHex(hsv.h, hsv.s, hsv.v));
|
||||
close();
|
||||
});
|
||||
|
||||
// ── Mount & render ──
|
||||
|
||||
document.body.append(backdrop, panel);
|
||||
updateAll();
|
||||
}
|
||||
|
||||
// ── ComfyUI Widget Definition ─────────────────────────────────────────
|
||||
|
||||
const AILabColorWidget = {
|
||||
COLORCODE: (key, val) => {
|
||||
const widget = {};
|
||||
widget.y = 0;
|
||||
widget.name = key;
|
||||
widget.type = 'COLORCODE';
|
||||
|
||||
|
||||
const defaultColor = '#222222';
|
||||
widget.options = { default: defaultColor };
|
||||
|
||||
@@ -50,6 +364,7 @@ const AILabColorWidget = {
|
||||
return;
|
||||
}
|
||||
|
||||
const actualWidth = node.size[0];
|
||||
const drawHeight = 22;
|
||||
const margin = 15;
|
||||
const radius = 10;
|
||||
@@ -58,7 +373,7 @@ const AILabColorWidget = {
|
||||
ctx.beginPath();
|
||||
const x = margin;
|
||||
const y = widgetY + (height - drawHeight) / 2;
|
||||
const w = widgetWidth - margin * 2;
|
||||
const w = actualWidth - margin * 2;
|
||||
const h = drawHeight;
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + w - radius, y);
|
||||
@@ -81,7 +396,7 @@ const AILabColorWidget = {
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const text = `${this.name} (${this.value})`;
|
||||
ctx.fillText(text, widgetWidth * 0.5, y + drawHeight * 0.65);
|
||||
ctx.fillText(text, actualWidth * 0.5, y + drawHeight * 0.65);
|
||||
};
|
||||
|
||||
widget.mouse = function (e, pos, node) {
|
||||
@@ -89,32 +404,21 @@ const AILabColorWidget = {
|
||||
const margin = 15;
|
||||
|
||||
if (pos[0] >= margin && pos[0] <= node.size[0] - margin) {
|
||||
const picker = document.createElement('input');
|
||||
picker.type = 'color';
|
||||
picker.value = this.value;
|
||||
|
||||
picker.style.position = 'absolute';
|
||||
picker.style.left = '-9999px';
|
||||
picker.style.top = '-9999px';
|
||||
|
||||
document.body.appendChild(picker);
|
||||
|
||||
picker.addEventListener('change', () => {
|
||||
this.value = picker.value;
|
||||
const widgetRef = this;
|
||||
openColorPicker(this.value, e.clientX, e.clientY, (newColor) => {
|
||||
widgetRef.value = newColor;
|
||||
widgetRef.callback?.(newColor);
|
||||
node.graph._version++;
|
||||
node.setDirtyCanvas(true, true);
|
||||
picker.remove();
|
||||
});
|
||||
|
||||
picker.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
widget.computeSize = function (width) {
|
||||
return [width, 22];
|
||||
widget.computeSize = function () {
|
||||
return [50, 22];
|
||||
};
|
||||
|
||||
return widget;
|
||||
@@ -129,7 +433,7 @@ app.registerExtension({
|
||||
COLORCODE: (node, inputName, inputData) => {
|
||||
return {
|
||||
widget: node.addCustomWidget(
|
||||
AILabColorWidget.COLORCODE(inputName, inputData)
|
||||
AILabColorWidget.COLORCODE(inputName, inputData)
|
||||
),
|
||||
minWidth: 150,
|
||||
minHeight: 22,
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
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 <canvas> + 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;
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user