@@ -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
@@ -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"}
|
||||
]
|
||||
|
||||
@@ -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],)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user