Add files via upload

This commit is contained in:
FunnyFinger
2025-04-15 22:43:13 +03:00
committed by GitHub
parent 29b78fc6a3
commit 71b1cd7fba
14 changed files with 1215 additions and 0 deletions
+358
View File
@@ -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.
+24
View File
@@ -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.
+69
View File
@@ -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.
+25
View File
@@ -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')
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

+159
View File
@@ -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);
}
});
}
}
});
+65
View File
@@ -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);
+235
View File
@@ -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);
}
}
});
+150
View File
@@ -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);
}
}
});
+19
View File
@@ -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);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+52
View File
@@ -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
}
+59
View File
@@ -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
}