Random selection nodes with freeze/lock capability for iterative prompt building. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
379 lines
14 KiB
JavaScript
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");
|