fix(preview): Pointer events broken in Node 2.0.21+
- Wheel zoom and pan on middle-click stopped working due to recent changes in Node's graph canvas container. - Implement capture-phase wheel event handling on the document to regain control of preview zoom independent of Node version. - Add safety checks against zero-size or detached canvases. - Ensure pointer events reach preview canvases even if parent containers have pointer-events:none applied. - Improve compatibility with Node 2.0 by adding fallbacks for widgets without proper addDOMWidget support.
This commit is contained in:
+10
-6
@@ -467,6 +467,7 @@ function registerImageOpsLivePreview() {
|
||||
startLoopIfVideo(node);
|
||||
}
|
||||
for (const w of node.widgets ?? []) {
|
||||
if (typeof w.callback !== "function" && typeof w.callback !== "undefined") continue;
|
||||
const orig = w.callback;
|
||||
w.callback = function() {
|
||||
const r = orig?.apply(this, arguments);
|
||||
@@ -575,12 +576,15 @@ function registerImageOpsLivePreview() {
|
||||
app.registerExtension({
|
||||
name: EXT_NAME,
|
||||
async beforeRegisterNodeDef(nodeType, _nodeData) {
|
||||
nodeType.prototype.onNodeCreated = /* @__PURE__ */ (function(orig) {
|
||||
return function() {
|
||||
orig?.apply(this, arguments);
|
||||
hookNode(this);
|
||||
};
|
||||
})(nodeType.prototype.onNodeCreated);
|
||||
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
origOnNodeCreated?.apply(this, arguments);
|
||||
hookNode(this);
|
||||
};
|
||||
},
|
||||
// Node 2.0 fallback: called per-instance after the node is fully constructed.
|
||||
nodeCreated(node) {
|
||||
hookNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,13 +131,17 @@ function attachInteractions(node, ctx) {
|
||||
st.drawPressureSizeInput?.addEventListener("change", syncDrawDynamics);
|
||||
st.drawPressureOpacityInput?.addEventListener("change", syncDrawDynamics);
|
||||
st.drawTiltSizeInput?.addEventListener("change", syncDrawDynamics);
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
const handleDrawWheel = (event) => {
|
||||
const target = event.target;
|
||||
if (target !== canvas && !canvas.contains(target)) return;
|
||||
if (!st.drawGeometry) return;
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return;
|
||||
const sx = canvas.width / rect.width;
|
||||
const sy = canvas.height / rect.height;
|
||||
const mx = (event.clientX - rect.left) * sx - canvas.width / 2;
|
||||
const my = (event.clientY - rect.top) * sy - canvas.height / 2;
|
||||
const factor = event.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||||
@@ -153,13 +157,15 @@ function attachInteractions(node, ctx) {
|
||||
const point = getCanvasPointer(canvas, event);
|
||||
const hover = canvasToSource(point.x, point.y);
|
||||
if (!hover.inside) return;
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
const current = widgetNumber(node, "brush_size", 10);
|
||||
const step = event.shiftKey ? 8 : 2;
|
||||
const direction = event.deltaY > 0 ? -step : step;
|
||||
ctx.setDrawBrushSize(node, current + direction);
|
||||
void ctx.renderDrawNode(node, 0);
|
||||
}, { passive: false });
|
||||
};
|
||||
document.addEventListener("wheel", handleDrawWheel, { capture: true, passive: false });
|
||||
canvas.addEventListener("keydown", (event) => {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "z") return;
|
||||
event.preventDefault();
|
||||
@@ -186,8 +192,9 @@ function attachInteractions(node, ctx) {
|
||||
} catch {
|
||||
}
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return;
|
||||
const sx = canvas.width / rect.width;
|
||||
const sy = canvas.height / rect.height;
|
||||
ctrlPanDrag = {
|
||||
pointerId: event.pointerId,
|
||||
startCanvasX: (event.clientX - rect.left) * sx,
|
||||
@@ -235,8 +242,9 @@ function attachInteractions(node, ctx) {
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
if (ctrlPanDrag && ctrlPanDrag.pointerId === event.pointerId) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return;
|
||||
const sx = canvas.width / rect.width;
|
||||
const sy = canvas.height / rect.height;
|
||||
const cx = (event.clientX - rect.left) * sx;
|
||||
const cy = (event.clientY - rect.top) * sy;
|
||||
st.previewPanX = ctrlPanDrag.startPanX + (cx - ctrlPanDrag.startCanvasX);
|
||||
|
||||
@@ -40,8 +40,9 @@ function drawOutputFormatBox(ctx, fit, label = "Output") {
|
||||
}
|
||||
function getCanvasPointer(canvas, event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / Math.max(1, rect.width);
|
||||
const scaleY = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return { x: 0, y: 0 };
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
return {
|
||||
x: (event.clientX - rect.left) * scaleX,
|
||||
y: (event.clientY - rect.top) * scaleY
|
||||
|
||||
@@ -15,31 +15,35 @@ function attachPreviewNavigation(node, canvasSize) {
|
||||
if (!st.previewLastSource) return;
|
||||
blit(node, st, st.previewLastSource, canvasSize);
|
||||
};
|
||||
const safeRect = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (rect.width < 1 || rect.height < 1) return null;
|
||||
return { rect, sx: canvas.width / rect.width, sy: canvas.height / rect.height };
|
||||
};
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 1) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
} catch {
|
||||
}
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
const r = safeRect();
|
||||
if (!r) return;
|
||||
st.previewPanDrag = {
|
||||
pointerId: event.pointerId,
|
||||
startCanvasX: (event.clientX - rect.left) * sx,
|
||||
startCanvasY: (event.clientY - rect.top) * sy,
|
||||
startCanvasX: (event.clientX - r.rect.left) * r.sx,
|
||||
startCanvasY: (event.clientY - r.rect.top) * r.sy,
|
||||
startPanX: st.previewPanX,
|
||||
startPanY: st.previewPanY
|
||||
};
|
||||
});
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
if (!st.previewPanDrag || st.previewPanDrag.pointerId !== event.pointerId) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
const cx = (event.clientX - rect.left) * sx;
|
||||
const cy = (event.clientY - rect.top) * sy;
|
||||
const r = safeRect();
|
||||
if (!r) return;
|
||||
const cx = (event.clientX - r.rect.left) * r.sx;
|
||||
const cy = (event.clientY - r.rect.top) * r.sy;
|
||||
st.previewPanX = st.previewPanDrag.startPanX + (cx - st.previewPanDrag.startCanvasX);
|
||||
st.previewPanY = st.previewPanDrag.startPanY + (cy - st.previewPanDrag.startCanvasY);
|
||||
reblitNow();
|
||||
@@ -58,14 +62,16 @@ function attachPreviewNavigation(node, canvasSize) {
|
||||
st.previewPanDrag = null;
|
||||
}
|
||||
});
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
if (isInteractiveNode(node)) return;
|
||||
const handleWheel = (event) => {
|
||||
const target = event.target;
|
||||
if (target !== canvas && !canvas.contains(target)) return;
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
const mx = (event.clientX - rect.left) * sx - canvas.width / 2;
|
||||
const my = (event.clientY - rect.top) * sy - canvas.height / 2;
|
||||
if (isInteractiveNode(node)) return;
|
||||
const r = safeRect();
|
||||
if (!r) return;
|
||||
const mx = (event.clientX - r.rect.left) * r.sx - canvas.width / 2;
|
||||
const my = (event.clientY - r.rect.top) * r.sy - canvas.height / 2;
|
||||
const factor = event.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||
const prevZoom = st.previewZoom ?? 1;
|
||||
const newZoom = clampPreviewZoom(prevZoom * factor);
|
||||
@@ -74,7 +80,8 @@ function attachPreviewNavigation(node, canvasSize) {
|
||||
st.previewPanY = my - (my - (st.previewPanY ?? 0)) * ratio;
|
||||
st.previewZoom = newZoom;
|
||||
reblitNow();
|
||||
}, { passive: false });
|
||||
};
|
||||
document.addEventListener("wheel", handleWheel, { capture: true, passive: false });
|
||||
canvas.addEventListener("dblclick", () => {
|
||||
if (isInteractiveNode(node)) return;
|
||||
if (st.previewZoom === 1 && st.previewPanX === 0 && st.previewPanY === 0) return;
|
||||
|
||||
@@ -623,22 +623,32 @@ function ensurePreviewWidget(node, progress, canvasSize) {
|
||||
if (drawControls) root.appendChild(drawControls);
|
||||
if (compControls) root.appendChild(compControls);
|
||||
root.appendChild(progressWrap);
|
||||
root.style.pointerEvents = "auto";
|
||||
const domMinHeight = getNodePreviewMinHeight(node);
|
||||
node.addDOMWidget("preview", "ImageOpsPreview", root, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
getMinHeight: () => {
|
||||
const mediaH = mediaWrap.style.display !== "none" ? mediaWrap.offsetHeight : 0;
|
||||
const imageH = Math.max(canvas.offsetHeight, mediaH);
|
||||
if (imageH === 0) return domMinHeight;
|
||||
const metaH = metaRow.offsetHeight + 6;
|
||||
const activeControls = previewControls ?? colorControls ?? drawControls ?? compControls;
|
||||
const controlsH = activeControls ? activeControls.offsetHeight + 8 : 0;
|
||||
const progressH = progressWrap.offsetHeight;
|
||||
const naturalH = imageH + metaH + controlsH + progressH + 12;
|
||||
return Math.max(domMinHeight, naturalH);
|
||||
if (typeof node.addDOMWidget === "function") {
|
||||
node.addDOMWidget("preview", "ImageOpsPreview", root, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
getMinHeight: () => {
|
||||
const mediaH = mediaWrap.style.display !== "none" ? mediaWrap.offsetHeight : 0;
|
||||
const imageH = Math.max(canvas.offsetHeight, mediaH);
|
||||
if (imageH === 0) return domMinHeight;
|
||||
const metaH = metaRow.offsetHeight + 6;
|
||||
const activeControls = previewControls ?? colorControls ?? drawControls ?? compControls;
|
||||
const controlsH = activeControls ? activeControls.offsetHeight + 8 : 0;
|
||||
const progressH = progressWrap.offsetHeight;
|
||||
const naturalH = imageH + metaH + controlsH + progressH + 12;
|
||||
return Math.max(domMinHeight, naturalH);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const domEl = node.domElement ?? node.element;
|
||||
if (domEl instanceof HTMLElement) {
|
||||
domEl.appendChild(root);
|
||||
} else {
|
||||
console.warn("[ImageOps] addDOMWidget unavailable and no DOM container found on node", node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
const widgets = node.widgets ?? [];
|
||||
const idx = widgets.findIndex((w) => w?.name === "preview");
|
||||
|
||||
+11
-7
@@ -524,6 +524,7 @@ export function registerImageOpsLivePreview(): void {
|
||||
}
|
||||
|
||||
for (const w of (node.widgets ?? [])) {
|
||||
if (typeof w.callback !== 'function' && typeof w.callback !== 'undefined') continue;
|
||||
const orig = w.callback;
|
||||
w.callback = function (this: any) {
|
||||
const r = orig?.apply(this, arguments as any);
|
||||
@@ -635,12 +636,15 @@ export function registerImageOpsLivePreview(): void {
|
||||
app.registerExtension({
|
||||
name: EXT_NAME,
|
||||
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, _nodeData: any) {
|
||||
nodeType.prototype.onNodeCreated = (function (orig: any) {
|
||||
return function (this: ComfyNode) {
|
||||
orig?.apply(this, arguments);
|
||||
hookNode(this);
|
||||
};
|
||||
})(nodeType.prototype.onNodeCreated);
|
||||
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function (this: ComfyNode) {
|
||||
origOnNodeCreated?.apply(this, arguments as any);
|
||||
hookNode(this);
|
||||
};
|
||||
},
|
||||
});
|
||||
// Node 2.0 fallback: called per-instance after the node is fully constructed.
|
||||
nodeCreated(node: ComfyNode) {
|
||||
hookNode(node);
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
@@ -168,15 +168,21 @@ export function attachInteractions(node: ComfyNode, ctx: DrawInteractionContext)
|
||||
st.drawPressureOpacityInput?.addEventListener("change", syncDrawDynamics);
|
||||
st.drawTiltSizeInput?.addEventListener("change", syncDrawDynamics);
|
||||
|
||||
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
||||
// ── Wheel: zoom (Ctrl+scroll) and brush size (scroll) ──
|
||||
// Use document capture phase to intercept before Node 2.0 graph container.
|
||||
const handleDrawWheel = (event: WheelEvent): void => {
|
||||
const target = event.target as Node | null;
|
||||
if (target !== canvas && !canvas.contains(target)) return;
|
||||
if (!st.drawGeometry) return;
|
||||
|
||||
// ── Ctrl+scroll: zoom the preview viewport ──────────────────────────────
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return;
|
||||
const sx = canvas.width / rect.width;
|
||||
const sy = canvas.height / rect.height;
|
||||
// Cursor position relative to canvas centre — zoom toward the cursor.
|
||||
const mx = (event.clientX - rect.left) * sx - canvas.width / 2;
|
||||
const my = (event.clientY - rect.top) * sy - canvas.height / 2;
|
||||
@@ -195,13 +201,15 @@ export function attachInteractions(node: ComfyNode, ctx: DrawInteractionContext)
|
||||
const point = getCanvasPointer(canvas, event as unknown as PointerEvent);
|
||||
const hover = canvasToSource(point.x, point.y);
|
||||
if (!hover.inside) return;
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
const current = widgetNumber(node, "brush_size", 10);
|
||||
const step = event.shiftKey ? 8 : 2;
|
||||
const direction = event.deltaY > 0 ? -step : step;
|
||||
ctx.setDrawBrushSize(node, current + direction);
|
||||
void ctx.renderDrawNode(node, 0);
|
||||
}, { passive: false });
|
||||
};
|
||||
document.addEventListener("wheel", handleDrawWheel, { capture: true, passive: false });
|
||||
|
||||
canvas.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "z") return;
|
||||
@@ -230,8 +238,9 @@ export function attachInteractions(node: ComfyNode, ctx: DrawInteractionContext)
|
||||
canvas.focus();
|
||||
try { canvas.setPointerCapture?.(event.pointerId); } catch {}
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return;
|
||||
const sx = canvas.width / rect.width;
|
||||
const sy = canvas.height / rect.height;
|
||||
ctrlPanDrag = {
|
||||
pointerId: event.pointerId,
|
||||
startCanvasX: (event.clientX - rect.left) * sx,
|
||||
@@ -286,8 +295,9 @@ export function attachInteractions(node: ComfyNode, ctx: DrawInteractionContext)
|
||||
// ── Ctrl+drag pan in progress ────────────────────────────────────────────
|
||||
if (ctrlPanDrag && ctrlPanDrag.pointerId === event.pointerId) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (rect.width < 1 || rect.height < 1) return;
|
||||
const sx = canvas.width / rect.width;
|
||||
const sy = canvas.height / rect.height;
|
||||
const cx = (event.clientX - rect.left) * sx;
|
||||
const cy = (event.clientY - rect.top) * sy;
|
||||
st.previewPanX = ctrlPanDrag.startPanX + (cx - ctrlPanDrag.startCanvasX);
|
||||
|
||||
@@ -75,8 +75,10 @@ export function drawOutputFormatBox(ctx: CanvasRenderingContext2D, fit: FitPlace
|
||||
|
||||
export function getCanvasPointer(canvas: HTMLCanvasElement, event: PointerEvent): { x: number; y: number } {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / Math.max(1, rect.width);
|
||||
const scaleY = canvas.height / Math.max(1, rect.height);
|
||||
// Guard against detached or zero-size canvas (Node 2.0 deferred DOM insertion).
|
||||
if (rect.width < 1 || rect.height < 1) return { x: 0, y: 0 };
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
return {
|
||||
x: (event.clientX - rect.left) * scaleX,
|
||||
y: (event.clientY - rect.top) * scaleY,
|
||||
|
||||
@@ -21,18 +21,25 @@ export function attachPreviewNavigation(node: ComfyNode, canvasSize: number): vo
|
||||
blit(node, st, st.previewLastSource, canvasSize);
|
||||
};
|
||||
|
||||
/** Safe rect helper — returns null if canvas is detached / zero-size. */
|
||||
const safeRect = (): { rect: DOMRect; sx: number; sy: number } | null => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (rect.width < 1 || rect.height < 1) return null;
|
||||
return { rect, sx: canvas.width / rect.width, sy: canvas.height / rect.height };
|
||||
};
|
||||
|
||||
// ── Middle-button pan ──
|
||||
canvas.addEventListener("pointerdown", (event: PointerEvent) => {
|
||||
if (event.button !== 1) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try { canvas.setPointerCapture(event.pointerId); } catch {}
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
const r = safeRect();
|
||||
if (!r) return;
|
||||
st.previewPanDrag = {
|
||||
pointerId: event.pointerId,
|
||||
startCanvasX: (event.clientX - rect.left) * sx,
|
||||
startCanvasY: (event.clientY - rect.top) * sy,
|
||||
startCanvasX: (event.clientX - r.rect.left) * r.sx,
|
||||
startCanvasY: (event.clientY - r.rect.top) * r.sy,
|
||||
startPanX: st.previewPanX,
|
||||
startPanY: st.previewPanY,
|
||||
};
|
||||
@@ -40,11 +47,10 @@ export function attachPreviewNavigation(node: ComfyNode, canvasSize: number): vo
|
||||
|
||||
canvas.addEventListener("pointermove", (event: PointerEvent) => {
|
||||
if (!st.previewPanDrag || st.previewPanDrag.pointerId !== event.pointerId) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
const cx = (event.clientX - rect.left) * sx;
|
||||
const cy = (event.clientY - rect.top) * sy;
|
||||
const r = safeRect();
|
||||
if (!r) return;
|
||||
const cx = (event.clientX - r.rect.left) * r.sx;
|
||||
const cy = (event.clientY - r.rect.top) * r.sy;
|
||||
st.previewPanX = st.previewPanDrag.startPanX + (cx - st.previewPanDrag.startCanvasX);
|
||||
st.previewPanY = st.previewPanDrag.startPanY + (cy - st.previewPanDrag.startCanvasY);
|
||||
reblitNow();
|
||||
@@ -64,15 +70,21 @@ export function attachPreviewNavigation(node: ComfyNode, canvasSize: number): vo
|
||||
});
|
||||
|
||||
// ── Wheel: zoom toward cursor (disabled for interactive nodes — they handle wheel themselves) ──
|
||||
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
||||
if (isInteractiveNode(node)) return;
|
||||
// In Node 2.0, graph-canvas-container intercepts wheel events in capture phase
|
||||
// before they reach our canvas. We therefore register our own capture-phase
|
||||
// handler on the document so we can stop the event first.
|
||||
const handleWheel = (event: WheelEvent): void => {
|
||||
const target = event.target as Node | null;
|
||||
if (target !== canvas && !canvas.contains(target)) return;
|
||||
// Always prevent graph zoom when cursor is on our preview canvas
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
if (isInteractiveNode(node)) return;
|
||||
const r = safeRect();
|
||||
if (!r) return;
|
||||
// Mouse position relative to canvas center
|
||||
const mx = (event.clientX - rect.left) * sx - canvas.width / 2;
|
||||
const my = (event.clientY - rect.top) * sy - canvas.height / 2;
|
||||
const mx = (event.clientX - r.rect.left) * r.sx - canvas.width / 2;
|
||||
const my = (event.clientY - r.rect.top) * r.sy - canvas.height / 2;
|
||||
const factor = event.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||
const prevZoom = st.previewZoom ?? 1;
|
||||
const newZoom = clampPreviewZoom(prevZoom * factor);
|
||||
@@ -82,7 +94,8 @@ export function attachPreviewNavigation(node: ComfyNode, canvasSize: number): vo
|
||||
st.previewPanY = my - (my - (st.previewPanY ?? 0)) * ratio;
|
||||
st.previewZoom = newZoom;
|
||||
reblitNow();
|
||||
}, { passive: false });
|
||||
};
|
||||
document.addEventListener("wheel", handleWheel, { capture: true, passive: false });
|
||||
|
||||
// ── Double-click: reset zoom/pan ──
|
||||
canvas.addEventListener("dblclick", () => {
|
||||
|
||||
@@ -735,24 +735,37 @@ export function ensurePreviewWidget(node: ComfyNode, progress: ProgressBus, canv
|
||||
if (compControls) root.appendChild(compControls);
|
||||
root.appendChild(progressWrap);
|
||||
|
||||
// Ensure pointer events reach our canvas even if Node 2.0 applies pointer-events:none on parent containers.
|
||||
root.style.pointerEvents = "auto";
|
||||
|
||||
const domMinHeight = getNodePreviewMinHeight(node);
|
||||
node.addDOMWidget("preview", "ImageOpsPreview", root, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
getMinHeight: () => {
|
||||
// Use canvas/mediaWrap rendered height (determined by node WIDTH, not height) to avoid
|
||||
// the vertical feedback loop where a tall node makes the DOM tall which makes the node tall.
|
||||
const mediaH = mediaWrap.style.display !== "none" ? mediaWrap.offsetHeight : 0;
|
||||
const imageH = Math.max(canvas.offsetHeight, mediaH);
|
||||
if (imageH === 0) return domMinHeight;
|
||||
const metaH = metaRow.offsetHeight + 6; // 6px marginTop
|
||||
const activeControls = previewControls ?? colorControls ?? drawControls ?? compControls;
|
||||
const controlsH = activeControls ? activeControls.offsetHeight + 8 : 0;
|
||||
const progressH = progressWrap.offsetHeight;
|
||||
const naturalH = imageH + metaH + controlsH + progressH + 12; // 12px root padding
|
||||
return Math.max(domMinHeight, naturalH);
|
||||
},
|
||||
});
|
||||
if (typeof node.addDOMWidget === "function") {
|
||||
node.addDOMWidget("preview", "ImageOpsPreview", root, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
getMinHeight: () => {
|
||||
// Use canvas/mediaWrap rendered height (determined by node WIDTH, not height) to avoid
|
||||
// the vertical feedback loop where a tall node makes the DOM tall which makes the node tall.
|
||||
const mediaH = mediaWrap.style.display !== "none" ? mediaWrap.offsetHeight : 0;
|
||||
const imageH = Math.max(canvas.offsetHeight, mediaH);
|
||||
if (imageH === 0) return domMinHeight;
|
||||
const metaH = metaRow.offsetHeight + 6; // 6px marginTop
|
||||
const activeControls = previewControls ?? colorControls ?? drawControls ?? compControls;
|
||||
const controlsH = activeControls ? activeControls.offsetHeight + 8 : 0;
|
||||
const progressH = progressWrap.offsetHeight;
|
||||
const naturalH = imageH + metaH + controlsH + progressH + 12; // 12px root padding
|
||||
return Math.max(domMinHeight, naturalH);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Node 2.0 fallback: inject the root element directly into the node's DOM container.
|
||||
const domEl = (node as any).domElement ?? (node as any).element;
|
||||
if (domEl instanceof HTMLElement) {
|
||||
domEl.appendChild(root);
|
||||
} else {
|
||||
console.warn("[ImageOps] addDOMWidget unavailable and no DOM container found on node", node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Force the preview widget to the top (before sliders), like KayTool.
|
||||
try {
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ export interface ComfyApp {
|
||||
export interface ComfyExtension {
|
||||
name: string;
|
||||
beforeRegisterNodeDef?(nodeType: ComfyNodeConstructor, nodeData: any): Promise<void> | void;
|
||||
nodeCreated?(node: ComfyNode): void;
|
||||
}
|
||||
|
||||
export interface ComfyNodeConstructor {
|
||||
@@ -86,7 +87,7 @@ export interface ComfyNode {
|
||||
onExecuted?(...args: any[]): any;
|
||||
onNodeCreated?(...args: any[]): any;
|
||||
setSize?(size: [number, number]): void;
|
||||
addDOMWidget(name: string, type: string, el: HTMLElement, opts: any): void;
|
||||
addDOMWidget?(name: string, type: string, el: HTMLElement, opts: any): void;
|
||||
addInput?(name: string, type?: string, extra_info?: any): void;
|
||||
removeInput?(slot: number): void;
|
||||
__imageops_state?: NodeState;
|
||||
|
||||
Reference in New Issue
Block a user