Files
marcoc2-ComfyUI-AnotherUtils/js/interactive_crop.js
T

413 lines
14 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const DEBUG = true;
function log(...args) {
if (DEBUG) console.log("[InteractiveCrop]", ...args);
}
function getImageUrl(imageWidgetValue) {
if (!imageWidgetValue) return null;
const url = api.apiURL(`/view?filename=${encodeURIComponent(imageWidgetValue)}&type=input`);
log("getImageUrl:", imageWidgetValue, "->", url);
return url;
}
app.registerExtension({
name: "AnotherUtils.InteractiveCrop",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
log("beforeRegisterNodeDef called for:", nodeData.name);
if (nodeData.name === "InteractiveCrop") {
log("Registering InteractiveCrop extension");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
log("onNodeCreated called, node id:", this.id);
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
// Debug: list all widgets
log("All widgets:", this.widgets?.map(w => ({ name: w.name, type: w.type, value: w.value })));
// Find the widgets by name (they are real widgets now)
this.cropWidgets = {};
for (const w of this.widgets || []) {
if (["image", "roi_width", "roi_height", "crop_x", "crop_y"].includes(w.name)) {
this.cropWidgets[w.name] = w;
log("Found widget:", w.name, "=", w.value, "type:", w.type);
}
}
log("cropWidgets keys:", Object.keys(this.cropWidgets));
// CRITICAL: Disable the built-in image preview from the image widget
// The image widget with image_upload:true adds its own preview
// We use defineProperty to intercept and block any attempts to set imgs
let _blockedImgs = null;
Object.defineProperty(this, 'imgs', {
get: () => null, // Always return null
set: (val) => {
log("BLOCKED attempt to set this.imgs to:", val);
_blockedImgs = val; // Store but don't use
},
configurable: true
});
let _blockedImages = null;
Object.defineProperty(this, 'images', {
get: () => null, // Always return null
set: (val) => {
log("BLOCKED attempt to set this.images to:", val);
_blockedImages = val;
},
configurable: true
});
this.imageIndex = null;
// Also check for the IMAGEUPLOAD widget that gets added
log("Looking for IMAGEUPLOAD widget...");
for (const w of this.widgets || []) {
log("Widget:", w.name, w.type, w);
if (w.name === "upload" || w.type === "IMAGEUPLOAD" || w.type === "button") {
log("Found upload widget:", w);
}
}
// Set callbacks for redraw
if (this.cropWidgets.image) {
this.cropWidgets.image.callback = () => {
// Clear any built-in preview that may have been set
this.imgs = null;
this.images = null;
this.imageIndex = null;
this.updateImage();
};
}
for (const name of ["roi_width", "roi_height", "crop_x", "crop_y"]) {
if (this.cropWidgets[name]) {
// Don't chain to original callback - it causes errors with new ComfyUI
this.cropWidgets[name].callback = () => {
this.setDirtyCanvas(true, true);
};
}
}
// Image element for drawing
this.img = new Image();
this.img.onload = () => {
log("Image loaded:", this.img.src, "size:", this.img.naturalWidth, "x", this.img.naturalHeight);
this.setDirtyCanvas(true, true);
};
this.img.onerror = (e) => {
log("Image load ERROR:", e, this.img.src);
};
// Drag state
this.isDragging = false;
this.dragStartMouse = { x: 0, y: 0 };
this.dragStartWidget = { x: 0, y: 0 };
this.imageRect = null;
// Initial load
if (this.cropWidgets.image && this.cropWidgets.image.value) {
this.updateImage();
}
return r;
};
nodeType.prototype.updateImage = function () {
log("updateImage called, image widget value:", this.cropWidgets.image?.value);
if (this.cropWidgets.image && this.cropWidgets.image.value) {
const url = getImageUrl(this.cropWidgets.image.value);
if (url) {
log("Setting image src to:", url);
this.img.src = url;
}
}
};
// --- Drawing ---
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
// Log every 30 frames to see more
this._drawCount = (this._drawCount || 0) + 1;
const shouldLog = this._drawCount % 30 === 1;
if (shouldLog) {
log("onDrawForeground called, node size:", this.size,
"img:", this.img?.src ? "loaded" : "not loaded",
"complete:", this.img?.complete,
"naturalSize:", this.img?.naturalWidth, "x", this.img?.naturalHeight);
log("Has original onDrawForeground?", !!onDrawForeground);
}
// Check what the node has that could draw images
if (shouldLog) {
log("Node properties check - imgs:", this.imgs, "images:", this.images,
"imageIndex:", this.imageIndex, "overIndex:", this.overIndex);
}
// DO NOT call original - we handle all drawing ourselves
// if (onDrawForeground) {
// if (shouldLog) log("Calling original onDrawForeground");
// onDrawForeground.apply(this, arguments);
// }
if (!this.img || !this.img.src || !this.img.complete || this.img.naturalWidth === 0) {
if (shouldLog) log("Image not ready, skipping draw");
this.imageRect = null;
return;
}
const maxWidth = this.size[0] - 20;
// Calculate topPadding based on actual widget height
// Each widget is roughly 20-25px, plus some margin
const widgetCount = this.widgets ? this.widgets.length : 5;
const topPadding = Math.max(150, widgetCount * 28 + 20); // Dynamic based on widget count
const maxHeight = this.size[1] - topPadding - 10;
if (this._drawCount % 30 === 1) {
log("Widget count:", widgetCount, "topPadding:", topPadding);
}
if (shouldLog) {
log("Draw calculations:", { maxWidth, topPadding, maxHeight, nodeSize: this.size });
}
if (maxHeight <= 0 || maxWidth <= 0) {
if (shouldLog) log("Not enough space to draw");
this.imageRect = null;
return;
}
const scale = Math.min(maxWidth / this.img.naturalWidth, maxHeight / this.img.naturalHeight);
const drawWidth = this.img.naturalWidth * scale;
const drawHeight = this.img.naturalHeight * scale;
const drawX = (this.size[0] - drawWidth) / 2;
const drawY = topPadding + (maxHeight - drawHeight) / 2;
this.imageRect = { x: drawX, y: drawY, w: drawWidth, h: drawHeight, scale: scale };
if (shouldLog) {
log("imageRect:", this.imageRect);
}
// Draw image
try {
ctx.drawImage(this.img, drawX, drawY, drawWidth, drawHeight);
if (shouldLog) log("Drew image at", drawX, drawY, drawWidth, drawHeight);
} catch (e) {
log("ERROR drawing image:", e);
return;
}
// Get ROI values safely
const cropX = this.cropWidgets.crop_x ? Number(this.cropWidgets.crop_x.value) || 0 : 0;
const cropY = this.cropWidgets.crop_y ? Number(this.cropWidgets.crop_y.value) || 0 : 0;
const roiW = this.cropWidgets.roi_width ? Number(this.cropWidgets.roi_width.value) || 100 : 100;
const roiH = this.cropWidgets.roi_height ? Number(this.cropWidgets.roi_height.value) || 100 : 100;
if (shouldLog) {
log("ROI values:", { cropX, cropY, roiW, roiH });
}
if (roiW > 0 && roiH > 0) {
const boxX = drawX + cropX * scale;
const boxY = drawY + cropY * scale;
const boxW = roiW * scale;
const boxH = roiH * scale;
if (shouldLog) {
log("Drawing ROI box at:", { boxX, boxY, boxW, boxH });
}
// Fill semi-transparent
ctx.fillStyle = "rgba(255, 255, 0, 0.25)";
ctx.fillRect(boxX, boxY, boxW, boxH);
// Stroke
ctx.setLineDash([]);
ctx.lineWidth = 2;
ctx.strokeStyle = "#FFFF00";
ctx.strokeRect(boxX, boxY, boxW, boxH);
}
// Draw resolution caption below the image
const caption = `${this.img.naturalWidth} x ${this.img.naturalHeight}`;
ctx.font = "12px Arial";
ctx.fillStyle = "#AAA";
ctx.textAlign = "center";
ctx.fillText(caption, this.size[0] / 2, drawY + drawHeight + 16);
};
// --- Mouse Interaction ---
const onMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function (event, pos, graphCanvas) {
log("onMouseDown called, pos:", pos, "imageRect:", this.imageRect);
// pos is relative to the node's top-left corner
if (!this.imageRect) {
log("No imageRect, passing to original handler");
return onMouseDown ? onMouseDown.apply(this, arguments) : false;
}
const x = pos[0];
const y = pos[1];
// Check if click is inside the image area
const isInsideImage = (
x >= this.imageRect.x &&
x <= this.imageRect.x + this.imageRect.w &&
y >= this.imageRect.y &&
y <= this.imageRect.y + this.imageRect.h
);
log("Click at", x, y, "isInsideImage:", isInsideImage,
"imageRect bounds:", this.imageRect.x, "-", this.imageRect.x + this.imageRect.w,
",", this.imageRect.y, "-", this.imageRect.y + this.imageRect.h);
if (isInsideImage) {
log("Starting drag!");
this.isDragging = true;
const scale = this.imageRect.scale;
const roiW = this.cropWidgets.roi_width ? Number(this.cropWidgets.roi_width.value) || 100 : 100;
const roiH = this.cropWidgets.roi_height ? Number(this.cropWidgets.roi_height.value) || 100 : 100;
// Convert mouse position to image coordinates
const imgX = (x - this.imageRect.x) / scale;
const imgY = (y - this.imageRect.y) / scale;
// Center the ROI on the click position
let newX = Math.floor(imgX - roiW / 2);
let newY = Math.floor(imgY - roiH / 2);
// Clamp to image bounds
const maxX = Math.max(0, this.img.naturalWidth - roiW);
const maxY = Math.max(0, this.img.naturalHeight - roiH);
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
// Update widgets
log("Setting crop_x to", newX, "crop_y to", newY);
if (this.cropWidgets.crop_x) this.cropWidgets.crop_x.value = newX;
if (this.cropWidgets.crop_y) this.cropWidgets.crop_y.value = newY;
// Store for drag
this.dragStartMouse = { x: x, y: y };
this.dragStartWidget = { x: newX, y: newY };
this.setDirtyCanvas(true, true);
// CRITICAL: Capture pointer to prevent node dragging
if (graphCanvas && graphCanvas.canvas) {
this._capturedCanvas = graphCanvas.canvas;
// Mark the graph canvas as not dragging
if (graphCanvas.dragging_canvas) {
graphCanvas.dragging_canvas = false;
}
}
log("Returning true to consume event");
return true; // Consume the event
}
log("Not inside image, passing to original handler");
return onMouseDown ? onMouseDown.apply(this, arguments) : false;
};
const onMouseMove = nodeType.prototype.onMouseMove;
nodeType.prototype.onMouseMove = function (event, pos, graphCanvas) {
if (this.isDragging && this.imageRect) {
const x = pos[0];
const y = pos[1];
const scale = this.imageRect.scale;
const dx = (x - this.dragStartMouse.x) / scale;
const dy = (y - this.dragStartMouse.y) / scale;
let newX = Math.floor(this.dragStartWidget.x + dx);
let newY = Math.floor(this.dragStartWidget.y + dy);
// Clamp
const roiW = this.cropWidgets.roi_width ? Number(this.cropWidgets.roi_width.value) || 100 : 100;
const roiH = this.cropWidgets.roi_height ? Number(this.cropWidgets.roi_height.value) || 100 : 100;
const maxX = Math.max(0, this.img.naturalWidth - roiW);
const maxY = Math.max(0, this.img.naturalHeight - roiH);
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
log("onMouseMove dragging, new position:", newX, newY);
if (this.cropWidgets.crop_x) this.cropWidgets.crop_x.value = newX;
if (this.cropWidgets.crop_y) this.cropWidgets.crop_y.value = newY;
this.setDirtyCanvas(true, true);
return true;
}
return onMouseMove ? onMouseMove.apply(this, arguments) : false;
};
const onMouseUp = nodeType.prototype.onMouseUp;
nodeType.prototype.onMouseUp = function (event, pos, graphCanvas) {
if (this.isDragging) {
log("onMouseUp, ending drag");
this.isDragging = false;
this._capturedCanvas = null;
this.setDirtyCanvas(true, true);
return true;
}
return onMouseUp ? onMouseUp.apply(this, arguments) : false;
};
// Ensure node is large enough to show the image
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
log("onConfigure called", info);
if (onConfigure) onConfigure.apply(this, arguments);
// Reload image on configure (workflow load)
if (this.cropWidgets && this.cropWidgets.image && this.cropWidgets.image.value) {
this.updateImage();
}
};
// Set minimum size
const computeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function () {
const size = computeSize ? computeSize.apply(this, arguments) : [200, 200];
// Ensure enough height for widgets + image preview
const widgetCount = this.widgets ? this.widgets.length : 5;
const minHeight = widgetCount * 28 + 200; // widgets + image area
return [Math.max(size[0], 320), Math.max(size[1], minHeight)];
};
// Debug: Check if node has images property (used for preview)
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (output) {
log("onExecuted called, output:", output);
log("this.images before:", this.images);
if (onExecuted) onExecuted.apply(this, arguments);
log("this.images after:", this.images);
// IMPORTANT: ComfyUI may set this.imgs for preview. Let's check:
log("this.imgs:", this.imgs);
};
// OVERRIDE onDrawBackground completely to prevent built-in image preview
nodeType.prototype.onDrawBackground = function (ctx) {
// Force clear any preview images that ComfyUI might have set
if (this.imgs || this.images) {
log("Clearing this.imgs/images that was set by ComfyUI, imgs:", this.imgs?.length, "images:", this.images?.length);
this.imgs = null;
this.images = null;
this.imageIndex = null;
}
// DO NOT call any original - we don't want the built-in preview
};
}
},
});