Merge pull request #13 from StableLlama/dynamic_nodes4

Dynamic nodes4
This commit is contained in:
StableLlama
2025-06-03 19:42:53 +02:00
committed by GitHub
12 changed files with 1020 additions and 442 deletions
+249 -86
View File
@@ -16,91 +16,271 @@ These nodes are very lightweight and require no additional dependencies.
# Features
- **STRING**: String manipulation nodes
- Basic functions: capitalize, casefold, center, concat, count, encode/decode, find/rfind, join, lower/upper, replace, split, strip, etc.
- String checking: contains, endswith, startswith
- Case conversion: lower, upper, swapcase, title, capitalize
- Validation: isalnum, isalpha, isdigit, isnumeric, etc.
## Nodes
- **LIST**: Python list manipulation nodes (as a single variable)
- Conversion: convert to data list
- Creation: any to LIST
- Modification: append, extend, insert, remove, pop, clear, set_item
- Access: get_item, slice, index, contains
- Information: length, count
- Operations: sort, reverse, min, max
### BOOLEAN: Boolean logic nodes
- Logic operations: and, or, not, xor, nand, nor
- **SET**: Python set manipulation nodes (as a single variable)
- Conversion: convert to data list, to LIST
- Creation: any to SET, LIST to SET
- Modification: add, remove, discard, pop, clear
- Information: length, contains
- Set operations: union, intersection, difference, symmetric_difference
- Comparison: is_subset, is_superset, is_disjoint
### cast: Type conversion nodes for ComfyUI data types
- Basic data type conversions:
- to BOOLEAN - Converts any input to a Boolean using Python's truthy/falsy rules
- to FLOAT - Converts numeric input to a floating-point number (raises ValueError for invalid inputs)
- to INT - Converts numeric input to an integer (raises ValueError for invalid inputs)
- to STRING - Converts any input to a string using Python's str() function
- Collection type conversions:
- to DICT - Converts compatible inputs (mappings or lists of key-value pairs) to a dictionary
- to LIST - Converts input to a LIST (wraps single items in a list, preserves existing lists)
- to SET - Converts input to a SET (creates a set from single items or collections, removing duplicates)
- **data list**: ComfyUI list manipulation nodes (for processing individual items)
- Creation: create_empty
- Modification: append, filter, extend, insert, remove, pop, clear, set_item
- Access: get_item, slice, index, contains
- Information: length, count
- Operations: sort, reverse, copy, zip, min, max
### Comparison: Value comparison nodes
- Basic comparisons: equal (==), not equal (!=), greater than (>), greater than or equal (>=), less than (<), less than or equal (<=)
- String comparison: StringComparison node with case-sensitive/insensitive options and various operators (==, !=, >, <, >=, <=)
- Special comparisons: NumberInRange (check if a value is within specified bounds), IsNull (check if a value is null)
- Container operations: CompareLength (compare the length of strings, lists, and other containers)
- **cast**: Type conversion nodes for ComfyUI data types
- Basic conversions: to STRING, to INT, to FLOAT, to BOOLEAN
- Collection conversions: to LIST, to SET, to DICT
- Special conversions: data list to LIST, data list to SET
### data list: ComfyUI list manipulation nodes (for processing individual items)
- Creation:
- create Data List - Generic creation from any type inputs (dynamically expandable)
- Type-specific creation:
- create Data List from BOOLEANs - Creates a data list from Boolean values
- create Data List from FLOATs - Creates a data list from floating-point values
- create Data List from INTs - Creates a data list from integer values
- create Data List from STRINGs - Creates a data list from string values
- Modification:
- append - Adds an item to the end of a data list
- extend - Combines elements from multiple data lists
- insert - Inserts an item at a specified position
- set item - Replaces an item at a specified position
- remove - Removes the first occurrence of a specified value
- pop - Removes and returns an item at a specified position
- pop random - Removes and returns a random element
- Filtering:
- filter - Filters a data list using boolean values
- filter select - Separates items into two lists based on boolean filters
- Access:
- get item - Retrieves an item at a specified position
- first - Returns the first element in a data list
- last - Returns the last element in a data list
- slice - Creates a subset of a data list using start/stop/step parameters
- index - Finds the position of a value in a data list
- contains - Checks if a data list contains a specified value
- Information:
- length - Returns the number of items in a data list
- count - Counts occurrences of a value in a data list
- Operations:
- sort - Orders items (with optional reverse parameter)
- reverse - Reverses the order of items
- zip - Combines multiple data lists element-wise
- min - Finds the minimum value in a list of numbers
- max - Finds the maximum value in a list of numbers
- Conversion:
- convert to LIST - Converts a data list to a LIST object
- convert to SET - Converts a data list to a SET (removing duplicates)
- **path**: File system path manipulation nodes
- Basic operations: join, split, splitext, basename, dirname, normalize
- Path information: abspath, exists, is_file, is_dir, is_absolute, get_size, get_extension
- Directory operations: list_dir, get_cwd
- Path searching: glob, common_prefix
- Path conversions: relative, expand_vars
### DICT: Dictionary manipulation nodes
- Creation: create (generic and type-specific versions), create from items (data list and LIST versions), create from lists, fromkeys
- Access: get, get_multiple, keys, values, items
- Modification: set, update, setdefault, merge
- Removal: pop, popitem, pop random, remove
- Information: length, contains_key
- Operations: filter_by_keys, exclude_keys, invert, compare
- Conversion: get_keys_values
- **DICT**: Dictionary manipulation nodes
- Creation: create, from_items, from_lists, fromkeys, any_to_DICT
- Access: get, get_multiple, keys, values, items
- Modification: set, update, setdefault, merge
- Removal: pop, popitem, remove, clear
- Information: length, contains_key
- Operations: copy, filter_by_keys, exclude_keys, invert, compare
- Conversion: get_keys_values
### FLOAT: Floating-point operation nodes
- Creation: create FLOAT from string
- Basic arithmetic: add, subtract, multiply, divide, divide (zero safe), power
- Formatting: round (to specified decimal places)
- Conversion: to_hex (hexadecimal representation), from_hex (create from hex string)
- Analysis: is_integer (check if float has no fractional part), as_integer_ratio (get numerator/denominator)
- **INT**: Integer operation nodes
- Basic operations: add, subtract, multiply, divide, modulus, power
- Bit operations: bit_length, to_bytes, from_bytes, bit_count
### Flow Control: Workflow control nodes
- Conditional branching:
- if/else - Basic conditional that returns one of two values based on a condition
- if/elif/.../else - Extended conditional with multiple conditions and outcomes
- Selection:
- switch/case - Selects from multiple options based on an integer index
- flow select - Directs a value to either "true" or "false" output path based on a condition
- Execution control:
- force execution order - Coordinates the execution sequence of nodes in a workflow
- **FLOAT**: Floating-point operation nodes
- Basic operations: add, subtract, multiply, divide, power, round
- Specialized: is_integer, as_integer_ratio, to_hex, from_hex
### INT: Integer operation nodes
- Creation: create INT (from string), create INT with base (convert string with specified base)
- Basic arithmetic: add, subtract, multiply, divide, divide (zero safe), modulus, power
- Bit operations: bit_length (bits needed to represent number), bit_count (count of 1 bits)
- Byte conversion: to_bytes (convert integer to bytes), from_bytes (convert bytes to integer)
- **BOOLEAN**: Boolean logic nodes
- Logic operations: and, or, not, xor, nand, nor
### LIST: Python list manipulation nodes (as a single variable)
- Creation: create LIST (generic), create from type-specific values (BOOLEANs, FLOATs, INTs, STRINGs)
- Modification: append, extend, insert, remove, pop, pop random, set_item
- Access: get_item, first, last, slice, index, contains
- Information: length, count
- Operations: sort (with optional reverse), reverse, min, max
- Conversion: convert to data list, convert to SET
- **Comparison**: Value comparison nodes
- Basic comparisons: equal (==), not equal (!=), greater than (>), less than (<), etc.
- String comparison: case-sensitive/insensitive string comparison with various operators
- Special comparisons: number in range, is null, compare length
- Container operations: length comparison for strings, lists, and other containers
### Math: Mathematical operations
- Trigonometric functions:
- Basic: sin, cos, tan - Calculate sine, cosine, and tangent of angles (in degrees or radians)
- Inverse: asin, acos, atan - Calculate inverse sine, cosine, and tangent (returns degrees or radians)
- Special: atan2 - Calculate arc tangent of y/x with correct quadrant handling
- Logarithmic/Exponential functions:
- log - Natural logarithm (base e) with optional custom base
- log10 - Base-10 logarithm
- exp - Exponential function (e^x)
- sqrt - Square root
- Constants:
- pi - Mathematical constant π (3.14159...)
- e - Mathematical constant e (2.71828...)
- Angle conversion:
- degrees - Convert radians to degrees
- radians - Convert degrees to radians
- Rounding operations:
- floor - Return largest integer less than or equal to input
- ceil - Return smallest integer greater than or equal to input
- Min/Max functions:
- min - Return minimum of two values
- max - Return maximum of two values
- Other:
- abs - Absolute value (magnitude without sign)
- **Flow Control**: Workflow control nodes
- Conditional: if/else for branching logic based on conditions
- Selection: switch/case for selecting from multiple options based on an index
### path: File system path manipulation nodes
- Basic operations:
- join - Joins multiple path components intelligently with correct separators
- split - Splits a path into directory and filename components
- splitext - Splits a path into name and extension components
- basename - Extracts the filename component from a path
- dirname - Extracts the directory component from a path
- normalize - Collapses redundant separators and resolves up-level references
- Path information:
- abspath - Returns the absolute (full) path by resolving relative components
- exists - Checks if a path exists in the filesystem
- is_file - Checks if a path points to a regular file
- is_dir - Checks if a path points to a directory
- is_absolute - Checks if a path is absolute (begins at root directory)
- get_size - Returns the size of a file in bytes
- get_extension - Extracts the file extension from a path (including the dot)
- Directory operations:
- list_dir - Lists files and directories in a specified path with filtering options
- get_cwd - Returns the current working directory
- Path searching:
- glob - Finds paths matching a pattern with wildcard support
- common_prefix - Finds the longest common leading component of given paths
- Path conversions:
- relative - Computes a relative path from a start path to a target path
- expand_vars - Replaces environment variables in a path with their values
- **Math**: Mathematical operations
- Trigonometric: sin, cos, tan, asin, acos, atan, atan2
- Logarithmic/Exponential: log, log10, exp, sqrt
- Numerical: min, max
- Constants: pi, e
- Conversion: degrees/radians
- Rounding: floor, ceil
- Other: abs (absolute value)
### SET: Python set manipulation nodes (as a single variable)
- Creation:
- create SET - Generic creation from any type inputs (dynamically expandable)
- Type-specific creation:
- create SET from BOOLEANs - Creates a set from Boolean values
- create SET from FLOATs - Creates a set from floating-point values
- create SET from INTs - Creates a set from integer values
- create SET from STRINGs - Creates a set from string values
- Modification:
- add - Adds an item to a set
- remove - Removes an item from a set (raises error if not present)
- discard - Removes an item if present (no error if missing)
- pop - Removes and returns an arbitrary element
- pop random - Removes and returns a random element
- Information:
- length - Returns the number of items in a set
- contains - Checks if a set contains a specified value
- Set operations:
- union - Combines elements from multiple sets
- intersection - Returns elements common to all input sets
- difference - Returns elements in first set but not in second set
- symmetric_difference - Returns elements in either set but not in both
- Set comparison:
- is_subset - Checks if first set is a subset of second set
- is_superset - Checks if first set is a superset of second set
- is_disjoint - Checks if two sets have no common elements
- Conversion:
- convert to data list - Converts a SET to a ComfyUI data list
- convert to LIST - Converts a SET to a LIST
### STRING: String manipulation nodes
Available nodes grouped by functionality:
#### Text case conversion
- capitalize - Converts first character to uppercase, rest to lowercase
- casefold - Aggressive lowercase for case-insensitive comparisons
- lower - Converts string to lowercase
- swapcase - Swaps case of all characters
- title - Converts string to titlecase
- upper - Converts string to uppercase
#### Text inspection and validation
- contains (in) - Checks if string contains a substring
- endswith - Checks if string ends with a specific suffix
- find - Finds first occurrence of a substring
- length - Returns the number of characters in the string
- rfind - Finds last occurrence of a substring
- startswith - Checks if string starts with a specific prefix
#### Character type checking
- isalnum - Checks if all characters are alphanumeric
- isalpha - Checks if all characters are alphabetic
- isascii - Checks if all characters are ASCII
- isdecimal - Checks if all characters are decimal
- isdigit - Checks if all characters are digits
- isidentifier - Checks if string is a valid Python identifier
- islower - Checks if all characters are lowercase
- isnumeric - Checks if all characters are numeric
- isprintable - Checks if all characters are printable
- isspace - Checks if all characters are whitespace
- istitle - Checks if string is titlecased
- isupper - Checks if all characters are uppercase
#### Text formatting and alignment
- center - Centers text within specified width
- expandtabs - Replaces tabs with spaces
- ljust - Left-aligns text within specified width
- rjust - Right-aligns text within specified width
- zfill - Pads string with zeros on the left
#### Text splitting and joining
- join (from data list) - Joins strings from a data list
- join (from LIST) - Joins strings from a LIST
- rsplit (from data list) - Splits string from right into a data list
- rsplit (from LIST) - Splits string from right into a LIST
- split (to data list) - Splits string into a data list
- split (to LIST) - Splits string into a LIST
- splitlines (from data list) - Splits string at line boundaries into a data list
- splitlines (to LIST) - Splits string at line boundaries into a LIST
#### Text modification
- concat - Combines two strings together
- count - Counts occurrences of a substring
- replace - Replaces occurrences of a substring
- strip - Removes leading and trailing characters
- lstrip - Removes leading characters
- rstrip - Removes trailing characters
- removeprefix - Removes prefix if present
- removesuffix - Removes suffix if present
#### Encoding and escaping
- decode - Converts bytes-like string to text
- encode - Converts string to bytes
- escape - Converts special characters to escape sequences
- unescape - Converts escape sequences to actual characters
- format_map - Formats string using values from dictionary
## Understanding LIST vs. data list vs. SET
ComfyUI has different data types that serve different purposes:
### 1. LIST datatype
### 1. data list
- A native ComfyUI list where **items are processed individually**
- Acts like a standard array/list in most programming contexts
- Items can be accessed individually by compatible nodes
- Supports built-in ComfyUI iteration over each item
- Best for:
- Working directly with multiple items in parallel
- Batch processing scenarios
- When you need to apply the same operation to multiple inputs
- When your operation needs to work with individual items separately
### 2. LIST datatype
- A Python list represented as a **single variable** in the workflow
- Treated as a self-contained object that can be passed between nodes
- Cannot directly connect to nodes that expect individual items
@@ -110,7 +290,7 @@ ComfyUI has different data types that serve different purposes:
- Passing collections between different parts of your workflow
- Complex data storage that shouldn't be split apart
### 2. SET datatype
### 3. SET datatype
- A Python set represented as a **single variable** in the workflow
- Stores unique values with no duplicates
- Supports mathematical set operations (union, intersection, etc.)
@@ -120,23 +300,6 @@ ComfyUI has different data types that serve different purposes:
- Set operations (union, difference, etc.)
- When element order doesn't matter
### 3. data list
- A native ComfyUI list where **items are processed individually**
- Acts like a standard array/list in most programming contexts
- Items can be accessed individually by compatible nodes
- Supports built-in ComfyUI iteration over each item
- Best for:
- Working directly with multiple items in parallel
- Batch processing scenarios
- When you need to apply the same operation to multiple inputs
- When your operation needs to work with individual items separately
### Converting between types
- Use `convert to data list` node to transform a LIST or SET into a ComfyUI data list
- Use `convert to LIST` node to transform a ComfyUI data list into a LIST object
- Use `LIST to SET` to convert a LIST to a SET (removing duplicates)
- Use `SET to LIST` to convert a SET to a LIST
### When to use which type
- Use **LIST** when you need:
- Ordered collection with potential duplicates
+20 -5
View File
@@ -4,13 +4,28 @@ build-backend = "setuptools.build_meta"
[project]
name = "basic_data_handling"
version = "0.3.4"
version = "0.3.5"
description = """NOTE: Still in development! Expect breaking changes!
Basic Python functions for manipulating data that every programmer is used to.
Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists.
Additionally supported Python data types as custom data types: DICT, LIST.
Data handling is supported with comparisons and control flow operations.
Also contains math, regex, and file system path manipulation functions."""
Comprehensive node collection for data manipulation in ComfyUI workflows.
Supported data types:
- ComfyUI native: BOOLEAN, FLOAT, INT, STRING, and data lists
- Python types as custom data types: DICT, LIST, SET
Feature categories:
- Boolean logic operations (and, or, not, xor, nand, nor)
- Type casting/conversion between all supported data types
- Comparison operations (equality, numerical comparison, range checking)
- Data structures manipulation (data lists, LIST, DICT, SET)
- Flow control (conditionals, branching, execution order)
- Mathematical operations (arithmetic, trigonometry, logarithmic functions)
- String manipulation (case conversion, formatting, splitting, validation)
- File system path handling (path operations, information, searching)
- SET operations (creation, modification, comparison, mathematical set theory)
All nodes are lightweight with no additional dependencies required."""
authors = [
{name = "StableLlama"}
]
+15 -11
View File
@@ -29,7 +29,7 @@ class DataListCreate(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number"}),
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -41,7 +41,8 @@ class DataListCreate(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return (list(kwargs.values()),)
values = list(kwargs.values())
return (values[:-1],)
class DataListCreateFromBoolean(ComfyNodeABC):
@@ -56,7 +57,7 @@ class DataListCreateFromBoolean(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.BOOLEAN, {"_dynamic": "number"}),
"item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -68,7 +69,8 @@ class DataListCreateFromBoolean(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return ([bool(value) for value in kwargs.values()],)
values = [bool(value) for value in kwargs.values()]
return (values[:-1],)
class DataListCreateFromFloat(ComfyNodeABC):
@@ -83,7 +85,7 @@ class DataListCreateFromFloat(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.FLOAT, {"_dynamic": "number"}),
"item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -95,7 +97,8 @@ class DataListCreateFromFloat(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return ([float(value) for value in kwargs.values()],)
values = [float(value) for value in kwargs.values()]
return (values[:-1],)
class DataListCreateFromInt(ComfyNodeABC):
@@ -110,7 +113,7 @@ class DataListCreateFromInt(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.INT, {"_dynamic": "number"}),
"item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -122,7 +125,8 @@ class DataListCreateFromInt(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return ([int(value) for value in kwargs.values()],)
values = [int(value) for value in kwargs.values()]
return (values[:-1],)
class DataListCreateFromString(ComfyNodeABC):
@@ -137,7 +141,7 @@ class DataListCreateFromString(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.STRING, {"_dynamic": "number"}),
"item_0": (IO.STRING, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -149,7 +153,8 @@ class DataListCreateFromString(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([str(value) for value in kwargs.values()],)
values = [str(value) for value in kwargs.values()]
return (values[:-1],)
class DataListAppend(ComfyNodeABC):
@@ -806,7 +811,6 @@ class DataListSlice(ComfyNodeABC):
start = kwargs.get('start', [0])[0]
stop = kwargs.get('stop', [INT_MAX])[0]
step = kwargs.get('step', [1])[0]
print(f"start: {start}, stop: {stop}, step: {step}; input_list: {input_list}")
return (input_list[start:stop:step],)
+15 -15
View File
@@ -27,8 +27,8 @@ class DictCreate(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0}),
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
"value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
})
}
@@ -40,7 +40,7 @@ class DictCreate(ComfyNodeABC):
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
for i in range(len(kwargs) // 2 - 1): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
@@ -59,8 +59,8 @@ class DictCreateFromBoolean(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}),
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
"value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
})
}
@@ -72,7 +72,7 @@ class DictCreateFromBoolean(ComfyNodeABC):
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
for i in range(len(kwargs) // 2 - 1): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
@@ -91,8 +91,8 @@ class DictCreateFromFloat(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0}),
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
"value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
})
}
@@ -104,7 +104,7 @@ class DictCreateFromFloat(ComfyNodeABC):
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
for i in range(len(kwargs) // 2 - 1): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
@@ -123,8 +123,8 @@ class DictCreateFromInt(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0}),
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
"value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
})
}
@@ -136,7 +136,7 @@ class DictCreateFromInt(ComfyNodeABC):
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
for i in range(len(kwargs) // 2 - 1): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
@@ -155,8 +155,8 @@ class DictCreateFromString(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
"value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}),
})
}
@@ -168,7 +168,7 @@ class DictCreateFromString(ComfyNodeABC):
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
for i in range(len(kwargs) // 2 - 1): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
+14 -9
View File
@@ -29,7 +29,7 @@ class ListCreate(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number"}),
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -39,7 +39,8 @@ class ListCreate(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return (list(kwargs.values()),)
values = list(kwargs.values())
return (values[:-1],)
class ListCreateFromBoolean(ComfyNodeABC):
@@ -54,7 +55,7 @@ class ListCreateFromBoolean(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.BOOLEAN, {"_dynamic": "number"}),
"item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -64,7 +65,8 @@ class ListCreateFromBoolean(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([bool(value) for value in kwargs.values()],)
values = [bool(value) for value in kwargs.values()]
return (values[:-1],)
class ListCreateFromFloat(ComfyNodeABC):
@@ -79,7 +81,7 @@ class ListCreateFromFloat(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.FLOAT, {"_dynamic": "number"}),
"item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -89,7 +91,8 @@ class ListCreateFromFloat(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([float(value) for value in kwargs.values()],)
values = [float(value) for value in list(kwargs.values())[:-1]]
return (values,)
class ListCreateFromInt(ComfyNodeABC):
@@ -104,7 +107,7 @@ class ListCreateFromInt(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.INT, {"_dynamic": "number"}),
"item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -114,7 +117,8 @@ class ListCreateFromInt(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([int(value) for value in kwargs.values()],)
values = [int(value) for value in list(kwargs.values())[:-1]]
return (values,)
class ListCreateFromString(ComfyNodeABC):
@@ -139,7 +143,8 @@ class ListCreateFromString(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([str(value) for value in kwargs.values()],)
values = [str(value) for value in list(kwargs.values())[:-1]]
return (values,)
class ListAppend(ComfyNodeABC):
+15 -10
View File
@@ -27,7 +27,7 @@ class SetCreate(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number"}),
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -37,7 +37,8 @@ class SetCreate(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set(kwargs.values()),)
values = list(kwargs.values())[:-1]
return (set(values),)
class SetCreateFromBoolean(ComfyNodeABC):
@@ -52,7 +53,7 @@ class SetCreateFromBoolean(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.BOOLEAN, {"_dynamic": "number"}),
"item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -62,7 +63,8 @@ class SetCreateFromBoolean(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([bool(value) for value in kwargs.values()]),)
values = [bool(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetCreateFromFloat(ComfyNodeABC):
@@ -77,7 +79,7 @@ class SetCreateFromFloat(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.FLOAT, {"_dynamic": "number"}),
"item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -87,7 +89,8 @@ class SetCreateFromFloat(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([float(value) for value in kwargs.values()]),)
values = [float(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetCreateFromInt(ComfyNodeABC):
@@ -102,7 +105,7 @@ class SetCreateFromInt(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.INT, {"_dynamic": "number"}),
"item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -112,7 +115,8 @@ class SetCreateFromInt(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([int(value) for value in kwargs.values()]),)
values = [int(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetCreateFromString(ComfyNodeABC):
@@ -127,7 +131,7 @@ class SetCreateFromString(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.STRING, {"_dynamic": "number"}),
"item_0": (IO.STRING, {"_dynamic": "number", "widgetType": "STRING"}),
})
}
@@ -137,7 +141,8 @@ class SetCreateFromString(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([str(value) for value in kwargs.values()]),)
values = [str(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetAdd(ComfyNodeABC):
+72
View File
@@ -890,6 +890,74 @@ class StringRemovesuffix(ComfyNodeABC):
return (string.removesuffix(suffix),)
class StringUnescape(ComfyNodeABC):
"""
Unescapes a string by converting escape sequences to their actual characters.
This node converts escape sequences like '\n' (two characters) to actual newlines (one character),
'\t' to tabs, '\\' to backslashes, etc. Useful for processing strings where escape sequences
are represented literally rather than interpreted.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string": (IO.STRING, {"default": ""}),
}
}
RETURN_TYPES = (IO.STRING,)
CATEGORY = "Basic/STRING"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "unescape"
def unescape(self, string):
# Decode escaped sequences only for control characters
result = (
string
.replace(r'\\', '\u0000')
.replace(r'\n', '\n')
.replace(r'\t', '\t')
.replace(r'\"', '"')
.replace(r'\'', "'")
.replace('\u0000', '\\')
)
return (result,)
class StringEscape(ComfyNodeABC):
"""
Escapes a string by converting special characters to escape sequences.
This node converts characters like newlines, tabs, quotes, and backslashes to their escaped
representation (like '\n', '\t', '\"', '\\'). Useful when you need to prepare strings
for formats that require escaped sequences instead of literal special characters.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string": (IO.STRING, {"default": ""}),
}
}
RETURN_TYPES = (IO.STRING,)
CATEGORY = "Basic/STRING"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "escape"
def escape(self, string):
result = (
string
.replace('\\', r'\\')
.replace('\n', r'\n')
.replace('\t', r'\t')
.replace('"', r'\"')
.replace("'", r"\'")
)
return (result,)
class StringReplace(ComfyNodeABC):
"""
Replaces occurrences of a substring with another substring.
@@ -1384,6 +1452,7 @@ NODE_CLASS_MAPPINGS = {
"Basic data handling: StringDecode": StringDecode,
"Basic data handling: StringEncode": StringEncode,
"Basic data handling: StringEndswith": StringEndswith,
"Basic data handling: StringEscape": StringEscape,
"Basic data handling: StringExpandtabs": StringExpandtabs,
"Basic data handling: StringFind": StringFind,
"Basic data handling: StringFormatMap": StringFormatMap,
@@ -1422,6 +1491,7 @@ NODE_CLASS_MAPPINGS = {
"Basic data handling: StringStrip": StringStrip,
"Basic data handling: StringSwapcase": StringSwapcase,
"Basic data handling: StringTitle": StringTitle,
"Basic data handling: StringUnescape": StringUnescape,
"Basic data handling: StringUpper": StringUpper,
"Basic data handling: StringZfill": StringZfill,
}
@@ -1435,6 +1505,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: StringDecode": "decode",
"Basic data handling: StringEncode": "encode",
"Basic data handling: StringEndswith": "endswith",
"Basic data handling: StringEscape": "escape",
"Basic data handling: StringExpandtabs": "expandtabs",
"Basic data handling: StringFind": "find",
"Basic data handling: StringFormatMap": "format_map",
@@ -1473,6 +1544,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: StringStrip": "strip",
"Basic data handling: StringSwapcase": "swapcase",
"Basic data handling: StringTitle": "title",
"Basic data handling: StringUnescape": "unescape",
"Basic data handling: StringUpper": "upper",
"Basic data handling: StringZfill": "zfill",
}
+4 -4
View File
@@ -55,7 +55,7 @@ def test_dict_set():
def test_dict_create_from_boolean():
node = DictCreateFromBoolean()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0=True, key_1="key2", value_1=False)
result = node.create(key_0="key1", value_0=True, key_1="key2", value_1=False, key_2="", value_2="")
assert result == ({"key1": True, "key2": False},)
# Test with empty inputs
assert node.create() == ({},)
@@ -64,7 +64,7 @@ def test_dict_create_from_boolean():
def test_dict_create_from_float():
node = DictCreateFromFloat()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0=1.5, key_1="key2", value_1=2.5)
result = node.create(key_0="key1", value_0=1.5, key_1="key2", value_1=2.5, key_2="", value_2="")
assert result == ({"key1": 1.5, "key2": 2.5},)
# Test with empty inputs
assert node.create() == ({},)
@@ -73,7 +73,7 @@ def test_dict_create_from_float():
def test_dict_create_from_int():
node = DictCreateFromInt()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0=1, key_1="key2", value_1=2)
result = node.create(key_0="key1", value_0=1, key_1="key2", value_1=2, key_2="", value_2="")
assert result == ({"key1": 1, "key2": 2},)
# Test with empty inputs
assert node.create() == ({},)
@@ -82,7 +82,7 @@ def test_dict_create_from_int():
def test_dict_create_from_string():
node = DictCreateFromString()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0="value1", key_1="key2", value_1="value2")
result = node.create(key_0="key1", value_0="value1", key_1="key2", value_1="value2", key_2="", value_2="")
assert result == ({"key1": "value1", "key2": "value2"},)
# Test with empty inputs
assert node.create() == ({},)
+10 -10
View File
@@ -176,36 +176,36 @@ def test_list_max():
def test_list_create():
node = ListCreate()
assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],)
assert node.create_list(item_0=1, item_1=2, item_2=3, item_3="") == ([1, 2, 3],)
assert node.create_list() == ([],) # Empty list
assert node.create_list(item_0="test") == (["test"],)
assert node.create_list(item_0="test", item_1="") == (["test"],)
def test_list_create_from_boolean():
node = ListCreateFromBoolean()
assert node.create_list(item_0=True, item_1=False, item_2=True) == ([True, False, True],)
assert node.create_list(item_0=True) == ([True],)
assert node.create_list(item_0=True, item_1=False, item_2=True, item_3="") == ([True, False, True],)
assert node.create_list(item_0=True, item_1="") == ([True],)
assert node.create_list() == ([],)
def test_list_create_from_float():
node = ListCreateFromFloat()
assert node.create_list(item_0=1.1, item_1=2.2, item_2=3.3) == ([1.1, 2.2, 3.3],)
assert node.create_list(item_0=0.0) == ([0.0],)
assert node.create_list(item_0=1.1, item_1=2.2, item_2=3.3, item_3="") == ([1.1, 2.2, 3.3],)
assert node.create_list(item_0=0.0, item_1="") == ([0.0],)
assert node.create_list() == ([],)
def test_list_create_from_int():
node = ListCreateFromInt()
assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],)
assert node.create_list(item_0=0) == ([0],)
assert node.create_list(item_0=1, item_1=2, item_2=3, item_3="") == ([1, 2, 3],)
assert node.create_list(item_0=0, item_1="") == ([0],)
assert node.create_list() == ([],)
def test_list_create_from_string():
node = ListCreateFromString()
assert node.create_list(item_0="a", item_1="b", item_2="c") == (["a", "b", "c"],)
assert node.create_list(item_0="test") == (["test"],)
assert node.create_list(item_0="a", item_1="b", item_2="c", item_3="") == (["a", "b", "c"],)
assert node.create_list(item_0="test", item_1="") == (["test"],)
assert node.create_list() == ([],)
+14 -14
View File
@@ -26,29 +26,29 @@ from src.basic_data_handling.set_nodes import (
def test_set_create():
node = SetCreate()
# Testing with kwargs to simulate dynamic inputs
assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},)
assert node.create_set(item_0="a", item_1="b") == ({"a", "b"},)
assert node.create_set(item_0=1, item_1=2, item_2=3, item_3="") == ({1, 2, 3},)
assert node.create_set(item_0="a", item_1="b", item_2="") == ({"a", "b"},)
assert node.create_set() == (set(),) # Empty set with no arguments
# Mixed types
assert node.create_set(item_0=1, item_1="b", item_2=True) == ({1, "b", True},)
assert node.create_set(item_0=1, item_1="b", item_2=True, item_3="") == ({1, "b", True},)
def test_set_create_from_int():
node = SetCreateFromInt()
assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},)
assert node.create_set(item_0=5) == ({5},) # Single item set
assert node.create_set(item_0=1, item_1=1) == ({1},) # Duplicate items become single item
assert node.create_set(item_0=1, item_1=2, item_2=3, item_3="") == ({1, 2, 3},)
assert node.create_set(item_0=5, item_1="") == ({5},) # Single item set
assert node.create_set(item_0=1, item_1=1, item_2="") == ({1},) # Duplicate items become single item
assert node.create_set() == (set(),) # Empty set with no arguments
def test_set_create_from_string():
node = SetCreateFromString()
result = node.create_set(item_0="apple", item_1="banana")
result = node.create_set(item_0="apple", item_1="banana", item_2="")
assert isinstance(result[0], set)
assert result[0] == {"apple", "banana"}
# Duplicate strings
result = node.create_set(item_0="apple", item_1="apple")
result = node.create_set(item_0="apple", item_1="apple", item_2="")
assert result[0] == {"apple"}
# Empty set
@@ -57,19 +57,19 @@ def test_set_create_from_string():
def test_set_create_from_float():
node = SetCreateFromFloat()
assert node.create_set(item_0=1.5, item_1=2.5) == ({1.5, 2.5},)
assert node.create_set(item_0=3.14) == ({3.14},) # Single item set
assert node.create_set(item_0=1.0, item_1=1.0) == ({1.0},) # Duplicate items
assert node.create_set(item_0=1.5, item_1=2.5, item_2="") == ({1.5, 2.5},)
assert node.create_set(item_0=3.14, item_1="") == ({3.14},) # Single item set
assert node.create_set(item_0=1.0, item_1=1.0, item_2="") == ({1.0},) # Duplicate items
assert node.create_set() == (set(),) # Empty set with no arguments
def test_set_create_from_boolean():
node = SetCreateFromBoolean()
assert node.create_set(item_0=True, item_1=False) == ({True, False},)
assert node.create_set(item_0=True, item_1=True) == ({True},) # Duplicate booleans
assert node.create_set(item_0=True, item_1=False, item_2="") == ({True, False},)
assert node.create_set(item_0=True, item_1=True, item_2="") == ({True},) # Duplicate booleans
assert node.create_set() == (set(),) # Empty set with no arguments
# Test conversion from non-boolean values
assert node.create_set(item_0=1, item_1=0) == ({True, False},)
assert node.create_set(item_0=1, item_1=0, item_2="") == ({True, False},)
def test_set_add():
+34
View File
@@ -10,6 +10,7 @@ from src.basic_data_handling.string_nodes import (
StringDecode,
StringEncode,
StringEndswith,
StringEscape,
StringExpandtabs,
StringFind,
StringFormatMap,
@@ -47,6 +48,7 @@ from src.basic_data_handling.string_nodes import (
StringStrip,
StringSwapcase,
StringTitle,
StringUnescape,
StringUpper,
StringZfill,
)
@@ -364,6 +366,38 @@ def test_zfill():
assert node.zfill("123", 2) == ("123",) # Width smaller than string length
assert node.zfill("", 3) == ("000",) # Empty string
def test_unescape():
node = StringUnescape()
# Test control characters
assert node.unescape(r"Hello\nWorld") == ("Hello\nWorld",) # Newline
assert node.unescape(r"Hello\tWorld") == ("Hello\tWorld",) # Tab
assert node.unescape(r"C:\\Program Files\\App") == (r"C:\Program Files\App",) # Backslash
assert node.unescape(r"Quote: \"Text\"") == ("Quote: \"Text\"",) # Double quote
assert node.unescape(r"Quote: \'Text\'") == ("Quote: 'Text'",) # Single quote
# Test that normal Unicode characters remain unchanged
assert node.unescape("German: äöüß") == ("German: äöüß",) # Umlauts should be preserved
assert node.unescape("Hello äöü\nWorld") == ("Hello äöü\nWorld",) # Umlauts with control char
# Test complex string with multiple escape sequences
assert node.unescape(r"Path: C:\\folder\\file.txt\nLine1\tLine2") == ("Path: C:\\folder\\file.txt\nLine1\tLine2",)
def test_escape():
node = StringEscape()
# Test control characters
assert node.escape("Hello\nWorld") == (r"Hello\nWorld",) # Newline
assert node.escape("Hello\tWorld") == (r"Hello\tWorld",) # Tab
assert node.escape(r"C:\Program Files\App") == (r"C:\\Program Files\\App",) # Backslash
assert node.escape('Quote: "Text"') == (r'Quote: \"Text\"',) # Double quote
assert node.escape("Quote: 'Text'") == (r"Quote: \'Text\'",) # Single quote
# Test that normal Unicode characters remain unchanged
assert node.escape("German: äöüß") == (r"German: äöüß",) # Umlauts should be preserved
assert node.escape("Hello äöü\nWorld") == (r"Hello äöü\nWorld",) # Umlauts with control char
# Test complex string with multiple special characters
assert node.escape("Path: C:\\folder\\file.txt\nLine1\tLine2") == (r"Path: C:\\folder\\file.txt\nLine1\tLine2",)
def test_length():
node = StringLength()
assert node.length("hello") == (5,)
+558 -278
View File
@@ -33,261 +33,489 @@ const TypeSlotEvent = {
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, app) {
// Filter: Only process nodes with class names starting with "Basic data handling:"
async beforeRegisterNodeDef(nodeType, nodeData, appInstance) {
if (!nodeType.comfyClass.startsWith('Basic data handling:')) {
return;
}
const combinedInputData = {
...nodeData?.input?.required ?? {},
...nodeData?.input?.optional ?? {}
...(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 combinedInputDataOrder = [
...nodeData?.input_order?.required ?? [],
...nodeData?.input_order?.optional ?? []
];
/** Array of (generic) dynamic inputs. */
const dynamicInputs = [];
/** Array of groups of (generic) dynamic inputs. */
const dynamicInputGroups = [];
for (const name of combinedInputDataOrder) {
const dynamic = combinedInputData[name][1]?._dynamic;
if (!combinedInputData[name]) continue;
const dynamicSetting = combinedInputData[name][1]?._dynamic;
const dynamicGroup = combinedInputData[name][1]?._dynamicGroup ?? 0;
if (dynamic) {
let matcher;
if (dynamicSetting) { // Only process for inputs marked with _dynamic
const inputOptions = combinedInputData[name][1] || {};
const dynamicComfyType = combinedInputData[name][0];
switch (dynamic) {
case 'number':
matcher = new RegExp(`^(${name.replace(/\d*$/, '')})(\\d+)$`);
break;
let isActuallyWidget = false;
let effectiveWidgetGuiType;
case 'letter':
matcher = new RegExp(`^()([a-zA-Z])$`);
break;
default:
continue;
if (inputOptions.widget?.type) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widget.type;
} else if (inputOptions.widget && typeof inputOptions.widget === 'string') {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widget;
}
const baseName = name.match(matcher)?.[1] ?? name;
const dynamicType = combinedInputData[name][0];
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, matcher, dynamic, dynamicType, 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;
}
/**
* Utility: Check if an input is dynamic.
* @param {string} inputName - Name of the input to check.
*/
const isDynamicInput = (inputName) =>
dynamicInputs.some((di) => di.matcher.test(inputName));
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);
/**
* Utility: Update inputs' slot indices after reordering.
* @param {ComfyNode} node - The node to update.
*/
const updateSlotIndices = (node) => {
node.inputs.forEach((input, index) => {
input.slot_index = index;
if (input.isConnected) {
const link = node.graph._links.get(input.link);
if (link) {
link.target_slot = index;
} else {
console.error(`Input ${index} has an invalid link.`);
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 || {};
// Add helper method to insert input at a specific position
nodeType.prototype.addInputAtPosition = function (name, type, position, isWidget, shape) {
if (isWidget) {
this.addWidget(type, name, '', ()=>{}, {});
const GET_CONFIG = Symbol();
const input = this.addInput(name, type, {
shape,
widget: {name, [GET_CONFIG]: () =>{}}
})
} else {
this.addInput(name, type, {shape}); // Add new input
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 newInput = this.inputs.pop(); // Fetch the newly added input (last item)
this.inputs.splice(position, 0, newInput); // Place it at the desired position
updateSlotIndices(this); // Update indices
return newInput;
};
// flag to prevent loops. It is ok to be "global" as the code is not
// running in parallel.
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;
// Override onConnectionsChange: Handle connections for dynamic inputs
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 result = onConnectionsChange?.apply(this, arguments);
if (type !== TypeSlot.Input || isProcessingConnection || !isDynamicInput(this.inputs[slotIndex].name)) {
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;
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);
}
}
return undefined;
}
isProcessingConnection = true;
try {
// Get dynamic input slots
const dynamicSlots = [];
const dynamicGroupCount = [];
const dynamicGroupConnected = [];
for (const [index, input] of this.inputs.entries()) {
const isDynamic = isDynamicInput(input.name);
if (isDynamic) {
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;
dynamicGroupCount[dynamicGroup]++;
}
} else {
dynamicGroupCount[dynamicGroup] = 1;
dynamicGroupConnected[dynamicGroup] = [connected];
}
dynamicSlots.push({
index,
name: input.name,
isWidget: input.widget !== undefined,
shape: input.shape,
connected,
isDynamic,
dynamicGroup,
dynamicGroupCount: dynamicGroupCount[dynamicGroup]
});
// sanity check to make sure every widget is in reality a widget. When loading a workflow this
// isn't the case so we must fix it ourselves.
if (this.widgets && !this.widgets.some((w) => w.name === input.name)) {
this.addWidget(input.type, input.name, '', ()=>{}, {});
}
}
}
// Handle connection event
if (isConnected === TypeSlotEvent.Connect) {
const hasEmptyDynamic = dynamicGroupConnected[0].some(dgc => !dgc);
if (!hasEmptyDynamic) {
// No empty slot - add a new one after the last dynamic input
const lastDynamicIdx = Math.max(...dynamicSlots.map((slot) => slot.index), -1);
let insertPosition = lastDynamicIdx + 1;
let inputInRange = true;
for (const groupMember of dynamicInputGroups[dynamicInputs[0].dynamicGroup]) {
const baseName = dynamicInputs[groupMember].baseName;
const dynamicType = dynamicInputs[groupMember].dynamicType;
let newName;
if (dynamicInputs[0].dynamic === 'letter') {
if (dynamicSlots.length >= 26) {
inputInRange = false;
}
// For letter type, use the next letter in sequence
newName = String.fromCharCode(97 + dynamicSlots.length); // 97 is ASCII for 'a'
} else {
// For number type, use baseName + index as before
newName = `${baseName}${dynamicSlots.length}`;
}
if (inputInRange) {
// Insert the new empty input at the correct position
this.addInputAtPosition(newName, dynamicType, insertPosition++, dynamicSlots[groupMember].isWidget, dynamicSlots[groupMember].shape);
// Renumber inputs after addition
this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic);
}
}
}
} else if (isConnected === TypeSlotEvent.Disconnect) {
let foundEmptyIndex = -1;
for (let idx = 0; idx < this.inputs.length; idx++) {
const input = this.inputs[idx];
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 (isLast) {
continue;
}
for (let i = idx + 1; i < this.inputs.length; i++) {
this.swapInputs(i - 1, i);
}
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);
}
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);
} catch (e) {
console.error(e);
debugger;
alert(e);
} finally {
isProcessingConnection = false;
}
return result;
return originalReturn;
};
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 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);
@@ -298,96 +526,148 @@ app.registerExtension({
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);
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 result;
return onRemoved?.apply(this, arguments);
}
// Method to swap two inputs in the "this.inputs" array by their indices
nodeType.prototype.swapInputs = function(indexA, indexB) {
// Validate indices
if (
indexA < 0 || indexB < 0 ||
indexA >= this.inputs.length ||
indexB >= this.inputs.length ||
indexA === indexB
) {
console.error("Invalid input indices for swapping:", indexA, indexB);
return;
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;
// reflect the swap with the widgets
if (this.inputs[indexA].widget !== undefined) {
if (this.inputs[indexB].widget === undefined) {
console.error("Bad swap: input A is a widget but input B is not", indexA, indexB);
}
const widgetIdxA = this.widgets.findIndex((w) => w.name === this.inputs[indexA].widget.name);
const widgetIdxB = this.widgets.findIndex((w) => w.name === this.inputs[indexB].widget.name);
[this.widgets[widgetIdxA].y, this.widgets[widgetIdxB].y] = [this.widgets[widgetIdxB].y, this.widgets[widgetIdxA].y];
[this.widgets[widgetIdxA].last_y, this.widgets[widgetIdxB].last_y] = [this.widgets[widgetIdxB].last_y, this.widgets[widgetIdxA].last_y];
[this.widgets[widgetIdxA], this.widgets[widgetIdxB]] = [this.widgets[widgetIdxB], this.widgets[widgetIdxA]];
if (this.widgets_values) {
[this.widgets_values[widgetIdxA], this.widgets_values[widgetIdxB]] = [this.widgets_values[widgetIdxB], this.widgets_values[widgetIdxA]];
}
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);
}
// Swap the inputs in the array
[this.inputs[indexA].boundingRect, this.inputs[indexB].boundingRect] = [this.inputs[indexB].boundingRect, this.inputs[indexA].boundingRect];
[this.inputs[indexA].pos, this.inputs[indexB].pos] = [this.inputs[indexB].pos, this.inputs[indexA].pos];
[this.inputs[indexA], this.inputs[indexB]] = [this.inputs[indexB], this.inputs[indexA]];
updateSlotIndices(this); // Refresh indices
// Redraw the node to ensure the graph updates properly
// -> not needed as the calling method must do it!
this.setDirtyCanvas(true, true);
isProcessingConnection = false;
return originalReturn;
};
// Add method to safely renumber dynamic inputs without breaking connections
nodeType.prototype.renumberDynamicInputs = function(baseName, dynamicInputs, dynamic) {
// Get current dynamic inputs info
const dynamicInputInfo = [];
nodeType.prototype.getDynamicSlots = function(filterDynamicGroup = null) { // (Content largely unchanged, relies on isDynamicInputEmpty which is updated)
const dynamicSlotsResult = [];
const dynamicGroupCount = {};
const dynamicGroupConnected = {};
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
const isDynamic = isDynamicInput(input.name);
const itemsState = new Map();
if (isDynamic && input.name.startsWith(baseName)) {
dynamicInputInfo.push({
index: i,
widgetIdx: input.widget !== undefined ? this.widgets.findIndex((w) => w.name === input.widget.name) : undefined,
name: input.name,
connected: input.isConnected
});
}
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,
});
}
// Just rename the inputs in place - don't remove/add to keep connections intact
for (let i = 0; i < dynamicInputInfo.length; i++) {
const info = dynamicInputInfo[i];
const input = this.inputs[info.index];
const newName = dynamic === "number" ? `${baseName}${i}` : String.fromCharCode(97 + i); // 97 is ASCII for 'a'
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));
});
if (input.widget !== undefined) {
const widgetIdx = info.widgetIdx;
input.widget.name = newName;
this.widgets[widgetIdx].name = newName;
this.widgets[widgetIdx].label = newName;
}
if (input.name !== newName) {
input.name = newName;
input.localized_name = newName;
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;
};
}
});