Buckets:
| /** | |
| * 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.