raubatz's picture
download
raw
31.1 kB
/**
* ComfyUI basic data handling.
* Copyright (C) 2025 StableLlama
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Purpose: Manage dynamic input nodes in ComfyUI.
* Inspired by cozy_ex_dynamic
*/
import { app } from "../../../scripts/app.js";
const TypeSlot = {
Input: 1,
Output: 2
};
const TypeSlotEvent = {
Connect: true,
Disconnect: false
};
// Helper for escaping strings to be used in RegExp
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
};
app.registerExtension({
name: 'Basic data handling: dynamic input',
async beforeRegisterNodeDef(nodeType, nodeData, appInstance) {
if (!nodeType.comfyClass.startsWith('Basic data handling:')) {
return;
}
const combinedInputData = {
...(nodeData?.input?.required ?? {}),
...(nodeData?.input?.optional ?? {})
};
let combinedInputDataOrder = [];
if (nodeData.input_order) {
if (nodeData.input_order.required) combinedInputDataOrder.push(...nodeData.input_order.required);
if (nodeData.input_order.optional) combinedInputDataOrder.push(...nodeData.input_order.optional);
} else if (nodeData.input) {
if (nodeData.input.required) combinedInputDataOrder.push(...Object.keys(nodeData.input.required));
if (nodeData.input.optional) combinedInputDataOrder.push(...Object.keys(nodeData.input.optional));
}
const dynamicInputs = [];
const dynamicInputGroups = [];
for (const name of combinedInputDataOrder) {
if (!combinedInputData[name]) continue;
const dynamicSetting = combinedInputData[name][1]?._dynamic;
const dynamicGroup = combinedInputData[name][1]?._dynamicGroup ?? 0;
if (dynamicSetting) { // Only process for inputs marked with _dynamic
const inputOptions = combinedInputData[name][1] || {};
const dynamicComfyType = combinedInputData[name][0];
let isActuallyWidget = false;
let effectiveWidgetGuiType;
if (inputOptions.widget?.type) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widget.type;
} else if (inputOptions.widget && typeof inputOptions.widget === 'string') {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widget;
}
else if (inputOptions._widgetType) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions._widgetType;
} else if (inputOptions.widgetType) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widgetType;
}
else {
const comfyTypeUpper = String(dynamicComfyType).toUpperCase();
const implicitWidgetGuiTypesMap = {
"STRING": "text", "INT": "number", "FLOAT": "number",
"NUMBER": "number", "BOOLEAN": "toggle"
};
if (implicitWidgetGuiTypesMap[comfyTypeUpper] && inputOptions.forceInput !== true && dynamicComfyType !== '*') {
isActuallyWidget = true;
effectiveWidgetGuiType = implicitWidgetGuiTypesMap[comfyTypeUpper];
} else if (comfyTypeUpper === "COMBO" && inputOptions.values) {
isActuallyWidget = true;
effectiveWidgetGuiType = "combo";
} else {
isActuallyWidget = false;
effectiveWidgetGuiType = dynamicComfyType;
}
}
let determinedBaseName;
let suffixExtractionMatcher;
if (dynamicSetting === 'number') {
const r = /^(.*?)(\d+)$/;
const m = name.match(r);
determinedBaseName = (m && m[1] !== undefined) ? m[1] : name;
suffixExtractionMatcher = new RegExp(`^(${escapeRegExp(determinedBaseName)})(\\d*)$`);
} else if (dynamicSetting === 'letter') {
const r = /^(.*?)([a-zA-Z])$/;
const m = name.match(r);
determinedBaseName = (m && m[1] !== undefined) ? m[1] : name;
suffixExtractionMatcher = new RegExp(`^(${escapeRegExp(determinedBaseName)})([a-zA-Z])$`);
} else {
console.warn(`[Basic Data Handling] Unknown dynamic type: ${dynamicSetting} for input ${name} on node ${nodeType.comfyClass}`);
continue;
}
if (dynamicInputGroups[dynamicGroup] === undefined) {
dynamicInputGroups[dynamicGroup] = [];
}
dynamicInputGroups[dynamicGroup].push(dynamicInputs.length);
dynamicInputs.push({
name,
baseName: determinedBaseName,
matcher: suffixExtractionMatcher,
dynamic: dynamicSetting,
dynamicComfyType,
dynamicGroup,
isWidget: isActuallyWidget,
actualWidgetGuiType: effectiveWidgetGuiType,
originalOptions: inputOptions
});
}
}
if (dynamicInputs.length === 0) {
return;
}
const getDynamicInputDefinition = (inputName) => {
for (const di of dynamicInputs) {
const m = inputName.match(di.matcher);
if (m && m[1] === di.baseName) {
return { definition: di, suffix: m[2] || "" };
}
}
return null;
};
const isDynamicInput = (inputName) => !!getDynamicInputDefinition(inputName);
const updateSlotIndices = (node) => {
node.inputs.forEach((input, index) => {
input.slot_index = index;
if (input.link !== null && input.link !== undefined) {
const linkInfo = node.graph.links[input.link];
if (linkInfo) {
linkInfo.target_slot = index;
}
}
});
};
const getWidgetDefaultValue = (widgetDef) => { // widgetDef = { type: actualWidgetGuiType, options: originalOptions }
const type = widgetDef.type;
const opts = widgetDef.options || {};
switch (String(type).toLowerCase()) {
case 'number':
return opts.default ?? 0;
case 'combo':
return opts.default ?? (opts.values && opts.values.length > 0 ? opts.values[0] : '');
case 'text':
case 'string':
return opts.default ?? '';
case 'toggle':
case 'boolean':
return opts.default ?? false;
default:
return opts.default ?? '';
}
};
const isDynamicInputEmpty = (node, inputIndex) => {
const input = node.inputs[inputIndex];
if (input.link !== null && input.link !== undefined) return false;
const diData = getDynamicInputDefinition(input.name);
if (diData && diData.definition.isWidget) {
if (input.widget && input.widget.name) {
const widget = node.widgets.find(w => w.name === input.widget.name);
if (widget) {
return widget.value === getWidgetDefaultValue({
type: diData.definition.actualWidgetGuiType,
options: diData.definition.originalOptions
});
} else {
return true;
}
} else {
return true;
}
}
return true;
};
const removeWidgetForInput = (node, inputIdx) => {
if (node.inputs[inputIdx].widget && node.inputs[inputIdx].widget.name) {
const widgetName = node.inputs[inputIdx].widget.name;
const widgetIdx = node.widgets.findIndex((w) => w.name === widgetName);
if (widgetIdx !== -1) {
node.widgets.splice(widgetIdx, 1);
}
}
};
nodeType.prototype.getDynamicGroup = function(inputName) {
const diData = getDynamicInputDefinition(inputName);
return diData ? diData.definition.dynamicGroup : undefined;
};
const addStandardWidget = function(name, widgetGuiType, defaultValue, fullConfigOptions = {}) {
let currentVal = defaultValue;
const widgetSpecificOpts = { ...fullConfigOptions };
if (String(widgetGuiType).toLowerCase() === "combo") {
if (!widgetSpecificOpts.values && Array.isArray(fullConfigOptions.default)) {
widgetSpecificOpts.values = fullConfigOptions.default;
} else if (!widgetSpecificOpts.values && fullConfigOptions.options?.values) {
widgetSpecificOpts.values = fullConfigOptions.options.values;
}
if (!widgetSpecificOpts.values) {
widgetSpecificOpts.values = [currentVal];
}
if (widgetSpecificOpts.values && !widgetSpecificOpts.values.includes(currentVal) && widgetSpecificOpts.values.length > 0) {
currentVal = widgetSpecificOpts.values[0];
}
}
return this.addWidget(widgetGuiType, name, currentVal, () => {}, widgetSpecificOpts);
};
nodeType.prototype.addInputAtPosition = function (name, comfyInputType, widgetGuiType, position, isWidgetFlag, shape, widgetDefaultVal, widgetConfOpts) {
if (isWidgetFlag) {
addStandardWidget.call(this, name, widgetGuiType, widgetDefaultVal, widgetConfOpts);
this.addInput(name, comfyInputType, { shape, widget: { name } });
} else {
this.addInput(name, comfyInputType, { shape });
}
const newInputIndex = this.inputs.length - 1;
if (position < newInputIndex && position < this.inputs.length -1 ) {
const newInput = this.inputs.pop();
this.inputs.splice(position, 0, newInput);
}
updateSlotIndices(this);
return this.inputs[position];
};
let isProcessingConnection = false;
const generateDynamicInputName = (dynamicBehavior, baseName, count) => {
if (dynamicBehavior === 'letter') {
return `${baseName}${String.fromCharCode(97 + count)}`;
} else { // 'number'
return `${baseName}${count}`;
}
};
nodeType.prototype.renumberDynamicInputs = function(baseNameToRenumber, dynamicBehavior) {
const inputsToRenumber = [];
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
const diData = getDynamicInputDefinition(input.name);
if (diData && diData.definition.baseName === baseNameToRenumber && diData.definition.dynamic === dynamicBehavior) {
inputsToRenumber.push({
inputRef: input,
widgetName: input.widget ? input.widget.name : null
});
}
}
for (let i = 0; i < inputsToRenumber.length; i++) {
const { inputRef, widgetName } = inputsToRenumber[i];
const newName = generateDynamicInputName(dynamicBehavior, baseNameToRenumber, i);
if (inputRef.name === newName) continue;
if (widgetName) {
const widget = this.widgets.find(w => w.name === widgetName);
if (widget) {
widget.name = newName;
}
if (inputRef.widget) inputRef.widget.name = newName;
}
inputRef.name = newName;
}
updateSlotIndices(this);
};
const handleEmptyDynamicInput = function() { // (Content largely unchanged from previous good version, ensure it uses new DI props if needed)
let overallChangesMade = false;
let scanAgainPass = true;
while (scanAgainPass) {
scanAgainPass = false;
const dynamicGroupInfo = new Map();
for (let i = 0; i < this.inputs.length; i++) {
const currentInput = this.inputs[i];
const diData = getDynamicInputDefinition(currentInput.name);
if (!diData) continue;
const { definition: di, suffix } = diData;
const group = di.dynamicGroup;
if (!dynamicGroupInfo.has(group)) {
dynamicGroupInfo.set(group, { items: new Map() });
}
const groupData = dynamicGroupInfo.get(group);
if (!groupData.items.has(suffix)) {
groupData.items.set(suffix, { inputsInfo: [], allEmpty: true });
}
const itemData = groupData.items.get(suffix);
itemData.inputsInfo.push({ diDefinition: di, originalInput: currentInput, originalIndex: i });
if (!isDynamicInputEmpty(this, i)) { // isDynamicInputEmpty uses new DI props
itemData.allEmpty = false;
}
}
for (const [groupId, groupData] of dynamicGroupInfo) {
const { items } = groupData;
if (items.size === 0) continue;
const sortedItemSuffixes = Array.from(items.keys()).sort((a, b) => {
const numA = parseInt(a, 10);
const numB = parseInt(b, 10);
if (!isNaN(numA) && !isNaN(numB)) return numA - numB;
if (a.length !== b.length) return a.length - b.length;
return String(a).localeCompare(String(b));
});
let activeItemCount = 0;
const emptyItemSuffixes = [];
for (const suffix of sortedItemSuffixes) {
if (items.get(suffix).allEmpty) {
emptyItemSuffixes.push(suffix);
} else {
activeItemCount++;
}
}
if (emptyItemSuffixes.length === 0) continue;
let placeholderSuffix = null;
if (activeItemCount === 0 && sortedItemSuffixes.length > 0) {
placeholderSuffix = sortedItemSuffixes[0];
} else if (activeItemCount > 0 && emptyItemSuffixes.length > 0) {
placeholderSuffix = emptyItemSuffixes[emptyItemSuffixes.length - 1];
}
const inputsToRemoveDetails = [];
for (const suffix of emptyItemSuffixes) {
if (suffix !== placeholderSuffix) {
const itemData = items.get(suffix);
for (const inputDetail of itemData.inputsInfo) {
inputsToRemoveDetails.push(inputDetail);
}
}
}
if (inputsToRemoveDetails.length > 0) {
inputsToRemoveDetails.sort((a, b) => b.originalIndex - a.originalIndex);
for (const { originalInput, originalIndex } of inputsToRemoveDetails) {
if (this.inputs[originalIndex] === originalInput) {
removeWidgetForInput(this, originalIndex);
this.removeInput(originalIndex);
scanAgainPass = true;
overallChangesMade = true;
} else {
scanAgainPass = true;
overallChangesMade = true;
}
}
if (scanAgainPass) break;
}
}
if (scanAgainPass) continue;
}
if (overallChangesMade) {
const renumberedBaseNames = new Set();
for (const groupIdx_str in dynamicInputGroups) {
const groupIdx = parseInt(groupIdx_str, 10);
if (dynamicInputGroups[groupIdx]) {
for (const memberIdx of dynamicInputGroups[groupIdx]) {
const { baseName, dynamic } = dynamicInputs[memberIdx]; // 'dynamic' here is behavior ('number'/'letter')
if (!renumberedBaseNames.has(baseName + dynamic)) { // Ensure unique combination
this.renumberDynamicInputs(baseName, dynamic);
renumberedBaseNames.add(baseName + dynamic);
}
}
}
}
this.setDirtyCanvas(true, true);
}
};
const handleDynamicInputActivation = function(dynamicGroup) {
const { slots: dynamicSlotsFromGetter } = this.getDynamicSlots();
for (const slot of dynamicSlotsFromGetter) {
if (slot.dynamicGroup === dynamicGroup && this.widgets &&
!this.widgets.some((w) => w.name === slot.name)) {
const diData = getDynamicInputDefinition(slot.name);
if (diData && diData.definition.isWidget) { // Use our enhanced definition
const originalInputDef = diData.definition;
addStandardWidget.call(this, slot.name,
originalInputDef.actualWidgetGuiType,
getWidgetDefaultValue({ type: originalInputDef.actualWidgetGuiType, options: originalInputDef.originalOptions }),
originalInputDef.originalOptions
);
}
}
}
let allExistingItemsActive = true;
const itemSuffixesInGroup = new Set();
const groupSlots = dynamicSlotsFromGetter.filter(s => s.dynamicGroup === dynamicGroup);
groupSlots.forEach(s => {
const diData = getDynamicInputDefinition(s.name);
if (diData) itemSuffixesInGroup.add(diData.suffix);
});
if (itemSuffixesInGroup.size === 0 && dynamicInputGroups[dynamicGroup]?.length > 0) {
allExistingItemsActive = true;
} else {
let hasCompletelyEmptyItem = false;
for (const suffix of itemSuffixesInGroup) {
const inputsOfThisItem = groupSlots.filter(s => {
const diData = getDynamicInputDefinition(s.name);
return diData && diData.suffix === suffix;
});
if (inputsOfThisItem.length > 0 && inputsOfThisItem.every(s => !s.connected)) { // s.connected from getDynamicSlots
hasCompletelyEmptyItem = true;
break;
}
}
allExistingItemsActive = !hasCompletelyEmptyItem;
}
if (allExistingItemsActive) {
let lastDynamicIdx = -1;
if (groupSlots.length > 0) {
lastDynamicIdx = Math.max(...groupSlots.map(slot => slot.index));
} else {
let maxIdx = -1;
for(const di of dynamicInputs){ // di is a full definition object
if(di.dynamicGroup < dynamicGroup) {
for(let i=this.inputs.length-1; i>=0; --i){
if(this.inputs[i].name.startsWith(di.baseName)){
maxIdx = Math.max(maxIdx, i);
break;
}
}
}
}
lastDynamicIdx = (maxIdx === -1) ? (this.inputs.length -1) : maxIdx;
}
this.addNewDynamicInputForGroup(dynamicGroup, lastDynamicIdx);
}
this.setDirtyCanvas(true, true);
};
// --- Event Handlers (onConnectionsChange, onConnectInput, onRemoved, onWidgetChanged) ---
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, link, ioSlot) {
const originalReturn = onConnectionsChange?.apply(this, arguments);
if (type === TypeSlot.Input && slotIndex < this.inputs.length && this.inputs[slotIndex] && isDynamicInput(this.inputs[slotIndex].name)) {
if (isProcessingConnection) return originalReturn;
isProcessingConnection = true;
const dynamicGroup = this.getDynamicGroup(this.inputs[slotIndex].name);
if (dynamicGroup !== undefined) {
if (isConnected === TypeSlotEvent.Connect) {
handleDynamicInputActivation.call(this, dynamicGroup);
} else if (isConnected === TypeSlotEvent.Disconnect) {
handleEmptyDynamicInput.call(this);
}
}
isProcessingConnection = false;
}
return originalReturn;
};
const onConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
const result = onConnectInput?.apply(this, arguments) ?? true;
if (this.inputs[inputIndex].link !== null && this.inputs[inputIndex].link !== undefined ) {
const pre_isProcessingConnection = isProcessingConnection;
isProcessingConnection = true;
this.disconnectInput(inputIndex, true);
isProcessingConnection = pre_isProcessingConnection;
}
return result;
}
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
if(!isProcessingConnection){
isProcessingConnection = true;
for (let i = this.inputs.length - 1; i >= 0; i--) {
if (this.inputs[i].link !== null && this.inputs[i].link !== undefined) {
this.disconnectInput(i, true);
}
}
isProcessingConnection = false;
}
return onRemoved?.apply(this, arguments);
}
const onWidgetChanged = nodeType.prototype.onWidgetChanged;
nodeType.prototype.onWidgetChanged = function (widgetName, newValue, oldValue, widgetObject) {
const originalReturn = onWidgetChanged?.apply(this, arguments);
if (isProcessingConnection || !isDynamicInput(widgetName)) {
return originalReturn;
}
const dynamicGroup = this.getDynamicGroup(widgetName);
if (dynamicGroup === undefined) return originalReturn;
const diData = getDynamicInputDefinition(widgetName);
const defaultValue = diData ? getWidgetDefaultValue({type: diData.definition.actualWidgetGuiType, options: diData.definition.originalOptions}) : getWidgetDefaultValue(widgetObject) /*fallback*/;
const wasEffectivelyEmpty = oldValue === defaultValue;
const isNowEffectivelyEmpty = newValue === defaultValue;
if (wasEffectivelyEmpty === isNowEffectivelyEmpty) return originalReturn;
isProcessingConnection = true;
if (wasEffectivelyEmpty && !isNowEffectivelyEmpty) {
handleDynamicInputActivation.call(this, dynamicGroup);
} else if (!wasEffectivelyEmpty && isNowEffectivelyEmpty) {
handleEmptyDynamicInput.call(this);
}
isProcessingConnection = false;
return originalReturn;
};
nodeType.prototype.getDynamicSlots = function(filterDynamicGroup = null) { // (Content largely unchanged, relies on isDynamicInputEmpty which is updated)
const dynamicSlotsResult = [];
const dynamicGroupCount = {};
const dynamicGroupConnected = {};
const itemsState = new Map();
for (const [index, input] of this.inputs.entries()) {
const diData = getDynamicInputDefinition(input.name);
if (!diData) continue;
const { definition: di, suffix } = diData;
const currentDynamicGroup = di.dynamicGroup;
if (filterDynamicGroup !== null && currentDynamicGroup !== filterDynamicGroup) continue;
if (!itemsState.has(currentDynamicGroup)) itemsState.set(currentDynamicGroup, new Map());
const groupItems = itemsState.get(currentDynamicGroup);
if (!groupItems.has(suffix)) groupItems.set(suffix, { isActive: false, inputCount: 0, activeInputCount: 0 });
const itemState = groupItems.get(suffix);
itemState.inputCount++;
const isInputActive = !isDynamicInputEmpty(this, index); // Uses updated isDynamicInputEmpty
if (isInputActive) itemState.activeInputCount++;
dynamicSlotsResult.push({
index, name: input.name, isWidget: input.widget !== undefined, shape: input.shape,
connected: isInputActive,
isDynamic: true, dynamicGroup: currentDynamicGroup,
});
}
for(const [groupId, groupItems] of itemsState) {
dynamicGroupCount[groupId] = groupItems.size;
dynamicGroupConnected[groupId] = [];
const sortedSuffixes = Array.from(groupItems.keys()).sort((a,b) => {
const numA = parseInt(a,10); const numB = parseInt(b,10);
if(!isNaN(numA) && !isNaN(numB)) return numA-numB;
if(a.length !== b.length) return a.length - b.length;
return String(a).localeCompare(String(b));
});
for(const suffix of sortedSuffixes){
const item = groupItems.get(suffix);
dynamicGroupConnected[groupId].push(item.activeInputCount > 0);
}
}
return { slots: dynamicSlotsResult, groupCount: dynamicGroupCount, groupConnected: dynamicGroupConnected };
};
nodeType.prototype.addNewDynamicInputForGroup = function(dynamicGroup, lastKnownInputIndexInGroup) {
let insertPosition = lastKnownInputIndexInGroup + 1;
let inputInRange = true;
const groupMemberDefinitions = dynamicInputGroups[dynamicGroup].map(idx => dynamicInputs[idx]);
const { slots: currentGroupSlots } = this.getDynamicSlots(dynamicGroup);
const existingSuffixes = new Set();
currentGroupSlots.forEach(s => {
const diData = getDynamicInputDefinition(s.name);
if(diData) existingSuffixes.add(diData.suffix);
});
let newItemNumericSuffix = 0;
// Find the smallest non-negative integer not in existingSuffixes (if they are numbers)
// or just use existingSuffixes.size if complex/letter based for simplicity (renumbering will fix)
// For simplicity here, using size, assuming renumbering will handle actual ordering.
// A more robust suffix generation might be needed if strict ordering before renumbering is critical.
const isNumericSuffix = groupMemberDefinitions.length > 0 && groupMemberDefinitions[0].dynamic === 'number';
if (isNumericSuffix) {
while (existingSuffixes.has(String(newItemNumericSuffix))) {
newItemNumericSuffix++;
}
} else { // letter or complex
newItemNumericSuffix = existingSuffixes.size; // This is the 'count' for letter generation
}
for (const diDefinition of groupMemberDefinitions) {
// Use the new properties from diDefinition
const { baseName, dynamic, dynamicComfyType, isWidget, actualWidgetGuiType, originalOptions } = diDefinition;
if (dynamic === 'letter' && newItemNumericSuffix >= 26) {
inputInRange = false; continue;
}
const newName = generateDynamicInputName(dynamic, baseName, newItemNumericSuffix);
const refSlot = currentGroupSlots.find(s => s.name.startsWith(baseName)) || currentGroupSlots[0];
const widgetDefault = getWidgetDefaultValue({ type: actualWidgetGuiType, options: originalOptions });
this.addInputAtPosition(
newName,
dynamicComfyType, // ComfyUI input type (e.g., "STRING", "*")
actualWidgetGuiType, // GUI widget type (e.g., "text", "number", "combo")
insertPosition++,
isWidget, // The crucial boolean
refSlot?.shape,
widgetDefault,
originalOptions // Full original options for widget config
);
}
return inputInRange;
};
}
});

Xet Storage Details

Size:
31.1 kB
·
Xet hash:
a2a2af9b46d2d4faef4bc64c5b70d8888073318f787bd9414efdb2d3fe26f4c9

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.