updated to v2.0.0
added new features such as dynamic sum adjustment and sum-lock dynamics
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
`;
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 389 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 776 KiB |
|
After Width: | Height: | Size: 870 KiB |
|
After Width: | Height: | Size: 982 KiB |
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||