475 lines
19 KiB
JavaScript
475 lines
19 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
import { $el } from "../../../scripts/ui.js";
|
|
import { ComfyWidgets } from "../../scripts/widgets.js";
|
|
|
|
app.registerExtension({
|
|
name: "loop.clickCrop",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeType.comfyClass !== "ImageCropLoop") return;
|
|
|
|
const DEFAULT_DIMENSIONS = {
|
|
WIDTH: 200,
|
|
HEIGHT: 200,
|
|
MARGIN: 0,
|
|
TITLE_HEIGHT: 24
|
|
};
|
|
|
|
class CoordinateConverter {
|
|
/* Core class that handles coordinate transformations */
|
|
constructor(originalDimensions) {
|
|
this.originalDimensions = originalDimensions;
|
|
this.previewDimensions = { width: 0, height: 0 };
|
|
}
|
|
|
|
toOriginalCoords(previewX, previewY, frame) {
|
|
/* Converts preview coordinates to original image coordinates */
|
|
if (!this.isValidDimensions()) return [previewX, previewY];
|
|
|
|
const scaleX = this.originalDimensions.width / frame.width;
|
|
const scaleY = this.originalDimensions.height / frame.height;
|
|
|
|
return [
|
|
Math.round(previewX * scaleX),
|
|
Math.round(previewY * scaleY)
|
|
];
|
|
}
|
|
|
|
toPreviewCoords(originalX, originalY, frame) {
|
|
/* Converts original coordinates to preview coordinates */
|
|
if (!this.isValidDimensions()) return [originalX, originalY];
|
|
|
|
const scaleX = frame.width / this.originalDimensions.width;
|
|
const scaleY = frame.height / this.originalDimensions.height;
|
|
|
|
return [
|
|
Math.round(originalX * scaleX),
|
|
Math.round(originalY * scaleY)
|
|
];
|
|
}
|
|
|
|
toOriginalSize(previewSize, frame) {
|
|
/* Converts preview size to original image size */
|
|
if (!this.isValidDimensions()) return previewSize;
|
|
return Math.round(previewSize * (this.originalDimensions.width / frame.width));
|
|
}
|
|
|
|
toPreviewSize(originalSize, frame) {
|
|
/* Converts original size to preview size */
|
|
if (!this.isValidDimensions()) return originalSize;
|
|
// First, ensure originalSize doesn't exceed original image dimensions
|
|
const maxOriginalSize = Math.min(originalSize, this.originalDimensions.width);
|
|
return Math.round(maxOriginalSize * (frame.width / this.originalDimensions.width));
|
|
}
|
|
|
|
constrainOriginalCoords(x, y, size) {
|
|
/* Ensures coordinates stay within image bounds */
|
|
if (!this.isValidDimensions()) return [x, y];
|
|
|
|
// Ensure size doesn't exceed original dimensions
|
|
const constrainedSize = Math.min(size, this.originalDimensions.width);
|
|
|
|
const maxX = Math.max(0, this.originalDimensions.width - constrainedSize);
|
|
const maxY = Math.max(0, this.originalDimensions.height - constrainedSize);
|
|
|
|
return [
|
|
Math.max(0, Math.min(x, maxX)),
|
|
Math.max(0, Math.min(y, maxY))
|
|
];
|
|
}
|
|
|
|
isValidDimensions() {
|
|
return this.originalDimensions?.width && this.originalDimensions?.height;
|
|
}
|
|
|
|
updateDimensions(originalWidth, originalHeight, previewWidth, previewHeight) {
|
|
this.originalDimensions = { width: originalWidth, height: originalHeight };
|
|
this.previewDimensions = { width: previewWidth, height: previewHeight };
|
|
}
|
|
}
|
|
|
|
// Define ClickCropWidget class inside the extension scope
|
|
class ClickCropWidget {
|
|
/* Handles the visual interface */
|
|
constructor(node) {
|
|
this.node = node;
|
|
this.type = "HTML";
|
|
this.name = "clickableFrame";
|
|
this.value = null;
|
|
|
|
// this.originalOnMouseEnter = node.onMouseEnter;
|
|
// this.originalOnMouseLeave = node.onMouseLeave;
|
|
|
|
// node.onMouseEnter = (e, pos) => {
|
|
// if (this.node) {
|
|
// this.node.flags = this.node.flags || {};
|
|
// this.node.flags.pinned = true;
|
|
// app.graph.setDirtyCanvas(true, false);
|
|
// }
|
|
// if (this.originalOnMouseEnter) {
|
|
// this.originalOnMouseEnter.call(node, e, pos);
|
|
// }
|
|
// };
|
|
|
|
// node.onMouseLeave = (e, pos) => {
|
|
// if (this.node) {
|
|
// this.node.flags = this.node.flags || {};
|
|
// this.node.flags.pinned = false;
|
|
// app.graph.setDirtyCanvas(true, false);
|
|
// }
|
|
// if (this.originalOnMouseLeave) {
|
|
// this.originalOnMouseLeave.call(node, e, pos);
|
|
// }
|
|
// };
|
|
}
|
|
|
|
draw(ctx, node, widget_width, y, widget_height) {
|
|
/* Main rendering function for the widget */
|
|
if (!this.isValidImage(node)) return;
|
|
|
|
this.updateOriginalDimensions(node);
|
|
const frame = this.calculateFrame(y, node.size[0], node.size[1]);
|
|
node.frameArea = frame;
|
|
|
|
this.drawFrame(ctx, frame);
|
|
this.drawMarker(ctx, node, frame);
|
|
}
|
|
|
|
isValidImage(node) {
|
|
return node.imgs?.[0]?.width && node.imgs?.[0]?.height;
|
|
}
|
|
|
|
updateOriginalDimensions(node) {
|
|
const img = node.imgs[0];
|
|
const match = img.src.match(/_(\d+)_(\d+)_/);
|
|
if (match) {
|
|
node.converter.updateDimensions(parseInt(match[1]), parseInt(match[2]));
|
|
this.updateMarkerPosition(node);
|
|
}
|
|
}
|
|
|
|
calculateFrame(y, nodeWidth, nodeHeight) {
|
|
/* Determines the frame dimensions and position */
|
|
const TITLE_HEIGHT = 24;
|
|
const img = this.node.imgs[0];
|
|
|
|
const availWidth = nodeWidth;
|
|
const availHeight = nodeHeight - TITLE_HEIGHT - y;
|
|
|
|
const imgRatio = img.naturalWidth / img.naturalHeight;
|
|
const containerRatio = availWidth / availHeight;
|
|
|
|
let frameWidth, frameHeight;
|
|
if (containerRatio > imgRatio) {
|
|
frameHeight = availHeight;
|
|
frameWidth = frameHeight * imgRatio;
|
|
} else {
|
|
frameWidth = availWidth;
|
|
frameHeight = frameWidth / imgRatio;
|
|
}
|
|
|
|
return {
|
|
left: (availWidth - frameWidth) / 2,
|
|
top: y + TITLE_HEIGHT + (availHeight - frameHeight) / 2,
|
|
width: frameWidth,
|
|
height: frameHeight
|
|
};
|
|
}
|
|
|
|
drawFrame(ctx, frame) {
|
|
/* Renders the frame border */
|
|
// ctx.strokeStyle = "#ff7500";
|
|
ctx.strokeStyle = "green";
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(frame.left, frame.top, frame.width, frame.height);
|
|
}
|
|
|
|
drawMarker(ctx, node, frame) {
|
|
/* Renders the selection marker */
|
|
if (!node.clickMarker) return;
|
|
|
|
const previewSize = node.converter.toPreviewSize(
|
|
node.widgets.find(w => w.name === 'size').value,
|
|
frame
|
|
);
|
|
ctx.strokeStyle = "green";
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(
|
|
node.clickMarker.x,
|
|
node.clickMarker.y,
|
|
previewSize,
|
|
previewSize
|
|
);
|
|
}
|
|
|
|
updateMarkerPosition(node) {
|
|
/* Updates marker position when coordinates change */
|
|
const frame = node.frameArea;
|
|
if (!node.clickMarker || !frame) return;
|
|
|
|
const xWidget = node.widgets.find(w => w.name === 'x');
|
|
const yWidget = node.widgets.find(w => w.name === 'y');
|
|
|
|
const [newPreviewX, newPreviewY] = node.converter.toPreviewCoords(
|
|
xWidget.value,
|
|
yWidget.value,
|
|
frame
|
|
);
|
|
node.clickMarker.x = frame.left + newPreviewX;
|
|
node.clickMarker.y = frame.top + newPreviewY;
|
|
}
|
|
}
|
|
|
|
nodeType.prototype.setupNode = function() {
|
|
/* Initializes the node with default values and widgets */
|
|
this.converter = new CoordinateConverter({ width: 0, height: 0 });
|
|
this.size = [DEFAULT_DIMENSIONS.WIDTH, DEFAULT_DIMENSIONS.HEIGHT];
|
|
|
|
const widgets = {
|
|
x: this.widgets.find(w => w.name === 'x'),
|
|
y: this.widgets.find(w => w.name === 'y'),
|
|
size: this.widgets.find(w => w.name === 'size')
|
|
};
|
|
|
|
this.setupWidgetCallbacks(widgets);
|
|
this.addCustomWidget(new ClickCropWidget(this));
|
|
this.setupMouseHandler(widgets);
|
|
|
|
this.serialize_widgets = false;
|
|
};
|
|
|
|
nodeType.prototype.initializeMarker = function(frame, widgets) {
|
|
/* Initializes the marker with default values and widgets */
|
|
if (!frame) return;
|
|
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
|
|
|
|
// Apply initial constraints
|
|
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(
|
|
xWidget.value,
|
|
yWidget.value,
|
|
sizeWidget.value
|
|
);
|
|
|
|
// Update widget values
|
|
xWidget.value = constrainedX;
|
|
yWidget.value = constrainedY;
|
|
|
|
// Convert to preview coordinates
|
|
const [previewX, previewY] = this.converter.toPreviewCoords(
|
|
constrainedX,
|
|
constrainedY,
|
|
frame
|
|
);
|
|
|
|
// Create marker
|
|
this.clickMarker = {
|
|
x: frame.left + previewX,
|
|
y: frame.top + previewY
|
|
};
|
|
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype.setupWidgetCallbacks = function(widgets) {
|
|
/* Sets up event handlers for widget value changes */
|
|
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
|
|
|
|
// Size widget callback with dynamic coordinate updates
|
|
sizeWidget.callback = function(value) {
|
|
const frame = this.frameArea;
|
|
if (!frame) return;
|
|
|
|
// Constrain size to original image dimensions
|
|
const maxSize = Math.min(this.converter.originalDimensions.width, this.converter.originalDimensions.height);
|
|
|
|
const constrainedValue = Math.min(value, maxSize);
|
|
|
|
// Round to nearest multiple of 8
|
|
const roundedValue = Math.round(constrainedValue / 8) * 8;
|
|
if (value !== roundedValue) {
|
|
sizeWidget.value = roundedValue;
|
|
this.onSizeChange(roundedValue, frame, widgets);
|
|
return;
|
|
}
|
|
|
|
if (!this.clickMarker) {
|
|
this.initializeMarker(frame, widgets);
|
|
} else {
|
|
this.onSizeChange(roundedValue, frame, widgets);
|
|
}
|
|
}.bind(this);
|
|
|
|
// X and Y widget callbacks with floor to ensure integer values
|
|
const createAutoMarkerCallback = (axis) => {
|
|
/* Creates handler for marker changes */
|
|
return function(value) {
|
|
const frame = this.frameArea;
|
|
if (!frame) return;
|
|
|
|
// Ensure integer values
|
|
const intValue = Math.floor(value);
|
|
if (value !== intValue) {
|
|
if (axis === 'x') {
|
|
xWidget.value = intValue;
|
|
} else {
|
|
yWidget.value = intValue;
|
|
}
|
|
value = intValue;
|
|
}
|
|
|
|
if (!this.clickMarker) {
|
|
this.initializeMarker(frame, widgets);
|
|
} else {
|
|
this.createCoordinateCallback(axis, xWidget, yWidget, sizeWidget).call(this, value);
|
|
}
|
|
}.bind(this);
|
|
};
|
|
|
|
xWidget.callback = createAutoMarkerCallback('x');
|
|
yWidget.callback = createAutoMarkerCallback('y');
|
|
};
|
|
|
|
nodeType.prototype.onSizeChange = function(newSize, frame, widgets) {
|
|
/* Handles size change events */
|
|
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
|
|
|
|
if (!frame || !this.converter.isValidDimensions()) return;
|
|
|
|
// Get current original coordinates
|
|
let currentX = Math.floor(xWidget.value);
|
|
let currentY = Math.floor(yWidget.value);
|
|
|
|
// Apply constraints with new size
|
|
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(
|
|
currentX,
|
|
currentY,
|
|
newSize
|
|
);
|
|
|
|
// Update widget values if they were constrained
|
|
if (currentX !== constrainedX || currentY !== constrainedY) {
|
|
xWidget.value = constrainedX;
|
|
yWidget.value = constrainedY;
|
|
}
|
|
|
|
// Convert to preview coordinates and update marker
|
|
const [previewX, previewY] = this.converter.toPreviewCoords(
|
|
constrainedX,
|
|
constrainedY,
|
|
frame
|
|
);
|
|
|
|
this.clickMarker = {
|
|
x: frame.left + previewX,
|
|
y: frame.top + previewY
|
|
};
|
|
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype.createCoordinateCallback = function(axis, xWidget, yWidget, sizeWidget) {
|
|
/* Creates handlers for coordinate changes */
|
|
return function(value) {
|
|
const frame = this.frameArea;
|
|
if (!frame || !this.clickMarker) return;
|
|
|
|
// Ensure integer values for coordinates
|
|
const currentCoords = axis === 'x'
|
|
? [Math.floor(value), Math.floor(yWidget.value)]
|
|
: [Math.floor(xWidget.value), Math.floor(value)];
|
|
|
|
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(
|
|
...currentCoords,
|
|
sizeWidget.value
|
|
);
|
|
|
|
const updatedValue = axis === 'x' ? constrainedX : constrainedY;
|
|
if (value !== updatedValue) {
|
|
if (axis === 'x') xWidget.value = updatedValue;
|
|
else yWidget.value = updatedValue;
|
|
return;
|
|
}
|
|
|
|
const [previewX, previewY] = this.converter.toPreviewCoords(
|
|
axis === 'x' ? constrainedX : xWidget.value,
|
|
axis === 'y' ? constrainedY : yWidget.value,
|
|
frame
|
|
);
|
|
|
|
const markerSize = this.converter.toPreviewSize(sizeWidget.value, frame);
|
|
this.clickMarker.x = frame.left + (axis === 'x' ? previewX : this.clickMarker.x - frame.left);
|
|
this.clickMarker.y = frame.top + (axis === 'y' ? previewY : this.clickMarker.y - frame.top);
|
|
|
|
this.setDirtyCanvas(true);
|
|
}.bind(this);
|
|
};
|
|
|
|
nodeType.prototype.createSizeCallback = function(xWidget, yWidget, sizeWidget) {
|
|
/* Creates handler for size changes */
|
|
return function(value) {
|
|
const frame = this.frameArea;
|
|
if (!frame || !this.clickMarker) return;
|
|
|
|
// Get current original coordinates
|
|
const [origX, origY] = this.converter.toOriginalCoords(
|
|
this.clickMarker.x - frame.left,
|
|
this.clickMarker.y - frame.top,
|
|
frame
|
|
);
|
|
|
|
// Constrain coordinates with new size value
|
|
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(origX, origY, value);
|
|
|
|
// Update x and y widgets if they were constrained
|
|
if (xWidget.value !== constrainedX) xWidget.value = constrainedX;
|
|
if (yWidget.value !== constrainedY) yWidget.value = constrainedY;
|
|
|
|
// Convert back to preview coordinates
|
|
const [previewX, previewY] = this.converter.toPreviewCoords(constrainedX, constrainedY, frame);
|
|
|
|
// Update marker position
|
|
this.clickMarker.x = frame.left + previewX;
|
|
this.clickMarker.y = frame.top + previewY;
|
|
|
|
this.setDirtyCanvas(true);
|
|
}.bind(this);
|
|
};
|
|
|
|
nodeType.prototype.setupMouseHandler = function(widgets) {
|
|
/* Handles mouse interactions for selection */
|
|
this.onMouseDown = (e, pos) => {
|
|
const frame = this.frameArea;
|
|
if (!frame ||
|
|
pos[0] < frame.left || pos[0] > frame.left + frame.width ||
|
|
pos[1] < frame.top || pos[1] > frame.top + frame.height) return;
|
|
|
|
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
|
|
const relativeX = pos[0] - frame.left;
|
|
const relativeY = pos[1] - frame.top;
|
|
|
|
const previewSize = this.converter.toPreviewSize(sizeWidget.value, frame);
|
|
const markerX = Math.max(0, Math.min(relativeX - previewSize / 2, frame.width - previewSize));
|
|
const markerY = Math.max(0, Math.min(relativeY - previewSize / 2, frame.height - previewSize));
|
|
|
|
const [origX, origY] = this.converter.toOriginalCoords(markerX, markerY, frame);
|
|
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(origX, origY, sizeWidget.value);
|
|
|
|
xWidget.value = constrainedX;
|
|
yWidget.value = constrainedY;
|
|
|
|
const [previewX, previewY] = this.converter.toPreviewCoords(constrainedX, constrainedY, frame);
|
|
this.clickMarker = { x: frame.left + previewX, y: frame.top + previewY };
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
};
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
onNodeCreated?.apply(this, arguments);
|
|
this.setupNode();
|
|
};
|
|
}
|
|
});
|
|
|
|
|