22 Commits
Author SHA1 Message Date
FunnyFinger b1700592b7 Merge pull request #2 from ComfyNodePRs/publish
Add Github Action for Publishing to Comfy Registry
2025-04-22 13:00:30 +03:00
FunnyFinger cea9797bcc Update pyproject.toml 2025-04-22 12:13:01 +03:00
FunnyFinger 1ae1f512d9 Create publish_action.yml 2025-04-22 12:02:49 +03:00
FunnyFinger 3be5fc7b48 Add files via upload 2025-04-22 11:59:34 +03:00
FunnyFinger ff2d86289a Update README.md 2025-04-20 12:03:49 +03:00
FunnyFinger 14cc3c41ae Update README.md 2025-04-20 11:53:39 +03:00
FunnyFinger ab2e2b110c Update README.md 2025-04-20 11:21:50 +03:00
FunnyFinger 66186634cc Merge pull request #3 from FunnyFinger/FunnyFinger-patch-1
example_workflow
2025-04-19 23:18:28 +03:00
FunnyFinger 8c45b394ba example_workflow
Embedded workflow with the .png
2025-04-19 23:07:57 +03:00
FunnyFinger 035cbf5461 Update README.md
New features are displayed, V 2.0.0 is tested with ComfyUI V 0.3.29
2025-04-19 21:27:45 +03:00
FunnyFinger 07503240ff Delete sliders_locksum_on.gif 2025-04-19 20:24:46 +03:00
FunnyFinger 5264964296 Delete context_reset.gif 2025-04-19 20:24:35 +03:00
FunnyFinger 6923536094 Delete sliders_sum_locksum_off.gif 2025-04-19 20:24:18 +03:00
FunnyFinger 8371675e78 Delete sliders_max_locksum_on.gif 2025-04-19 20:24:06 +03:00
FunnyFinger 116bff8935 Delete sliders_max_locksum_off.gif 2025-04-19 20:23:56 +03:00
FunnyFinger 5b71703d29 Delete context_min.gif 2025-04-19 20:23:43 +03:00
FunnyFinger ad2d658c06 Delete context.gif 2025-04-19 20:23:29 +03:00
FunnyFinger 31f2ace08d Delete context_max.gif 2025-04-19 20:23:17 +03:00
FunnyFinger 7b1a986387 Delete context_avg.gif 2025-04-19 20:23:06 +03:00
FunnyFinger a92aaa7159 updated to v2.0.0
added new features such as dynamic sum adjustment and sum-lock dynamics
2025-04-19 20:22:11 +03:00
FunnyFinger 218d28b435 Add files via upload 2025-04-19 20:02:20 +03:00
snomiao 5ffeb11bd1 chore(publish): Add Github Action for Publishing to Comfy Registry 2025-04-14 17:58:59 +00:00
35 changed files with 2749 additions and 307 deletions
+56
View File
@@ -0,0 +1,56 @@
---
description:
globs:
alwaysApply: true
---
# Clean Code Guidelines
## Constants Over Magic Numbers
- Replace hard-coded values with named constants
- Use descriptive constant names that explain the value's purpose
- Keep constants at the top of the file or in a dedicated constants file
## Meaningful Names
- Variables, functions, and classes should reveal their purpose
- Names should explain why something exists and how it's used
- Avoid abbreviations unless they're universally understood
## Smart Comments
- Don't comment on what the code does - make the code self-documenting
- Use comments to explain why something is done a certain way
- Document APIs, complex algorithms, and non-obvious side effects
## Single Responsibility
- Each function should do exactly one thing
- Functions should be small and focused
- If a function needs a comment to explain what it does, it should be split
## DRY (Don't Repeat Yourself)
- Extract repeated code into reusable functions
- Share common logic through proper abstraction
- Maintain single sources of truth
## Clean Structure
- Keep related code together
- Organize code in a logical hierarchy
- Use consistent file and folder naming conventions
## Encapsulation
- Hide implementation details
- Expose clear interfaces
- Move nested conditionals into well-named functions
## Code Quality Maintenance
- Refactor continuously
- Fix technical debt early
- Leave code cleaner than you found it
## Testing
- Write tests before fixing bugs
- Keep tests readable and maintainable
- Test edge cases and error conditions
## Version Control
- Write clear commit messages
- Make small, focused commits
- Use meaningful branch names
+48
View File
@@ -0,0 +1,48 @@
---
description:
globs:
alwaysApply: true
---
# Code Quality Guidelines
## Verify Information
Always verify information before presenting it. Do not make assumptions or speculate without clear evidence.
## File-by-File Changes
Make changes file by file and give me a chance to spot mistakes.
## No Apologies
Never use apologies.
## No Understanding Feedback
Avoid giving feedback about understanding in comments or documentation.
## No Whitespace Suggestions
Don't suggest whitespace changes.
## No Summaries
Don't summarize changes made.
## No Inventions
Don't invent changes other than what's explicitly requested.
## No Unnecessary Confirmations
Don't ask for confirmation of information already provided in the context.
## Preserve Existing Code
Don't remove unrelated code or functionalities. Pay attention to preserving existing structures.
## Single Chunk Edits
Provide all edits in a single chunk instead of multiple-step instructions or explanations for the same file.
## No Implementation Checks
Don't ask the user to verify implementations that are visible in the provided context.
## No Unnecessary Updates
Don't suggest updates or changes to files when there are no actual modifications needed.
## Provide Real File Links
Always provide links to the real files, not x.md.
## No Current Implementation
Don't show or discuss the current implementation unless specifically requested.
+51
View File
@@ -0,0 +1,51 @@
---
description:
globs:
alwaysApply: true
---
# ComfyUI Backend (Python) Node Development Guidelines
These rules supplement general clean code guidelines and focus specifically on creating Python backend code for ComfyUI custom nodes, based on official documentation patterns.
## Node Class Structure
- **Define nodes as Python classes.**
- **Implement `INPUT_TYPES(cls)`:**
- Return a dictionary with keys `"required"`, `"optional"`, and potentially `"hidden"`.
- Each value should be a dictionary mapping input names (strings) to input definitions.
- Input definitions are tuples: `(TYPE_STRING, OPTIONS_DICT)`.
- **Implement `RETURN_TYPES`:** Define as a tuple of output type strings (e.g., `("IMAGE", "MASK")`).
- **Implement `RETURN_NAMES`:** Define as a tuple of output name strings, matching `RETURN_TYPES`.
- **Implement `FUNCTION`:** String name of the main execution method.
- **Define `CATEGORY`:** String specifying the node's category in the UI menu (e.g., `"MyNodes/Utils"`).
- **Define `OUTPUT_NODE`:** Set to `True` if the node doesn't modify inputs for the execution graph (useful for display nodes, save nodes, etc.), otherwise `False` (or omit).
## Naming Conventions
- **Use `NODE_CLASS_MAPPINGS`:** Dictionary mapping an internal node name (string) to the node class.
- **Use `NODE_DISPLAY_NAME_MAPPINGS`:** Dictionary mapping the internal node name to a user-friendly display name.
- **Choose descriptive display names.** Internal names can be more specific if needed for uniqueness.
## Input/Output Handling
- **Main Function Signature:** The method named in `FUNCTION` must accept arguments corresponding *exactly* to the keys defined in the `"required"` and `"optional"` dictionaries within `INPUT_TYPES`. Use `**kwargs` to handle arbitrary inputs if using dynamic input techniques.
- **Input Options (`OPTIONS_DICT`):**
- Use standard options like `default`, `min`, `max`, `step` for numerical types.
- Use `multiline` for STRING inputs.
- Use `forceInput: True` for custom datatypes or types that should *always* be inputs, never widgets.
- **Return Values:** The main function *must* return a tuple with values corresponding exactly in order and type to `RETURN_TYPES`. Ensure return types are correct even during error conditions where possible.
- **Datatypes:** Use standard ComfyUI types (e.g., `INT`, `FLOAT`, `STRING`, `IMAGE`, `MASK`, `MODEL`, `CLIP`, `VAE`, `LATENT`, `CONDITIONING`). Custom types are possible but require `forceInput`.
- **Wildcard Inputs (`*`)**: Use sparingly. If using `"*"`, the node must be prepared to handle any datatype and potentially skip backend validation (e.g., by accepting `input_types` in `VALIDATE_INPUTS`).
## Hidden Inputs
- Use the `"hidden"` dictionary in `INPUT_TYPES` to access specific server information.
- **`"unique_id": "UNIQUE_ID"`:** Gets the node's unique ID from the workflow.
- **`"prompt": "PROMPT"`:** Gets the full prompt JSON structure.
- **`"extra_pnginfo": "EXTRA_PNGINFO"`:** Gets a dictionary to which metadata can be added for saving in output PNGs.
## Node Lifecycle & Behavior
- **`IS_CHANGED(self, ...)`:** Implement this method for inputs (often hidden/internal ones like prompts or seeds) where the *value* itself might not change, but its *meaning* or the desired output does. Return a hash or unique value based on the relevant changing data. This helps ComfyUI invalidate caches correctly.
- **`VALIDATE_INPUTS(self, ...)`:** Implement this optional method to perform custom validation *before* the main function executes. Return `True` if valid, `False` or raise an exception if invalid. Can accept `input_types` argument to handle wildcard inputs.
- **State:** Avoid storing excessive state directly on the node class instance if possible, as node instances might be reused unexpectedly. Prefer recalculating based on inputs.
## Error Handling & Logging
- Use standard Python `try...except` blocks for operations that might fail (e.g., file access, model loading, complex computations).
- Provide informative error messages using `print()` or Python's `logging` module.
- Ensure the node returns data matching `RETURN_TYPES` even in case of handled errors (e.g., return default values or empty structures), or raise an exception for unrecoverable errors.
+283
View File
@@ -0,0 +1,283 @@
---
description:
globs:
alwaysApply: false
---
# ComfyUI Custom Node Development Guidelines
## Specialization
- You are an AI assistant specialized in ComfyUI custom node development.
## General Guidelines
- Follow these guidelines when helping with this project:
## Project Context
- This is a custom node for ComfyUI called "Dynamic Sliders Stack"
- The node provides dynamic slider widgets that can be configured and controlled as a group
- It has both Python backend (defining node structure and processing) and JavaScript frontend (UI behavior)
- ComfyUI has significantly changed its frontend architecture, requiring adaptation for compatibility
## Architecture Knowledge
- ComfyUI nodes are defined as Python classes with specific methods like INPUT_TYPES() and FUNCTION
- Node UI behavior is controlled by JavaScript files that use ComfyUI extension system
- Communication between frontend and backend happens through serialized values
- ComfyUI uses a node-based graph structure where nodes perform operations and are connected via links
- Workflow data is stored in JSON format that defines nodes, connections, and widget values
- The modern ComfyUI frontend is built with Vue 3, TypeScript, and Pinia for state management
- The legacy frontend and new frontend can have different API patterns and requirements
## Frontend Architecture Versions
- Legacy: Vanilla JavaScript-based system
- Modern: Vue 3 + TypeScript with PrimeVue and TailwindCSS
- Custom nodes need to detect and adapt to the frontend version being used
- The modern frontend uses a different extension registration pattern and API
## JavaScript (Frontend) Patterns - Legacy
- Use `app.registerExtension({})` to register UI behavior
- Use the nodeCreated callback to initialize node behavior
- Widget values should be intercepted to handle user interactions
- Always prevent feedback loops when programmatically changing widget values
- Add `_isUpdatingInternally` flags to prevent update loops
- Consider using DOM mutation observers to detect changes to node elements
- Set up proper cleanup in onRemoved callbacks to prevent memory leaks
## JavaScript (Frontend) Patterns - Modern
- Use the new extension registration API with proper naming conventions
- Handle UI components through Vue composition APIs rather than direct DOM manipulation
- Use the new dialog, toast, and settings APIs for user interactions
- Rely on the component system rather than direct DOM manipulation
- Use the new event system for communication between components
- Follow the TypeScript typing conventions for better integration
- Consider using the new sidebar, commands, and keybinding registration APIs
## Compatibility Strategy
- Consider supporting both legacy and modern frontends using feature detection
- Create proper cleanup for event listeners and DOM modifications
- Use the frontend version flags to conditionally use appropriate APIs
- Test thoroughly on both legacy and modern frontends
- Consider using the ComfyUI Legacy Frontend option during transition
- Import legacy paths conditionally and with fallbacks
- Consider gradual migration rather than complete rewrite when feasible
- Use the new import paths for modern frontend: `app.extensionManager.*` APIs
## Python (Backend) Patterns
- Define INPUT_TYPES() to specify node inputs and widgets
- Define RETURN_TYPES and RETURN_NAMES for outputs
- Implement the main processing function as specified in FUNCTION
- Use good error handling for invalid inputs
- Properly type cast values received from the frontend
- Follow the node definition schema for proper integration with ComfyUI
- Consider adding category, display_name and description attributes for better organization
## Node Definition Structure
- Each node must define INPUT_TYPES() method returning a dictionary of inputs
- Widget types include INT, FLOAT, STRING, BOOLEAN, COMBO with appropriate parameters
- Widgets can have properties like default, min/max, step, display type
- Optional attributes like 'hidden', 'default_input', and 'tooltip' provide additional control
- RETURN_TYPES defines the output data types that other nodes can connect to
- FUNCTION attribute specifies the main method that processes inputs and returns outputs
- Define CATEGORY to organize where your node appears in the menu
## Modern Frontend APIs
- Use `app.registerExtension({name: 'MyExtension', ...})` for registration
- For dialog interfaces: `app.extensionManager.dialog.prompt/confirm/alert()`
- For notifications: `app.extensionManager.toast.add()`
- For settings: Register with `app.registerExtension({settings: [...]})`
- For accessing settings: `app.extensionManager.setting.get/set()`
- For custom UI: Use the sidebar tab API, bottom panel tabs API, and toolbox APIs
- For keybindings: Register with `app.registerExtension({commands: [...], keybindings: [...]})`
- For menus: Add to topbar with `app.registerExtension({menuCommands: [...]})`
## Workflow JSON Understanding
- ComfyUI workflows are stored as JSON with nodes, links, and widget values
- Each node has an ID, type, position, inputs, outputs, and widget values
- Links connect outputs of one node to inputs of another
- Understanding the JSON structure helps with debugging and programmatic workflow creation
- Custom nodes should properly serialize/deserialize their state within this format
## Styling and Conventions
- Use camelCase for JavaScript variables and functions
- Use snake_case for Python variables and functions
- Add detailed comments for complex logic
- Follow the existing code formatting conventions
- Keep logic modular and functions focused on a single responsibility
- For modern frontend, follow Vue 3 and TypeScript conventions
- Consider using PrimeVue icons and components for consistent UI
## Specific Project Knowledge
- Slider values can be normalized to maintain relative proportions
- The "total_sum" widget shows the sum of all active sliders
- Widget values are intercepted to handle complex interactions
- The node UI updates dynamically based on slider_count changes
## Read-Only Widget Implementation
- For truly read-only widgets, use a multi-layered constraint approach:
1. Replace the widget's DOM element with a styled read-only display
2. Constrain the widget's min/max values to be extremely close to the actual value
3. Use periodic checks to ensure constraints are maintained
4. Add mutation observers to detect DOM changes and reapply protection
5. Set both inputEl.readOnly and style.pointerEvents to block interaction
- The `constrainWidgetRange` function is the preferred way to make widgets immovable
- Key components needed:
- A function to calculate the precise epsilon based on widget precision
- A display element that visually indicates read-only state
- A value property interceptor that maintains constraints
- A periodic check interval for persistent protection
- Apply this pattern to any widgets that should display calculated values without user modification
- For modern frontend, investigate component-based approaches rather than DOM manipulation
## Preventing Overshooting in Interactive UI Elements
- When implementing interactive UI elements with rapid value changes, use these techniques to prevent overshooting:
1. Use immutable reference values to maintain stable relationships between values
- Store original relative values (like offsets or ratios) separate from current state
- Never modify these reference values during user interactions
- Always derive new values from these stable references, not from current values
2. Implement precise mathematical boundaries
- Pre-calculate the exact theoretical minimum and maximum values
- Consider all constraints and relationships between elements when determining limits
- Apply hard limits before any UI updates occur
3. Use direct value calculation instead of incremental changes
- Prefer formula-based updates over incremental adjustments
- Calculate target values directly from input parameters
- Avoid chains of operations that can accumulate rounding errors
4. Handle edge cases explicitly with special logic
- Implement special handling for boundary values (minimum, maximum)
- At boundaries, set exact values rather than attempting to calculate them
- For grouped elements, consider relative relationships at boundaries
5. Use atomic updates for related elements
- Calculate all new values before applying any changes
- Apply all updates in a single batch to prevent inconsistent intermediate states
- Verify the resulting state meets expected constraints
6. Implement post-update verification
- Check if any values exceed defined limits after updates
- Add correction mechanisms for any unexpected values
- Use appropriate flags to prevent recursive update loops
7. Consider the timing of events
- Be aware of when events fire during rapid user interactions
- Add appropriate flags to track update source (user vs. programmatic)
- Avoid recalculating reference values during interaction-driven updates
## Publishing and Distribution
- Add proper documentation and examples to help users understand your node
- Create a comprehensive README.md with installation instructions and examples
- Consider following the Registry standards for better integration with ComfyUI Manager
- Include a pyproject.toml file with metadata about your custom node
- Set up proper version control and tagging for releases
- Test your node on different platforms and ComfyUI versions
- Specify frontend version compatibility in your documentation
- Consider providing legacy and modern versions if needed
## Best Practices
- Always test suggested changes with edge cases
- Ensure backward compatibility where possible
- Keep performance in mind, especially for operations that might run many times
- Use descriptive variable names that reflect their purpose
- Validate user inputs before processing
- Clean up intervals and observers when nodes are removed
- Prefer component-based approaches over direct DOM manipulation in modern frontend
- Consider adding tooltips to explain widget functionality
- Refactor whenever possible
- Test with both legacy and modern frontend versions
- simplyfy the code as much as possible
- factorize the code as much as possible
- use the best practices for the modern frontend
- use loops and arrays as much as possible
- reduce the number of lines of code as much as possible
- use default and native methods as much as possible
- do not reinvent the wheel, use the best tools and libraries available
- do nit exceed 500 lines of code in any file
## Common Challenges
- DOM elements might not be available immediately after node creation
- Widget value updates can cause infinite loops if not carefully managed
- ComfyUI might use older JavaScript standards, avoid very modern syntax
- Consider both desktop and mobile interactions for widgets
- Widget elements may be regenerated by ComfyUI, requiring reapplication of custom behavior
- Handle backward compatibility when updating node definitions
- Import paths may change between frontend versions
- Extension registration patterns differ between legacy and modern frontends
- Component-based UIs require different interaction patterns than direct DOM manipulation
## Testing
- Test with different slider counts
- Verify sum calculations are accurate
- Check that normalization preserves relative proportions
- Ensure read-only widgets remain non-interactive under all conditions
- Test with rapid interactions to verify stability
- Test your node with different workflows and edge cases
- Verify proper cleanup when nodes are removed from the workflow
- Test with both legacy and modern frontend versions
- Test with different frontend version flags
## Troubleshooting Frontend Version Issues
- Run ComfyUI with legacy frontend for testing: `--front-end-version Comfy-Org/ComfyUI_legacy_frontend@latest`
- Try specific versions for compatibility testing: `--front-end-version Comfy-Org/ComfyUI_frontend@x.y.z`
- Check browser console for API-related errors
- Use feature detection rather than version checking when possible
- Implement graceful fallbacks for unsupported features
- Consider conditional imports based on detected frontend version
- Watch for API deprecation warnings in console output
- for debugging, avoid triggering many logs in loops
-
## Vue 3 Composition API .cursorrules
## Vue 3 Composition API best practices
```javascript
const vue3CompositionApiBestPractices = [
"Use setup() function for component logic",
"Utilize ref and reactive for reactive state",
"Implement computed properties with computed()",
"Use watch and watchEffect for side effects",
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
"Utilize provide/inject for dependency injection",
"Use vue 3.5 style of default prop declaration. Example:",
"const { nodes, showTotal = true } = defineProps<{",
" nodes: ApiNodeCost[]",
" showTotal?: boolean",
"}>()",
"",
"",
"Organize vue component in <template> <script> <style> order",
]
```
## Folder structure
```
const folderStructure = `
src/
components/
constants/
composables/
views/
stores/
services/
App.vue
main.ts
`;
```
## Tailwind CSS best practices
```javascript
const tailwindCssBestPractices = [
"Use Tailwind CSS for styling",
"Implement responsive design with Tailwind CSS",
]
```
## Additional instructions
```
const additionalInstructions = `
1. Leverage VueUse functions for performance-enhancing styles
2. Use lodash for utility functions
3. Use TypeScript for type safety
4. Implement proper props and emits definitions
5. Utilize Vue 3's Teleport component when needed
6. Use Suspense for async components
7. Implement proper error handling
8. Follow Vue 3 style guide and naming conventions
9. Use Vite for fast development and building
10. Use vue-i18n in composition API for any string literals. Place new translation
entries in src/locales/en/main.json.
`;
```
+52
View File
@@ -0,0 +1,52 @@
---
description:
globs:
alwaysApply: true
---
# ComfyUI Frontend (JavaScript) Node Development Guidelines
These rules supplement general clean code guidelines and focus specifically on creating JavaScript frontend code for ComfyUI custom nodes, based on official documentation patterns.
## Extension Structure
- **Use `app.registerExtension({...})`:** Define all frontend logic within this structure.
- **Provide a unique `name`:** Use a pattern like `"Comfy.MyAuthorName.MyNodeOrFeatureName"`.
- **Use appropriate hooks:** Implement methods within the extension object corresponding to ComfyUI hooks (`nodeCreated`, `beforeRegisterNodeDef`, `setup`, etc.).
## Key JavaScript Hooks
- **`beforeRegisterNodeDef(nodeType, nodeData, app)`:**
- Called *once per node type* before registration.
- Use this to modify the *prototype* for all future instances of a specific node type.
- Identify the target node type using `nodeData.name` (the internal Python class name mapping key).
- Common use: Hijacking existing prototype methods (e.g., `onDrawForeground`, `onConfigure`, `onConnectionsChange`) to add custom behavior. Remember to store and call the original method using `originalMethod.apply(this, arguments)`.
- **`nodeCreated(node)`:**
- Called *every time* a specific node *instance* is created (either new or loaded).
- Use this for instance-specific setup, like adding listeners to that node's widgets or applying initial state based on properties.
- Identify the target node using `node.type` (internal name) or `node.title` (display name), or custom checks.
- **`setup(app)`:**
- Called once after the core UI (graph, canvas) is initialized but before nodes might be loaded.
- Use for global setup, adding overall UI elements, or registering global listeners.
- **`nodeSetup(node)` / `loadedGraphNode(node)`:** (Often used interchangeably or via hijacking `onConfigure`/`onAdded` within `beforeRegisterNodeDef`)
- Called when nodes are loaded as part of a workflow.
- Useful for applying state or UI modifications to existing nodes based on saved workflow data.
## Widget Manipulation
- **Access Widgets:** Use `node.widgets` (an array) or helper functions like `node.widgets.find(w => w.name === 'widget_name')`.
- **Intercept Value Changes:** Use `Object.defineProperty(widget, 'value', { get(){...}, set(newVal){...} })` to react when a widget's value is changed by the user or programmatically.
- **Dynamic Visibility (Standard Method):**
- **Hide:** Set `widget.hidden = true; widget.computeSize = () => [0, -4];`
- **Show:** Set `widget.hidden = false; widget.computeSize = originalComputeSize || null;` (Restore cached original `computeSize` function or set to null/default).
- Cache original widget properties (`type`, `computeSize`, `hidden`) before modifying them for hiding.
- **Modifying Other Properties:** You can change `widget.type`, `widget.options`, `widget.name` etc., but be cautious as it might break expected behavior.
## Node Appearance & Drawing
- **Basic Styling:** Set `node.bgcolor`, `node.color`, `node.shape` directly on the node instance (often in `nodeCreated` or hijacked `onAdded`/`onConfigure`).
- **Advanced Styling (CSS):** Inject CSS using `document.createElement('style')` and `document.head.appendChild()` for complex rules or overriding theme styles. Use specific selectors targeting node type (`[data-type="NodeType"]`) or title (`[title="NodeTitle"]`) and potentially internal elements (`.litenode-title`, `.widget-container`). Use `!important` judiciously.
- **Custom Drawing (`onDrawForeground`):** Hijack `nodeType.prototype.onDrawForeground` in `beforeRegisterNodeDef`. Use the `ctx` (CanvasRenderingContext2D) argument to draw directly onto the node's canvas *after* the default node is drawn. Useful for overlays or completely custom looks. Call the original method first.
- **Redrawing:** After making visual changes (widget visibility, color, size), call `node.setDirtyCanvas(true, true)` to tell LiteGraph to redraw. If node dimensions changed, calculate the new size (`node.computeSize()`) and set it (`node.setSize([w, h])`), preserving width if only height changed.
- **Use `requestAnimationFrame`:** Wrap UI updates (especially those involving size or visibility changes) in `requestAnimationFrame(() => { ... })` to ensure they run after the browser has finished its current rendering cycle, preventing layout issues and race conditions.
## Compatibility & Best Practices
- **Identify Nodes Reliably:** Check both `node.type` (internal name) and `node.title` (display name) or use specific widget checks, as titles can sometimes be changed by users or other nodes.
- **Minimize Core Hijacking:** Avoid modifying core `app` or `LiteGraph` prototype methods unless absolutely necessary, as this is prone to break with ComfyUI updates or conflicts with other extensions.
- **Check for Existence:** Always check if objects and methods exist before using them (e.g., `if (node.graph) { node.graph.setDirtyCanvas(...) }`, `if (originalMethod) { originalMethod.apply(...) }`).
- **Clean Console:** Remove debug `console.log` statements before finalizing, keeping only essential loading messages or error logs.
+22
View File
@@ -0,0 +1,22 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
[*.py]
indent_size = 4
max_line_length = 100
[*.js]
indent_size = 2
max_line_length = 120
[*.{json,yaml,yml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+120
View File
@@ -0,0 +1,120 @@
{
"env": {
"browser": true,
"es2022": true
},
"extends": [
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"globals": {
"app": "readonly",
"LiteGraph": "readonly",
"LGraphCanvas": "readonly",
"LGraphNode": "readonly",
"LiteGraph": "readonly",
"globalThis": "readonly"
},
"rules": {
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"no-console": [
"warn",
{
"allow": [
"warn",
"error",
"info"
]
}
],
"prefer-const": "warn",
"no-var": "warn",
"semi": [
"warn",
"always"
],
"quotes": [
"warn",
"double"
],
"indent": [
"warn",
2,
{
"SwitchCase": 1
}
],
"max-len": [
"warn",
{
"code": 120,
"ignoreComments": true,
"ignoreStrings": true
}
],
"camelcase": [
"warn",
{
"properties": "never"
}
],
"no-undef": "error",
"eqeqeq": [
"warn",
"always"
],
"curly": [
"warn",
"all"
],
"brace-style": [
"warn",
"1tbs"
],
"comma-dangle": [
"warn",
"never"
],
"comma-spacing": [
"warn",
{
"before": false,
"after": true
}
],
"no-trailing-spaces": "warn",
"no-duplicate-imports": "error",
"no-multiple-empty-lines": [
"warn",
{
"max": 2,
"maxEOF": 1
}
],
"object-curly-spacing": [
"warn",
"always"
],
"space-before-function-paren": [
"warn",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
],
"space-before-blocks": [
"warn",
"always"
],
"space-infix-ops": "warn"
}
}
+28
View File
@@ -0,0 +1,28 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'FunnyFinger' }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+20
View File
@@ -0,0 +1,20 @@
name: Publish to Comfy registry
on:
workflow_dispatch: # Allows manual triggering from the Actions tab
push:
branches:
- main # Change this if your main branch is named differently (e.g., master)
paths:
- "pyproject.toml" # Trigger only when pyproject.toml changes
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} # Uses the secret you created
+78
View File
@@ -0,0 +1,78 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Node/JS
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
yarn.lock
# Editors
.idea/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.sublime-workspace
*.swp
*.swo
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# ComfyUI specific
output/
input/
temp/
test_outputs/
custom_nodes/*
!custom_nodes/Dynamic_sliders_stack/
# Testing and notebooks
.ipynb_checkpoints/
.coverage
htmlcov/
.pytest_cache/
.mypy_cache/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Distribution / packaging
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
+16
View File
@@ -0,0 +1,16 @@
{
"semi": true,
"singleQuote": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "auto",
"jsxSingleQuote": false,
"bracketSameLine": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"quoteProps": "as-needed"
}
+65
View File
@@ -0,0 +1,65 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": null,
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"[python]": {
"editor.defaultFormatter": "ms-python.python",
"editor.formatOnSave": true,
"editor.rulers": [
100
],
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.rulers": [
120
]
},
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.provider": "autopep8",
"python.formatting.autopep8Args": [
"--max-line-length",
"100"
],
"javascript.format.semicolons": "insert",
"javascript.preferences.quoteStyle": "double",
"files.associations": {
"*.py": "python",
"*.js": "javascript"
},
"search.exclude": {
"**/__pycache__": true,
"**/.git": true
},
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.py": "${capture}.pyc, ${capture}__pycache__",
"package.json": "package-lock.json"
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
// ComfyUI-specific settings
"workbench.colorCustomizations": {
"editorRuler.foreground": "#5c5c5c36"
},
// AI-assisted development settings
"github.copilot.enable": {
"*": true,
"yaml": true,
"plaintext": true,
"markdown": true,
"python": true,
"javascript": true
}
}
+128
View File
@@ -0,0 +1,128 @@
# ComfyUI Custom Node Development Rules
This document outlines the development standards and best practices for creating custom nodes for ComfyUI with AI assistance.
## Project Structure
```
custom_nodes/your_node_name/
├── __init__.py # Node registration
├── node_implementation.py # Main Python implementation
├── js/
│ ├── main.js # Frontend implementation
│ └── widget_implementations.js
├── .vscode/
│ └── settings.json # VSCode settings
├── .eslintrc.json # JavaScript linting rules
├── .prettierrc # Code formatting rules
├── .editorconfig # Editor configuration
├── setup.cfg # Python linting configuration
├── .gitignore # Git exclusion patterns
└── README.md # Documentation
```
## Development Guidelines
### Python (Backend)
1. **Node Structure**:
- Define nodes as classes with clear `INPUT_TYPES`, `RETURN_TYPES`, and `FUNCTION` methods
- Keep node implementations focused on a single responsibility
- Use descriptive variable names and add docstrings
2. **Type Annotation**:
- Use type hints to improve code readability and AI assistance quality
- Annotate function parameters and return values
3. **Error Handling**:
- Implement proper error handling with meaningful error messages
- Validate user inputs before processing
### JavaScript (Frontend)
1. **Widget Integration**:
- Follow ComfyUI's widget patterns for consistency
- Implement proper event handling and update mechanisms
2. **Naming Conventions**:
- Use camelCase for variables and functions
- Use PascalCase for class names
- Prefix private functions with underscore (_)
3. **DOM Manipulation**:
- Use ComfyUI's built-in methods when possible
- Avoid direct DOM manipulation where possible
## AI-Assisted Development Tips
1. **Clear Prompting**:
- Provide context about ComfyUI architecture in your prompts
- Reference existing implementations for similar functionality
- Specify dependencies and environment constraints
2. **Code Review**:
- Always review AI-generated code before implementation
- Test edge cases that AI might have missed
- Verify all imports and dependencies are properly handled
3. **Iterative Development**:
- Break complex features into smaller, manageable tasks for AI
- Implement core functionality first, then add refinements
- Maintain a clear record of implementation steps
## Testing Custom Nodes
1. **Manual Testing**:
- Test nodes in various workflows and with different inputs
- Verify error handling works as expected
- Test compatibility with different ComfyUI versions
2. **Documentation**:
- Create clear usage examples
- Document any special requirements or limitations
- Include screenshots or GIFs demonstrating functionality
## Publishing Guidelines
1. **Pre-release Checklist**:
- Remove debug console logs
- Ensure proper error handling
- Verify compatibility with latest ComfyUI version
- Check for hardcoded paths or values
2. **Documentation**:
- Provide clear installation instructions
- Include usage examples and screenshots
- List dependencies and requirements
- Explain widget behavior and functionality
3. **Distribution**:
- Include license information
- Create a proper README with installation and usage instructions
- Consider providing sample workflows demonstrating your nodes
## Linting and Formatting Tools
This project uses the following tools for code quality:
- **ESLint**: JavaScript code quality
- **Prettier**: Code formatting
- **Flake8/autopep8**: Python code quality and formatting
- **EditorConfig**: Consistent editor settings
Configure your editor to use these tools for the best development experience.
## Version Control Best Practices
1. **Commit Messages**:
- Use clear, descriptive commit messages
- Reference issues or feature requests when applicable
2. **Branching**:
- Use feature branches for new functionality
- Create hotfix branches for critical fixes
3. **Pull Requests**:
- Include screenshots or videos demonstrating functionality
- List any breaking changes
- Tag relevant reviewers
+124 -38
View File
@@ -1,25 +1,81 @@
# Dynamic Sliders Stack for ComfyUI
![Slider Stacker and Slider Receiver](dynamic_slider_stacker_reciever_nodes.png)
<div align="center">
<img src="dynamic_slider_stacker_reciever_nodes.png" alt="Slider Stacker and Slider Receiver" />
</div>
A custom node pack for ComfyUI providing a stack of named sliders with dynamic behavior.
[![Version](https://img.shields.io/badge/version-v2.0.0-blue)](https://github.com/FunnyFinger/Dynamic_Sliders_stack/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Issues](https://img.shields.io/github/issues/FunnyFinger/Dynamic_Sliders_stack)](https://github.com/FunnyFinger/Dynamic_Sliders_stack/issues)
[![Pull Requests](https://img.shields.io/github/issues-pr/FunnyFinger/Dynamic_Sliders_stack)](https://github.com/FunnyFinger/Dynamic_Sliders_stack/pulls)
[![Stars](https://img.shields.io/github/stars/FunnyFinger/Dynamic_Sliders_stack?style=social)](https://github.com/FunnyFinger/Dynamic_Sliders_stack/stargazers)
---
## 🚀 Features
A custom node pack for **ComfyUI** providing a stack of named sliders with dynamic behavior.
- **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.
---
![Scaling Example](scaling.gif)
## Workflow
- **Dynamic Individual Sliders**: Adjusting an individual `slider_wt` updates the `slider_max_strength` to reflect the new highest value among all sliders.
- **String Output**: Outputs a comma-separated string of the current weights for all active sliders.
- **Dynamic UI**: 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).
- An Example workflow is attached with the following image, just drag and drop this image in ComfyUI Canvas:
![Context Menu Example](context.gif)
<div align="center">
<img src="example_workflow.png" alt="Slider Stacker and Slider Receiver" />
</div>
---
## 🎥 Demo
### Contextual Features
- **Average Contextual Adjustment**
Dynamically adjusts all sliders to their average value using the context menu option. This feature is helpful for balancing all sliders evenly without manual adjustments.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/context_avg.gif" alt="Average Context" width="50%" />
</div>
- **Maximum Contextual Adjustment**
Sets all sliders to the maximum slider value using the context menu option. This ensures that all sliders are at their highest weight for uniform maximum intensity.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/context_max.gif" alt="Maximum Context" width="50%" />
</div>
- **Minimum Contextual Adjustment**
Adjusts all sliders to the minimum slider value using the context menu option. This is useful for resetting sliders to their lowest intensity.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/context_min.gif" alt="Minimum Context" width="50%" />
</div>
- **Context Reset**
Resets all sliders to their default values (e.g., 1.0). This feature helps quickly return to baseline settings for all sliders.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/context_reset.gif" alt="Context Reset" width="50%" />
</div>
### Sum Lock Toggle and Sliders Sum Widget
- **Sliders with Lock Sum On**
Demonstrates how the sum lock feature maintains a constant total sum of sliders while allowing individual adjustments. It ensures that the overall weight does not exceed or drop below a set total.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/sliders_locksum_on.gif" alt="Sliders Lock Sum On" width="50%" />
</div>
- **Sliders Max with Lock Sum Off**
Allows sliders to be adjusted proportionally to their maximum value without maintaining the sum lock. This is ideal for scenarios where the total weight does not need to be constrained, but you wish to control the highest value while maintaining proportionality between sliders. This has an overall scaling effect that scales the sliders with respect to the minimum (zero).
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/sliders_max_locksum_off.gif" alt="Sliders Max Lock Sum Off" width="50%" />
</div>
- **Sliders Max with Lock Sum On**
Demonstrates how the sum lock can be maintained while adjusting the highest value slider. This ensures all sliders are proportionally scaled with respect to their average without exceeding the allowed total sum.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/sliders_max_locksum_on.gif" alt="Sliders Max Lock Sum On" width="50%" />
</div>
- **Sliders Sum with Lock Sum Off**
Highlights how sliders can be adjusted freely without sum constraints while dynamically displaying the total sum. This is useful for adjusting the sliders as a whole without changing their offsets.
<div align="center">
<img src="https://github.com/FunnyFinger/Dynamic_Sliders_stack/blob/main/gif/sliders_sum_locksum_off.gif" alt="Sliders Sum Lock Sum Off" width="50%" />
</div>
---
@@ -28,10 +84,12 @@ A custom node pack for ComfyUI providing a stack of named sliders with dynamic b
### 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).
- `sliders_max` (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.
- `({i})` (FLOAT Widget): The individual weight for this slider (0.0–2.0). Adjusting this updates the `sliders_max` if this slider becomes the new maximum.
- `sliders_sum` (FLOAT): Adjust this to change the total sum of all sliders *while preserving their relative differences* (when Sum Lock is off). Also displays the current sum.
- **Sum Lock Toggle**: A toggle to lock the total sum of all slider values. When enabled, adjusting one slider redistributes the difference proportionally across the other sliders to maintain the locked sum. When enabled, the `sliders_sum` field becomes read-only.
- **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").
@@ -48,50 +106,78 @@ A custom node pack for ComfyUI providing a stack of named sliders with dynamic b
- **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).
- `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 the index is out of bounds or input is invalid).
---
## 🛠️ Installation
## 📦 Installation Methods
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.
Choose any method that suits your setup:
### ✅ Method 1: Via ComfyUI Manager (Recommended)
1. Open ComfyUI.
2. Click on the **Manager** tab (top bar).
3. Go to **Install Custom Nodes**.
4. Paste this URL into the install field:
```
https://github.com/FunnyFinger/Dynamic_Sliders_stack.git
```
5. Click **Install**.
6. Restart ComfyUI.
---
## 📖 Usage Example
### 🧰 Method 2: Manual Git Clone
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.
```bash
cd YOUR_COMFYUI_PATH/custom_nodes
git clone https://github.com/FunnyFinger/Dynamic_Sliders_stack.git
```
Then restart ComfyUI.
---
## 💡 Use Case Scenarios
### 📁 Method 3: Manual ZIP Download
This node setup provides a centralized control panel for managing multiple numerical weights within your workflow. Here are a few examples:
1. Go to the GitHub repo: [Dynamic Sliders Stack](https://github.com/FunnyFinger/Dynamic_Sliders_stack).
2. Click the green **Code** button → **Download ZIP**.
3. Extract the ZIP file.
4. Move the extracted folder into your ComfyUI `custom_nodes/` directory.
5. Restart ComfyUI.
- **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` to retrieve specific weights.
- **Prompt Segment Weighting**: Control the emphasis of different parts of a prompt.
- **Embedding/Textual Inversion Strength**: Manage multiple embeddings by connecting `Slider Receiver` outputs to nodes that adjust their strength.
- **Style/Character Consistency**: Group controls for multiple elements (LoRAs, weights) onto one `Slider Stacker` for centralized adjustments.
- **Controlling Custom Node Parameters**: Control FLOAT inputs for strength, weight, etc., in custom nodes with `Slider Receiver` outputs.
- **Centralized Control Panel**: Use one or more `Slider Stacker` nodes to create a 'dashboard' for managing weights.
---
## 🚀 Usage Instructions
1. **Add the Nodes:** Double-click on the ComfyUI canvas or right-click -> "Add Node". Search for "Slider Stacker" or "Slider Receiver". You can also find them under the "Dynamic Sliders Stack" category.
2. Add the **Sliders Stacker** node to your graph.
3. Set the `slider_count` field (e.g., 5 sliders).
4. Adjust the maximum value for the highest value slider via `sliders_max` and all sliders will adjust proportionally.
5. Adjust the target total sum value for all sliders via `sliders_sum` and all sliders will adjust accordingly while maintaining their offsets (only effective when Sum Lock is off).
6. Optionally set individual titles for each slider.
7. Adjust the sliders as needed in many modes: individually, proportionally (via `sliders_max`), offset (via `sliders_sum` with lock off), sliders_sum with sum-lock mode, individually with sum-lock mode.
8. Connect the `SLIDER_WEIGHTS` output port to the **Sliders Receiver** node (add this node the same way as the Stacker) and set the index to the required slider number.
9. Combine this custom node with other custom nodes like prompt composer to give controllable weight for parts of your prompt.
10. Right-click the **Slider Stacker** node for utilities such as Average All, Reset All, Max All and Min All.
---
## 🎯 Contributing
We welcome contributions! If you have suggestions for improvements or new features, please [open an issue](https://github.com/FunnyFinger/Dynamic_Sliders_stack/issues) or [submit a pull request](https://github.com/FunnyFinger/Dynamic_Sliders_stack/pulls).
---
## 📜 License
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.
This project is licensed under the [MIT License](LICENSE).
---
This version improves readability and organizes the sections more effectively. Let me know if you’d like further adjustments!
Made with ❤️ by [FunnyFinger](https://github.com/FunnyFinger)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

+1043 -53
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -13,7 +13,7 @@ const COLOR_THEMES = {
// Updated NODE_COLORS map
const NODE_COLORS = {
"Slider Stacker": "purple",
"Slider Stacker": "purple",
"Slider Receiver": "purple", // Added receiver, same color
// Add other nodes here if needed
};
@@ -30,7 +30,6 @@ 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;
@@ -62,4 +61,7 @@ const ext = {
}
};
app.registerExtension(ext);
// Add a console log for loading confirmation
// console.log("Loaded Dynamic Sliders Stack Appearance Script");
app.registerExtension(ext);
+73 -48
View File
@@ -1,6 +1,6 @@
import { app } from "../../scripts/app.js";
console.log("Loading context.js for Slider Stacker");
// Removed console log
// --- Helper Functions (Copied from Dynamic.js for self-containment) ---
@@ -14,9 +14,28 @@ const getWidgetValue = (widget) => {
return isNaN(num) ? 0 : num;
};
// Function to check if node is our Slider Stacker using multiple identification methods
function isSliderStackerNode(node) {
// Check the display title
const nodeTitle = node.getTitle ? node.getTitle() : node.title;
if (nodeTitle === "Slider Stacker") return true;
// Check internal type name
if (node.type === "Slider Stacker (DSS)") return true;
// Check properties that are unique to our node
if (findWidgetByName(node, "sliders_count") &&
findWidgetByName(node, "sliders_max") &&
findWidgetByName(node, "slider_name_1")) {
return true;
}
return false;
}
// Gets the actual max strength based on current slider values and activity
function recalculateCurrentMaxStrength(node) {
const sliderCountWidget = findWidgetByName(node, "slider_count");
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const count = getWidgetValue(sliderCountWidget);
let currentMaxValue = 0;
for (let i = 1; i <= count; i++) {
@@ -25,7 +44,7 @@ function recalculateCurrentMaxStrength(node) {
currentMaxValue = Math.max(currentMaxValue, getWidgetValue(wtWidget));
}
}
const maxStrengthWidget = findWidgetByName(node, "slider_max_strength");
const maxStrengthWidget = findWidgetByName(node, "sliders_max");
const overallMax = maxStrengthWidget?.options?.max ?? 2.0;
const overallMin = maxStrengthWidget?.options?.min ?? 0.0;
return Math.max(overallMin, Math.min(currentMaxValue, overallMax));
@@ -34,7 +53,7 @@ function recalculateCurrentMaxStrength(node) {
// 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 sliderCountWidget = findWidgetByName(node, "sliders_count");
const maxSliders = sliderCountWidget?.options?.max || 50;
for (let i = 1; i <= maxSliders; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
@@ -50,9 +69,9 @@ function updateNormalization(node, currentMaxStrength) {
// 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;
if (!node.normalizedSliderWeights) return;
node._isUpdatingInternally = true; // Set flag for this operation
const count = getWidgetValue(findWidgetByName(node, "slider_count"));
const count = getWidgetValue(findWidgetByName(node, "sliders_count"));
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
@@ -60,17 +79,19 @@ function updateAllSliderDisplaysFromContext(node, newMaxStrength) {
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));
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);
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
}
node._isUpdatingInternally = false;
}
// --- Function to add menu options ---
// --- Function to add menu options ---
function addContextMenuOptions(node) {
const originalGetExtraMenuOptions = node.getExtraMenuOptions;
@@ -83,7 +104,7 @@ function addContextMenuOptions(node) {
options.push({
content: "Average Slider Values",
callback: () => {
const count = getWidgetValue(findWidgetByName(this, "slider_count"));
const count = getWidgetValue(findWidgetByName(this, "sliders_count"));
let sum = 0;
let activeCount = 0;
for (let i = 1; i <= count; i++) {
@@ -94,27 +115,29 @@ function addContextMenuOptions(node) {
}
}
const average = activeCount > 0 ? sum / activeCount : 0;
this._isUpdatingInternally = true;
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");
const masterWidget = findWidgetByName(this, "sliders_max");
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);
if (this.graph) {
this.graph.setDirtyCanvas(true, true);
}
}
});
@@ -122,25 +145,27 @@ function addContextMenuOptions(node) {
options.push({
content: "Reset Slider Values",
callback: () => {
const count = getWidgetValue(findWidgetByName(this, "slider_count"));
const defaultValue = 1.0;
const count = getWidgetValue(findWidgetByName(this, "sliders_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");
updateNormalization(this, newMax);
const masterWidget = findWidgetByName(this, "sliders_max");
if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) {
masterWidget.value = newMax;
} else {
updateAllSliderDisplaysFromContext(this, newMax);
}
this.graph.setDirtyCanvas(true, true);
if (this.graph) {
this.graph.setDirtyCanvas(true, true);
}
}
});
@@ -148,7 +173,7 @@ function addContextMenuOptions(node) {
options.push({
content: "Set All to Max Value",
callback: () => {
const count = getWidgetValue(findWidgetByName(this, "slider_count"));
const count = getWidgetValue(findWidgetByName(this, "sliders_count"));
let maxValue = -Infinity; // Start low
// First pass: Find the maximum value among visible sliders
@@ -158,37 +183,36 @@ function addContextMenuOptions(node) {
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;
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");
const masterWidget = findWidgetByName(this, "sliders_max");
// Update master slider directly - its setter handles updating displays
if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) {
masterWidget.value = newMax;
masterWidget.value = newMax;
}
if (this.graph) {
this.graph.setDirtyCanvas(true, true);
}
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"));
const count = getWidgetValue(findWidgetByName(this, "sliders_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})`);
@@ -196,40 +220,41 @@ function addContextMenuOptions(node) {
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;
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");
updateNormalization(this, newMax);
const masterWidget = findWidgetByName(this, "sliders_max");
// Update master slider directly
if (masterWidget && Math.abs(getWidgetValue(masterWidget) - newMax) > 0.001) {
masterWidget.value = newMax;
}
this.graph.setDirtyCanvas(true, true);
if (this.graph) {
this.graph.setDirtyCanvas(true, true);
}
}
});
};
}
// --- ComfyUI Extension ---
// --- ComfyUI Extension ---
app.registerExtension({
name: "Comfy.DynamicSlidersStack.ContextMenu",
nodeCreated(node) {
const nodeTitle = node.getTitle ? node.getTitle() : node.title;
if (nodeTitle === "Slider Stacker") {
// Use our robust identification instead of title
if (isSliderStackerNode(node)) {
addContextMenuOptions(node);
}
}
});
});
// console.log("Loaded Dynamic Sliders Stack Context Menu Script");
+247
View File
@@ -0,0 +1,247 @@
/**
* Dynamic Sliders Stack - Modern Widget Hider
* Manages widget visibility based on slider count
*/
// REMOVE top-level import attempts for app
/*
// Import app with fallbacks
let app;
try { app = await import('/scripts/app.js'); } catch (e) {
try { app = await import('../../../scripts/app.js'); } catch (e) {
app = window.app;
}
}
*/
// REMOVE top-level check
// const isModernFrontend = app && app.extensionManager;
// Config
const NODE_CONFIG = {
controlWidgets: 3, // Number of control widgets
maxSliders: 50, // Maximum number of sliders
// nodeRadius removed as ComfyUI default is round
};
let domObserver = null; // DOM Mutation observer
// Helper to find node element
const findNodeElement = (nodeId) => document?.getElementById(nodeId) ||
document?.querySelector(`.litegraph-node[data-node-id="${nodeId}"]`) ||
document?.querySelector(`.comfy-node[data-node-id="${nodeId}"]`) ||
document?.querySelector(`.node_box[data-litegraph-id="${nodeId}"]`) ||
document?.querySelector(`.litegraph.litecontextualmenu .node_box[data-litegraph-id='${nodeId}']`);
// Extension definition
const extension = {
app: null, // Add property to store app instance
/** Set up the extension */
setup(app) { // Receive app as argument
// console.log("[DSS Modern Hider] setup() called.");
this.app = app; // Store app instance
// No need to check isModernFrontend here, this script should only load in modern
this.createGlobalStyles();
this.setupNodeTracking();
// console.log("[DSS Modern Hider] setup() completed.");
return true; // Indicate setup was successful (or based on actual success)
},
/** Update widget visibility (internal & DOM) */
updateWidgetVisibility(node, sliderCount) {
// console.log(`[DSS Modern Hider] updateWidgetVisibility called for node ${node.id}, sliderCount: ${sliderCount}`); // Log entry
if (!node?.widgets) {
// console.warn(`[DSS Modern Hider] No widgets found for node ${node.id}`);
return;
}
// Sanitize slider count
const originalSliderCount = sliderCount;
sliderCount = Math.max(1, Math.min(Math.floor(sliderCount), NODE_CONFIG.maxSliders));
if (originalSliderCount !== sliderCount) {
// console.log(`[DSS Modern Hider] Sanitized sliderCount from ${originalSliderCount} to ${sliderCount}`);
}
// Update internal widget visibility
let hiddenChangedCount = 0;
node.widgets.slice(NODE_CONFIG.controlWidgets).forEach((widget, i) => {
const shouldBeHidden = Math.floor(i / 2) + 1 > sliderCount;
if (widget.hidden !== shouldBeHidden) {
widget.hidden = shouldBeHidden;
hiddenChangedCount++;
// console.log(`[DSS Modern Hider] Set widget ${widget.name} (index ${i}) hidden: ${shouldBeHidden}`); // Optional detailed log
}
});
// console.log(`[DSS Modern Hider] Updated 'hidden' property for ${hiddenChangedCount} widgets.`);
// Remove the DOM manipulation block entirely
/*
// Update DOM widget visibility <-- REMOVED
const nodeElement = findNodeElement(node.id);
if (nodeElement) {
const widgetContainers = nodeElement.querySelectorAll('.slotcontainer .for_widget');
if (widgetContainers?.length > NODE_CONFIG.controlWidgets) {
widgetContainers.forEach((container, i) => {
if (i >= NODE_CONFIG.controlWidgets) {
container.style.display = Math.floor((i - NODE_CONFIG.controlWidgets) / 2) + 1 > sliderCount ? 'none' : '';
}
});
}
}
*/
},
/** Create global CSS */
createGlobalStyles() {
if (document.getElementById('dynamic-slider-stack-styles')) return;
const style = document.createElement('style');
style.id = 'dynamic-slider-stack-styles';
style.textContent = `
/* Hide disabled widgets */
.widget.disabled { display: none !important; }
`;
document.head.appendChild(style);
},
/** Setup node tracking */
setupNodeTracking() {
// console.log("[DSS Modern Hider] setupNodeTracking() called.");
// Use this.app instead of global app
if (!this.app?.graph) {
// console.warn("[DSS Modern Hider] this.app.graph not available during setupNodeTracking.");
return;
}
// Process existing nodes
// console.log(`[DSS Modern Hider] Processing ${this.app.graph.nodes?.length || 0} existing nodes.`);
this.app.graph.nodes?.forEach(node => {
// Add more comprehensive check including the suffixed name
if (node.type === "Slider Stacker (DSS)" || node.type === "SliderStacker" || node.title === "Slider Stacker") {
// console.log(`[DSS Modern Hider] Found existing Slider Stacker node (ID: ${node.id}, Type: ${node.type}, Title: ${node.title}). Processing...`);
this.processNewNode(node);
}
});
// Listen for new nodes
// console.log("[DSS Modern Hider] Adding nodeCreated listener.");
// Use this.app instead of global app
this.app.graph.addEventListener("nodeCreated", (node) => {
// console.log(`[DSS Modern Hider] nodeCreated event fired for node (ID: ${node.id}, Type: ${node.type}, Title: ${node.title})`);
// Add more comprehensive check including the suffixed name
if (node.type === "Slider Stacker (DSS)" || node.type === "SliderStacker" || node.title === "Slider Stacker") {
// console.log(`[DSS Modern Hider] nodeCreated is a Slider Stacker. Scheduling processNewNode...`);
// Pass 'this' context if processNewNode uses it, although it doesn't currently
setTimeout(() => this.processNewNode(node), 0);
} else if (node.title === "Slider Receiver") { // Also log receiver for context
// console.log(`[DSS Modern Hider] nodeCreated is a Slider Receiver.`);
// Potentially call processNewNode if receiver needs processing later
}
});
// Setup DOM observer
// Pass 'this' context if setupDOMObserver uses it (it doesn't currently)
this.setupDOMObserver();
// console.log("[DSS Modern Hider] setupNodeTracking() completed.");
},
/** Setup DOM observer */
setupDOMObserver() {
if (domObserver) domObserver.disconnect();
domObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes?.forEach(domNode => { // Rename to domNode to avoid confusion with LiteGraph node
if (domNode.classList?.contains('comfy-node') && // Check for modern class name
domNode.dataset?.nodeType?.includes('Slider Stacker')) { // Check data attribute if available
// console.log(`[DSS Modern Hider] DOM Observer detected node addition (ID: ${domNode.dataset?.nodeId}, Type: ${domNode.dataset?.nodeType})`);
// Attempt to find the LiteGraph node and re-process it
// Use this.app
const lgNode = this.app?.graph?.getNodeById(parseInt(domNode.dataset?.nodeId));
if (lgNode) {
// console.log(`[DSS Modern Hider] Found corresponding LiteGraph node ${lgNode.id}. Processing...`);
this.processNewNode(lgNode);
} else {
// console.warn(`[DSS Modern Hider] Could not find LiteGraph node for DOM node ID ${domNode.dataset?.nodeId}`);
}
}
});
});
});
// Assuming the main graph container might have a different ID or structure in modern
// Let's try a more generic approach or rely on setupNodeTracking initially
// const graphCanvas = document.getElementById('graph-canvas'); // This might be legacy
// For now, let's observe the body, which is less efficient but might catch additions
// Consider refining this later if needed
// console.log("[DSS Modern Hider] Setting up DOM Observer on document.body.");
domObserver.observe(document.body, { childList: true, subtree: true });
},
/** Process a new node */
processNewNode(node) {
// console.log(`[DSS Modern Hider] processNewNode called for node (ID: ${node.id}, Type: ${node.type}, Title: ${node.title})`); // Log process entry
if (!node) {
// console.warn("[DSS Modern Hider] processNewNode called with null node.");
return;
}
// Intercept widget and update visibility (only for Stacker)
// Use more robust check
if (node.type === "Slider Stacker (DSS)" || node.type === "SliderStacker" || node.title === "Slider Stacker") {
// console.log(`[DSS Modern Hider] Node ${node.id} confirmed as Slider Stacker, calling interceptAndUpdate.`);
this.interceptAndUpdate(node);
}
// No styling needed here, assuming ComfyUI default round shape
},
/** Intercept sliders_count widget setter and update visibility */
interceptAndUpdate(node) {
const widget = node.widgets?.find(w => w.name === "sliders_count");
if (!widget) {
// console.warn(`[DSS Modern Hider] 'sliders_count' widget not found for node ${node.id}`);
return;
}
if (widget._valueSetterOverridden) {
// console.log(`[DSS Modern Hider] Setter already overridden for node ${node.id}. Triggering initial update.`);
this.updateWidgetVisibility(node, widget.value);
return;
}
// console.log(`[DSS Modern Hider] Intercepting 'sliders_count' setter for node ${node.id}`);
const originalSetter = Object.getOwnPropertyDescriptor(widget, "value")?.set;
widget._valueSetterOverridden = true;
Object.defineProperty(widget, "value", {
get: function() { return this._value; },
set: function(v) {
// console.log(`[DSS Modern Hider] 'sliders_count' setter triggered for node ${node.id}. New value: ${v}, Old value: ${this._value}`);
const oldValue = this._value;
this._value = v;
if (originalSetter) originalSetter.call(this, v);
if (oldValue !== v) {
// console.log(`[DSS Modern Hider] Value changed, calling updateWidgetVisibility.`);
extension.updateWidgetVisibility(node, v);
} else {
// console.log(`[DSS Modern Hider] Value did not change, skipping update.`);
}
},
enumerable: true, configurable: true
});
// console.log(`[DSS Modern Hider] Triggering initial update for node ${node.id} with value ${widget.value}`);
this.updateWidgetVisibility(node, widget.value); // Initial update
},
/** Clean up resources */
cleanup() {
if (domObserver) { domObserver.disconnect(); domObserver = null; }
const style = document.getElementById('dynamic-slider-stack-styles');
if (style) style.remove();
}
};
export default extension;
+195 -136
View File
@@ -1,150 +1,209 @@
import { app } from "../../scripts/app.js";
// --- Start: Simplified code snippets ---
let origProps = {};
let initialized = false;
// Helper function to find a widget by name
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) {
// Function to check if node is our Slider Stacker
function isSliderStackerNode(node) {
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);
}
if (nodeTitle === "Slider Stacker") return true;
if (node.type === "Slider Stacker (DSS)") return true;
return false;
}
// --- End: Simplified code snippets ---
// Constants for hiding
const HIDDEN_TAG = "_hidden_"; // Needs a unique suffix if other nodes use similar tags
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;
// Cache for original widget properties
let originalWidgetProps = {};
for (const w of node.widgets || []) {
// Use updated title for handler check
if (nodeWidgetHandlers[nodeTitle]?.[w.name]) {
hasSliderStackerHandler = true;
}
}
// Function to toggle widget visibility using standard JS properties
function toggleWidgetVisibility(node, widget, show) {
if (!widget) return false; // Return false if no widget
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);
// Store original properties if not already saved
const widgetKey = `${node.id}_${widget.name}`;
if (!originalWidgetProps[widgetKey]) {
originalWidgetProps[widgetKey] = {
origType: widget.type,
origComputeSize: widget.computeSize,
origHidden: widget.hidden // Store original hidden state if available
};
}
const origProps = originalWidgetProps[widgetKey];
// Check both hidden property and our custom type tag
const isCurrentlyHidden = widget.hidden || widget.type === HIDDEN_TAG;
// Only change if the state needs toggling
if (show && !isCurrentlyHidden) return false; // Return false: no change needed
if (!show && isCurrentlyHidden) return false; // Return false: no change needed
if (show) {
// Restore original properties
widget.type = origProps.origType;
widget.hidden = origProps.origHidden || false;
widget.computeSize = origProps.origComputeSize || null; // Restore original or set to null
} else {
// Hide the widget
widget.type = HIDDEN_TAG;
widget.hidden = true;
widget.computeSize = () => [0, -4]; // Standard way to hide widget space
}
// Mark the node as needing redraw
node.setDirtyCanvas(true, false); // Only redraw foreground (nodes)
return true; // Return true: change was made
}
// Function to handle the actual update of slider visibility
function updateSliderWidgetsVisibility(node) {
if (!isSliderStackerNode(node) || !node.widgets) return;
const countWidget = findWidgetByName(node, "sliders_count");
if (!countWidget) {
// Don't log error repeatedly if widget is just missing temporarily during load
return;
}
let currentCount = 1;
try {
currentCount = parseInt(countWidget.value) || 1;
currentCount = Math.max(1, Math.min(currentCount, 50)); // Clamp value (min 1)
} catch (e) {
console.error("Slider Stacker: Error parsing sliders_count value", e); // Keep error log
currentCount = 1; // Default to 1 on error
}
const maxSliders = 50; // Assuming max 50 sliders based on Python code
let visibilityChanged = false;
// Store current width to preserve it
const currentWidth = node.size[0];
for (let i = 1; i <= maxSliders; i++) {
const nameWidget = findWidgetByName(node, `slider_name_${i}`);
const sliderWidget = findWidgetByName(node, `(${i})`);
const shouldShow = i <= currentCount;
let nameChanged = false;
let sliderChanged = false;
if (nameWidget) {
nameChanged = toggleWidgetVisibility(node, nameWidget, shouldShow);
}
if (sliderWidget) {
sliderChanged = toggleWidgetVisibility(node, sliderWidget, shouldShow);
}
if (nameChanged || sliderChanged) {
visibilityChanged = true;
}
}
});
// Trigger a resize and redraw if visibility actually changed
if (visibilityChanged && node.graph) {
// Recalculate the required size based on visible widgets
let newHeight = node.size[1]; // Default to current height
if (node.computeSize) {
try {
const computedSize = node.computeSize();
if (computedSize && computedSize.length === 2) {
newHeight = computedSize[1]; // Get the computed height
}
} catch (e) {
// Keep error log
console.error("Slider Stacker: Error computing node size after visibility change", e);
}
}
// Set the size, preserving width and using the new height
node.setSize([currentWidth, newHeight]);
// Mark the whole graph canvas as dirty to ensure redraw and layout adjustment
node.graph.setDirtyCanvas(true, true);
}
}
// Main App Extension
app.registerExtension({
name: "Comfy.DynamicSlidersStack.WidgetHider.JS",
// Called once per node type before registration
beforeRegisterNodeDef(nodeType, nodeData, appInstance) {
if (nodeData.name === "Slider Stacker (DSS)") {
// Store original onConfigure if it exists
const originalOnConfigure = nodeType.prototype.onConfigure;
// Override onConfigure to apply initial visibility
nodeType.prototype.onConfigure = function(info) {
if (originalOnConfigure) {
originalOnConfigure.apply(this, arguments);
}
requestAnimationFrame(() => updateSliderWidgetsVisibility(this));
};
// Store original onConnectionsChange if it exists
const originalOnConnectionsChange = nodeType.prototype.onConnectionsChange;
// Override onConnectionsChange to potentially update visibility if needed
nodeType.prototype.onConnectionsChange = function(side, slot, connect, link_info, output) {
if (originalOnConnectionsChange) {
originalOnConnectionsChange.apply(this, arguments);
}
requestAnimationFrame(() => updateSliderWidgetsVisibility(this));
};
}
},
// Called every time a node instance is created
nodeCreated(node) {
if (!isSliderStackerNode(node)) return;
const countWidget = findWidgetByName(node, "sliders_count");
if (!countWidget) {
// Don't log error repeatedly
return;
}
// Intercept the 'value' property of the sliders_count widget
let widgetValue = countWidget.value;
const descriptor = Object.getOwnPropertyDescriptor(countWidget, 'value');
Object.defineProperty(countWidget, 'value', {
configurable: true,
enumerable: true,
get() {
return descriptor?.get ? descriptor.get.call(countWidget) : widgetValue;
},
set(newVal) {
let changedValue = newVal;
// Ensure value is at least 1
changedValue = Math.max(1, parseInt(changedValue) || 1);
if (descriptor?.set) {
descriptor.set.call(countWidget, changedValue);
} else {
widgetValue = changedValue;
}
// Update input element visually if it exists
if(countWidget.inputEl) {
countWidget.inputEl.value = changedValue;
}
requestAnimationFrame(() => updateSliderWidgetsVisibility(node));
}
});
// Initial setup - apply visibility based on the default/loaded value
requestAnimationFrame(() => updateSliderWidgetsVisibility(node));
},
// Called when node is added to the graph (might be redundant with nodeCreated)
nodeAdded(node) {
if (!isSliderStackerNode(node)) return;
requestAnimationFrame(() => updateSliderWidgetsVisibility(node));
}
});
// console.log("Loaded Dynamic Sliders Stack Widget Hider Script");
+15
View File
@@ -0,0 +1,15 @@
# pyproject.toml
[project]
name = "Dynamic_Sliders_stack"
description = "A custom node for ComfyUI to have many sliders with interactive dynamic behavior."
version = "2.0.0"
license = "LICENSE"
dependencies = []
[project.urls]
Repository = "https://github.com/FunnyFinger/Dynamic_Sliders_stack"
[tool.comfy]
PublisherId = "funnyfinger"
DisplayName = "Dynamic Sliders Stack"
# Icon = ""
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

+28
View File
@@ -0,0 +1,28 @@
[flake8]
max-line-length = 100
exclude = .git,__pycache__,build,dist
ignore = E203, W503
[pep8]
max-line-length = 100
[tool:isort]
profile = black
line_length = 100
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
[mypy]
python_version = 3.9
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
+18 -9
View File
@@ -1,5 +1,5 @@
import folder_paths
import re
import re
# Updated Node name for registration
RECEIVER_NODE_NAME = "Slider Receiver (DSS)" # Internal name
@@ -9,11 +9,10 @@ RECEIVER_NODE_NAME = "Slider Receiver (DSS)" # Internal name
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}),
"index": ("INT", {"default": 1, "min": 1, "max": 50, "step": 1}),
}
}
@@ -23,25 +22,35 @@ class TSC_Slider_Receiver_Standalone: # Keep class name for now, just update reg
# Updated Category
CATEGORY = "Dynamic Sliders Stack"
# ... (get_weight_at_index function remains the same) ...
# Add compatibility attributes for newer ComfyUI versions
TITLE = "Slider Receiver"
TYPE = "Slider Receiver (DSS)"
# Add getWidgets method for consistency
def getWidgets(self):
# Just return all widgets since this node doesn't need filtering
return self.widgets if hasattr(self, 'widgets') else None
def get_weight_at_index(self, weights_string, index):
selected_weight = 0.0
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])
selected_weight_str = weights_str_list[index - 1]
selected_weight = float(selected_weight_str)
else:
print(f"Warning: Index {index} out of bounds for weights string: '{weights_string}' (Length: {len(weights_str_list)}). Returning 0.0.")
print(f"Warning: Index {index} out of bounds for weights list (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 ---
# --- Mappings ---
NODE_CLASS_MAPPINGS = {
RECEIVER_NODE_NAME: TSC_Slider_Receiver_Standalone
}
@@ -49,4 +58,4 @@ NODE_CLASS_MAPPINGS = {
# Updated Display Name
NODE_DISPLAY_NAME_MAPPINGS = {
RECEIVER_NODE_NAME: "Slider Receiver" # Removed suffix
}
}
+34 -20
View File
@@ -8,24 +8,28 @@ NODE_NAME = "Slider Stacker (DSS)" # Internal name, keep suffix for uniqueness
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}),
"sliders_max": ("FLOAT", {"default": 1.00, "min": 0.0, "max": 2.0, "step": 0.01, "precision": 2, "display": "number"}),
"sliders_count": ("INT", {"default": 1, "min": 1, "max": max_sliders, "step": 1}),
"sliders_sum": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 0.01, "precision": 2, "display": "number"}),
"lock_sum": ("BOOLEAN", {"default": False, "label_on": "on", "label_off": "off"})
}
}
# Define all possible widgets without the hidden flag initially
for i in range(1, max_sliders + 1):
inputs["required"][f"slider_name_{i}"] = ("STRING", {"multiline": False, "default": "None"})
inputs["required"][f"slider_name_{i}"] = ("STRING", {"multiline": False, "default": "None"}) # Removed hidden: True
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"
inputs["required"][widget_name] = ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.01,
"precision": 2,
"display": "slider"
# Removed hidden: True
})
return inputs
@@ -35,16 +39,26 @@ class TSC_Slider_Stacker_Standalone:
# 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]
# Add compatibility attributes for newer ComfyUI versions
TITLE = "Slider Stacker"
TYPE = "Slider Stacker (DSS)"
# Removed __init__, onNodeLoad, update_slider_visibility, getWidgets, onExecute overrides
# Let JS handle the dynamic visibility
def slider_stacker(self, sliders_max, sliders_count, sliders_sum=None, lock_sum=False, **kwargs):
# Clamp sliders_count to a valid range (1 to 50)
sliders_count = max(1, min(int(sliders_count), 50))
# Regular processing using the validated sliders_count
sliders_available = [kwargs.get(f"slider_name_{i}") for i in range(1, sliders_count + 1)]
weights_raw = [kwargs.get(f"({i})") for i in range(1, sliders_count + 1)]
safe_max_strength = max(0.0, sliders_max)
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}")
for i in range(sliders_count):
active_weights.append(f"{weights_clamped_final[i]:.2f}")
weights_string = ", ".join(active_weights)
return (weights_string,)
@@ -56,4 +70,4 @@ NODE_CLASS_MAPPINGS = {
# Updated Display Name
NODE_DISPLAY_NAME_MAPPINGS = {
NODE_NAME: "Slider Stacker" # Removed suffix for cleaner UI
}
}