Files
CkrestandClaude Opus 4.5 f2edcf1e0c Initial commit: ComfyUI Easy Randomize
Random selection nodes with freeze/lock capability for iterative prompt building.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 04:34:37 -06:00

379 lines
14 KiB
JavaScript

/**
* Freezable Nodes Extension for ComfyUI
* Updates display widgets after execution to show current/frozen values.
*/
import { app } from "/scripts/app.js";
// Helper to update widgets from UI message
function updateWidgetsFromMessage(node, message) {
if (!message || !node.widgets) return;
for (const [key, values] of Object.entries(message)) {
if (Array.isArray(values) && values.length > 0) {
const widget = node.widgets.find(w => w.name === key);
if (widget) {
const newValue = values[0];
widget.value = newValue;
// Trigger callback to update UI (important for INT/FLOAT widgets)
if (widget.callback) {
widget.callback(newValue);
}
}
}
}
// Trigger redraw
if (node.setDirtyCanvas) {
node.setDirtyCanvas(true, true);
}
}
// FreezableInput - single input
app.registerExtension({
name: "my_nodes.FreezableInput",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "FreezableInput") return;
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
updateWidgetsFromMessage(this, message);
};
},
});
// RandomSelector - update display widget after execution
app.registerExtension({
name: "my_nodes.RandomSelector",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "RandomSelector") return;
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
updateWidgetsFromMessage(this, message);
};
},
});
// FreezableStringCombiner - 10 inputs with spacers between groups
app.registerExtension({
name: "my_nodes.FreezableStringCombiner",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "FreezableStringCombiner") return;
// Add visual spacers between slot groups for better organization
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Store original widgets
const originalWidgets = [...this.widgets];
// Clear and rebuild with spacers
this.widgets = [];
for (const widget of originalWidgets) {
this.widgets.push(widget);
// Add spacer after each freeze_N toggle (but not freeze_all)
if (widget.name.startsWith("freeze_") && widget.name !== "freeze_all") {
const spacerDiv = document.createElement("div");
spacerDiv.style.height = "10px";
spacerDiv.style.width = "100%";
const spacerWidget = this.addDOMWidget(
`spacer_${widget.name}`,
"div",
spacerDiv,
{
serialize: false,
hideOnZoom: false,
}
);
spacerWidget.computedHeight = 15;
}
}
this.setSize(this.computeSize());
};
// Update widgets from Python's UI return
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
updateWidgetsFromMessage(this, message);
};
},
});
// SmartSelector - dynamic show/hide based on mode and use_file
app.registerExtension({
name: "my_nodes.SmartSelector",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "SmartSelector") return;
// Random-only widgets
const randomWidgets = ["allow_duplicates"];
// Sequential-only widgets
const sequentialWidgets = ["index", "wrap"];
// Function to update widget visibility based on mode (random vs sequential)
const updateModeVisibility = function(mode) {
if (!this.widgets) return;
for (const widget of this.widgets) {
if (randomWidgets.includes(widget.name)) {
widget.hidden = (mode !== "random");
if (widget.options) widget.options.hidden = (mode !== "random");
}
if (sequentialWidgets.includes(widget.name)) {
widget.hidden = (mode !== "sequential");
if (widget.options) widget.options.hidden = (mode !== "sequential");
}
}
};
// Function to update widget visibility based on use_file toggle
const updateSourceVisibility = function(useFile) {
if (!this.widgets) return;
for (const widget of this.widgets) {
if (widget.name === "items") {
// Show items widget when NOT using file
widget.hidden = useFile;
if (widget.options) widget.options.hidden = useFile;
}
if (widget.name === "file_path") {
// Show file_path widget when using file
widget.hidden = !useFile;
if (widget.options) widget.options.hidden = !useFile;
}
}
};
// Combined update function
const updateAllVisibility = function() {
const modeWidget = this.widgets?.find(w => w.name === "mode");
const useFileWidget = this.widgets?.find(w => w.name === "use_file");
if (modeWidget) updateModeVisibility.call(this, modeWidget.value);
if (useFileWidget) updateSourceVisibility.call(this, useFileWidget.value);
this.setSize(this.computeSize());
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Store update function on node for reuse
this._updateAllVisibility = updateAllVisibility.bind(this);
// Find mode widget and add callback
const modeWidget = this.widgets.find(w => w.name === "mode");
if (modeWidget) {
const origCallback = modeWidget.callback;
modeWidget.callback = (value) => {
if (origCallback) origCallback.call(this, value);
this._updateAllVisibility();
};
}
// Find use_file widget and add callback
const useFileWidget = this.widgets.find(w => w.name === "use_file");
if (useFileWidget) {
const origCallback = useFileWidget.callback;
useFileWidget.callback = (value) => {
if (origCallback) origCallback.call(this, value);
this._updateAllVisibility();
};
}
// Set initial visibility
this._updateAllVisibility();
};
// Handle loading saved workflow - update visibility after values are restored
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
if (onConfigure) onConfigure.apply(this, arguments);
// Update visibility based on loaded values
if (this._updateAllVisibility) {
this._updateAllVisibility();
}
};
// Handle executed message - update display and index widgets
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
updateWidgetsFromMessage(this, message);
};
},
});
// WidgetTest - experimental node for testing MARKDOWN spacers
app.registerExtension({
name: "my_nodes.WidgetTest",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "WidgetTest") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Dynamically import ComfyWidgets
const { ComfyWidgets } = await import("/scripts/widgets.js");
// Store original widgets (text_1, text_2, text_3)
const originalWidgets = [...this.widgets];
// Clear widgets array to rebuild with spacers
this.widgets = [];
// Rebuild with spacers after each text input
for (let i = 0; i < originalWidgets.length; i++) {
// Add the original text widget
this.widgets.push(originalWidgets[i]);
// Create a simple div spacer with exact height control
const spacerDiv = document.createElement("div");
spacerDiv.style.height = "10px";
spacerDiv.style.width = "100%";
const spacerWidget = this.addDOMWidget(
`spacer_${i + 1}`,
"div",
spacerDiv,
{
serialize: false,
hideOnZoom: false,
}
);
spacerWidget.computedHeight = 15;
}
// Resize node to fit new widgets
this.setSize(this.computeSize());
};
// Handle executed message - update spacer widgets
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
// Update spacer widgets with their corresponding text values
for (let i = 1; i <= 3; i++) {
const key = `spacer_${i}`;
if (message?.[key]) {
const value = Array.isArray(message[key]) ? message[key][0] : message[key];
const widget = this.widgets?.find(w => w.name === key);
if (widget) widget.value = value;
}
}
};
},
});
// PreviewImageWithText - update text_display when image updates
app.registerExtension({
name: "my_nodes.PreviewImageWithText",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "PreviewImageWithText") return;
// Handle executed message - update text_display widget
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
updateWidgetsFromMessage(this, message);
};
},
});
// ListFilter - dynamic show/hide based on use_file toggle
app.registerExtension({
name: "my_nodes.ListFilter",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "ListFilter") return;
// Function to update widget visibility based on use_file toggle
const updateSourceVisibility = function(useFile) {
if (!this.widgets) return;
for (const widget of this.widgets) {
if (widget.name === "exclude_items") {
// Show exclude_items widget when NOT using file
widget.hidden = useFile;
if (widget.options) widget.options.hidden = useFile;
}
if (widget.name === "file_path") {
// Show file_path widget when using file
widget.hidden = !useFile;
if (widget.options) widget.options.hidden = !useFile;
}
}
};
const updateAllVisibility = function() {
const useFileWidget = this.widgets?.find(w => w.name === "use_file");
if (useFileWidget) updateSourceVisibility.call(this, useFileWidget.value);
this.setSize(this.computeSize());
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Store update function on node for reuse
this._updateAllVisibility = updateAllVisibility.bind(this);
// Find use_file widget and add callback
const useFileWidget = this.widgets.find(w => w.name === "use_file");
if (useFileWidget) {
const origCallback = useFileWidget.callback;
useFileWidget.callback = (value) => {
if (origCallback) origCallback.call(this, value);
this._updateAllVisibility();
};
}
// Set initial visibility
this._updateAllVisibility();
};
// Handle loading saved workflow - update visibility after values are restored
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
if (onConfigure) onConfigure.apply(this, arguments);
// Update visibility based on loaded values
if (this._updateAllVisibility) {
this._updateAllVisibility();
}
};
// Handle executed message - update display widget
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
updateWidgetsFromMessage(this, message);
};
},
});
console.log("[Freezable Nodes] Extension loaded");