diff --git a/Instructions.txt b/Instructions.txt new file mode 100644 index 0000000..a26aab6 --- /dev/null +++ b/Instructions.txt @@ -0,0 +1,358 @@ +TL;DR:"I want to create a custom ComfyUI node for the desktop version. It must include JavaScript that runs inside the ComfyUI front-end when the UI loads. The node folder must contain a /web/js/ directory with the JavaScript file inside. In __init__.py, you must define WEB_DIRECTORY = './web/js' and include WEB_DIRECTORY in the __all__ list, so that ComfyUI will load the JavaScript into the UI. Also add a clear console.log() in the .js file to verify that it's loaded. The final node should work in the local ComfyUI desktop version without additional setup." + + +Example Output Expected +======================== + +*Directory layout: + +MyCustomNode/ +├── __init__.py +├── src/ +│ └── MyCustomNode/ +│ ├── __init__.py +│ └── nodes.py +├── other stuff +└── web/ + └── js/ + └── my_custom_script.js + +*Inside __init__.py: + +from .my_node import NODE_CLASS_MAPPINGS + +WEB_DIRECTORY = "./web/js" +__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY'] + +*Inside my_custom_script.js: + +console.log("[MyCustomNode] ✅ JavaScript loaded successfully."); + +======================= + +before attempting to create the nodes check the repo of another working nodes like : + +>> https://github.com/yolain/ComfyUI-Easy-Use/tree/main +>> https://github.com/jags111/efficiency-nodes-comfyui/tree/main +>> https://github.com/M1kep/ComfyLiterals/tree/master + +IMPORTANT: at start before going deeper, create the directories with simple nodes to check if the nodes are working and JavaScript .js extensions are loaded successfully. + +function and behaviors of the nodes : + +1- Dynamic Sliders Stack node +- has "sliders_count" widget to control how many sliders visible (type=int, min=1, step=1, max=10, default=5) +- has "sliders_Max" widget to control the Maximum value of each slider (type=float, min=0, step=0.01, max=2, default=1) +- has title and slider widget pairs. +- the title widget is directly on top of it's corresponding slider widget and it allows to type in the title of the slider below it. +- the sliders widgets are colored bars that allow for dragging to adjust the slider value, it should contain a displayed name similar to the title (type= float, min=0, step=0.01, max=sliders_Max, default= 1) +- increasing sliders_count by 1, adds a new title-slider pair and the opposite is true. The node shape and appearance updates based on the number of widgets visible. the node should not have extra empty space, or lacks the space for the displayed widgets. this behaviour is similar how the Lora_stack works as in the efficiency_nodes. the node should be manually sized without any issue. +- the Sliders_Max value is the cap value for all sliders. for example if slider_1 has the maximum value compared to the rest of sliders and is set to 2, and I adjusted the sliders_max value to 0.5, the Slider_1 value should dynamically move to 0.5 and update it's value in the UI live. also, the rest of sliders shall decrease while maintaining the ratios between them. sliders_Max is like a scaling variable that scale the whole sliders such that the slider with maximum value is set to the sliders_Max value and the rest of sliders follow while keeping the ratio between them. also, the Sliders_Max value shall equal to the value of the slider with maximum value all the time (caped at 2). +- silder_(i) = slider_normalized_(i) * slider_Max << this uses normalized values so that the sliders values are maintained proportionally. +- the node has one output (type=string) it has the values of the displayed sliders only as a series e.g. (0.01, 0.50, 1.00, ... ). + + + +2- Stack Receiver node +- it has one input port (labeled: Input) , and one input widget (Labelled= Slider Index). +- the input port accepts the output of the Dynamic Sliders Stack node. +- the input widget set the index of the Slider (index start from 1 to 10 ) +- the node has one output port that gives the value of the Indexed Slider. + + + +general requirements to be considered: +- I want a complete working, error_free Custom node folder, with all the necessary .js , .py , .text, etc.. files. +- It must stick to comfyui custom nodes standard folder structure and file naming. +- javascript is allowed. +- the two node must be in different .py files. +- consistency between nodes in terms of variables names and such. +- in comfyui UI I should be able to right click and navigate to add node >> custom nodes >> Dynamic_Slider_Pack and find my two nodes. +- I'm using comfyui desktop app +- the path to comfyui : D:\ComfyUi +- utilize the use of loops to make adjustment easier. + + + + +///references: + +///example custom node file : + +class Example: + """ + A example node + + Class methods + ------------- + INPUT_TYPES (dict): + Tell the main program input parameters of nodes. + IS_CHANGED: + optional method to control when the node is re executed. + + Attributes + ---------- + RETURN_TYPES (`tuple`): + The type of each element in the output tuple. + RETURN_NAMES (`tuple`): + Optional: The name of each output in the output tuple. + FUNCTION (`str`): + The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() + OUTPUT_NODE ([`bool`]): + If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example. + The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected. + Assumed to be False if not present. + CATEGORY (`str`): + The category the node should appear in the UI. + DEPRECATED (`bool`): + Indicates whether the node is deprecated. Deprecated nodes are hidden by default in the UI, but remain + functional in existing workflows that use them. + EXPERIMENTAL (`bool`): + Indicates whether the node is experimental. Experimental nodes are marked as such in the UI and may be subject to + significant changes or removal in future versions. Use with caution in production workflows. + execute(s) -> tuple || None: + The entry point method. The name of this method must be the same as the value of property `FUNCTION`. + For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`. + """ + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". + Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. + The type can be a list for selection. + + Returns: `dict`: + - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` + - Value input_fields (`dict`): Contains input fields config: + * Key field_name (`string`): Name of a entry-point method's argument + * Value field_config (`tuple`): + + First value is a string indicate the type of field or a list for selection. + + Second value is a config for type "INT", "STRING" or "FLOAT". + """ + return { + "required": { + "image": ("IMAGE",), + "int_field": ("INT", { + "default": 0, + "min": 0, #Minimum value + "max": 4096, #Maximum value + "step": 64, #Slider's step + "display": "number", # Cosmetic only: display as "number" or "slider" + "lazy": True # Will only be evaluated if check_lazy_status requires it + }), + "float_field": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 10.0, + "step": 0.01, + "round": 0.001, #The value representing the precision to round to, will be set to the step value by default. Can be set to False to disable rounding. + "display": "number", + "lazy": True + }), + "print_to_screen": (["enable", "disable"],), + "string_field": ("STRING", { + "multiline": False, #True if you want the field to look like the one on the ClipTextEncode node + "default": "Hello World!", + "lazy": True + }), + }, + } + + RETURN_TYPES = ("IMAGE",) + #RETURN_NAMES = ("image_output_name",) + + FUNCTION = "test" + + #OUTPUT_NODE = False + + CATEGORY = "Example" + + def check_lazy_status(self, image, string_field, int_field, float_field, print_to_screen): + """ + Return a list of input names that need to be evaluated. + + This function will be called if there are any lazy inputs which have not yet been + evaluated. As long as you return at least one field which has not yet been evaluated + (and more exist), this function will be called again once the value of the requested + field is available. + + Any evaluated inputs will be passed as arguments to this function. Any unevaluated + inputs will have the value None. + """ + if print_to_screen == "enable": + return ["int_field", "float_field", "string_field"] + else: + return [] + + def test(self, image, string_field, int_field, float_field, print_to_screen): + if print_to_screen == "enable": + print(f"""Your input contains: + string_field aka input text: {string_field} + int_field: {int_field} + float_field: {float_field} + """) + #do some processing on the image, in this example I just invert it + image = 1.0 - image + return (image,) + + """ + The node will always be re executed if any of the inputs change but + this method can be used to force the node to execute again even when the inputs don't change. + You can make this node return a number or a string. This value will be compared to the one returned the last time the node was + executed, if it is different the node will be executed again. + This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash + changes between executions the LoadImage node is executed again. + """ + #@classmethod + #def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): + # return "" + +# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension +# WEB_DIRECTORY = "./somejs" + + +# Add custom API routes, using router +from aiohttp import web +from server import PromptServer + +@PromptServer.instance.routes.get("/hello") +async def get_hello(request): + return web.json_response("hello") + + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "Example": Example +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "Example": "Example Node" +} + +/// reddit page : +https://www.reddit.com/r/comfyui/comments/18wp6oj/tutorial_create_a_custom_node_in_5_minutes/?rdt=32812 + +/// typical Directory Structure and templates files : + +my_custom_nodepack/ *Name folder of the project. +├── .editorconfig *Configuration file to define and maintain consistent coding styles between different editors and IDEs. +├── .gitignore *File specifying files and directories to be ignored by Git. +├── .pre-commit-config.yaml *Configuration file for pre-commit hooks, used to automate checks before commits. +├── .vscode/ *Folder containing configurations specific to Visual Studio Code. +│ └── settings.json *Configuration file for workspace settings in VS Code. +├── LICENSE *File containing the project's license +├── MANIFEST.in *File used to include additional files in the package during distribution. +├── README.md *Main documentation file for the project, often used to describe the project, how to install and use it +├── pyproject.toml *File containing project metadata and dependencies. +├── src/ *Folder containing the source code of the project. +│ └── my_custom_nodepack/ *Folder containing the source code of the custom Node. +│ ├── __init__.py *File to make Python treat the directory as a package. +│ └── nodes.py *File containing the custom Node class. +├── tests/ *Folder containing test files. +│ ├── __init__.py *File to make Python treat the directory as a package. +│ ├── conftest.py *File containing fixtures for the tests. +│ ├── pytest.ini *Configuration file for pytest. +│ └── test_my_custom_nodepack.py *File containing tests for the custom Node. +├── web/ *Folder containing web assets. +│ └── js/ *Folder containing JavaScript files. +│ └── example.js *JavaScript file. +└── .github/ *Folder containing GitHub-specific files. + ├── ISSUE_TEMPLATE.md *Template for creating issues. + └── workflows/ *Folder containing GitHub Actions workflows. + ├── build-pipeline.yml *Workflow file for the build pipeline. + ├── publish_node.yml *Workflow file for publishing the custom Node. + └── validate.yml *Workflow file for validating the custom Node. + + +ComfyUI JavaScript Injection Guide (Prompt-Ready Format) +======================================================== + +TL;DR (AI-Optimized) +--------------------- +I want to create a custom ComfyUI node for the desktop version. It must include JavaScript +that runs inside the ComfyUI front-end automatically when UI loads. + +✅ Place the JS in `/web/js/` +✅ Declare `WEB_DIRECTORY = './web/js'` in `__init__.py` +✅ JavaScript must use `app.registerExtension()` wrapped in a `setInterval` until `app` is available. + + +Directory Layout +---------------- +MyCustomNode/ +├── __init__.py # maps node + JS +├── nodes/ +│ └── my_node.py # Python logic of your node +└── web/ + └── js/ + └── my_script.js # Front-end logic (UI behaviors, etc.) + + +__init__.py Template +-------------------- +from .nodes.my_node import MyNode + +NODE_CLASS_MAPPINGS = { + "MyNode": MyNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "MyNode": "My Custom Node" +} + +WEB_DIRECTORY = "./web/js" # 👈 Key line to inject JS into UI + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] + + +JavaScript Template (Guaranteed to Work) +---------------------------------------- +(function () { + console.log("[MyNode] ✅ Injecting via UI event hook"); + + const extension = { + name: "MyNode", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "MyNode") return; + + nodeType.prototype.onNodeCreated = function () { + const node = this; + // 🔁 Customize widget logic here + console.log("[MyNode] UI logic running..."); + }; + } + }; + + // 🔁 Poll until ComfyUI's `app` is available + const waitForApp = setInterval(() => { + if (typeof app !== 'undefined') { + app.registerExtension(extension); + clearInterval(waitForApp); + } + }, 100); +})(); + + +Quick Test for Success +---------------------- +When ComfyUI loads: +- Open browser devtools +- You should see: + [MyNode] ✅ Injecting via UI event hook + + +References +---------- +- ComfyLiterals: https://github.com/M1kep/ComfyLiterals +- ComfyUI-Easy-Use: https://github.com/yolain/ComfyUI-Easy-Use +- Efficiency Nodes: https://github.com/jags111/efficiency-nodes-comfyui + +Just rename your node logic and identifiers. Works for any custom node JS. + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..12cecb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +## LICENSE File (MIT License) + +```text name=LICENSE +MIT License + +Copyright (c) 2024 Your Name or Project Name Here + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b6c7bb --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Dynamic Sliders Stack for ComfyUI + +A custom node pack for ComfyUI providing a stack of named sliders with dynamic behavior. + +## Features + +* **Slider Stacker**: A node with a configurable number of named sliders (up to 50). +* **Slider Receiver**: Extracts the weight value of a specific slider from the stack output based on its index. +* **Dynamic Max Strength**: Adjusting the `slider_max_strength` scales all individual sliders proportionally. +* **Dynamic Individual Sliders**: Adjusting an individual `slider_wt` updates the `slider_max_strength` to reflect the new highest value among all sliders. +* **String Output**: The `Slider Stacker` outputs a comma-separated string of the current weights for all active sliders. +* **Dynamic UI**: The stacker node automatically shows/hides slider input rows based on the `slider_count` setting. +* **Context Menu**: Right-click options for batch slider operations (Average, Reset, Set All to Max/Min). + +## Nodes + +### Slider Stacker + +* **Inputs**: + * `slider_max_strength` (FLOAT): The master maximum value. Adjusting this scales all sliders below it. + * `slider_count` (INT): Controls how many slider rows are visible and processed (1-50). + * `slider_name_{i}` (STRING): A text field to name or describe the slider (optional). + * `({i})` (FLOAT Widget): The individual weight for this slider (0.0-2.0). Adjusting this updates the `slider_max_strength` if this slider becomes the new maximum. (Widget name uses parentheses around the index). +* **Output**: + * `SLIDER_WEIGHTS` (STRING): A comma-separated string of the weight values for sliders 1 through `slider_count`, formatted to two decimal places (e.g., "1.00, 0.50, 0.75"). +* **Right-Click Options**: + * `Average Slider Values`: Sets all visible sliders to their average value. + * `Reset Slider Values`: Sets all visible sliders to the default value (1.0). + * `Set All to Max Value`: Finds the highest value among visible sliders and sets all visible sliders to that value. + * `Set All to Min Value`: Finds the lowest value among visible sliders and sets all visible sliders to that value. + +### Slider Receiver + +* **Inputs**: + * `weights_string` (STRING): Connect the `SLIDER_WEIGHTS` output from the `Slider Stacker` here. + * `index` (INT): The 1-based index of the slider whose weight you want to extract (1-50). +* **Output**: + * `WEIGHT` (FLOAT): The floating-point value of the slider at the specified index (returns 0.0 if index is out of bounds or input is invalid). + +## Installation + +1. Ensure the folder containing these files is named `Dynamic_sliders_stack`. +2. Place the `Dynamic_sliders_stack` folder inside your ComfyUI `custom_nodes` directory. +3. Restart ComfyUI. + +## Usage Example + +1. Add a `Slider Stacker` node (found under the `Dynamic Sliders Stack` category). +2. Set the `slider_count`. +3. Adjust sliders or use right-click options. +4. Add a `Slider Receiver` node. +5. Connect `SLIDER_WEIGHTS` to `weights_string`. +6. Set the `index` on the Receiver. +7. Connect the `WEIGHT` output. + +## Use Case Scenarios + +This node setup provides a centralized control panel for managing multiple numerical weights within your workflow. Here are a few examples: + +* **Multiple LoRA Weights:** Assign each slider to a different LoRA. Use the `Slider Stacker` to adjust the overall impact (`slider_max_strength`) or fine-tune individual LoRA weights. Use `Slider Receiver` nodes to pipe the individual weights into the `strength_model` and/or `strength_clip` inputs of `LoRA Loader` nodes. +* **Prompt Segment Weighting:** If using nodes that allow weighting different parts of a prompt (e.g., via attention syntax or specialized nodes), you could use sliders to control the emphasis on different elements like `(subject:WEIGHT)`, `(style:WEIGHT)`, `(background:WEIGHT)`. Connect `Slider Receiver` outputs to the relevant weight inputs. +* **Embedding/Textual Inversion Strength:** Control the influence of multiple embeddings by connecting `Slider Receiver` outputs to nodes that adjust their strength (if available). +* **Style/Character Consistency:** If you have multiple elements defining a style or character (multiple LoRAs, specific prompt weights), group their controls onto one `Slider Stacker` for easier holistic adjustments. +* **Controlling Custom Node Parameters:** Any custom node that accepts a FLOAT input for strength, influence, weight, etc., can potentially be controlled by connecting a `Slider Receiver`'s output to it. +* **Centralized Control Panel:** Create a 'dashboard' near the start of your workflow using one or more `Slider Stacker` nodes to manage key weights without needing to hunt down individual nodes later in the graph. + +## License + +Please check the LICENSE file. Remember to update the placeholder copyright line `Copyright (c) 2024 Your Name or Project Name Here` with the correct year and your name/project name. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d42ef6d --- /dev/null +++ b/__init__.py @@ -0,0 +1,25 @@ +import os +import sys +import __main__ + +# Add the node's directory to the system path +NODE_DIR = os.path.dirname(__file__) +COMFYUI_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__main__.__file__))) +sys.path.append(NODE_DIR) + +# Import the node classes and mappings +from .slider_stacker_node import NODE_CLASS_MAPPINGS as stacker_mappings, NODE_DISPLAY_NAME_MAPPINGS as stacker_display_mappings +from .slider_receiver_node import NODE_CLASS_MAPPINGS as receiver_mappings, NODE_DISPLAY_NAME_MAPPINGS as receiver_display_mappings + +# Path for the javascript directory (relative to this file) +WEB_DIRECTORY = "js" + +# Combine mappings +NODE_CLASS_MAPPINGS = {**stacker_mappings, **receiver_mappings} +NODE_DISPLAY_NAME_MAPPINGS = {**stacker_display_mappings, **receiver_display_mappings} + +# Export mappings for ComfyUI +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] + +# Updated print statement +print('Loaded Dynamic Sliders Stack nodes') \ No newline at end of file diff --git a/context.gif b/context.gif new file mode 100644 index 0000000..1568e7c Binary files /dev/null and b/context.gif differ diff --git a/dynamic_slider_stacker_reciever_nodes.png b/dynamic_slider_stacker_reciever_nodes.png new file mode 100644 index 0000000..05f8bc8 Binary files /dev/null and b/dynamic_slider_stacker_reciever_nodes.png differ diff --git a/js/Dynamic.js b/js/Dynamic.js new file mode 100644 index 0000000..fec9fce --- /dev/null +++ b/js/Dynamic.js @@ -0,0 +1,159 @@ +import { app } from "../../scripts/app.js"; + +console.log("Loading Dynamic.js for Slider Stacker (Refactored)"); + +// --- Helper Functions --- (remain the same) ... + +const findWidgetByName = (node, name) => { + return node.widgets ? node.widgets.find((w) => w.name === name) : null; +}; +const getWidgetValue = (widget) => { + const val = widget?.value; + const num = Number(val); + return isNaN(num) ? 0 : num; +}; +function interceptWidgetValue(node, widgetName, setterCallback) { + const widget = findWidgetByName(node, widgetName); + if (!widget) return; + let internalValueTracker = widget.value; + let originalDescriptor = Object.getOwnPropertyDescriptor(widget, 'value'); + if (!originalDescriptor) { + let proto = Object.getPrototypeOf(widget); + while (proto && !originalDescriptor && proto !== Object.prototype) { + originalDescriptor = Object.getOwnPropertyDescriptor(proto, 'value'); + proto = Object.getPrototypeOf(proto); + } + } + Object.defineProperty(widget, 'value', { + configurable: true, + enumerable: true, + get() { /* ... */ return originalDescriptor?.get ? originalDescriptor.get.call(widget) : internalValueTracker; }, + set(newValue) { + if (node._isUpdatingInternally) { /* ... */ + if (originalDescriptor?.set) { originalDescriptor.set.call(widget, newValue); } else { internalValueTracker = newValue; } + if (widget.inputEl) { widget.inputEl.value = Number(newValue).toFixed(widget.options?.precision ?? 2); } + return; + } + const clampedNewValue = Math.max(widget.options?.min ?? -Infinity, Math.min(Number(newValue) || 0, widget.options?.max ?? Infinity)); + if (originalDescriptor?.set) { originalDescriptor.set.call(widget, clampedNewValue); } + internalValueTracker = clampedNewValue; + if(widget.inputEl) { widget.inputEl.value = internalValueTracker.toFixed(widget.options?.precision ?? 2); } + setterCallback(clampedNewValue); + } + }); +} + +// --- Core Logic Functions --- (remain the same) ... + +function recalculateCurrentMaxStrength(node) { /* ... */ + const sliderCountWidget = findWidgetByName(node, "slider_count"); + const count = getWidgetValue(sliderCountWidget); + let currentMaxValue = 0; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(node, `(${i})`); + if (wtWidget) { + currentMaxValue = Math.max(currentMaxValue, getWidgetValue(wtWidget)); + } + } + const maxStrengthWidget = findWidgetByName(node, "slider_max_strength"); + const overallMax = maxStrengthWidget?.options?.max ?? 2.0; + const overallMin = maxStrengthWidget?.options?.min ?? 0.0; + return Math.max(overallMin, Math.min(currentMaxValue, overallMax)); + } +function updateNormalization(node, currentMaxStrength) { /* ... */ + if (!node.normalizedSliderWeights) node.normalizedSliderWeights = {}; + const sliderCountWidget = findWidgetByName(node, "slider_count"); + const maxSliders = sliderCountWidget?.options?.max || 50; + for (let i = 1; i <= maxSliders; i++) { + const wtWidget = findWidgetByName(node, `(${i})`); + if (wtWidget) { + const currentWt = getWidgetValue(wtWidget); + node.normalizedSliderWeights[i] = (currentMaxStrength > 0) ? (currentWt / currentMaxStrength) : (node.normalizedSliderWeights[i] !== undefined ? node.normalizedSliderWeights[i] : 0); + } + } +} +function updateAllSliderDisplays(node, newMaxStrength) { /* ... */ + if (!node.normalizedSliderWeights || node._isUpdatingInternally) return; + node._isUpdatingInternally = true; + const sliderCountWidget = findWidgetByName(node, "slider_count"); + const count = getWidgetValue(sliderCountWidget); + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(node, `(${i})`); + if (wtWidget) { + const normalizedValue = node.normalizedSliderWeights[i] || 0; + let newDisplayValue = normalizedValue * newMaxStrength; + const widgetMax = wtWidget.options?.max ?? 2.0; + const widgetMin = wtWidget.options?.min ?? 0.0; + newDisplayValue = Math.max(widgetMin, Math.min(newDisplayValue, widgetMax, newMaxStrength)); + if (Math.abs(getWidgetValue(wtWidget) - newDisplayValue) > 0.001) { + wtWidget.value = newDisplayValue; + } + } + } + node.graph.setDirtyCanvas(true, true); + node._isUpdatingInternally = false; +} + +// --- ComfyUI Extension --- + +app.registerExtension({ + // Updated extension name + name: "Comfy.DynamicSlidersStack.DynamicBehavior", + + nodeCreated(node) { + const nodeTitle = node.getTitle ? node.getTitle() : node.title; + // Check for new display name + if (nodeTitle !== "Slider Stacker") return; + + node.normalizedSliderWeights = {}; + node._isUpdatingInternally = false; + + const maxStrengthWidget = findWidgetByName(node, "slider_max_strength"); + const sliderCountWidget = findWidgetByName(node, "slider_count"); + + if (!maxStrengthWidget || !sliderCountWidget) { + console.error("Slider Stacker Dynamic.js: Setup failed, required widgets not found."); + return; + } + + // --- Initialize --- (remains the same) ... + setTimeout(() => { /* ... */ + const initialMax = recalculateCurrentMaxStrength(node); + if (Math.abs(initialMax - getWidgetValue(maxStrengthWidget)) > 0.001) { + maxStrengthWidget.value = initialMax; + } + updateNormalization(node, initialMax); + updateAllSliderDisplays(node, initialMax); + }, 150); + + + // --- Set up Widget Interceptors --- (remain the same, logic relies on widget names) + + interceptWidgetValue(node, "slider_max_strength", (newMax) => { /* ... */ + updateAllSliderDisplays(node, newMax); + const actualMaxAfterUpdate = recalculateCurrentMaxStrength(node); + if (Math.abs(actualMaxAfterUpdate - newMax) > 0.001) { + node._isUpdatingInternally = true; + maxStrengthWidget.value = actualMaxAfterUpdate; + node._isUpdatingInternally = false; + } + }); + + const maxSlidersIntercept = sliderCountWidget?.options?.max || 50; + for (let i = 1; i <= maxSlidersIntercept; i++) { + interceptWidgetValue(node, `(${i})`, (newWt) => { /* ... */ + const currentMaxStrengthWidget = findWidgetByName(node, "slider_max_strength"); + const currentMaxDisplayed = getWidgetValue(currentMaxStrengthWidget); + const newCalculatedMax = recalculateCurrentMaxStrength(node); + updateNormalization(node, newCalculatedMax); + if (Math.abs(newCalculatedMax - currentMaxDisplayed) > 0.001) { + node._isUpdatingInternally = true; + currentMaxStrengthWidget.value = newCalculatedMax; + node._isUpdatingInternally = false; + } else { + updateAllSliderDisplays(node, currentMaxDisplayed); + } + }); + } + } +}); \ No newline at end of file diff --git a/js/appearance.js b/js/appearance.js new file mode 100644 index 0000000..6842d13 --- /dev/null +++ b/js/appearance.js @@ -0,0 +1,65 @@ +import { app } from "../../scripts/app.js"; + +const COLOR_THEMES = { + red: { nodeColor: "#332222", nodeBgColor: "#553333" }, + green: { nodeColor: "#223322", nodeBgColor: "#335533" }, + blue: { nodeColor: "#222233", nodeBgColor: "#333355" }, + pale_blue: { nodeColor: "#2a363b", nodeBgColor: "#3f5159" }, + cyan: { nodeColor: "#223333", nodeBgColor: "#335555" }, + purple: { nodeColor: "#332233", nodeBgColor: "#553355" }, + yellow: { nodeColor: "#443322", nodeBgColor: "#665533" }, + none: { nodeColor: null, nodeBgColor: null } // no color +}; + +// Updated NODE_COLORS map +const NODE_COLORS = { + "Slider Stacker": "purple", + "Slider Receiver": "purple", // Added receiver, same color + // Add other nodes here if needed + }; + +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; // Swap elements + } +} + +let colorKeys = Object.keys(COLOR_THEMES).filter(key => key !== "none"); +shuffleArray(colorKeys); // Shuffle the color themes initially + +function setNodeColors(node, theme) { + if (!theme) {return;} + node.shape = "box"; // Apply box shape if desired + if(theme.nodeColor && theme.nodeBgColor) { + node.color = theme.nodeColor; + node.bgcolor = theme.nodeBgColor; + } +} + +const ext = { + // Updated extension name + name: "Comfy.DynamicSlidersStack.Appearance", + + nodeCreated(node) { + const nodeTitle = node.getTitle ? node.getTitle() : node.title; + // Check if the node title exists in our updated map + if (NODE_COLORS.hasOwnProperty(nodeTitle)) { + let colorKey = NODE_COLORS[nodeTitle]; + + // Random color logic (remains the same, applies if "random" is used) + if (colorKey === "random") { + if (colorKeys.length === 0 || !COLOR_THEMES[colorKeys[colorKeys.length - 1]]) { + colorKeys = Object.keys(COLOR_THEMES).filter(key => key !== "none"); + shuffleArray(colorKeys); + } + colorKey = colorKeys.pop(); + } + + const theme = COLOR_THEMES[colorKey]; + setNodeColors(node, theme); + } + } +}; + +app.registerExtension(ext); \ No newline at end of file diff --git a/js/context.js b/js/context.js new file mode 100644 index 0000000..ebd6e06 --- /dev/null +++ b/js/context.js @@ -0,0 +1,235 @@ +import { app } from "../../scripts/app.js"; + +console.log("Loading context.js for Slider Stacker"); + +// --- Helper Functions (Copied from Dynamic.js for self-containment) --- + +const findWidgetByName = (node, name) => { + return node.widgets ? node.widgets.find((w) => w.name === name) : null; +}; + +const getWidgetValue = (widget) => { + const val = widget?.value; + const num = Number(val); + return isNaN(num) ? 0 : num; +}; + +// Gets the actual max strength based on current slider values and activity +function recalculateCurrentMaxStrength(node) { + const sliderCountWidget = findWidgetByName(node, "slider_count"); + const count = getWidgetValue(sliderCountWidget); + let currentMaxValue = 0; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(node, `(${i})`); + if (wtWidget) { + currentMaxValue = Math.max(currentMaxValue, getWidgetValue(wtWidget)); + } + } + const maxStrengthWidget = findWidgetByName(node, "slider_max_strength"); + const overallMax = maxStrengthWidget?.options?.max ?? 2.0; + const overallMin = maxStrengthWidget?.options?.min ?? 0.0; + return Math.max(overallMin, Math.min(currentMaxValue, overallMax)); +} + +// Updates the stored normalized weights based on current displayed values and max strength +function updateNormalization(node, currentMaxStrength) { + if (!node.normalizedSliderWeights) node.normalizedSliderWeights = {}; // Ensure it exists + const sliderCountWidget = findWidgetByName(node, "slider_count"); + const maxSliders = sliderCountWidget?.options?.max || 50; + for (let i = 1; i <= maxSliders; i++) { + const wtWidget = findWidgetByName(node, `(${i})`); + if (wtWidget) { + const currentWt = getWidgetValue(wtWidget); + node.normalizedSliderWeights[i] = (currentMaxStrength > 0) + ? (currentWt / currentMaxStrength) + : (node.normalizedSliderWeights[i] !== undefined ? node.normalizedSliderWeights[i] : 0); + } + } +} + +// Updates the displayed values of individual sliders based on normalized weights and new max strength +// Note: Needs access to node._isUpdatingInternally if called directly, but here it's used post-operation +function updateAllSliderDisplaysFromContext(node, newMaxStrength) { + if (!node.normalizedSliderWeights) return; + node._isUpdatingInternally = true; // Set flag for this operation + const count = getWidgetValue(findWidgetByName(node, "slider_count")); + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(node, `(${i})`); + if (wtWidget) { + const normalizedValue = node.normalizedSliderWeights[i] || 0; + let newDisplayValue = normalizedValue * newMaxStrength; + const widgetMax = wtWidget.options?.max ?? 2.0; + const widgetMin = wtWidget.options?.min ?? 0.0; + newDisplayValue = Math.max(widgetMin, Math.min(newDisplayValue, widgetMax, newMaxStrength)); + if (Math.abs(getWidgetValue(wtWidget) - newDisplayValue) > 0.001) { + wtWidget.value = newDisplayValue; // This should use the intercepted setter + } + } + } + node.graph.setDirtyCanvas(true, true); + node._isUpdatingInternally = false; +} + +// --- Function to add menu options --- + +function addContextMenuOptions(node) { + const originalGetExtraMenuOptions = node.getExtraMenuOptions; + node.getExtraMenuOptions = function(_, options) { + if (originalGetExtraMenuOptions) { + originalGetExtraMenuOptions.apply(this, arguments); + } + + // Option 1: Average Values + options.push({ + content: "Average Slider Values", + callback: () => { + const count = getWidgetValue(findWidgetByName(this, "slider_count")); + let sum = 0; + let activeCount = 0; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if(wtWidget) { + sum += getWidgetValue(wtWidget); + activeCount++; + } + } + const average = activeCount > 0 ? sum / activeCount : 0; + + this._isUpdatingInternally = true; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if (wtWidget) { wtWidget.value = average; } + } + this._isUpdatingInternally = false; + + // Recalculate max and update normalization *after* loop + const newMax = recalculateCurrentMaxStrength(this); + updateNormalization(this, newMax); + + // Trigger update on master slider to sync everything + const masterWidget = findWidgetByName(this, "slider_max_strength"); + if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) { + masterWidget.value = newMax; // This uses the setter from Dynamic.js + } else { + // If max didn't change, still redraw displays based on new average + updateAllSliderDisplaysFromContext(this, newMax); + } + this.graph.setDirtyCanvas(true, true); + } + }); + + // Option 2: Reset Values + options.push({ + content: "Reset Slider Values", + callback: () => { + const count = getWidgetValue(findWidgetByName(this, "slider_count")); + const defaultValue = 1.0; + + this._isUpdatingInternally = true; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if (wtWidget) { wtWidget.value = defaultValue; } + } + this._isUpdatingInternally = false; + + const newMax = recalculateCurrentMaxStrength(this); + updateNormalization(this, newMax); + const masterWidget = findWidgetByName(this, "slider_max_strength"); + if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) { + masterWidget.value = newMax; + } else { + updateAllSliderDisplaysFromContext(this, newMax); + } + this.graph.setDirtyCanvas(true, true); + } + }); + + // Option 3: Set All to Max Value + options.push({ + content: "Set All to Max Value", + callback: () => { + const count = getWidgetValue(findWidgetByName(this, "slider_count")); + let maxValue = -Infinity; // Start low + + // First pass: Find the maximum value among visible sliders + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if(wtWidget) { + maxValue = Math.max(maxValue, getWidgetValue(wtWidget)); + } + } + // Ensure maxValue is within widget bounds if needed (though setters handle clamping) + // const widgetMaxBound = findWidgetByName(this, `(1)`)?.options?.max ?? 2.0; + // maxValue = Math.min(maxValue, widgetMaxBound); + + // Second pass: Set all visible sliders to the found maximum value + this._isUpdatingInternally = true; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if (wtWidget) { wtWidget.value = maxValue; } // Use setter + } + this._isUpdatingInternally = false; + + // Recalculate max (should be == maxValue), normalize, and update master slider + const newMax = recalculateCurrentMaxStrength(this); // Will be maxValue + updateNormalization(this, newMax); + const masterWidget = findWidgetByName(this, "slider_max_strength"); + // Update master slider directly - its setter handles updating displays + if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) { + masterWidget.value = newMax; + } + this.graph.setDirtyCanvas(true, true); + } + }); + + // Option 4: Set All to Min Value + options.push({ + content: "Set All to Min Value", + callback: () => { + const count = getWidgetValue(findWidgetByName(this, "slider_count")); + let minValue = Infinity; // Start high + + // First pass: Find the minimum value among visible sliders + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if(wtWidget) { + minValue = Math.min(minValue, getWidgetValue(wtWidget)); + } + } + // Ensure minValue isn't below widget bounds + // const widgetMinBound = findWidgetByName(this, `(1)`)?.options?.min ?? 0.0; + // minValue = Math.max(minValue, widgetMinBound); + + // Second pass: Set all visible sliders to the found minimum value + this._isUpdatingInternally = true; + for (let i = 1; i <= count; i++) { + const wtWidget = findWidgetByName(this, `(${i})`); + if (wtWidget) { wtWidget.value = minValue; } // Use setter + } + this._isUpdatingInternally = false; + + // Recalculate max (will be == minValue), normalize, and update master slider + const newMax = recalculateCurrentMaxStrength(this); // Will be minValue + updateNormalization(this, newMax); + const masterWidget = findWidgetByName(this, "slider_max_strength"); + // Update master slider directly + if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) { + masterWidget.value = newMax; + } + this.graph.setDirtyCanvas(true, true); + } + }); + }; +} + +// --- ComfyUI Extension --- + +app.registerExtension({ + name: "Comfy.DynamicSlidersStack.ContextMenu", + nodeCreated(node) { + const nodeTitle = node.getTitle ? node.getTitle() : node.title; + if (nodeTitle === "Slider Stacker") { + addContextMenuOptions(node); + } + } +}); \ No newline at end of file diff --git a/js/slider_stacker_widget_hider.js b/js/slider_stacker_widget_hider.js new file mode 100644 index 0000000..c901437 --- /dev/null +++ b/js/slider_stacker_widget_hider.js @@ -0,0 +1,150 @@ +import { app } from "../../scripts/app.js"; + +// --- Start: Simplified code snippets --- + +let origProps = {}; +let initialized = false; + +const findWidgetByName = (node, name) => { + return node.widgets ? node.widgets.find((w) => w.name === name) : null; +}; + +const doesInputWithNameExist = (node, name) => { + return node.inputs ? node.inputs.some((input) => input.name === name) : false; +}; + +const HIDDEN_TAG = "tschide"; +function toggleWidget(node, widget, show = false, suffix = "") { + // ... (toggleWidget function remains the same) ... + if (!widget || doesInputWithNameExist(node, widget.name)) return; + if (!origProps[widget.name]) { + origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize }; + } + const origSize = node.size; + widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix; + widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4]; + widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show)); + let newHeight = node.size[1]; + try { + if (node.computeSize) { + newHeight = node.computeSize()[1]; + } else if (node.onResize) { + node.onResize(node.size); + newHeight = node.size[1]; + } + } catch (e) { + console.error("Error computing node size for", node.getTitle(), e); + } + node.setSize([node.size[0], newHeight]); +} + +function generateWidgetNames(baseName, count) { + return Array.from({ length: count }, (_, i) => `${baseName}_${i + 1}`); +} + +function handleSliderVisibility(node, countValue) { + const node_type = "Slider Stacker"; + const baseNamesMap = { + "Slider Stacker": ["slider_name", ""] + }; + const baseNames = baseNamesMap[node_type]; + if (!baseNames) return; + const sliderCountWidget = findWidgetByName(node, "slider_count"); + const maxSliders = sliderCountWidget?.options?.max || 50; + for (let i = 1; i <= maxSliders; i++) { + const nameWidget = findWidgetByName(node, `${baseNames[0]}_${i}`); + const weightWidget = findWidgetByName(node, `(${i})`); + const currentCount = Number(countValue) || 0; + const show = i <= currentCount; + toggleWidget(node, nameWidget, show); + toggleWidget(node, weightWidget, show); + } + node.graph.setDirtyCanvas(true, true); +} + +function handleSliderStackerSliderCount(node, widget) { + handleSliderVisibility(node, widget.value); +} + +// Updated handler map key to match new display name +const nodeWidgetHandlers = { + "Slider Stacker": { // Updated node title check + 'slider_count': handleSliderStackerSliderCount + } +}; + +function widgetLogic(node, widget) { + const nodeTitle = node.getTitle ? node.getTitle() : node.title; + // Use the potentially updated title for the handler check + const handler = nodeWidgetHandlers[nodeTitle]?.[widget.name]; + if (handler) { + handler(node, widget); + } +} + +// --- End: Simplified code snippets --- + +app.registerExtension({ + // Updated extension name + name: "Comfy.DynamicSlidersStack.WidgetHider", + nodeCreated(node) { + const nodeTitle = node.getTitle ? node.getTitle() : node.title; + // Check for the new display name + if (nodeTitle === "Slider Stacker") { + let hasSliderStackerHandler = false; + + for (const w of node.widgets || []) { + // Use updated title for handler check + if (nodeWidgetHandlers[nodeTitle]?.[w.name]) { + hasSliderStackerHandler = true; + } + } + + if(hasSliderStackerHandler) { + for (const w of node.widgets) { + // Use updated title for handler check + if (nodeWidgetHandlers[nodeTitle]?.[w.name]) { + // ... (value interception logic remains the same) ... + let widgetValue = w.value; + let originalDescriptor = Object.getOwnPropertyDescriptor(w, 'value'); + if (!originalDescriptor) { + let proto = Object.getPrototypeOf(w); + while (proto && !originalDescriptor && proto !== Object.prototype) { + originalDescriptor = Object.getOwnPropertyDescriptor(proto, 'value'); + proto = Object.getPrototypeOf(proto); + } + } + Object.defineProperty(w, 'value', { + configurable: true, + enumerable: true, + get() { + let valueToReturn = originalDescriptor?.get + ? originalDescriptor.get.call(w) + : widgetValue; + return valueToReturn; + }, + set(newVal) { + if (originalDescriptor?.set) { + originalDescriptor.set.call(w, newVal); + } else { + widgetValue = newVal; + } + widgetLogic(node, w); + } + }); + widgetLogic(node, w); + } + } + } + // Trigger full node redraw after setup + setTimeout(() => { + const sliderCountWidget = findWidgetByName(node, "slider_count"); + if (sliderCountWidget) { + handleSliderVisibility(node, sliderCountWidget.value); + } else { + node.graph.setDirtyCanvas(true, true); + } + }, 50); + } + } +}); \ No newline at end of file diff --git a/js/workflowfix.js b/js/workflowfix.js new file mode 100644 index 0000000..aaa4634 --- /dev/null +++ b/js/workflowfix.js @@ -0,0 +1,19 @@ +// Detect and update Efficiency Nodes from v1.92 to v2.00 changes (Final update?) +import { app } from '../../scripts/app.js' + +// Note: This file originally contained fixes for various Efficiency Nodes. +// No specific fixes needed for DSS currently, but structure kept. + +const ext = { + // Updated extension name + name: "Comfy.DynamicSlidersStack.WorkflowFix", +}; + +// Original logic remains commented out. +/* +ext.loadedGraphNode = function(node, app) { + // ... +} +*/ + +app.registerExtension(ext); \ No newline at end of file diff --git a/scaling.gif b/scaling.gif new file mode 100644 index 0000000..6f2d7b1 Binary files /dev/null and b/scaling.gif differ diff --git a/slider_receiver_node.py b/slider_receiver_node.py new file mode 100644 index 0000000..58674dc --- /dev/null +++ b/slider_receiver_node.py @@ -0,0 +1,52 @@ +import folder_paths +import re + +# Updated Node name for registration +RECEIVER_NODE_NAME = "Slider Receiver (DSS)" # Internal name + +######################################################################################################################## +# TSC Slider Receiver (DSS) +class TSC_Slider_Receiver_Standalone: # Keep class name for now, just update registration + @classmethod + def INPUT_TYPES(cls): + # ... (INPUT_TYPES remain the same) ... + return { + "required": { + "weights_string": ("STRING", {"multiline": False, "default": ""}), + "index": ("INT", {"default": 1, "min": 1, "max": 50, "step": 1}), + } + } + + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("WEIGHT",) + FUNCTION = "get_weight_at_index" + # Updated Category + CATEGORY = "Dynamic Sliders Stack" + + # ... (get_weight_at_index function remains the same) ... + def get_weight_at_index(self, weights_string, index): + selected_weight = 0.0 + if not weights_string or not isinstance(weights_string, str): + return (selected_weight,) + try: + weights_str_list = re.split(r'\s*,\s*', weights_string.strip()) + weights_str_list = [w for w in weights_str_list if w] + if 1 <= index <= len(weights_str_list): + selected_weight = float(weights_str_list[index - 1]) + else: + print(f"Warning: Index {index} out of bounds for weights string: '{weights_string}' (Length: {len(weights_str_list)}). Returning 0.0.") + except ValueError: + print(f"Warning: Could not convert value to float in weights string: '{weights_string}' at index {index}. Returning 0.0.") + except Exception as e: + print(f"Error processing weights string '{weights_string}': {e}. Returning 0.0.") + return (selected_weight,) + +# --- Mappings --- +NODE_CLASS_MAPPINGS = { + RECEIVER_NODE_NAME: TSC_Slider_Receiver_Standalone +} + +# Updated Display Name +NODE_DISPLAY_NAME_MAPPINGS = { + RECEIVER_NODE_NAME: "Slider Receiver" # Removed suffix +} \ No newline at end of file diff --git a/slider_stacker_node.py b/slider_stacker_node.py new file mode 100644 index 0000000..c2ae53d --- /dev/null +++ b/slider_stacker_node.py @@ -0,0 +1,59 @@ +import folder_paths + +# Updated Node name for registration (used in JS) +NODE_NAME = "Slider Stacker (DSS)" # Internal name, keep suffix for uniqueness + +######################################################################################################################## +# TSC Slider Stacker (DSS) - String Inputs for Names +class TSC_Slider_Stacker_Standalone: + @classmethod + def INPUT_TYPES(cls): + # ... (rest of INPUT_TYPES remains the same) ... + max_sliders = 50 + inputs = { + "required": { + "slider_max_strength": ("FLOAT", {"default": 1.00, "min": 0.0, "max": 2.0, "step": 0.01, "precision": 2, "display": "number"}), + "slider_count": ("INT", {"default": 1, "min": 0, "max": max_sliders, "step": 1}), + } + } + for i in range(1, max_sliders + 1): + inputs["required"][f"slider_name_{i}"] = ("STRING", {"multiline": False, "default": "None"}) + widget_name = f"({i})" + inputs["required"][widget_name] = ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 2.0, + "step": 0.01, + "precision": 2, + "display": "slider" + }) + return inputs + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("SLIDER_WEIGHTS",) + FUNCTION = "slider_stacker" + # Updated Category + CATEGORY = "Dynamic Sliders Stack" + + # ... (slider_stacker function remains the same) ... + def slider_stacker(self, slider_max_strength, slider_count, **kwargs): + sliders_available = [kwargs.get(f"slider_name_{i}") for i in range(1, slider_count + 1)] + weights_raw = [kwargs.get(f"({i})") for i in range(1, slider_count + 1)] + safe_max_strength = max(0.0, slider_max_strength) + weights_clamped_by_widget = [max(0.0, min(w, 2.0)) for w in weights_raw] + weights_clamped_final = [min(w, safe_max_strength) for w in weights_clamped_by_widget] + active_weights = [] + for i in range(slider_count): + active_weights.append(f"{weights_clamped_final[i]:.2f}") + weights_string = ", ".join(active_weights) + return (weights_string,) + +# --- Mappings --- +NODE_CLASS_MAPPINGS = { + NODE_NAME: TSC_Slider_Stacker_Standalone +} + +# Updated Display Name +NODE_DISPLAY_NAME_MAPPINGS = { + NODE_NAME: "Slider Stacker" # Removed suffix for cleaner UI +} \ No newline at end of file