Files
2025-04-22 19:29:59 +02:00

985 lines
31 KiB
JavaScript

import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
import { allow_debug } from "./js_shared.js";
import { Shapes } from "./utils.js";
import { BaseSmartWidget, BaseSmartWidgetManager } from "./makadi/BaseSmartWidget.js";
export class LiteInfo extends BaseSmartWidget {
constructor(x, y, width, height, node, text, options = {}) {
super(node);
this.myX = x;
this.myY = 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.myX + radius, this.myY);
ctx.lineTo(this.myX + this.width - radius, this.myY);
ctx.arcTo(this.myX + this.width, this.myY, this.myX + this.width, this.myY + radius, radius);
ctx.lineTo(this.myX + this.width, this.myY + this.height - radius);
ctx.arcTo(
this.myX + this.width,
this.myY + this.height,
this.myX + this.width - radius,
this.myY + this.height,
radius
);
ctx.lineTo(this.myX + radius, this.myY + this.height);
ctx.arcTo(this.myX, this.myY + this.height, this.myX, this.myY + this.height - radius, radius);
ctx.lineTo(this.myX, this.myY + radius);
ctx.arcTo(this.myX, this.myY, this.myX + radius, this.myY, 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.myX + this.width / 2 + this.textXoffset,
this.myY + 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 = {};
this.myX = 0;
this.myY = 80 + 20 + 20;
this.yOffset = this.myY + 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.myY]);
node.resizable = false;
this.resizeRatio = null;
this.multipleFactor = 64;
this.info = new LiteInfo(this.nodeSize / 2 - 40, this.myY + 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[4]) {
if (allow_debug) console.log("node has crop data value");
this.value = node.widgets_values[4];
this.node.properties.value = node.widgets_values[4];
} 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 Grid Step Changed
this.node.widgets[1].callback = () => {
this.adjustMultipleFactor();
};
// 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
for (let i = 0; i < 30 && this.img !== this.node.imgs[0]; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (this.img !== this.node.imgs[0]) {
if (allow_debug) console.log("Timeout waiting for image sync");
return;
}
this.cropNewImage();
};
this.node.onMouseUp = (e) => {
this.hideInfo();
};
this.node.onMouseLeave = (e) => {};
}
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.myY + (this.nodeSize - this.height) / 2;
this.node.setDirtyCanvas(true, true);
}
setResizeRatio() {
function ratioToFraction(ratio) {
if (ratio === "free") return null;
if (ratio === "grid") 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.myX, this.myY, this.node.width, this.node.height - this.yOffset);
// // Fill with background color
// ctx.fillStyle = this.node.bgcolor;
// ctx.fillRect(this.myX, this.myY, 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.myX + 10; // 10 pixels from the left edge of the node
const croppedImageY = this.myY + 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) {
this.adjustNewImageSize();
}
// 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");
// Cash values
this.cropWidth = cropWidth;
this.cropHeight = cropHeight;
// 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;
}
}
if (allow_debug) console.log("cropped image");
}
handleDown(e) {
const { x, y } = this.mousePos;
this.adjustMultipleFactor();
// 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;
// Helper function to round a number to the nearest multiple of
const roundToMultipleOf = (value) => {
return Math.round(value / this.multipleFactor) * this.multipleFactor;
};
if (this.cropping || this.resizing) {
if (this.resizing) {
let newWidth, newHeight;
const isCornerResizing =
this.resizing === "top-left" ||
this.resizing === "top-right" ||
this.resizing === "bottom-left" ||
this.resizing === "bottom-right";
const shouldMaintainRatio = !["free", "grid"].includes(this.node.widgets[0].value);
// Calculate 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);
if (shouldMaintainRatio) {
// For corners with aspect ratio, calculate height based on width
newHeight = newWidth / this.resizeRatio;
}
}
if (this.resizing.includes("top") || this.resizing.includes("bottom")) {
newHeight = Math.abs(this.resizing.includes("top") ? this.endY - y : y - this.startY);
if (shouldMaintainRatio) {
// For corners with aspect ratio, calculate width based on height
newWidth = newHeight * this.resizeRatio;
}
}
// Round width and height to the nearest multiple of this.multipleFactor
newWidth = roundToMultipleOf(newWidth);
newHeight = roundToMultipleOf(newHeight);
// Ensure the new dimensions respect the image boundaries
if (this.resizing.includes("left")) {
newWidth = Math.min(newWidth, this.endX - this.imgOffsetX);
} else if (this.resizing.includes("right")) {
newWidth = Math.min(newWidth, this.imgOffsetX + this.width - this.startX);
}
if (this.resizing.includes("top")) {
newHeight = Math.min(newHeight, this.endY - this.imgOffsetY);
} else if (this.resizing.includes("bottom")) {
newHeight = Math.min(newHeight, this.imgOffsetY + this.height - this.startY);
}
// For corner resizing with ratio maintenance, choose the constraining dimension
if (shouldMaintainRatio) {
// Calculate which dimension is more constrained
const widthRatio = newWidth / (newHeight * this.resizeRatio);
if (widthRatio < 1) {
// Width is more constrained, adjust height accordingly
newHeight = newWidth / this.resizeRatio;
} else {
// Height is more constrained, adjust width accordingly
newWidth = newHeight * this.resizeRatio;
}
// Re-round after aspect ratio adjustment
newWidth = roundToMultipleOf(newWidth);
newHeight = roundToMultipleOf(newHeight);
}
// Apply the new dimensions for the resizing handle
switch (this.resizing) {
case "top-left":
this.startX = this.endX - newWidth;
this.startY = this.endY - newHeight;
break;
case "top-right":
this.endX = this.startX + newWidth;
this.startY = this.endY - newHeight;
break;
case "bottom-left":
this.startX = this.endX - newWidth;
this.endY = this.startY + newHeight;
break;
case "bottom-right":
this.endX = this.startX + newWidth;
this.endY = this.startY + newHeight;
break;
case "top-mid":
this.startY = this.endY - newHeight;
break;
case "bottom-mid":
this.endY = this.startY + newHeight;
break;
case "left-mid":
this.startX = this.endX - newWidth;
break;
case "right-mid":
this.endX = this.startX + newWidth;
break;
}
// Ensure the crop area stays within the image boundaries
this.startX = Math.max(this.imgOffsetX, this.startX);
this.startY = Math.max(this.imgOffsetY, this.startY);
this.endX = Math.min(this.imgOffsetX + this.width, this.endX);
this.endY = Math.min(this.imgOffsetY + this.height, this.endY);
this.showInfo();
} else {
this.adjustMultipleFactor();
// Calculate the new endX and endY based on the mouse position
let newEndX = roundToMultipleOf(Math.max(this.imgOffsetX, Math.min(this.imgOffsetX + this.width, x)));
let newEndY = roundToMultipleOf(Math.max(this.imgOffsetY, Math.min(this.imgOffsetY + this.height, y)));
// Calculate the width and height of the crop area
let cropWidth = newEndX - this.startX;
let cropHeight = newEndY - this.startY;
// Round the width and height to the nearest multiple of this.multipleFactor
cropWidth = roundToMultipleOf(cropWidth);
cropHeight = roundToMultipleOf(cropHeight);
// Adjust the endX and endY to maintain the rounded width and height
this.endX = this.startX + cropWidth;
this.endY = this.startY + cropHeight;
// Ensure the crop area stays within the image boundaries
this.endX = Math.min(this.endX, this.imgOffsetX + this.width);
this.endY = Math.min(this.endY, this.imgOffsetY + this.height);
this.showInfo();
}
}
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;
return;
}
}
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}`;
}
adjustMultipleFactor() {
const mode = this.node.widgets[0].value;
if (allow_debug) console.log("mode", mode);
if (mode === "grid") {
const value = this.node.widgets[1].value;
// 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.floor(Math.abs(this.endX - this.startX) * scaleX);
const cropHeight = Math.floor(Math.abs(this.endY - this.startY) * scaleY);
// Adjust the multipleFactor based on the user's input and the scaling factors
// The multipleFactor should be such that when multiplied by the scaling factor,
// it results in the desired multiple in the true output image.
this.multipleFactor = value / Math.min(scaleX, scaleY);
} else {
this.multipleFactor = 1;
}
}
handleClick(e) {
if (this.isMouseOnImage()) {
if (this.startX && this.endX && this.startY && this.endY) {
this.isCroppingDone = true;
this.cropNewImage();
this.saveCropData();
}
// Reset resizing state
this.cropping = false;
this.resizing = null;
this.dragging = false;
}
}
isMouseDown() {
return app.canvas.pointer.isDown;
}
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;
}
// Wait for init
for (let i = 0; i < 30 && node.graph === null; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (node.graph === null) {
if (allow_debug) console.log("Timeout waiting for graph initialization");
return;
}
// Await for ImagePreviewWidget
const previewWidget = node.widgets.find((widget) => (widget.type == "custom"));
for (let i = 0; i < 30 && !previewWidget; i++) {
if (allow_debug) console.log("wait...", i);
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (!previewWidget) {
if (allow_debug) console.log("ImagePreviewWidget not found");
}
if(allow_debug) console.log('previewWidget',previewWidget);
// Override draw function in ImagePreviewWidget
const originalDraw = previewWidget.draw;
if(allow_debug) console.log('originalDraw',originalDraw);
previewWidget.draw = function (ctx, node, widget_width, y, widget_height, ...args) {};
node.setDirtyCanvas(true, true);
//START POINT
const cropPreview = new CropWidget(node);
node.addCustomWidget(cropPreview);
// if (allow_debug) console.log("cropPreviewNode", 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;
};
node.onRemoved = () => {
node.widgets = []
};
// Store the original onMouseDown handler
const originalOnMouseDown = app.canvas.onMouseDown;
app.canvas.onMouseDown = (e) => {
// Call original handler if it exists
if (originalOnMouseDown) {
originalOnMouseDown.call(app.canvas, e);
}
const nodes = app.graph.nodes;
nodes.forEach((n) => {
if (n.type === "iToolsCropImage") {
n.widgets.forEach((w) => {
w.handleDown?.(e);
});
}
});
};
// Store the original onDrawForeground handler
const originalOnDrawForeground = app.canvas.onDrawForeground;
app.canvas.onDrawForeground = (ctx) => {
// Call original handler if it exists
if (originalOnDrawForeground) {
originalOnDrawForeground.call(app.canvas, ctx);
}
const nodes = app.graph.nodes;
nodes.forEach((n) => {
if (n.type === "iToolsCropImage") {
n.widgets.forEach((w) => {
w.handleMove?.(ctx);
});
}
});
};
// Store the original onclick handler
const originalOnClick = app.canvas.canvas.onclick;
app.canvas.canvas.onclick = (e) => {
// Call original handler if it exists
if (originalOnClick) {
originalOnClick.call(app.canvas.canvas, e);
}
const nodes = app.graph.nodes;
nodes.forEach((n) => {
if (n.type === "iToolsCropImage") {
n.widgets.forEach((w) => {
w.handleClick?.(e);
});
}
});
};
app.canvas.onMouseUp = (e) => {
if (allow_debug) {
console.log("mouse up");
}
};
},
});