Files
MohammadAboulEla-ComfyUI-iT…/web/crop_node.js
T
2025-02-24 14:56:29 +02:00

952 lines
32 KiB
JavaScript

import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
import { allow_debug } from "./js_shared.js";
import {
Shapes,
Colors,
lightenColor,
canvasRatios,
canvasScales,
commonColors,
trackMouseColor,
fakeMouseDown,
getIndexByDimensions,
} from "./utils.js";
import { BaseSmartWidget, SmartInfo } from "./makadi.js";
function fakeGraphClick(graph, x, y) {
const canvas = graph.canvas;
if (!canvas) return;
const eventProps = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
pointerId: 1,
pointerType: "mouse",
};
// const pointerDownEvent = new PointerEvent("pointerdown", eventProps);
// canvas.dispatchEvent(pointerDownEvent);
setTimeout(() => {
// const upEvent = new MouseEvent("pointerup", eventProps);
// canvas.dispatchEvent(upEvent);
const clickEvent = new MouseEvent("click", eventProps);
canvas.dispatchEvent(clickEvent);
}, 50);
}
export class LiteInfo extends BaseSmartWidget {
constructor(x, y, width, height, node, text, options = {}) {
super(node);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.originalWidth = width;
this.originalHeight = height;
this.originalY = y;
this.radius = width / 2;
this._shape = Shapes.ROUND;
this.color = LiteGraph.WIDGET_BGCOLOR || "crimson";
this.outline = true;
this.outlineColor = "#434343";
this.outlineWidth = 0.8;
this.text = text;
this.textWidth = null;
this.textColor = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR || "white";
this.textYoffset = 0.7;
this.textXoffset = 0.0;
this.textAlign = "center";
this.textBaseline = "middle";
this.font = "14px Arial Bold";
this.previewDuration = 2000;
this.isVisible = false;
// Apply options if provided
Object.assign(this, options);
this.originalColor = this.color; // Store original color
this.originalTextColor = this.textColor;
// add self to the node
//node.addCustomWidget(this);
}
handleDown() {}
handleClick() {}
handleMove() {}
draw(ctx) {
if (!this.isVisible || !this.text) return;
// Draw rounded rectangle
if (this.shape === Shapes.ROUND) {
const radius = Math.min(this.width, this.height) / 5; // Adjust rounding level
ctx.beginPath();
ctx.moveTo(this.x + radius, this.y);
ctx.lineTo(this.x + this.width - radius, this.y);
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
ctx.lineTo(this.x + radius, this.y + this.height);
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
ctx.lineTo(this.x, this.y + radius);
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
// Draw outline if enabled
if (this.outline) {
ctx.strokeStyle = this.outlineColor;
ctx.lineWidth = this.outlineWidth;
ctx.stroke();
}
}
// Draw text
if (this.text) {
ctx.fillStyle = this.textColor;
ctx.font = this.font;
ctx.textAlign = this.textAlign;
ctx.textBaseline = this.textBaseline;
ctx.fillText(this.text, this.x + this.width / 2 + this.textXoffset, this.y + this.height / 2 + this.textYoffset);
}
}
updateText(newText) {
this.text = newText;
}
set shape(value) {
this._shape = value;
if (value === Shapes.CIRCLE) this.height = this.width;
}
get shape() {
return this._shape;
}
}
class CropWidget {
constructor(node) {
this.node = node;
this.value = {}
if(node.widgets_values && node.widgets_values[3]){
this.value = node.widgets_values[3];
}
this.x = 0;
this.y = 80 + 17;
this.yOffset = this.y + 30;
this.width = 50;
this.height = 50;
this._markDelete = false;
this._mousePos = { x: 0, y: 0 };
this.color = "crimson";
this.isVisible = true;
this.img = null;
this.imgOffsetX = 0;
this.imgOffsetY = 0;
this.cropFillColor = "rgba(255, 100, 0, 0.2)"; //"rgba(255, 81, 0, 0.2)";
// NODE SETTINGS
this.nodeSize = 384;
node.setSize([this.nodeSize, this.nodeSize + this.y]);
node.resizable = false;
this.resizeRatio = null;
this.info = new LiteInfo(this.nodeSize/2-40,80+17+5,80,15,node,"")
this.cropping = false;
this.isCroppingDone = false;
this.drawCropPreview = false;
this.startX = null;
this.startY = null;
this.endX = null;
this.endY = null;
this.resizing = null; // Track which handle is being resized
this.resizeThreshold = 10; // Sensitivity for detecting resize handles
if (node.widgets_values && node.widgets_values[3]) {
if (allow_debug) console.log("node has crop data value");
this.node.properties.value = node.widgets_values[3];
} else {
if (allow_debug) console.log("node does not has crop data value");
}
if (node.properties.value) {
this.loadCropData();
this.cropNewImage();
} else {
node.properties.value = {};
}
this.adjustNewImageSize(); // init
this.setResizeRatio(); // init ratio
// ON Ratio Changed
this.node.widgets[0].callback = () => {
this.setResizeRatio();
};
// ON Image Changed
const originalOnChanged = this.node.widgets[1].callback;
this.node.widgets[1].callback = async (img) => {
originalOnChanged.apply(this);
this.img = null;
//this.resetCroppingData(); // disabled
while (this.img !== this.node.imgs[0]) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
this.cropNewImage();
};
this.node.onMouseUp = (e) => {
this.hideInfo()
};
}
loadCropData() {
this.startX = this.node.properties.value.startX;
this.startY = this.node.properties.value.startY;
this.endX = this.node.properties.value.endX;
this.endY = this.node.properties.value.endY;
//if(allow_debug) console.log('loaded',this.node.properties.value);
this.isCroppingDone = true;
this.node.setDirtyCanvas(true, true);
}
saveCropData() {
if (this.startX && this.endX && this.startY && this.endY) {
this.value.startX = this.startX;
this.value.startY = this.startY;
this.value.endX = this.endX;
this.value.endY = this.endY;
this.node.properties.value.startX = this.startX;
this.node.properties.value.startY = this.startY;
this.node.properties.value.endX = this.endX;
this.node.properties.value.endY = this.endY;
if (allow_debug) console.log("crop data saved");
}
}
autoPinNode() {
const safeZone = 50;
const { x, y } = this.mousePos;
if (y > 50 && y < this.node.height + safeZone) {
if (x > this.node.width + safeZone || x < -safeZone) {
this.node.flags.pinned = false;
} else {
this.node.flags.pinned = true;
}
} else {
this.node.flags.pinned = false;
}
}
resetCroppingData() {
this.croppedImage = null;
this.value = {};
this.value.data = null;
this.cropping = false;
this.isCroppingDone = false;
this.startX = null;
this.startY = null;
this.endX = null;
this.endY = null;
this.resizing = null;
}
getResizeHandle(x, y) {
const handleSize = 10;
const cropX = Math.min(this.startX, this.endX);
const cropY = Math.min(this.startY, this.endY);
const cropW = Math.abs(this.endX - this.startX);
const cropH = Math.abs(this.endY - this.startY);
// Check corners
if (x >= cropX - handleSize && x <= cropX + handleSize && y >= cropY - handleSize && y <= cropY + handleSize) {
return "top-left";
}
if (
x >= cropX + cropW - handleSize &&
x <= cropX + cropW + handleSize &&
y >= cropY - handleSize &&
y <= cropY + handleSize
) {
return "top-right";
}
if (
x >= cropX - handleSize &&
x <= cropX + handleSize &&
y >= cropY + cropH - handleSize &&
y <= cropY + cropH + handleSize
) {
return "bottom-left";
}
if (
x >= cropX + cropW - handleSize &&
x <= cropX + cropW + handleSize &&
y >= cropY + cropH - handleSize &&
y <= cropY + cropH + handleSize
) {
return "bottom-right";
}
// Only check midpoints if the ratio is set to "free"
if (this.resizeRatio === null) {
if (
x >= cropX + cropW / 2 - handleSize &&
x <= cropX + cropW / 2 + handleSize &&
y >= cropY - handleSize &&
y <= cropY + handleSize
) {
return "top-mid";
}
if (
x >= cropX + cropW / 2 - handleSize &&
x <= cropX + cropW / 2 + handleSize &&
y >= cropY + cropH - handleSize &&
y <= cropY + cropH + handleSize
) {
return "bottom-mid";
}
if (
x >= cropX - handleSize &&
x <= cropX + handleSize &&
y >= cropY + cropH / 2 - handleSize &&
y <= cropY + cropH / 2 + handleSize
) {
return "left-mid";
}
if (
x >= cropX + cropW - handleSize &&
x <= cropX + cropW + handleSize &&
y >= cropY + cropH / 2 - handleSize &&
y <= cropY + cropH / 2 + handleSize
) {
return "right-mid";
}
}
return null;
}
adjustNewImageSize() {
const img = this.node.imgs[0];
if (this.img === img) return; // Avoid redundant assignment
this.img = img;
const longestSide = Math.max(img.width, img.height);
const scale = this.nodeSize / longestSide;
this.width = img.width * scale;
this.height = img.height * scale;
// Center the image correctly
this.imgOffsetX = (this.node.width - this.width) / 2;
this.imgOffsetY = this.y + (this.nodeSize - this.height) / 2;
this.node.setDirtyCanvas(true, true);
}
setResizeRatio() {
function ratioToFraction(ratio) {
if (ratio === "free") return null;
let [num, den] = ratio.split(":").map(Number);
return num / den;
}
const ratio = ratioToFraction(this.node.widgets[0].value);
this.resizeRatio = ratio;
}
draw(ctx) {
if (!this.isVisible) return;
// Clear original image
ctx.clearRect(this.x, this.y, this.node.width, this.node.height - this.yOffset);
// Fill with background color
ctx.fillStyle = this.node.bgcolor;
ctx.fillRect(this.x, this.y, this.node.width, this.node.height - this.yOffset);
// Draw loaded image
this.adjustNewImageSize();
ctx.drawImage(this.img, this.imgOffsetX, this.imgOffsetY, this.width, this.height);
// Draw crop rect fill area
if (this.cropping || this.isCroppingDone) {
// Calculate the crop area boundaries
const cropX = Math.min(this.startX, this.endX);
const cropY = Math.min(this.startY, this.endY);
const cropW = Math.abs(this.endX - this.startX);
const cropH = Math.abs(this.endY - this.startY);
ctx.fillStyle = this.cropFillColor;
ctx.fillRect(cropX, cropY, cropW, cropH);
// Draw a dashed outline around the crop rect
ctx.strokeStyle = "#333";
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 5]);
ctx.strokeRect(cropX, cropY, cropW, cropH);
// Draw handles at corners and midpoints
const handleSize = 10;
ctx.strokeStyle = "#333";
ctx.lineWidth = 1.5;
ctx.setLineDash([]); // Reset dash pattern for solid lines
// Corner handles
ctx.strokeRect(cropX - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize); // Top-left
ctx.strokeRect(cropX + cropW - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize); // Top-right
ctx.strokeRect(cropX - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize); // Bottom-left
ctx.strokeRect(cropX + cropW - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize); // Bottom-right
// Only draw midpoint handles if the ratio is set to "free"
if (this.resizeRatio === null) {
ctx.strokeRect(cropX + cropW / 2 - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize); // Top-mid
ctx.strokeRect(cropX + cropW / 2 - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize); // Bottom-mid
ctx.strokeRect(cropX - handleSize / 2, cropY + cropH / 2 - handleSize / 2, handleSize, handleSize); // Left-mid
ctx.strokeRect(cropX + cropW - handleSize / 2, cropY + cropH / 2 - handleSize / 2, handleSize, handleSize); // Right-mid
}
}
if (this.croppedImage && this.drawCropPreview && !this.cropping && !this.resizing) {
// Calculate the crop area dimensions
const cropWidth = Math.abs(this.endX - this.startX);
const cropHeight = Math.abs(this.endY - this.startY);
// Define a maximum size for the preview (e.g., 100x100 pixels)
const maxPreviewSize = 100;
// Calculate the aspect ratio of the crop area
const aspectRatio = cropWidth / cropHeight;
// Determine the preview dimensions while maintaining the aspect ratio
let croppedImageWidth, croppedImageHeight;
if (cropWidth > cropHeight) {
croppedImageWidth = maxPreviewSize;
croppedImageHeight = maxPreviewSize / aspectRatio;
} else {
croppedImageHeight = maxPreviewSize;
croppedImageWidth = maxPreviewSize * aspectRatio;
}
// Define the position for the cropped image in the left corner
const croppedImageX = this.x + 10; // 10 pixels from the left edge of the node
const croppedImageY = this.y + 10; // 10 pixels from the top edge of the node
try {
// Draw the cropped image
ctx.drawImage(this.croppedImage, croppedImageX, croppedImageY, croppedImageWidth, croppedImageHeight);
} catch (error) {}
}
this.info.draw(ctx)
}
cropNewImage() {
if (!this.img) return;
// Calculate the scale factors for the image
const scaleX = this.img.naturalWidth / this.width;
const scaleY = this.img.naturalHeight / this.height;
// Calculate the crop area in the original image coordinates
const cropX = (Math.min(this.startX, this.endX) - this.imgOffsetX) * scaleX;
const cropY = (Math.min(this.startY, this.endY) - this.imgOffsetY) * scaleY;
const cropWidth = Math.abs(this.endX - this.startX) * scaleX;
const cropHeight = Math.abs(this.endY - this.startY) * scaleY;
// Create a canvas to hold the cropped image
const canvas = document.createElement("canvas");
canvas.width = cropWidth;
canvas.height = cropHeight;
const ctx = canvas.getContext("2d");
// Draw the cropped portion of the image onto the canvas
ctx.drawImage(
this.img,
cropX,
cropY,
cropWidth,
cropHeight, // Source rectangle (cropped area)
0,
0,
cropWidth,
cropHeight // Destination rectangle (canvas)
);
// Create a new image from the canvas and store it
this.croppedImage = new Image();
this.croppedImage.src = canvas.toDataURL();
// for python
if (this.croppedImage) {
if (
this.croppedImage.src === "data:;" ||
this.croppedImage.src.startsWith("data:,") ||
this.croppedImage.src.length < 50
) {
if (allow_debug) console.log("Image has small or empty data.");
this.name = "crop";
this.value.data = null;
this.isCroppingDone = false;
} else {
this.name = "crop";
this.value.data = this.croppedImage.src;
}
}
}
handleDown(e) {
const { x, y } = this.mousePos;
// if (this.isMouseOnImage()) {
// if (allow_debug) console.log("mouse in");
// } else {
// if (allow_debug)console.log("mouse out");
// }
// Swap startX/endX and startY/endY if necessary during drawing
if (this.startX > this.endX) {
[this.startX, this.endX] = [this.endX, this.startX];
}
if (this.startY > this.endY) {
[this.startY, this.endY] = [this.endY, this.startY];
}
if (this.isMouseInCropArea()) {
const cropX = Math.min(this.startX, this.endX);
const cropY = Math.min(this.startY, this.endY);
const cropW = Math.abs(this.endX - this.startX);
const cropH = Math.abs(this.endY - this.startY);
if (allow_debug) {
console.log("mouse in crop area");
}
this.dragging = true;
this.dragOffsetX = x - cropX;
this.dragOffsetY = y - cropY;
return;
}
// Check if the mouse is over a resize handle
if (this.isCroppingDone && this.getResizeHandle(x, y)) {
this.resizing = this.getResizeHandle(x, y); // Start resizing
} else if (!this.cropping && this.isMouseOnImage()) {
this.cropping = true;
this.isCroppingDone = false;
this.startX = x;
this.startY = y;
this.endX = x;
this.endY = y;
}
}
handleMove(ctx) {
this.autoPinNode();
const { x, y } = this.mousePos;
if (this.cropping || this.resizing) {
if (this.resizing) {
let newWidth, newHeight;
if (this.resizeRatio) {
// Determine new dimensions based on mouse movement
if (this.resizing.includes("left") || this.resizing.includes("right")) {
newWidth = Math.abs(this.resizing.includes("left") ? this.endX - x : x - this.startX);
newHeight = newWidth / this.resizeRatio;
} else if (this.resizing.includes("top") || this.resizing.includes("bottom")) {
newHeight = Math.abs(this.resizing.includes("top") ? this.endY - y : y - this.startY);
newWidth = newHeight * this.resizeRatio;
}
// Round dimensions to whole pixels
newWidth = Math.round(newWidth);
newHeight = Math.round(newHeight);
// Adjust based on specific handle boundaries so the crop stays inside the image
if (this.resizing === "top-left") {
if (this.endX - newWidth < this.imgOffsetX) {
newWidth = Math.round(this.endX - this.imgOffsetX);
newHeight = Math.round(newWidth / this.resizeRatio);
}
if (this.endY - newHeight < this.imgOffsetY) {
newHeight = Math.round(this.endY - this.imgOffsetY);
newWidth = Math.round(newHeight * this.resizeRatio);
}
} else if (this.resizing === "top-right") {
if (this.startX + newWidth > this.imgOffsetX + this.width) {
newWidth = Math.round(this.imgOffsetX + this.width - this.startX);
newHeight = Math.round(newWidth / this.resizeRatio);
}
if (this.endY - newHeight < this.imgOffsetY) {
newHeight = Math.round(this.endY - this.imgOffsetY);
newWidth = Math.round(newHeight * this.resizeRatio);
}
} else if (this.resizing === "bottom-left") {
if (this.endX - newWidth < this.imgOffsetX) {
newWidth = Math.round(this.endX - this.imgOffsetX);
newHeight = Math.round(newWidth / this.resizeRatio);
}
if (this.startY + newHeight > this.imgOffsetY + this.height) {
newHeight = Math.round(this.imgOffsetY + this.height - this.startY);
newWidth = Math.round(newHeight * this.resizeRatio);
}
} else if (this.resizing === "bottom-right") {
if (this.startX + newWidth > this.imgOffsetX + this.width) {
newWidth = Math.round(this.imgOffsetX + this.width - this.startX);
newHeight = Math.round(newWidth / this.resizeRatio);
}
if (this.startY + newHeight > this.imgOffsetY + this.height) {
newHeight = Math.round(this.imgOffsetY + this.height - this.startY);
newWidth = Math.round(newHeight * this.resizeRatio);
}
} else if (this.resizing === "top-mid") {
// Enforce aspect ratio for top-mid resizing
newHeight = Math.round(Math.abs(this.endY - y));
newWidth = Math.round(newHeight * this.resizeRatio);
if (this.endY - newHeight < this.imgOffsetY) {
newHeight = Math.round(this.endY - this.imgOffsetY);
newWidth = Math.round(newHeight * this.resizeRatio);
}
} else if (this.resizing === "bottom-mid") {
// Enforce aspect ratio for bottom-mid resizing
newHeight = Math.round(Math.abs(y - this.startY));
newWidth = Math.round(newHeight * this.resizeRatio);
if (this.startY + newHeight > this.imgOffsetY + this.height) {
newHeight = Math.round(this.imgOffsetY + this.height - this.startY);
newWidth = Math.round(newHeight * this.resizeRatio);
}
} else if (this.resizing === "left-mid") {
// Enforce aspect ratio for left-mid resizing
newWidth = Math.round(Math.abs(this.endX - x));
newHeight = Math.round(newWidth / this.resizeRatio);
if (this.endX - newWidth < this.imgOffsetX) {
newWidth = Math.round(this.endX - this.imgOffsetX);
newHeight = Math.round(newWidth / this.resizeRatio);
}
} else if (this.resizing === "right-mid") {
// Enforce aspect ratio for right-mid resizing
newWidth = Math.round(Math.abs(x - this.startX));
newHeight = Math.round(newWidth / this.resizeRatio);
if (this.startX + newWidth > this.imgOffsetX + this.width) {
newWidth = Math.round(this.imgOffsetX + this.width - this.startX);
newHeight = Math.round(newWidth / this.resizeRatio);
}
}
// Apply the new dimensions for the resizing handle
switch (this.resizing) {
case "top-left":
this.startX = Math.round(this.endX - newWidth);
this.startY = Math.round(this.endY - newHeight);
break;
case "top-right":
this.endX = Math.round(this.startX + newWidth);
this.startY = Math.round(this.endY - newHeight);
break;
case "bottom-left":
this.startX = Math.round(this.endX - newWidth);
this.endY = Math.round(this.startY + newHeight);
break;
case "bottom-right":
this.endX = Math.round(this.startX + newWidth);
this.endY = Math.round(this.startY + newHeight);
break;
case "top-mid":
this.startY = Math.round(this.endY - newHeight);
this.startX = Math.round(this.endX - newWidth); // Adjust X to maintain aspect ratio
break;
case "bottom-mid":
this.endY = Math.round(this.startY + newHeight);
this.endX = Math.round(this.startX + newWidth); // Adjust X to maintain aspect ratio
break;
case "left-mid":
this.startX = Math.round(this.endX - newWidth);
this.startY = Math.round(this.endY - newHeight); // Adjust Y to maintain aspect ratio
break;
case "right-mid":
this.endX = Math.round(this.startX + newWidth);
this.endY = Math.round(this.startY + newHeight); // Adjust Y to maintain aspect ratio
break;
}
} else {
// Non-ratio resizing (unchanged, but rounded)
if (this.resizing === "top-left") {
this.startX = Math.round(Math.max(this.imgOffsetX, x));
this.startY = Math.round(Math.max(this.imgOffsetY, y));
} else if (this.resizing === "top-right") {
this.endX = Math.round(Math.min(this.imgOffsetX + this.width, x));
this.startY = Math.round(Math.max(this.imgOffsetY, y));
} else if (this.resizing === "bottom-left") {
this.startX = Math.round(Math.max(this.imgOffsetX, x));
this.endY = Math.round(Math.min(this.imgOffsetY + this.height, y));
} else if (this.resizing === "bottom-right") {
this.endX = Math.round(Math.min(this.imgOffsetX + this.width, x));
this.endY = Math.round(Math.min(this.imgOffsetY + this.height, y));
} else if (this.resizing === "top-mid") {
this.startY = Math.round(Math.max(this.imgOffsetY, y));
} else if (this.resizing === "bottom-mid") {
this.endY = Math.round(Math.min(this.imgOffsetY + this.height, y));
} else if (this.resizing === "left-mid") {
this.startX = Math.round(Math.max(this.imgOffsetX, x));
} else if (this.resizing === "right-mid") {
this.endX = Math.round(Math.min(this.imgOffsetX + this.width, x));
}
}
this.showInfo()
} else {
// Regular cropping mode: clamp x,y to image boundaries and round values
this.endX = Math.round(Math.max(this.imgOffsetX, Math.min(this.imgOffsetX + this.width, x)));
this.endY = Math.round(Math.max(this.imgOffsetY, Math.min(this.imgOffsetY + this.height, y)));
}
}
if (this.dragging) {
const cropW = Math.round(Math.abs(this.endX - this.startX));
const cropH = Math.round(Math.abs(this.endY - this.startY));
let newStartX = Math.round(Math.max(this.imgOffsetX, x - this.dragOffsetX));
let newStartY = Math.round(Math.max(this.imgOffsetY, y - this.dragOffsetY));
// Clamp to ensure the crop box stays within image boundaries
newStartX = Math.round(Math.min(newStartX, this.imgOffsetX + this.width - cropW));
newStartY = Math.round(Math.min(newStartY, this.imgOffsetY + this.height - cropH));
this.startX = newStartX;
this.startY = newStartY;
this.endX = this.startX + cropW;
this.endY = this.startY + cropH;
this.showInfo()
return;
}
}
handleClick(e) {
if (allow_debug) console.log("this.node.widgets", this.node.widgets);
if (this.startX && this.endX && this.startY && this.endY) {
this.isCroppingDone = true;
this.saveCropData();
this.cropNewImage();
}
// Reset resizing state
this.cropping = false;
this.resizing = null;
this.dragging = false;
}
isMouseDown() {
return app.canvas.pointer.isDown;
}
showInfo() {
this.info.isVisible = true
// Calculate the scale factors for the image
const scaleX = this.img.naturalWidth / this.width;
const scaleY = this.img.naturalHeight / this.height;
// Calculate the crop area in the original image coordinates
const cropWidth = Math.round(Math.abs(this.endX - this.startX) * scaleX);
const cropHeight = Math.round(Math.abs(this.endY - this.startY) * scaleY);
this.info.text = `${cropWidth} x ${cropHeight}`
// if (allow_debug) console.log(`${cropWidth} x ${cropHeight}`);
}
hideInfo(){
this.info.isVisible = true
setTimeout(() => {
this.info.isVisible = false
}, 2000);
}
isMouseInCropArea(margin = 5) {
const { x, y } = this.mousePos;
const cropX = Math.min(this.startX, this.endX) + margin;
const cropY = Math.min(this.startY, this.endY) + margin;
const cropW = Math.abs(this.endX - this.startX) - 2 * margin;
const cropH = Math.abs(this.endY - this.startY) - 2 * margin;
return x >= cropX && x <= cropX + cropW && y >= cropY && y <= cropY + cropH;
}
isMouseOnImage() {
const { x, y } = this.mousePos;
return (
x >= this.imgOffsetX &&
x <= this.imgOffsetX + this.width &&
y >= this.imgOffsetY &&
y <= this.imgOffsetY + this.height
);
}
isMouseInResizeArea() {
const { x: mouseX, y: mouseY } = this.mousePos;
const threshold = this.resizeThreshold;
const cropX = Math.min(this.startX, this.endX);
const cropY = Math.min(this.startY, this.endY);
const cropW = Math.abs(this.endX - this.startX);
const cropH = Math.abs(this.endY - this.startY);
const dx = mouseX - (cropX + cropW / 2);
const dy = mouseY - (cropY + cropH / 2);
const radianAngle = -(this.rotationAngle * Math.PI) / 180;
const cosA = Math.cos(radianAngle);
const sinA = Math.sin(radianAngle);
const rotatedX = dx * cosA - dy * sinA;
const rotatedY = dx * sinA + dy * cosA;
const rx = rotatedX + cropW / 2;
const ry = rotatedY + cropH / 2;
const topLeft = rx >= -threshold && rx <= threshold && ry >= -threshold && ry <= threshold;
const topRight = rx >= cropW - threshold && rx <= cropW + threshold && ry >= -threshold && ry <= threshold;
const bottomLeft = rx >= -threshold && rx <= threshold && ry >= cropH - threshold && ry <= cropH + threshold;
const bottomRight =
rx >= cropW - threshold && rx <= cropW + threshold && ry >= cropH - threshold && ry <= cropH + threshold;
const top = rx >= 0 && rx <= cropW && ry >= -threshold && ry <= threshold;
const bottom = rx >= 0 && rx <= cropW && ry >= cropH - threshold && ry <= cropH + threshold;
const left = rx >= -threshold && rx <= threshold && ry >= 0 && ry <= cropH;
const right = rx >= cropW - threshold && rx <= cropW + threshold && ry >= 0 && ry <= cropH;
return topLeft || topRight || bottomLeft || bottomRight || top || bottom || left || right;
}
isLowQuality() {
const canvas = app.canvas;
const scale = canvas?.ds?.scale ?? 1; // Get scale, default to 1 if undefined
return scale <= 0.5;
}
delete() {
this.node.widgets = this.node.widgets.filter((widget) => widget !== this);
this.node.setDirtyCanvas(true, true);
}
get markDelete() {
return this._markDelete;
}
set markDelete(value) {
this._markDelete = Boolean(value);
}
get mousePos() {
const graphMouse = app.canvas.graph_mouse;
return {
x: graphMouse[0] - this.node.pos[0],
y: graphMouse[1] - this.node.pos[1],
};
}
}
app.registerExtension({
name: "iTools.cropImage",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "iToolsCropImage") {
return;
}
// const originalOnExecuted = nodeType.prototype.onExecuted;
// nodeType.prototype.onExecuted = async function (message) {
// originalOnExecuted?.apply(this, arguments);
// if(allow_debug) console.log('iToolsCropImage executed',message);
// }
},
async nodeCreated(node) {
if (node.comfyClass !== "iToolsCropImage") {
return;
}
// if (allow_debug) console.log("node.widgets_values", node.widgets_values);
// wait for init
const timeout = 3000; // 3 seconds
const startTime = Date.now();
while (!node.graph) {
if (Date.now() - startTime > timeout) {
if (allow_debug) console.error("Timeout: Failed to load graph.");
break;
}
if (allow_debug) console.log("loading ...");
await new Promise((resolve) => setTimeout(resolve, 200));
}
//START POINT
const cropPreview = new CropWidget(node);
node.addCustomWidget(cropPreview);
if (allow_debug) console.log("node", node);
const originalOnExecuted = node.onExecuted;
node.onExecuted = async function (message) {
originalOnExecuted?.apply(this, arguments);
if (allow_debug) console.log("iToolsCropImage executed", message);
};
node.clone = () => {
console.warn("Cloning is disabled for this node.");
return null;
};
app.canvas.onMouseDown = (e) => {
const nodes = app.graph.nodes;
nodes.forEach((n) => {
if (n.type === "iToolsCropImage") {
n.widgets[3]?.handleDown?.(e);
}
});
};
app.canvas.onDrawForeground = (ctx) => {
const nodes = app.graph.nodes;
nodes.forEach((n) => {
if (n.type === "iToolsCropImage") {
n.widgets[3]?.handleMove?.(ctx);
}
});
};
app.canvas.canvas.onclick = (e) => {
const nodes = app.graph.nodes;
nodes.forEach((n) => {
if (n.type === "iToolsCropImage") {
n.widgets[3]?.handleClick?.(e);
}
});
};
app.canvas.onMouseUp = (e) => {
if (allow_debug) {
console.log("ee");
}
};
},
});