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); }; } }, });