From 043efe8e106189e1df6cc22d2f530d94de28de77 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Thu, 29 May 2025 23:38:01 +0200 Subject: [PATCH] Make dynamic inputs work better --- src/basic_data_handling/control_flow_nodes.py | 10 +- src/basic_data_handling/data_list_nodes.py | 22 +-- src/basic_data_handling/dict_nodes.py | 24 +-- src/basic_data_handling/list_nodes.py | 16 +- src/basic_data_handling/math_formula_node.py | 6 +- src/basic_data_handling/set_nodes.py | 22 +-- web/js/dynamicnode.js | 156 ++++++++---------- 7 files changed, 125 insertions(+), 131 deletions(-) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 99a931b..ef4ee4b 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -15,6 +15,8 @@ except: ComfyNodeABC = object ExecutionBlocker = lambda x: x +from ._dynamic_input import ContainsDynamicDict + class IfElse(ComfyNodeABC): """ @@ -70,11 +72,11 @@ class IfElifElse(ComfyNodeABC): "if": (IO.BOOLEAN, {"forceInput": True}), "then": (IO.ANY, {"lazy": True}), }, - "optional": { + "optional": ContainsDynamicDict({ "elif_0": (IO.BOOLEAN, {"forceInput": True, "lazy": True, "_dynamic": "number", "_dynamicGroup": 0}), "then_0": (IO.ANY, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0}), "else": (IO.ANY, {"lazy": True}), - } + }) } RETURN_TYPES = (IO.ANY,) @@ -152,10 +154,10 @@ class SwitchCase(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "required": { + "required": ContainsDynamicDict({ "selector": (IO.INT, {"default": 0, "min": 0}), "case_0": (IO.ANY, {"lazy": True, "_dynamic": "number"}), - }, + }), "optional": { "default": (IO.ANY, {"lazy": True}), } diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index e074448..4c157bd 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -13,6 +13,8 @@ except: ANY = "*" ComfyNodeABC = object +from ._dynamic_input import ContainsDynamicDict + INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes @@ -26,9 +28,9 @@ class DataListCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.ANY, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.ANY,) @@ -53,9 +55,9 @@ class DataListCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.BOOLEAN,) @@ -80,9 +82,9 @@ class DataListCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.FLOAT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.FLOAT,) @@ -107,9 +109,9 @@ class DataListCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.INT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.INT,) @@ -134,9 +136,9 @@ class DataListCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.STRING, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.STRING,) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 1198dcd..d243aa2 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -13,6 +13,8 @@ except: ANY = "*" ComfyNodeABC = object +from ._dynamic_input import ContainsDynamicDict + class DictCreate(ComfyNodeABC): """ @@ -24,10 +26,10 @@ class DictCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) @@ -56,10 +58,10 @@ class DictCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), - "value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}), - } + "value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}), + }) } RETURN_TYPES = ("DICT",) @@ -88,10 +90,10 @@ class DictCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) @@ -120,10 +122,10 @@ class DictCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) @@ -152,10 +154,10 @@ class DictCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 265fe11..11d8300 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -28,9 +28,9 @@ class ListCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.ANY, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -53,9 +53,9 @@ class ListCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -78,9 +78,9 @@ class ListCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.FLOAT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -103,9 +103,9 @@ class ListCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.INT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) diff --git a/src/basic_data_handling/math_formula_node.py b/src/basic_data_handling/math_formula_node.py index 957ffdc..9041983 100644 --- a/src/basic_data_handling/math_formula_node.py +++ b/src/basic_data_handling/math_formula_node.py @@ -12,6 +12,8 @@ except: ANY = "*" ComfyNodeABC = object +from ._dynamic_input import ContainsDynamicDict + class MathFormula(ComfyNodeABC): """ A node that evaluates a mathematical formula provided as a string without using eval. @@ -30,10 +32,10 @@ class MathFormula(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "required": { + "required": ContainsDynamicDict({ "formula": (IO.STRING, {"default": "a + b"}), "a": (IO.NUMBER, {"default": 0.0, "_dynamic": "letter"}), - }, + }), } RETURN_TYPES = (IO.FLOAT,) diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index d1a72ed..ea9014e 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -13,6 +13,8 @@ except: ANY = "*" ComfyNodeABC = object +from ._dynamic_input import ContainsDynamicDict + class SetCreate(ComfyNodeABC): """ @@ -24,9 +26,9 @@ class SetCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.ANY, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -49,9 +51,9 @@ class SetCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -74,9 +76,9 @@ class SetCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.FLOAT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -99,9 +101,9 @@ class SetCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.INT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -124,9 +126,9 @@ class SetCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.STRING, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) diff --git a/web/js/dynamicnode.js b/web/js/dynamicnode.js index fa00950..1157c19 100644 --- a/web/js/dynamicnode.js +++ b/web/js/dynamicnode.js @@ -82,7 +82,7 @@ app.registerExtension({ dynamicInputGroups[dynamicGroup].push( dynamicInputs.length ) - dynamicInputs.push({name, baseName, matcher, /*forceInput,*/ dynamic, dynamicType, dynamicGroup}); + dynamicInputs.push({name, baseName, matcher, dynamic, dynamicType, dynamicGroup}); } } if (dynamicInputs.length === 0) { @@ -104,7 +104,6 @@ app.registerExtension({ node.inputs.forEach((input, index) => { input.slot_index = index; if (input.isConnected) { - //if (input.link !== null) { const link = node.graph._links.get(input.link); if (link) { link.target_slot = index; @@ -116,45 +115,16 @@ app.registerExtension({ }; - // Override onConfigure: Ensure dynamic inputs are continuously ordered - /* - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function() { - const result = onConfigure?.apply(this, arguments); - let lastDynamicIdx = -1 - for (let idx = 0; idx < this.inputs.length; idx++) { - const input = this.inputs[idx]; - const isDynamic = isDynamicInput(input.name); - if (isDynamic) { - if (lastDynamicIdx < 0 || lastDynamicIdx === idx - 1) { - lastDynamicIdx = idx; - } else { - // non-continuous dynamic inputs -> move up - for (let i = idx-1; i > lastDynamicIdx; i--) { - this.swapInputs(i, i + 1); - } - lastDynamicIdx++; - } - } - } - return result; - } - */ - // Add helper method to insert input at a specific position nodeType.prototype.addInputAtPosition = function (name, type, position, isWidget, shape) { - console.warn("Adding input at position:", name, type, position, isWidget, shape, app); if (isWidget) { this.addWidget(type, name, '', ()=>{}, {}); const GET_CONFIG = Symbol(); const input = this.addInput(name, type, { shape, - //widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 } widget: {name, [GET_CONFIG]: () =>{}} }) - - //widget.dynamicPrompts = true; } else { this.addInput(name, type, {shape}); // Add new input } @@ -177,6 +147,16 @@ app.registerExtension({ return result; } + function getDynamicGroup(inputName) { + // Find the dynamicGroup by matching the baseName with input name + for (const di of dynamicInputs) { + if (inputName.startsWith(di.baseName)) { + return di.dynamicGroup; + } + } + return undefined; + } + isProcessingConnection = true; try { @@ -187,16 +167,8 @@ app.registerExtension({ for (const [index, input] of this.inputs.entries()) { const isDynamic = isDynamicInput(input.name); if (isDynamic) { - const connected = input.isConnected; //.link !== null; - const dynamicGroup = (() => { - // Find the dynamicGroup by matching the baseName with input name - for (const di of dynamicInputs) { - if (input.name.startsWith(di.baseName)) { - return di.dynamicGroup; - } - } - return undefined; - })(); + const connected = input.isConnected; + const dynamicGroup = getDynamicGroup(input.name); if (dynamicGroup in dynamicGroupCount) { if (input.name.startsWith(dynamicInputs[dynamicInputGroups[dynamicGroup][0]].baseName)) { dynamicGroupConnected[dynamicGroup][dynamicGroupCount[dynamicGroup]] ||= connected; @@ -259,66 +231,48 @@ app.registerExtension({ } } } else if (isConnected === TypeSlotEvent.Disconnect) { - let foundEmtpyGroup = -1; - let inGroup = -1; - let countInGroup = 0; + let foundEmptyIndex = -1; + for (let idx = 0; idx < this.inputs.length; idx++) { const input = this.inputs[idx]; - const isDynamic = isDynamicInput(input.name); - let hasEmptyDynamic = true; - if (isDynamic) { - for (const [i, dIG] of dynamicInputGroups.entries()) { - if (input.name.startsWith(dynamicInputs[dIG[0]].baseName)) { - inGroup = i; - break; - } + if (!isDynamicInput(input.name)) { + continue; + } + + if (!input.isConnected) { // Check if the input is empty + // remove empty input - but only when it's not the last one + const dynamicGroup = getDynamicGroup(input.name); + let isLast = true; + for (let i = idx + 1; i < this.inputs.length; i++) { + isLast &&= !this.inputs[i].name.startsWith(dynamicInputs[dynamicInputGroups[dynamicGroup][0]].baseName); } - if (inGroup === -1) { + if (isLast) { continue; } - for (let i = 0; i < dynamicInputGroups[inGroup].length; i++) { - if (this.inputs[idx+i].name.startsWith(dynamicInputs[dynamicInputGroups[inGroup][i]].baseName)) { - hasEmptyDynamic &&= this.inputs[idx+i].link === null; - } else { - console.error("Bad dynamic group input count!"); - debugger; - } + + for (let i = idx + 1; i < this.inputs.length; i++) { + this.swapInputs(i - 1, i); } - if (hasEmptyDynamic) { - if (foundEmtpyGroup !== -1) { - // only remove when we have more than one empty group - for (let i = 0; i < dynamicInputGroups[inGroup].length; i++) { - for (let j=0; j < this.widgets.length; j++) { - if (this.widgets[j].name === this.inputs[idx].name) { - this.widgets.splice(j, 1); - this.widgets_values?.splice(j, 1); - } - } - this.removeInput(idx); - } - } else { - foundEmtpyGroup = idx; - } - } else { - if (foundEmtpyGroup !== -1) { - for (let i = 0; i < dynamicInputGroups[inGroup].length; i++) { - this.swapInputs(foundEmtpyGroup + i, idx + i); - } - foundEmtpyGroup = idx; - } + const lastIdx = this.inputs.length - 1; + if (this.inputs[lastIdx].widget !== undefined) { + const widgetIdx = this.widgets.findIndex((w) => w.name === this.inputs[lastIdx].widget.name) + this.widgets.splice(widgetIdx, 1); + this.widgets_values?.splice(widgetIdx, 1); } - inGroup = -1; + this.removeInput(lastIdx); } } + + // Renumber dynamic inputs to ensure proper ordering for (const groupMember of dynamicInputGroups[dynamicInputs[0].dynamicGroup]) { const baseName = dynamicInputs[groupMember].baseName; this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic); } + } this.setDirtyCanvas(true, true); - //this.arrange(); } catch (e) { console.error(e); debugger; @@ -327,9 +281,39 @@ app.registerExtension({ isProcessingConnection = false; } + this.graph.links.forEach(l=>console.log(`post ${l.id}: ${l.origin_id}:${l.origin_slot} -> ${l.target_id}:${l.target_slot}`,l));Object.entries(this.graph._nodes_by_id).forEach(n=>console.log(n[0],n[1].title)) return result; }; + const onConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function(inputIndex, outputType, outputSlot, outputNode, outputIndex) { + const result = onRemoved?.apply(this, arguments) ?? true; + + if (this.inputs[inputIndex].isConnected) { + const pre_isProcessingConnection = isProcessingConnection; + isProcessingConnection = true; + this.disconnectInput(inputIndex, true); + isProcessingConnection = pre_isProcessingConnection; + } + return result; + } + + const onRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function () { + const result = onRemoved?.apply(this, arguments); + + // When this is called, the input links are already removed - but + // due to the implementation of the remove() method it might not + // have worked with the dynamic inputs. So we need to fix it here. + for (let i = this.inputs.length-1; i >= 0; i--) { + if ( this.inputs[i].isConnected) { + this.disconnectInput(i, true); + } + } + + return result; + } + // Method to swap two inputs in the "this.inputs" array by their indices nodeType.prototype.swapInputs = function(indexA, indexB) { // Validate indices @@ -383,7 +367,7 @@ app.registerExtension({ index: i, widgetIdx: input.widget !== undefined ? this.widgets.findIndex((w) => w.name === input.widget.name) : undefined, name: input.name, - connected: input.isConnected // .link !== null + connected: input.isConnected }); } } @@ -395,7 +379,7 @@ app.registerExtension({ const newName = dynamic === "number" ? `${baseName}${i}` : String.fromCharCode(97 + i); // 97 is ASCII for 'a' if (input.widget !== undefined) { - const widgetIdx = info.widgetIdx; // this.widgets.findIndex((w) => w.name === input.widget.name); + const widgetIdx = info.widgetIdx; input.widget.name = newName; this.widgets[widgetIdx].name = newName; this.widgets[widgetIdx].label = newName;