757 lines
39 KiB
JavaScript
757 lines
39 KiB
JavaScript
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);
|
||
};
|
||
}
|
||
},
|
||
});
|