comfy-dynamic-widgets 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_dynamic_widgets-0.0.6/.github/workflows/publish.yml +67 -0
- comfy_dynamic_widgets-0.0.6/.gitignore +35 -0
- comfy_dynamic_widgets-0.0.6/LICENSE +21 -0
- comfy_dynamic_widgets-0.0.6/PKG-INFO +105 -0
- comfy_dynamic_widgets-0.0.6/README.md +81 -0
- comfy_dynamic_widgets-0.0.6/comfy_dynamic_widgets/__init__.py +36 -0
- comfy_dynamic_widgets-0.0.6/comfy_dynamic_widgets/generator.py +45 -0
- comfy_dynamic_widgets-0.0.6/comfy_dynamic_widgets/scanner.py +150 -0
- comfy_dynamic_widgets-0.0.6/comfy_dynamic_widgets/web/js/dynamic_widgets.js +141 -0
- comfy_dynamic_widgets-0.0.6/pyproject.toml +40 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: Bump Version & Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
bump-and-publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
id-token: write
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
20
|
+
|
|
21
|
+
- name: Setup Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: '3.11'
|
|
25
|
+
|
|
26
|
+
- name: Get current version
|
|
27
|
+
id: get_version
|
|
28
|
+
run: |
|
|
29
|
+
current=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/' | tr -d '\r')
|
|
30
|
+
echo "current=$current" >> $GITHUB_OUTPUT
|
|
31
|
+
|
|
32
|
+
- name: Bump patch version
|
|
33
|
+
id: bump_version
|
|
34
|
+
run: |
|
|
35
|
+
current="${{ steps.get_version.outputs.current }}"
|
|
36
|
+
IFS='.' read -r major minor patch <<< "$current"
|
|
37
|
+
new_patch=$((patch + 1))
|
|
38
|
+
new_version="${major}.${minor}.${new_patch}"
|
|
39
|
+
sed -i "s/^version = \".*\"/version = \"${new_version}\"/" pyproject.toml
|
|
40
|
+
echo "new_version=$new_version" >> $GITHUB_OUTPUT
|
|
41
|
+
echo "Bumped version: $current -> $new_version"
|
|
42
|
+
|
|
43
|
+
- name: Commit version bump
|
|
44
|
+
run: |
|
|
45
|
+
git config user.name "github-actions[bot]"
|
|
46
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
47
|
+
git add pyproject.toml
|
|
48
|
+
git commit -m "Bump version to ${{ steps.bump_version.outputs.new_version }} [skip ci]"
|
|
49
|
+
git tag "v${{ steps.bump_version.outputs.new_version }}"
|
|
50
|
+
git push origin main --tags
|
|
51
|
+
|
|
52
|
+
- name: Install build tools
|
|
53
|
+
run: pip install build
|
|
54
|
+
|
|
55
|
+
- name: Build package
|
|
56
|
+
run: python -m build
|
|
57
|
+
|
|
58
|
+
- name: Create GitHub Release
|
|
59
|
+
uses: softprops/action-gh-release@v1
|
|
60
|
+
with:
|
|
61
|
+
tag_name: v${{ steps.bump_version.outputs.new_version }}
|
|
62
|
+
name: v${{ steps.bump_version.outputs.new_version }}
|
|
63
|
+
generate_release_notes: true
|
|
64
|
+
files: dist/*
|
|
65
|
+
|
|
66
|
+
- name: Publish to PyPI
|
|
67
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
*.egg
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
ENV/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
|
|
22
|
+
# Jupyter
|
|
23
|
+
.ipynb_checkpoints/
|
|
24
|
+
|
|
25
|
+
# Testing
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# Generated files
|
|
35
|
+
web/js/mappings.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andrea Pozzetti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: comfy-dynamic-widgets
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: Dynamic widget visibility for ComfyUI nodes via simple metadata
|
|
5
|
+
Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-dynamic-widgets
|
|
6
|
+
Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-dynamic-widgets
|
|
7
|
+
Project-URL: Issues, https://github.com/PozzettiAndrea/comfy-dynamic-widgets/issues
|
|
8
|
+
Author: Andrea Pozzetti
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: comfyui,conditional,dynamic,visibility,widgets
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# comfy-dynamic-widgets
|
|
26
|
+
|
|
27
|
+
A Python library that enables conditional widget visibility in ComfyUI nodes via simple metadata.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install comfy-dynamic-widgets
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### For Node Authors
|
|
38
|
+
|
|
39
|
+
Add `visible_when` metadata to widget definitions:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
@classmethod
|
|
43
|
+
def INPUT_TYPES(cls):
|
|
44
|
+
return {
|
|
45
|
+
"required": {
|
|
46
|
+
"backend": (["option_a", "option_b", "option_c"], {"default": "option_a"}),
|
|
47
|
+
},
|
|
48
|
+
"optional": {
|
|
49
|
+
"param_for_a": ("FLOAT", {
|
|
50
|
+
"default": 0.1,
|
|
51
|
+
"visible_when": {"backend": ["option_a"]},
|
|
52
|
+
}),
|
|
53
|
+
"param_for_b_and_c": ("INT", {
|
|
54
|
+
"default": 10,
|
|
55
|
+
"visible_when": {"backend": ["option_b", "option_c"]},
|
|
56
|
+
}),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### For Custom Node Package Authors
|
|
62
|
+
|
|
63
|
+
In your `prestartup_script.py`:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import json
|
|
67
|
+
import os
|
|
68
|
+
|
|
69
|
+
def generate_widget_mappings():
|
|
70
|
+
try:
|
|
71
|
+
from comfy_dynamic_widgets import scan_all_nodes, generate_mappings
|
|
72
|
+
|
|
73
|
+
configs = scan_all_nodes()
|
|
74
|
+
if not configs:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
mappings = generate_mappings(configs)
|
|
78
|
+
|
|
79
|
+
output_path = os.path.join(
|
|
80
|
+
os.path.dirname(__file__), "web", "js", "mappings.json"
|
|
81
|
+
)
|
|
82
|
+
with open(output_path, "w") as f:
|
|
83
|
+
json.dump(mappings, f, indent=2)
|
|
84
|
+
|
|
85
|
+
except ImportError:
|
|
86
|
+
print("comfy-dynamic-widgets not installed")
|
|
87
|
+
|
|
88
|
+
generate_widget_mappings()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Copy the `dynamic_widgets.js` from this package to your `web/js/` folder and update the mappings path.
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### `scan_all_nodes() -> dict`
|
|
96
|
+
|
|
97
|
+
Scans all registered ComfyUI nodes and extracts `visible_when` metadata.
|
|
98
|
+
|
|
99
|
+
### `generate_mappings(configs: dict) -> dict`
|
|
100
|
+
|
|
101
|
+
Transforms node visibility configs into a JS-friendly JSON format.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# comfy-dynamic-widgets
|
|
2
|
+
|
|
3
|
+
A Python library that enables conditional widget visibility in ComfyUI nodes via simple metadata.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install comfy-dynamic-widgets
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### For Node Authors
|
|
14
|
+
|
|
15
|
+
Add `visible_when` metadata to widget definitions:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
@classmethod
|
|
19
|
+
def INPUT_TYPES(cls):
|
|
20
|
+
return {
|
|
21
|
+
"required": {
|
|
22
|
+
"backend": (["option_a", "option_b", "option_c"], {"default": "option_a"}),
|
|
23
|
+
},
|
|
24
|
+
"optional": {
|
|
25
|
+
"param_for_a": ("FLOAT", {
|
|
26
|
+
"default": 0.1,
|
|
27
|
+
"visible_when": {"backend": ["option_a"]},
|
|
28
|
+
}),
|
|
29
|
+
"param_for_b_and_c": ("INT", {
|
|
30
|
+
"default": 10,
|
|
31
|
+
"visible_when": {"backend": ["option_b", "option_c"]},
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### For Custom Node Package Authors
|
|
38
|
+
|
|
39
|
+
In your `prestartup_script.py`:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
def generate_widget_mappings():
|
|
46
|
+
try:
|
|
47
|
+
from comfy_dynamic_widgets import scan_all_nodes, generate_mappings
|
|
48
|
+
|
|
49
|
+
configs = scan_all_nodes()
|
|
50
|
+
if not configs:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
mappings = generate_mappings(configs)
|
|
54
|
+
|
|
55
|
+
output_path = os.path.join(
|
|
56
|
+
os.path.dirname(__file__), "web", "js", "mappings.json"
|
|
57
|
+
)
|
|
58
|
+
with open(output_path, "w") as f:
|
|
59
|
+
json.dump(mappings, f, indent=2)
|
|
60
|
+
|
|
61
|
+
except ImportError:
|
|
62
|
+
print("comfy-dynamic-widgets not installed")
|
|
63
|
+
|
|
64
|
+
generate_widget_mappings()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Copy the `dynamic_widgets.js` from this package to your `web/js/` folder and update the mappings path.
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
### `scan_all_nodes() -> dict`
|
|
72
|
+
|
|
73
|
+
Scans all registered ComfyUI nodes and extracts `visible_when` metadata.
|
|
74
|
+
|
|
75
|
+
### `generate_mappings(configs: dict) -> dict`
|
|
76
|
+
|
|
77
|
+
Transforms node visibility configs into a JS-friendly JSON format.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (C) 2025 Comfy-DynamicWidgets Contributors
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Comfy Dynamic Widgets
|
|
6
|
+
|
|
7
|
+
A standalone package that enables conditional widget visibility in ComfyUI nodes
|
|
8
|
+
via simple metadata. Node authors can use "visible_when" in widget definitions
|
|
9
|
+
to show/hide widgets based on selector values.
|
|
10
|
+
|
|
11
|
+
Usage in node definitions:
|
|
12
|
+
"voxel_size": ("FLOAT", {
|
|
13
|
+
"default": 0.1,
|
|
14
|
+
"visible_when": {"backend": ["blender_voxel"]},
|
|
15
|
+
})
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
__version__ = "0.0.3"
|
|
21
|
+
|
|
22
|
+
from .scanner import scan_all_nodes
|
|
23
|
+
from .generator import generate_mappings
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_js_path() -> str:
|
|
27
|
+
"""Return path to the dynamic_widgets.js file in this package."""
|
|
28
|
+
return os.path.join(os.path.dirname(__file__), "web", "js", "dynamic_widgets.js")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_web_dir() -> str:
|
|
32
|
+
"""Return path to the web/js directory in this package."""
|
|
33
|
+
return os.path.join(os.path.dirname(__file__), "web", "js")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["scan_all_nodes", "generate_mappings", "get_js_path", "get_web_dir", "__version__"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (C) 2025 ComfyUI-DynamicWidgets Contributors
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Mapping Generator - Transforms node visibility configs into JS-friendly JSON format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_mappings(node_configs: dict) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Transform node visibility configs into JS-friendly format.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
node_configs: Output from scan_all_nodes()
|
|
15
|
+
{
|
|
16
|
+
"NodeClassName": {
|
|
17
|
+
"selectors": {
|
|
18
|
+
"backend": {
|
|
19
|
+
"blender_voxel": ["voxel_size"],
|
|
20
|
+
...
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
dict: JS-friendly mapping format
|
|
28
|
+
{
|
|
29
|
+
"version": 1,
|
|
30
|
+
"nodes": {
|
|
31
|
+
"NodeClassName": {
|
|
32
|
+
"selectors": {
|
|
33
|
+
"backend": {
|
|
34
|
+
"blender_voxel": ["voxel_size"],
|
|
35
|
+
...
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
return {
|
|
43
|
+
"version": 1,
|
|
44
|
+
"nodes": node_configs,
|
|
45
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (C) 2025 ComfyUI-DynamicWidgets Contributors
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Node Scanner - Introspects ComfyUI nodes to extract visible_when metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def scan_all_nodes() -> dict[str, dict]:
|
|
12
|
+
"""
|
|
13
|
+
Scan all registered ComfyUI nodes and extract visible_when metadata.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
dict: Mapping of node_class -> visibility configuration
|
|
17
|
+
{
|
|
18
|
+
"NodeClassName": {
|
|
19
|
+
"selectors": {
|
|
20
|
+
"selector_widget_name": {
|
|
21
|
+
"selector_value": ["widget1", "widget2"],
|
|
22
|
+
...
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
import nodes
|
|
30
|
+
except ImportError:
|
|
31
|
+
print("[DynamicWidgets] Warning: Could not import ComfyUI nodes module")
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
# Get the node class mappings from ComfyUI
|
|
35
|
+
node_mappings = getattr(nodes, "NODE_CLASS_MAPPINGS", {})
|
|
36
|
+
|
|
37
|
+
results = {}
|
|
38
|
+
|
|
39
|
+
for node_name, node_class in node_mappings.items():
|
|
40
|
+
config = _scan_node_class(node_name, node_class)
|
|
41
|
+
if config:
|
|
42
|
+
results[node_name] = config
|
|
43
|
+
|
|
44
|
+
return results
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _scan_node_class(node_name: str, node_class: type) -> dict | None:
|
|
48
|
+
"""
|
|
49
|
+
Scan a single node class for visible_when metadata.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
node_name: The registered name of the node
|
|
53
|
+
node_class: The node class to scan
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
dict or None: Visibility configuration if any visible_when found
|
|
57
|
+
"""
|
|
58
|
+
# Get INPUT_TYPES - it's typically a classmethod
|
|
59
|
+
if not hasattr(node_class, "INPUT_TYPES"):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
input_types = node_class.INPUT_TYPES()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"[DynamicWidgets] Warning: Failed to call INPUT_TYPES for {node_name}: {e}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
if not isinstance(input_types, dict):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Collect all visible_when entries
|
|
72
|
+
# Structure: selector_name -> selector_value -> [widget_names]
|
|
73
|
+
selectors: dict[str, dict[str, list[str]]] = {}
|
|
74
|
+
|
|
75
|
+
# Scan both required and optional inputs
|
|
76
|
+
for section in ["required", "optional"]:
|
|
77
|
+
section_inputs = input_types.get(section, {})
|
|
78
|
+
if not isinstance(section_inputs, dict):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
for widget_name, widget_def in section_inputs.items():
|
|
82
|
+
visible_when = _extract_visible_when(widget_def)
|
|
83
|
+
if visible_when:
|
|
84
|
+
_add_to_selectors(selectors, widget_name, visible_when)
|
|
85
|
+
|
|
86
|
+
if not selectors:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
return {"selectors": selectors}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _extract_visible_when(widget_def: Any) -> dict | None:
|
|
93
|
+
"""
|
|
94
|
+
Extract visible_when metadata from a widget definition.
|
|
95
|
+
|
|
96
|
+
Widget definitions are typically tuples like:
|
|
97
|
+
("FLOAT", {"default": 0.1, "visible_when": {"backend": ["blender_voxel"]}})
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
widget_def: The widget definition tuple/list
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict or None: The visible_when dict if present
|
|
104
|
+
"""
|
|
105
|
+
if not isinstance(widget_def, (tuple, list)):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if len(widget_def) < 2:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Second element should be the options dict
|
|
112
|
+
options = widget_def[1]
|
|
113
|
+
if not isinstance(options, dict):
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
visible_when = options.get("visible_when")
|
|
117
|
+
if not isinstance(visible_when, dict):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return visible_when
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _add_to_selectors(
|
|
124
|
+
selectors: dict[str, dict[str, list[str]]],
|
|
125
|
+
widget_name: str,
|
|
126
|
+
visible_when: dict
|
|
127
|
+
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Add widget visibility rules to the selectors structure.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
selectors: The selectors dict to update
|
|
133
|
+
widget_name: The name of the widget with visible_when
|
|
134
|
+
visible_when: The visible_when dict, e.g. {"backend": ["blender_voxel"]}
|
|
135
|
+
"""
|
|
136
|
+
for selector_name, selector_values in visible_when.items():
|
|
137
|
+
if not isinstance(selector_values, list):
|
|
138
|
+
# Allow single value without list wrapper
|
|
139
|
+
selector_values = [selector_values]
|
|
140
|
+
|
|
141
|
+
if selector_name not in selectors:
|
|
142
|
+
selectors[selector_name] = {}
|
|
143
|
+
|
|
144
|
+
for value in selector_values:
|
|
145
|
+
value_str = str(value)
|
|
146
|
+
if value_str not in selectors[selector_name]:
|
|
147
|
+
selectors[selector_name][value_str] = []
|
|
148
|
+
|
|
149
|
+
if widget_name not in selectors[selector_name][value_str]:
|
|
150
|
+
selectors[selector_name][value_str].append(widget_name)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Widget Visibility for ComfyUI
|
|
3
|
+
*
|
|
4
|
+
* Shows/hides widgets based on selector widget values.
|
|
5
|
+
* Mappings are loaded from mappings.json (generated by prestartup_script.py).
|
|
6
|
+
*
|
|
7
|
+
* Usage in node definitions:
|
|
8
|
+
* "voxel_size": ("FLOAT", {
|
|
9
|
+
* "default": 0.1,
|
|
10
|
+
* "visible_when": {"backend": ["blender_voxel"]},
|
|
11
|
+
* })
|
|
12
|
+
*/
|
|
13
|
+
import { app } from "../../../scripts/app.js";
|
|
14
|
+
|
|
15
|
+
console.log("[DynamicWidgets] Loading extension...");
|
|
16
|
+
|
|
17
|
+
// Will be populated from JSON
|
|
18
|
+
let MAPPINGS = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetch mappings from generated JSON (relative to this script's location)
|
|
22
|
+
*/
|
|
23
|
+
async function loadMappings() {
|
|
24
|
+
// Get the directory this script is loaded from
|
|
25
|
+
const scriptUrl = new URL(import.meta.url);
|
|
26
|
+
const baseDir = scriptUrl.pathname.substring(0, scriptUrl.pathname.lastIndexOf('/'));
|
|
27
|
+
|
|
28
|
+
// Load mappings.json from same directory
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(baseDir + "/mappings.json");
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
MAPPINGS = data;
|
|
34
|
+
const nodeCount = Object.keys(data.nodes || {}).length;
|
|
35
|
+
console.log(`[DynamicWidgets] Loaded mappings for ${nodeCount} nodes`);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.warn("[DynamicWidgets] Could not load mappings.json:", e);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load mappings on startup
|
|
45
|
+
loadMappings();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the node configuration from mappings
|
|
49
|
+
*/
|
|
50
|
+
function getNodeConfig(nodeClass) {
|
|
51
|
+
if (!MAPPINGS || !MAPPINGS.nodes) return null;
|
|
52
|
+
return MAPPINGS.nodes[nodeClass] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update widget visibility based on selector value
|
|
57
|
+
*/
|
|
58
|
+
function updateWidgetVisibility(node, selectorName, selectedValue) {
|
|
59
|
+
const nodeConfig = getNodeConfig(node.comfyClass);
|
|
60
|
+
if (!nodeConfig || !nodeConfig.selectors) return;
|
|
61
|
+
|
|
62
|
+
const selectorConfig = nodeConfig.selectors[selectorName];
|
|
63
|
+
if (!selectorConfig) return;
|
|
64
|
+
|
|
65
|
+
// Get widgets that should be visible for this selector value
|
|
66
|
+
const visibleWidgets = selectorConfig[selectedValue] || [];
|
|
67
|
+
|
|
68
|
+
// Get all widgets controlled by this selector
|
|
69
|
+
const allControlledWidgets = new Set();
|
|
70
|
+
for (const widgets of Object.values(selectorConfig)) {
|
|
71
|
+
for (const w of widgets) {
|
|
72
|
+
allControlledWidgets.add(w);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`[DynamicWidgets] ${node.comfyClass}: ${selectorName}="${selectedValue}", visible=[${visibleWidgets.join(', ')}]`);
|
|
77
|
+
|
|
78
|
+
let visibilityChanged = false;
|
|
79
|
+
|
|
80
|
+
for (const widget of node.widgets || []) {
|
|
81
|
+
// Only control widgets that are in our mapping
|
|
82
|
+
if (!allControlledWidgets.has(widget.name)) continue;
|
|
83
|
+
|
|
84
|
+
const shouldShow = visibleWidgets.includes(widget.name);
|
|
85
|
+
const wasHidden = widget.hidden;
|
|
86
|
+
widget.hidden = !shouldShow;
|
|
87
|
+
|
|
88
|
+
if (wasHidden !== widget.hidden) {
|
|
89
|
+
visibilityChanged = true;
|
|
90
|
+
console.log(`[DynamicWidgets] Widget "${widget.name}": hidden=${widget.hidden}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (visibilityChanged) {
|
|
95
|
+
node.setSize(node.computeSize());
|
|
96
|
+
app.graph.setDirtyCanvas(true, true);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set up visibility control for a selector widget
|
|
102
|
+
*/
|
|
103
|
+
function setupSelectorWidget(node, selectorName) {
|
|
104
|
+
const selectorWidget = node.widgets?.find(w => w.name === selectorName);
|
|
105
|
+
if (!selectorWidget) {
|
|
106
|
+
console.log(`[DynamicWidgets] Selector widget "${selectorName}" not found on ${node.comfyClass}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Store original callback
|
|
111
|
+
const originalCallback = selectorWidget.callback;
|
|
112
|
+
|
|
113
|
+
// Override callback to update visibility on change
|
|
114
|
+
selectorWidget.callback = function(value) {
|
|
115
|
+
if (originalCallback) originalCallback.call(this, value);
|
|
116
|
+
updateWidgetVisibility(node, selectorName, value);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Initial visibility update (with delay to ensure widgets are created)
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
updateWidgetVisibility(node, selectorName, selectorWidget.value);
|
|
122
|
+
}, 50);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
app.registerExtension({
|
|
126
|
+
name: "Comfy.DynamicWidgets",
|
|
127
|
+
|
|
128
|
+
nodeCreated(node) {
|
|
129
|
+
const nodeConfig = getNodeConfig(node.comfyClass);
|
|
130
|
+
if (!nodeConfig || !nodeConfig.selectors) return;
|
|
131
|
+
|
|
132
|
+
console.log(`[DynamicWidgets] Setting up node: ${node.comfyClass}`);
|
|
133
|
+
|
|
134
|
+
// Set up each selector
|
|
135
|
+
for (const selectorName of Object.keys(nodeConfig.selectors)) {
|
|
136
|
+
setupSelectorWidget(node, selectorName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log("[DynamicWidgets] Extension registered");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "comfy-dynamic-widgets"
|
|
3
|
+
version = "0.0.6"
|
|
4
|
+
description = "Dynamic widget visibility for ComfyUI nodes via simple metadata"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Andrea Pozzetti"}
|
|
10
|
+
]
|
|
11
|
+
keywords = ["comfyui", "widgets", "dynamic", "visibility", "conditional"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest", "ruff"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/PozzettiAndrea/comfy-dynamic-widgets"
|
|
28
|
+
Repository = "https://github.com/PozzettiAndrea/comfy-dynamic-widgets"
|
|
29
|
+
Issues = "https://github.com/PozzettiAndrea/comfy-dynamic-widgets/issues"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["comfy_dynamic_widgets"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 100
|
|
40
|
+
target-version = "py310"
|