Files
robertvoy-ComfyUI-Flux-Cont…/web/tabs.js
T

521 lines
18 KiB
JavaScript

import { app } from "../../../scripts/app.js";
/**
* Constants for node and tab dimensions and appearance
* @readonly
*/
const NODE_METRICS = {
TITLE_HEIGHT: 30,
TAB_HEIGHT: 48,
DEFAULT_TAB_X_OFFSET: 10,
DEFAULT_SECOND_TAB_OFFSET: 80,
MIN_TAB_SPACING: 5 // Minimum spacing between tabs
};
/**
* Style configuration for tabs
* @readonly
*/
const TAB_STYLE = {
defaultWidth: 65,
minWidth: 30, // Minimum allowed tab width
maxWidth: 200, // Maximum allowed tab width
height: 18,
fontSize: 9,
normalColor: "#5a5a5a",
textColor: "white",
borderRadius: 4,
fontFamily: "'Segoe UI', Arial, sans-serif"
};
/**
* Utility class for bounding box calculations
* Optimizes memory usage by reusing a single Float32Array
*/
class BoundingBoxCalculator {
constructor() {
this.boundingBoxBuffer = new Float32Array(4);
}
/**
* Calculate the bounding box containing all provided rectangles
* @param {Array<{x: number, y: number, width: number, height: number}>} rects - Array of rectangles
* @returns {Float32Array} Bounding box as [x, y, width, height]
*/
calculate(rects) {
if (!rects || rects.length === 0) {
this.boundingBoxBuffer.set([0, 0, 0, 0]);
return this.boundingBoxBuffer;
}
let minX = rects[0].x;
let minY = rects[0].y;
let maxX = rects[0].x + rects[0].width;
let maxY = rects[0].y + rects[0].height;
for (let i = 1; i < rects.length; i++) {
const rect = rects[i];
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
this.boundingBoxBuffer.set([minX, minY, maxX - minX, maxY - minY]);
return this.boundingBoxBuffer;
}
}
// Create a single instance of the calculator for reuse
const boundingBoxCalculator = new BoundingBoxCalculator();
/**
* Class to handle tab interactions and rendering
*/
class TabManager {
/**
* Get the width of a tab, with validation
* @param {Object} config - Tab configuration
* @param {boolean} isSecondTab - Whether this is the second tab
* @returns {number} - The tab width
*/
static getTabWidth(config, isSecondTab = false) {
try {
if (!config) {
return TAB_STYLE.defaultWidth;
}
let width;
if (isSecondTab) {
width = config.secondTabWidth || TAB_STYLE.defaultWidth;
} else {
width = config.width || TAB_STYLE.defaultWidth;
}
// Validate width is within acceptable range
return Math.min(Math.max(width, TAB_STYLE.minWidth), TAB_STYLE.maxWidth);
} catch (error) {
console.warn("Error getting tab width:", error);
return TAB_STYLE.defaultWidth;
}
}
/**
* Check if tabs would overlap with current configuration
* @param {Object} properties - Node properties
* @returns {boolean} - True if tabs would overlap
*/
static wouldTabsOverlap(properties) {
if (!properties.hasSecondTab) return false;
const tab1End = properties.tabXOffset + properties.tabWidth;
const tab2Start = properties.secondTabOffset;
return tab2Start < tab1End + NODE_METRICS.MIN_TAB_SPACING;
}
/**
* Draw a tab on the canvas
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - X position
* @param {number} y - Y position
* @param {string} text - Tab text
* @param {number} width - Tab width
*/
static drawTab(ctx, x, y, text, width) {
const { height, fontSize, normalColor, textColor, borderRadius, fontFamily } = TAB_STYLE;
ctx.save();
ctx.fillStyle = normalColor;
// Draw tab background
ctx.beginPath();
ctx.moveTo(x + borderRadius, y);
ctx.lineTo(x + width - borderRadius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + borderRadius);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y + borderRadius);
ctx.quadraticCurveTo(x, y, x + borderRadius, y);
ctx.closePath();
ctx.fill();
// Text rendering with optimizations
ctx.fillStyle = textColor;
ctx.textRendering = 'optimizeLegibility';
ctx.imageSmoothingEnabled = true;
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// Truncate text if it's too long for the tab
let displayText = text;
const textWidth = ctx.measureText(text).width;
if (textWidth > width - 10) {
// Create ellipsis if text is too long
while (ctx.measureText(displayText + "...").width > width - 10 && displayText.length > 0) {
displayText = displayText.slice(0, -1);
}
displayText += "...";
}
ctx.fillText(displayText, x + width / 2, y + height / 2);
ctx.restore();
}
/**
* Handle tab click to reorder the node
* @param {LiteGraph.LGraphNode} node - The node being clicked
* @param {boolean} toFront - Whether to bring the node to front
* @returns {Function} - Cleanup function
*/
static handleNodeOrder(node, toFront = true) {
const { graph } = node;
if (!graph?._nodes) return null;
const index = graph._nodes.indexOf(node);
if (index === -1) return null;
let frameId;
const handleDeselection = () => {
frameId = requestAnimationFrame(() => {
app.canvas.deselectNode(node);
node.setDirtyCanvas(true, true);
});
};
// Reorder the node
graph._nodes.splice(index, 1);
if (toFront) {
graph._nodes.push(node);
} else {
graph._nodes.unshift(node);
}
handleDeselection();
// Return cleanup function
return () => {
if (frameId) {
cancelAnimationFrame(frameId);
}
};
}
}
/**
* Helper function to check if a point is inside a rectangle
* @param {number} x - Point X coordinate
* @param {number} y - Point Y coordinate
* @param {number} rx - Rectangle X coordinate
* @param {number} ry - Rectangle Y coordinate
* @param {number} rw - Rectangle width
* @param {number} rh - Rectangle height
* @returns {boolean} - True if point is inside rectangle
*/
const isInsideRect = (x, y, rx, ry, rw, rh) => (
x >= rx && x <= rx + rw && y >= ry && y <= ry + rh
);
/**
* Main class to handle node tab functionality
*/
class NodeWithTabs {
/**
* Initialize a node with tab properties
* @param {LiteGraph.LGraphNode} node - The node being created
*/
static onNodeCreated(node) {
// Add properties to the node when it's created
node.addProperty("enableTabs", false, "boolean", "Enable tabs above the node");
node.addProperty("tabWidth", TAB_STYLE.defaultWidth, "number", "Width of the main tab");
node.addProperty("tabXOffset", NODE_METRICS.DEFAULT_TAB_X_OFFSET, "number", "X offset for the main tab");
node.addProperty("hasSecondTab", false, "boolean", "Enable a second tab");
node.addProperty("secondTabText", "Send Back", "string", "Text for the second tab");
node.addProperty("secondTabOffset", NODE_METRICS.DEFAULT_SECOND_TAB_OFFSET, "number", "X offset for second tab");
node.addProperty("secondTabWidth", TAB_STYLE.defaultWidth, "number", "Width of the second tab");
// Add a method to validate tab configuration
node.validateTabConfiguration = function() {
// Ensure tabs don't overlap
if (TabManager.wouldTabsOverlap(this.properties)) {
this.properties.secondTabOffset = this.properties.tabXOffset +
this.properties.tabWidth + NODE_METRICS.MIN_TAB_SPACING;
}
// Validate tab widths
this.properties.tabWidth = TabManager.getTabWidth({ width: this.properties.tabWidth });
this.properties.secondTabWidth = TabManager.getTabWidth(
{ secondTabWidth: this.properties.secondTabWidth },
true
);
};
}
}
/**
* ComfyUI NodeTabExtension
*
* This extension adds customizable tabs to ComfyUI nodes.
* Features:
* - Add one or two tabs to a node
* - Customize tab text, width, and position
* - Click tabs to bring node to front or send to back
*
* Usage:
* 1. Enable tabs on a node by setting the "enableTabs" property to true
* 2. Customize the main tab width and position with "tabWidth" and "tabXOffset"
* 3. Enable a second tab with "hasSecondTab" and customize with "secondTabText",
* "secondTabOffset", and "secondTabWidth"
*/
app.registerExtension({
name: "NodeTabExtension",
// Using the modern nodeCreated hook that works with the latest ComfyUI version
nodeCreated(node) {
try {
// Store original methods to call them when appropriate
const originalMethods = {
onNodeCreated: node.onNodeCreated,
getBounding: node.getBounding,
isPointInside: node.isPointInside,
onMouseDown: node.onMouseDown,
onMouseUp: node.onMouseUp,
onDrawForeground: node.onDrawForeground,
onPropertyChanged: node.onPropertyChanged
};
// Override onNodeCreated method
node.onNodeCreated = function() {
if (originalMethods.onNodeCreated) {
originalMethods.onNodeCreated.apply(this, arguments);
}
NodeWithTabs.onNodeCreated(this);
};
// Call onNodeCreated immediately if the node is already created
if (node.flags && !node._tabsInitialized) {
NodeWithTabs.onNodeCreated(node);
node._tabsInitialized = true;
}
// Add support for property validation
node.onPropertyChanged = function(property, value) {
if (originalMethods.onPropertyChanged) {
originalMethods.onPropertyChanged.call(this, property, value);
}
// If a tab-related property changed, validate the configuration
const tabProperties = [
"tabWidth", "tabXOffset", "hasSecondTab",
"secondTabOffset", "secondTabWidth"
];
if (tabProperties.includes(property)) {
this.validateTabConfiguration();
}
};
/**
* Get the bounding box for the node including tabs
*/
node.getBounding = function(out) {
if (!this.properties?.enableTabs || this.flags.collapsed) {
return originalMethods.getBounding.call(this, out);
}
// Validate tab configuration
this.validateTabConfiguration();
out = out || new Float32Array(4);
const [width, height] = this.size;
const [nodeX, nodeY] = this.pos;
const rects = [
// Body and title
{
x: nodeX,
y: nodeY - NODE_METRICS.TITLE_HEIGHT,
width,
height: height + NODE_METRICS.TITLE_HEIGHT
},
// Main tab
{
x: nodeX + this.properties.tabXOffset,
y: nodeY - NODE_METRICS.TAB_HEIGHT,
width: this.properties.tabWidth,
height: TAB_STYLE.height
}
];
if (this.properties.hasSecondTab) {
rects.push({
x: nodeX + this.properties.secondTabOffset,
y: nodeY - NODE_METRICS.TAB_HEIGHT,
width: this.properties.secondTabWidth,
height: TAB_STYLE.height
});
}
// Calculate the bounding box
const result = boundingBoxCalculator.calculate(rects);
// Copy values to the out parameter
out.set(result);
return out;
};
/**
* Check if a point is inside the node or its tabs
*/
node.isPointInside = function(x, y, margin = 0) {
if (!this.properties?.enableTabs || this.flags.collapsed) {
return originalMethods.isPointInside.call(this, x, y, margin);
}
// Validate tab configuration
this.validateTabConfiguration();
const [nodeX, nodeY] = this.pos;
const [width, height] = this.size;
// During rectangle selection, use a simpler hit test that includes the margin
if (margin > 0) {
const boundingBox = this.getBounding();
return isInsideRect(
x, y,
boundingBox[0] - margin,
boundingBox[1] - margin,
boundingBox[2] + 2 * margin,
boundingBox[3] + 2 * margin
);
}
// For regular clicking, keep the detailed hit testing
const bodyAndTitle = isInsideRect(x, y,
nodeX, nodeY - NODE_METRICS.TITLE_HEIGHT,
width, height + NODE_METRICS.TITLE_HEIGHT
);
if (bodyAndTitle) return true;
const mainTabHit = isInsideRect(x, y,
nodeX + this.properties.tabXOffset,
nodeY - NODE_METRICS.TAB_HEIGHT,
this.properties.tabWidth,
TAB_STYLE.height
);
if (mainTabHit) return true;
if (this.properties.hasSecondTab) {
return isInsideRect(x, y,
nodeX + this.properties.secondTabOffset,
nodeY - NODE_METRICS.TAB_HEIGHT,
this.properties.secondTabWidth,
TAB_STYLE.height
);
}
return false;
};
/**
* Handle mouse down event on the node or its tabs
*/
node.onMouseDown = function(event, local_pos, graphCanvas) {
if (!this.properties?.enableTabs || this.flags.collapsed) {
return originalMethods.onMouseDown?.call(this, event, local_pos, graphCanvas) ?? false;
}
// Validate tab configuration
this.validateTabConfiguration();
const [localX, localY] = local_pos;
let tabHandled = false;
let cleanup = null;
// Check main tab click
const mainTabHit = isInsideRect(localX, localY,
this.properties.tabXOffset, -NODE_METRICS.TAB_HEIGHT,
this.properties.tabWidth, TAB_STYLE.height
);
if (mainTabHit) {
cleanup = TabManager.handleNodeOrder(this, true);
tabHandled = true;
}
// Check second tab click
if (!tabHandled && this.properties.hasSecondTab) {
const secondTabHit = isInsideRect(localX, localY,
this.properties.secondTabOffset, -NODE_METRICS.TAB_HEIGHT,
this.properties.secondTabWidth, TAB_STYLE.height
);
if (secondTabHit) {
cleanup = TabManager.handleNodeOrder(this, false);
tabHandled = true;
}
}
// If tab was handled, return early
if (tabHandled) {
// Store cleanup function to be called later
this._tabCleanup = cleanup;
return true;
}
// If no tab was clicked, delegate to original handler
const originalResult = originalMethods.onMouseDown?.call(this, event, local_pos, graphCanvas) ?? false;
return originalResult;
};
/**
* Clean up any pending tab operations when mouse is released
*/
node.onMouseUp = function(event) {
// Call any pending tab cleanup
if (this._tabCleanup) {
this._tabCleanup();
this._tabCleanup = null;
}
// Call original handler
if (originalMethods.onMouseUp) {
return originalMethods.onMouseUp.call(this, event);
}
};
/**
* Draw the tabs above the node
*/
node.onDrawForeground = function(ctx) {
// Call original method first
if (originalMethods.onDrawForeground) {
originalMethods.onDrawForeground.call(this, ctx);
}
if (!this.properties?.enableTabs || this.flags.collapsed) {
return;
}
// Validate tab configuration
this.validateTabConfiguration();
ctx.save();
const baseY = -NODE_METRICS.TAB_HEIGHT;
// Draw main tab
TabManager.drawTab(ctx, this.properties.tabXOffset, baseY, this.title, this.properties.tabWidth);
// Draw second tab if enabled
if (this.properties.hasSecondTab) {
TabManager.drawTab(ctx, this.properties.secondTabOffset, baseY,
this.properties.secondTabText, this.properties.secondTabWidth);
}
ctx.restore();
};
} catch (error) {
console.error("NodeTabExtension: Error initializing node", error);
}
}
});