Files
2025-05-08 01:38:03 +00:00

209 lines
11 KiB
JavaScript

import { app } from "../../../scripts/app.js";
// Helper function to map a value from one range to another
function mapValue(value, fromMin, fromMax, toMin, toMax) {
// Handle the case where the input range is zero
if (fromMax === fromMin) {
return (toMin + toMax) / 2; // Return the middle of the target range
}
// Perform linear interpolation
const mapped = toMin + (toMax - toMin) * ((value - fromMin) / (fromMax - fromMin));
// Clamp the value to the target range to prevent drawing outside bounds
// return Math.max(toMin, Math.min(toMax, mapped)); // Optional clamping
return mapped;
}
app.registerExtension({
// Unique name for this extension
name: "comfyui.curve_visualizer_display",
// This function runs when a node is created in the UI graph
nodeCreated(node) {
// Check if the created node is our 'Curve Visualizer' type
// 'Curve Visualizer' is the KEY used in NODE_CLASS_MAPPINGS in Python
if (node.comfyClass === "Curve Visualizer") {
console.log(`[CurveVisualizerDisplay] Found node: ${node.title} (ID: ${node.id})`);
// --- Create the Custom Widget Object ---
const widget = {
type: "curve_plot_display", // Define a custom type for this widget
name: "curvePlot", // Give the widget instance a name
y: 0, // Current Y position (updated in draw)
computedHeight: 120, // Default/initial height
_value: null, // Internal storage for the data list received from Python
// Use getter/setter for the data value
get value() {
return this._value;
},
set value(newVal) {
this._value = newVal;
},
// --- The main drawing function for the widget ---
draw: function (ctx, node, widgetWidth, widgetY, H) {
this.y = widgetY; // Update the widget's Y position
const margin = 10; // Pixels of padding around the graph
const graphRect = { // Define the plotting area bounds
x: margin,
y: widgetY + margin,
width: widgetWidth - margin * 2,
// Use computed height to ensure consistency
height: this.computedHeight - margin * 2
};
// Draw the background for the entire widget space
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR || "#222";
ctx.fillRect(0, widgetY, widgetWidth, this.computedHeight);
// Draw the background and border for the graph plotting area
ctx.strokeStyle = "#555"; // Border color
ctx.fillStyle = "#111"; // Background color for plot area
ctx.lineWidth = 1;
ctx.strokeRect(graphRect.x, graphRect.y, graphRect.width, graphRect.height);
ctx.fillRect(graphRect.x, graphRect.y, graphRect.width, graphRect.height);
// Get the data that was stored by the event listener
const data = this.value;
// --- Handle cases with no data or insufficient data ---
if (!data || !Array.isArray(data) || data.length < 1) {
ctx.fillStyle = "#888"; // Text color for messages
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "12px Arial";
ctx.fillText("No Data Received", graphRect.x + graphRect.width / 2, graphRect.y + graphRect.height / 2);
return; // Stop drawing here
}
if (data.length === 1) {
ctx.fillStyle = "#888";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "12px Arial";
// Display the single scalar value
ctx.fillText(`Scalar Value: ${data[0].toFixed(4)}`, graphRect.x + graphRect.width / 2, graphRect.y + graphRect.height / 2);
return; // Stop drawing here
}
// --- Draw the Line Graph ---
ctx.strokeStyle = "orange"; // Line color
ctx.lineWidth = 1.5; // Line thickness
ctx.beginPath(); // Start drawing the line path
// --- Calculate Data Range for Scaling ---
let yMin = data[0];
let yMax = data[0];
for (let i = 1; i < data.length; i++) {
if (data[i] < yMin) yMin = data[i];
if (data[i] > yMax) yMax = data[i];
}
// Add vertical padding if data range is very small or zero
if (yMax === yMin) {
yMin -= 0.5;
yMax += 0.5;
} else {
const yRange = yMax - yMin;
yMin -= yRange * 0.05; // 5% padding below min
yMax += yRange * 0.05; // 5% padding above max
}
// --- Plot the points ---
// Map the very first point
let canvasX = mapValue(0, 0, data.length - 1, graphRect.x, graphRect.x + graphRect.width);
// Note: Canvas Y coordinates are inverted (0 is top), so map to graphRect.y+height (bottom) -> graphRect.y (top)
let canvasY = mapValue(data[0], yMin, yMax, graphRect.y + graphRect.height, graphRect.y);
ctx.moveTo(canvasX, canvasY);
// Map and draw lines to the remaining points
for (let i = 1; i < data.length; i++) {
canvasX = mapValue(i, 0, data.length - 1, graphRect.x, graphRect.x + graphRect.width);
canvasY = mapValue(data[i], yMin, yMax, graphRect.y + graphRect.height, graphRect.y);
ctx.lineTo(canvasX, canvasY);
}
ctx.stroke(); // Render the line path
// --- Optional: Draw Labels ---
ctx.fillStyle = "#999"; // Label text color
ctx.font = "10px Arial";
ctx.textAlign = "left";
// Max label (top-left)
ctx.textBaseline = "top";
ctx.fillText(`Max: ${yMax.toFixed(3)}`, graphRect.x + 3, graphRect.y + 3);
// Min label (bottom-left)
ctx.textBaseline = "bottom";
ctx.fillText(`Min: ${yMin.toFixed(3)}`, graphRect.x + 3, graphRect.y + graphRect.height - 3);
// Point count label (bottom-right)
ctx.textAlign = "right";
ctx.fillText(`${data.length} pts`, graphRect.x + graphRect.width - 3, graphRect.y + graphRect.height - 3);
}, // End of draw function
// --- Define the widget's size ---
computeSize: function (width) {
// You can adjust the fixed height here
const height = 120;
this.computedHeight = height; // Store for use in draw function
// Return [width, height]. Use 'null' for width to make it use the node's width.
return [null, height];
},
}; // End of widget definition
// --- Add the widget to the node ---
node.addCustomWidget(widget);
console.log(`[CurveVisualizerDisplay] Widget added to node ${node.id}`);
// --- Event Listener to receive data when the node executes ---
const onExecuted = (event) => {
// Check if the 'executed' event is specifically for this node instance
// Use loose comparison (==) to handle potential string/number type differences for ID
if (event.detail?.node == node.id) {
// Access the data sent from Python within the 'ui' -> 'visualization_data' structure
const data = event.detail?.output?.visualization_data;
console.log(`[CurveVisualizerDisplay] Node ${node.id} executed. Received data:`, (data ? `Array[${data.length}]` : data)); // Log confirmation
if (data !== undefined && Array.isArray(data)) {
widget.value = data; // Store the received data array in the widget
node.setDirtyCanvas(true, true); // IMPORTANT: Trigger a redraw to show the new graph
} else {
console.warn(`[CurveVisualizerDisplay] Node ${node.id} executed, but 'visualization_data' was missing or not an array in output.ui.`);
widget.value = null; // Clear previous data if none received
node.setDirtyCanvas(true, true); // Redraw to show "No Data"
}
}
};
// Store the listener function ON THE NODE object itself
// This allows us to easily find and remove it later
node.curveVisualizerListener = onExecuted;
// Register the listener with ComfyUI's API
app.api.addEventListener("executed", onExecuted);
console.log(`[CurveVisualizerDisplay] Added 'executed' listener for node ${node.id}.`);
// --- Cleanup Logic: Remove listener when node is removed ---
// Store the original onRemoved function if it exists
const originalOnRemoved = node.onRemoved;
node.onRemoved = () => {
// Check if our listener exists on the node
if (node.curveVisualizerListener) {
console.log(`[CurveVisualizerDisplay] Removing 'executed' listener for node ${node.id}`);
// Remove the listener from the API
app.api.removeEventListener("executed", node.curveVisualizerListener);
node.curveVisualizerListener = null; // Clear the reference on the node
}
// Call the original onRemoved function if it existed, preserving other cleanup behavior
originalOnRemoved?.apply(node, arguments);
};
// Give the node an initial size calculation and redraw
node.computeSize();
node.setDirtyCanvas(true, true);
} // End if (node.comfyClass === "Curve Visualizer")
} // End nodeCreated method
}); // End registerExtension