Files

757 lines
39 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../scripts/app.js";
import { $el } from "../../../scripts/ui.js"; // Import $el for creating elements if needed
import { SmartImage } from "./image.js"; // Import our SmartImage class
// Displays an image preview directly within the node with a draggable rectangle region
app.registerExtension({
name: "Comfy.SmartNodes.SmartImageRegion",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SmartImageRegion") {
// Find the region_data input widget
function getRegionWidget(node) {
return node.widgets.find(w => w.name === "region_data");
}
// Find the type widget
function getTypeWidget(node) {
return node.widgets.find(w => w.name === "type");
}
// Initialize region properties
const initProperties = (ctx) => {
if (!ctx.properties) {
ctx.properties = {};
}
// Get the hidden widget
const regionWidget = getRegionWidget(ctx);
const typeWidget = getTypeWidget(ctx);
const regionType = typeWidget ? typeWidget.value : "Rectangle";
if (regionType === "Rectangle") {
let region = { x1: 0.25, y1: 0.25, x2: 0.75, y2: 0.75 };
if (regionWidget && regionWidget.value) {
// Parse region from the widget value
try {
const parsed = JSON.parse(regionWidget.value);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
region = {
x1: parsed.x1 !== undefined ? parseFloat(parsed.x1) : 0.25,
y1: parsed.y1 !== undefined ? parseFloat(parsed.y1) : 0.25,
x2: parsed.x2 !== undefined ? parseFloat(parsed.x2) : 0.75,
y2: parsed.y2 !== undefined ? parseFloat(parsed.y2) : 0.75
};
}
} catch (e) {
console.error("Error parsing region_data:", e);
}
}
// Store region in properties
ctx.properties.region = region;
} else { // Polygon
// Default polygon (triangle)
let points = [
{ x: 0.25, y: 0.25 },
{ x: 0.75, y: 0.25 },
{ x: 0.5, y: 0.75 }
];
if (regionWidget && regionWidget.value) {
// Parse polygon points from the widget value
try {
const parsed = JSON.parse(regionWidget.value);
if (Array.isArray(parsed) && parsed.length >= 3) {
// Validate points
points = parsed.map(point => ({
x: point.x !== undefined ? Math.max(0, Math.min(1, parseFloat(point.x))) : 0.5,
y: point.y !== undefined ? Math.max(0, Math.min(1, parseFloat(point.y))) : 0.5
}));
}
} catch (e) {
console.error("Error parsing polygon points:", e);
}
}
// Store points in properties
ctx.properties.points = points;
}
// Update widget with initial value based on type
if (regionWidget) {
if (regionType === "Rectangle") {
regionWidget.value = JSON.stringify(ctx.properties.region);
} else { // Polygon
regionWidget.value = JSON.stringify(ctx.properties.points);
}
}
ctx.dragCorner = null; // Which corner/point is being dragged: null, "tl", "tr", "bl", "br" or point index
ctx.isDragging = false;
ctx.resetButtonRect = null;
ctx.addButtonRect = null;
ctx.removeButtonRect = null;
// Initialize SmartImage
ctx.smartImage = new SmartImage();
};
// Function to update widget when region changes
const updateRegionWidget = (node) => {
const regionWidget = getRegionWidget(node);
const typeWidget = getTypeWidget(node);
if (!regionWidget) return;
const regionType = typeWidget ? typeWidget.value : "Rectangle";
if (regionType === "Rectangle") {
if (!node.properties.region) return;
regionWidget.value = JSON.stringify(node.properties.region);
} else { // Polygon
if (!node.properties.points) return;
regionWidget.value = JSON.stringify(node.properties.points);
}
// Notify ComfyUI that a parameter changed
if (regionWidget.callback) {
regionWidget.callback(regionWidget.value);
}
};
// Initialize on node creation
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated?.apply(this, arguments);
initProperties(this);
// Setup callback on type widget to reinitialize properties
const typeWidget = getTypeWidget(this);
if (typeWidget) {
const originalCallback = typeWidget.callback;
typeWidget.callback = (value) => {
if (originalCallback) {
originalCallback(value);
}
// Re-initialize properties when type changes
initProperties(this);
this.setDirtyCanvas(true, true);
};
}
return result;
};
// Store image data and dimensions
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
let newImageLoaded = false;
if (message?.images && message?.images[0]) {
newImageLoaded = this.smartImage.loadImage(message.images[0]);
} else {
this.smartImage.loadImage(null);
}
if (message?.dimensions && message?.dimensions[0]) {
this.smartImage.setDimensions(message.dimensions[0]);
} else {
this.smartImage.setDimensions(null);
}
// Get the region type
const typeWidget = getTypeWidget(this);
const regionType = typeWidget ? typeWidget.value : "Rectangle";
// Check if region_data is provided from the backend
if (message?.region_data) {
try {
// Sanitize input before parsing
const regionDataString = String(message.region_data).trim();
// Skip invalid or empty JSON
if (!regionDataString || regionDataString === "null" || regionDataString === "undefined") {
console.warn("Empty region_data received");
} else {
const regionFromBackend = JSON.parse(regionDataString);
if (regionType === "Rectangle") {
// Rectangle mode
if (typeof regionFromBackend === 'object' && regionFromBackend !== null && !Array.isArray(regionFromBackend)) {
// Validate and ensure all required properties exist
const validRegion = {
x1: 'x1' in regionFromBackend ? parseFloat(regionFromBackend.x1) : 0.25,
y1: 'y1' in regionFromBackend ? parseFloat(regionFromBackend.y1) : 0.25,
x2: 'x2' in regionFromBackend ? parseFloat(regionFromBackend.x2) : 0.75,
y2: 'y2' in regionFromBackend ? parseFloat(regionFromBackend.y2) : 0.75
};
// Ensure all values are in valid range
validRegion.x1 = Math.max(0, Math.min(1, validRegion.x1));
validRegion.y1 = Math.max(0, Math.min(1, validRegion.y1));
validRegion.x2 = Math.max(0, Math.min(1, validRegion.x2));
validRegion.y2 = Math.max(0, Math.min(1, validRegion.y2));
// Update region properties
if (newImageLoaded || JSON.stringify(this.properties.region) !== JSON.stringify(validRegion)) {
this.properties.region = validRegion;
}
} else {
console.warn("Rectangle region_data is not a valid object:", regionFromBackend);
}
} else { // Polygon
// Polygon mode
if (Array.isArray(regionFromBackend) && regionFromBackend.length >= 3) {
// Validate and normalize points
const validPoints = regionFromBackend.map(point => ({
x: 'x' in point ? Math.max(0, Math.min(1, parseFloat(point.x))) : 0.5,
y: 'y' in point ? Math.max(0, Math.min(1, parseFloat(point.y))) : 0.5
}));
// Update points
if (newImageLoaded || JSON.stringify(this.properties.points) !== JSON.stringify(validPoints)) {
this.properties.points = validPoints;
}
} else {
console.warn("Polygon region_data is not a valid array:", regionFromBackend);
}
}
}
} catch (e) {
console.error("Error parsing region_data from backend:", e, "\nRaw value:", message.region_data);
}
}
this.setDirtyCanvas(true, true);
};
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
onDrawForeground?.apply(this, arguments);
// Ensure properties are initialized if they were somehow lost
if (!this.properties || (!this.properties.region && !this.properties.points) || !this.smartImage) {
initProperties(this);
}
const margin = 5;
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT || 20;
const bottomBarHeight = 16; // Height for buttons and text
const buttonWidth = 60; // Larger button
const smallButtonWidth = 40; // Smaller button for Add/Remove
const textFontSize = 10;
const nodeWidth = this.size[0];
const nodeHeight = this.size[1];
const imageTopMargin = 30;
// Get canvas zoom and adjust handle size accordingly
const baseHandleSize = 5;
const canvasScale = 1; //app.canvas?.ds?.scale || 1;
const handleSize = baseHandleSize / canvasScale;
// Get the region type
const typeWidget = getTypeWidget(this);
const regionType = typeWidget ? typeWidget.value : "Rectangle";
// Draw the image using SmartImage
const imageRect = this.smartImage.drawImage(ctx, 0, imageTopMargin, nodeWidth, nodeHeight - bottomBarHeight - imageTopMargin);
if (imageRect) {
if (regionType === "Rectangle") {
// RECTANGLE MODE
// Get normalized region coordinates
const region = this.properties.region;
// Convert normalized coordinates to canvas coordinates
const x1Canvas = imageRect.x + region.x1 * imageRect.w;
const y1Canvas = imageRect.y + region.y1 * imageRect.h;
const x2Canvas = imageRect.x + region.x2 * imageRect.w;
const y2Canvas = imageRect.y + region.y2 * imageRect.h;
// Draw rectangle outline
ctx.strokeStyle = "rgba(0, 255, 255, 0.8)"; // Cyan color
ctx.lineWidth = 1;
ctx.beginPath();
ctx.rect(
Math.min(x1Canvas, x2Canvas),
Math.min(y1Canvas, y2Canvas),
Math.abs(x2Canvas - x1Canvas),
Math.abs(y2Canvas - y1Canvas)
);
ctx.stroke();
// Draw semi-transparent fill
ctx.fillStyle = "rgba(0, 255, 255, 0.2)"; // Cyan with transparency
ctx.fill();
// Draw corner handles (squares)
const cornerPoints = [
{ x: x1Canvas, y: y1Canvas, id: "tl" }, // Top-left
{ x: x2Canvas, y: y1Canvas, id: "tr" }, // Top-right
{ x: x1Canvas, y: y2Canvas, id: "bl" }, // Bottom-left
{ x: x2Canvas, y: y2Canvas, id: "br" } // Bottom-right
];
ctx.fillStyle = "rgba(255, 255, 255, 0.9)"; // White fill
ctx.strokeStyle = "rgba(0, 0, 0, 0.9)"; // Black outline
ctx.lineWidth = 0.5;
for (const corner of cornerPoints) {
// Draw corner handle (centered on corner point)
ctx.beginPath();
ctx.rect(
corner.x - handleSize/2,
corner.y - handleSize/2,
handleSize,
handleSize
);
ctx.fill();
ctx.stroke();
}
// Bottom bar text for rectangle
const regionWidth = Math.abs(this.properties.region.x2 - this.properties.region.x1);
const regionHeight = Math.abs(this.properties.region.y2 - this.properties.region.y1);
let displayText = `Region: ${(regionWidth*100).toFixed(0)}% × ${(regionHeight*100).toFixed(0)}%`;
if (this.smartImage.dimensions) {
const pixelWidth = Math.round(regionWidth * this.smartImage.dimensions[0]);
const pixelHeight = Math.round(regionHeight * this.smartImage.dimensions[1]);
displayText += ` (${pixelWidth}×${pixelHeight}px)`;
}
// Draw Bottom Bar with rectangle info and reset button
const bottomY = nodeHeight - margin - bottomBarHeight;
// Display region dimensions as text
ctx.fillStyle = "#CCC";
ctx.font = `${textFontSize}px Arial`;
ctx.textAlign = "left";
ctx.fillText(displayText, margin, bottomY + bottomBarHeight/2 + textFontSize/3);
// "Reset" Button (right aligned)
this.resetButtonRect = {
x: nodeWidth - margin - buttonWidth,
y: bottomY,
w: buttonWidth,
h: bottomBarHeight
};
ctx.fillStyle = "#444"; // Button background
ctx.fillRect(this.resetButtonRect.x, this.resetButtonRect.y, this.resetButtonRect.w, this.resetButtonRect.h);
ctx.fillStyle = "#CCC";
ctx.textAlign = "center";
ctx.fillText("Reset", this.resetButtonRect.x + buttonWidth/2, bottomY + bottomBarHeight/2 + textFontSize/3);
// Clear polygon-specific buttons
this.addButtonRect = null;
this.removeButtonRect = null;
} else {
// POLYGON MODE
// Get normalized polygon points
const points = this.properties.points;
if (points.length < 3) {
// Ensure we have at least 3 points
this.properties.points = [
{ x: 0.25, y: 0.25 },
{ x: 0.75, y: 0.25 },
{ x: 0.5, y: 0.75 }
];
}
// Convert normalized coordinates to canvas coordinates
const canvasPoints = points.map(point => ({
x: imageRect.x + point.x * imageRect.w,
y: imageRect.y + point.y * imageRect.h
}));
// Draw polygon outline
ctx.strokeStyle = "rgba(0, 255, 255, 0.8)"; // Cyan color
ctx.lineWidth = 1;
ctx.beginPath();
if (canvasPoints.length > 0) {
ctx.moveTo(canvasPoints[0].x, canvasPoints[0].y);
for (let i = 1; i < canvasPoints.length; i++) {
ctx.lineTo(canvasPoints[i].x, canvasPoints[i].y);
}
ctx.closePath();
}
ctx.stroke();
// Draw semi-transparent fill
ctx.fillStyle = "rgba(0, 255, 255, 0.2)"; // Cyan with transparency
ctx.fill();
// Draw vertex handles (squares)
ctx.fillStyle = "rgba(255, 255, 255, 0.9)"; // White fill
ctx.strokeStyle = "rgba(0, 0, 0, 0.9)"; // Black outline
ctx.lineWidth = 0.5;
for (let i = 0; i < canvasPoints.length; i++) {
const point = canvasPoints[i];
// Draw vertex handle
ctx.beginPath();
ctx.rect(
point.x - handleSize/2,
point.y - handleSize/2,
handleSize,
handleSize
);
ctx.fill();
ctx.stroke();
}
// Bottom bar for polygon
const bottomY = nodeHeight - margin - bottomBarHeight;
// Display polygon vertex count
ctx.fillStyle = "#CCC";
ctx.font = `${textFontSize}px Arial`;
ctx.textAlign = "left";
let displayText = `Polygon: ${points.length} vertices`;
ctx.fillText(displayText, margin + 2*smallButtonWidth + 10, bottomY + bottomBarHeight/2 + textFontSize/3);
// "Add" Button (left aligned)
this.addButtonRect = {
x: margin,
y: bottomY,
w: smallButtonWidth,
h: bottomBarHeight
};
ctx.fillStyle = "#2a662a"; // Green button background
ctx.fillRect(this.addButtonRect.x, this.addButtonRect.y, this.addButtonRect.w, this.addButtonRect.h);
ctx.fillStyle = "#CCC";
ctx.textAlign = "center";
ctx.fillText("Add", this.addButtonRect.x + smallButtonWidth/2, bottomY + bottomBarHeight/2 + textFontSize/3);
// "Remove" Button (second from left)
this.removeButtonRect = {
x: margin + smallButtonWidth + 5,
y: bottomY,
w: smallButtonWidth,
h: bottomBarHeight
};
ctx.fillStyle = points.length <= 3 ? "#666" : "#662a2a"; // Red button background (gray if disabled)
ctx.fillRect(this.removeButtonRect.x, this.removeButtonRect.y, this.removeButtonRect.w, this.removeButtonRect.h);
ctx.fillStyle = "#CCC";
ctx.textAlign = "center";
ctx.fillText("Remove", this.removeButtonRect.x + smallButtonWidth/2, bottomY + bottomBarHeight/2 + textFontSize/3);
// "Reset" Button (right aligned)
this.resetButtonRect = {
x: nodeWidth - margin - buttonWidth,
y: bottomY,
w: buttonWidth,
h: bottomBarHeight
};
ctx.fillStyle = "#444"; // Button background
ctx.fillRect(this.resetButtonRect.x, this.resetButtonRect.y, this.resetButtonRect.w, this.resetButtonRect.h);
ctx.fillStyle = "#CCC";
ctx.textAlign = "center";
ctx.fillText("Reset", this.resetButtonRect.x + buttonWidth/2, bottomY + bottomBarHeight/2 + textFontSize/3);
}
}
};
nodeType.prototype.onMouseDown = function(e, localPos, graphCanvas) {
if (!e.isPrimary) return false; // Only left click
const clickPos = { x: localPos[0], y: localPos[1] };
const typeWidget = getTypeWidget(this);
const regionType = typeWidget ? typeWidget.value : "Rectangle";
// Check buttons regardless of type
// Check Reset Button Click
if (this.resetButtonRect && this.smartImage.pointInRect(clickPos, this.resetButtonRect)) {
if (regionType === "Rectangle") {
// Reset to default rectangle
this.properties.region = { x1: 0.25, y1: 0.25, x2: 0.75, y2: 0.75 };
} else {
// Reset to default triangle
this.properties.points = [
{ x: 0.25, y: 0.25 },
{ x: 0.75, y: 0.25 },
{ x: 0.5, y: 0.75 }
];
}
updateRegionWidget(this);
this.setDirtyCanvas(true, true);
return true; // Event handled
}
// Polygon-specific buttons
if (regionType === "Polygon") {
// Add button
if (this.addButtonRect && this.smartImage.pointInRect(clickPos, this.addButtonRect)) {
const points = this.properties.points;
if (points && points.length >= 3) {
// Add a new point by averaging the last two points
const lastPoint = points[points.length - 1];
const secondLastPoint = points[points.length - 2];
const newPoint = {
x: (lastPoint.x + secondLastPoint.x) / 2,
y: (lastPoint.y + secondLastPoint.y) / 2
};
// Offset slightly to make it visible
newPoint.x += 0.05;
newPoint.y += 0.05;
// Ensure in bounds
newPoint.x = Math.max(0, Math.min(1, newPoint.x));
newPoint.y = Math.max(0, Math.min(1, newPoint.y));
points.push(newPoint);
updateRegionWidget(this);
this.setDirtyCanvas(true, true);
}
return true; // Event handled
}
// Remove button (only if we have more than 3 points)
if (this.removeButtonRect && this.smartImage.pointInRect(clickPos, this.removeButtonRect)) {
const points = this.properties.points;
if (points && points.length > 3) {
points.pop(); // Remove the last point
updateRegionWidget(this);
this.setDirtyCanvas(true, true);
}
return true; // Event handled
}
}
// Process region/points dragging based on type
if (this.smartImage.imageDrawRect) {
const imageRect = this.smartImage.imageDrawRect;
if (regionType === "Rectangle") {
// RECTANGLE DRAG HANDLING
const region = this.properties.region;
// Convert normalized coordinates to canvas coordinates
const x1Canvas = imageRect.x + region.x1 * imageRect.w;
const y1Canvas = imageRect.y + region.y1 * imageRect.h;
const x2Canvas = imageRect.x + region.x2 * imageRect.w;
const y2Canvas = imageRect.y + region.y2 * imageRect.h;
// Define corner hit areas with zoom adjustment
const baseHandleSize = 12; // Slightly larger than visual size for easier clicking
const canvasScale = 1;
const handleSize = baseHandleSize / canvasScale;
const cornerPoints = [
{ x: x1Canvas, y: y1Canvas, id: "tl" }, // Top-left
{ x: x2Canvas, y: y1Canvas, id: "tr" }, // Top-right
{ x: x1Canvas, y: y2Canvas, id: "bl" }, // Bottom-left
{ x: x2Canvas, y: y2Canvas, id: "br" } // Bottom-right
];
// Check if click is on a corner handle
for (const corner of cornerPoints) {
const dx = clickPos.x - corner.x;
const dy = clickPos.y - corner.y;
if (Math.abs(dx) <= handleSize/2 && Math.abs(dy) <= handleSize/2) {
this.isDragging = true;
this.dragCorner = corner.id;
this.setDirtyCanvas(true, false);
return true; // Event handled
}
}
// Check if click is inside rectangle (for moving the entire rectangle)
const rectLeft = Math.min(x1Canvas, x2Canvas);
const rectTop = Math.min(y1Canvas, y2Canvas);
const rectWidth = Math.abs(x2Canvas - x1Canvas);
const rectHeight = Math.abs(y2Canvas - y1Canvas);
if (clickPos.x >= rectLeft && clickPos.x <= rectLeft + rectWidth &&
clickPos.y >= rectTop && clickPos.y <= rectTop + rectHeight) {
// Store drag start position and region state for move operation
this.isDragging = true;
this.dragCorner = "move";
this.dragStartPos = { x: clickPos.x, y: clickPos.y };
this.dragStartRegion = { ...this.properties.region };
this.setDirtyCanvas(true, false);
return true; // Event handled
}
} else {
// POLYGON DRAG HANDLING
const points = this.properties.points;
if (!points || points.length < 3) return false;
// Convert normalized coordinates to canvas coordinates
const canvasPoints = points.map(point => ({
x: imageRect.x + point.x * imageRect.w,
y: imageRect.y + point.y * imageRect.h
}));
// Define vertex hit area size
const baseHandleSize = 12; // Slightly larger than visual for easier clicking
const canvasScale = 1;
const handleSize = baseHandleSize / canvasScale;
// Check if click is on a vertex handle
for (let i = 0; i < canvasPoints.length; i++) {
const point = canvasPoints[i];
const dx = clickPos.x - point.x;
const dy = clickPos.y - point.y;
if (Math.abs(dx) <= handleSize/2 && Math.abs(dy) <= handleSize/2) {
this.isDragging = true;
this.dragCorner = i; // Store vertex index
this.setDirtyCanvas(true, false);
return true; // Event handled
}
}
// Check if click is inside polygon (for moving entire polygon)
// This implementation uses the even-odd rule to check if a point is in a polygon
let isInside = false;
for (let i = 0, j = canvasPoints.length - 1; i < canvasPoints.length; j = i++) {
if (((canvasPoints[i].y > clickPos.y) != (canvasPoints[j].y > clickPos.y)) &&
(clickPos.x < (canvasPoints[j].x - canvasPoints[i].x) * (clickPos.y - canvasPoints[i].y) /
(canvasPoints[j].y - canvasPoints[i].y) + canvasPoints[i].x)) {
isInside = !isInside;
}
}
if (isInside) {
// Store drag start position and polygon state for move operation
this.isDragging = true;
this.dragCorner = "move";
this.dragStartPos = { x: clickPos.x, y: clickPos.y };
this.dragStartPoints = JSON.parse(JSON.stringify(this.properties.points)); // Deep copy
this.setDirtyCanvas(true, false);
return true; // Event handled
}
}
}
return false; // Event not handled
};
nodeType.prototype.onMouseMove = function(e, localPos, graphCanvas) {
if (!this.isDragging || this.dragCorner === null || !this.smartImage.imageDrawRect) return false;
const rect = this.smartImage.imageDrawRect;
const typeWidget = getTypeWidget(this);
const regionType = typeWidget ? typeWidget.value : "Rectangle";
// Convert mouse position to normalized image coordinates
const mouseNormX = Math.max(0, Math.min(1, (localPos[0] - rect.x) / rect.w));
const mouseNormY = Math.max(0, Math.min(1, (localPos[1] - rect.y) / rect.h));
if (regionType === "Rectangle") {
// RECTANGLE MOVE/RESIZE
// Get current region
const region = this.properties.region;
if (this.dragCorner === "move") {
// Moving the entire rectangle
// Calculate the delta in canvas coordinates
const deltaCanvasX = localPos[0] - this.dragStartPos.x;
const deltaCanvasY = localPos[1] - this.dragStartPos.y;
// Convert to normalized coordinates
const deltaNormX = deltaCanvasX / rect.w;
const deltaNormY = deltaCanvasY / rect.h;
// Original width and height
const width = this.dragStartRegion.x2 - this.dragStartRegion.x1;
const height = this.dragStartRegion.y2 - this.dragStartRegion.y1;
// Calculate new position ensuring it stays within bounds
let newX1 = Math.max(0, Math.min(1 - width, this.dragStartRegion.x1 + deltaNormX));
let newY1 = Math.max(0, Math.min(1 - height, this.dragStartRegion.y1 + deltaNormY));
// Update all corners preserving width/height
region.x1 = newX1;
region.y1 = newY1;
region.x2 = newX1 + width;
region.y2 = newY1 + height;
} else {
// Dragging a corner
switch (this.dragCorner) {
case "tl": // Top-left
region.x1 = mouseNormX;
region.y1 = mouseNormY;
break;
case "tr": // Top-right
region.x2 = mouseNormX;
region.y1 = mouseNormY;
break;
case "bl": // Bottom-left
region.x1 = mouseNormX;
region.y2 = mouseNormY;
break;
case "br": // Bottom-right
region.x2 = mouseNormX;
region.y2 = mouseNormY;
break;
}
}
} else {
// POLYGON VERTEX MOVE
const points = this.properties.points;
if (this.dragCorner === "move") {
// Moving the entire polygon
if (!this.dragStartPoints) return false;
// Calculate the delta in canvas coordinates
const deltaCanvasX = localPos[0] - this.dragStartPos.x;
const deltaCanvasY = localPos[1] - this.dragStartPos.y;
// Convert to normalized coordinates
const deltaNormX = deltaCanvasX / rect.w;
const deltaNormY = deltaCanvasY / rect.h;
// Apply delta to all points with bounds checking
for (let i = 0; i < points.length; i++) {
if (i < this.dragStartPoints.length) {
points[i].x = Math.max(0, Math.min(1, this.dragStartPoints[i].x + deltaNormX));
points[i].y = Math.max(0, Math.min(1, this.dragStartPoints[i].y + deltaNormY));
}
}
} else if (typeof this.dragCorner === 'number' && this.dragCorner >= 0 && this.dragCorner < points.length) {
// Moving a specific vertex
points[this.dragCorner].x = mouseNormX;
points[this.dragCorner].y = mouseNormY;
}
}
// Update widget with new region values
updateRegionWidget(this);
this.setDirtyCanvas(true, false);
return true; // Event handled
};
nodeType.prototype.onMouseUp = function(e, localPos, graphCanvas) {
if (this.isDragging) {
this.isDragging = false;
this.dragCorner = null;
this.dragStartPos = null;
this.dragStartRegion = null;
this.dragStartPoints = null;
this.setDirtyCanvas(true, false);
return true; // Event handled
}
return false;
};
// Make node resizable
nodeType.prototype.onResize = function(size) {
this.setDirtyCanvas(true, true);
};
}
},
});