diff --git a/.gitattributes b/.gitattributes
index 6495c20cac745fc9d834627050dbf60f4c27a857..d574385711064b11e45895bd1bf068ce9598e044 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -250,3 +250,8 @@ v3-nodes/rgthree-comfy/docs/rgthree_advanced.png filter=lfs diff=lfs merge=lfs -
v3-nodes/rgthree-comfy/docs/rgthree_advanced_metadata.png filter=lfs diff=lfs merge=lfs -text
v3-nodes/rgthree-comfy/docs/rgthree_context.png filter=lfs diff=lfs merge=lfs -text
v3-nodes/rgthree-comfy/docs/rgthree_context_metadata.png filter=lfs diff=lfs merge=lfs -text
+v3-nodes/cg-use-everywhere/docs/simple-example-image.png filter=lfs diff=lfs merge=lfs -text
+v3-nodes/cg-use-everywhere/docs/simple-example.png filter=lfs diff=lfs merge=lfs -text
+v3-nodes/cg-use-everywhere/tests/compare.png filter=lfs diff=lfs merge=lfs -text
+v3-nodes/cg-use-everywhere/tests/test.png filter=lfs diff=lfs merge=lfs -text
+v3-nodes/cg-use-everywhere/tests/test2.png filter=lfs diff=lfs merge=lfs -text
diff --git a/v3-nodes/cg-use-everywhere/.gitattributes b/v3-nodes/cg-use-everywhere/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..dfe0770424b2a19faf507a501ebfc23be8f54e7b
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/bug_report.yaml b/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c54f47a6ca0f4a6a5de1b08c7aed686c7b553a7c
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,68 @@
+name: Bug report
+description: Report a bug in the UE nodes
+title: "[Bug]: "
+labels: 'Incoming'
+assignees: 'chrisgoringe'
+body:
+- type: markdown
+ attributes:
+ value: I haven't made any of the fields below mandatory, but the more infomation you give me, the better the chance I will be able to help! |See [this discussion](https://github.com/chrisgoringe/cg-use-everywhere/discussions/294) for more details.
+
+- type: textarea
+ id: description
+ attributes:
+ label: Describe the bug
+
+- type: markdown
+ attributes:
+ value: "# Versions"
+
+- type: markdown
+ attributes:
+ value: You can find the ComfyUI and ComfyUI_frontend numbers in Settings -> About, and the Use Everywhere version in Settings -> Use Everywhere.
+
+- type: input
+ id: version
+ attributes:
+ label: ComfyUI version
+
+- type: input
+ id: uiversion
+ attributes:
+ label: ComfyUI_frontend version
+
+- type: input
+ id: UEversion
+ attributes:
+ label: Use Everywhere Version
+
+- type: input
+ id: OS
+ attributes:
+ label: Operating System and Browser
+
+- type: markdown
+ attributes:
+ value: "# Javascript Console"
+
+- type: markdown
+ attributes:
+ value: Press f12 on your browser when the bug occurs and see if there is anthing in the console that looks like it might be relevant - copy and paste it here if so.
+
+- type: textarea
+ id: console
+ attributes:
+ label: Javascript Console Log
+
+- type: markdown
+ attributes:
+ value: "# Anything Else?"
+
+- type: markdown
+ attributes:
+ value: Anything else that might help - screenshots, an example workflow (as simple as possible to illustrate the problem), anything you think might help.
+
+- type: textarea
+ id: anythingelse
+ attributes:
+ label: Anything Else
diff --git a/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/config.yml b/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0086358db1eb971c0cfa8739c27518bbc18a5ff4
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/feature_request.yaml b/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..adc4ed14b6d7a75cc1c6227e0bccf6b87b6da705
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,20 @@
+name: Feature Request
+description: Suggest a new feature for the UE nodes
+title: "[Feature]: "
+labels: 'Incoming'
+assignees: 'chrisgoringe'
+body:
+- type: textarea
+ id: description
+ attributes:
+ label: What do you want to see added to the UE nodes?
+
+- type: textarea
+ id: reason
+ attributes:
+ label: How will this help you, or other users?
+
+- type: textarea
+ id: anythingelse
+ attributes:
+ label: Anything else that might help - screenshots, workflow etc.
diff --git a/v3-nodes/cg-use-everywhere/.github/workflows/publish_action.yml b/v3-nodes/cg-use-everywhere/.github/workflows/publish_action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15df04da140fd45d319136115afb931a8b05c9e8
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.github/workflows/publish_action.yml
@@ -0,0 +1,24 @@
+name: Publish to Comfy registry
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ paths:
+ - "pyproject.toml"
+
+permissions:
+ issues: write
+
+jobs:
+ publish-node:
+ name: Publish Custom Node to registry
+ runs-on: ubuntu-latest
+ if: ${{ github.repository_owner == 'chrisgoringe' }}
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+ - name: Publish Custom Node
+ uses: Comfy-Org/publish-node-action@v1
+ with:
+ personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
diff --git a/v3-nodes/cg-use-everywhere/.gitignore b/v3-nodes/cg-use-everywhere/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ce24d0951c678d97b66241693c0a87bcf91ea01a
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.gitignore
@@ -0,0 +1,155 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+.DS_Store
+workflow.pastel.json
+workflow.pfixed.json
diff --git a/v3-nodes/cg-use-everywhere/.vscode/settings.json b/v3-nodes/cg-use-everywhere/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..a8c200329368ddfdbe180fdbc2deda24ed7a9ce4
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "python-envs.defaultEnvManager": "ms-python.python:conda",
+ "python-envs.defaultPackageManager": "ms-python.python:conda",
+ "python-envs.pythonProjects": []
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/LICENSE b/v3-nodes/cg-use-everywhere/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..b09cd7856d58590578ee1a4f3ad45d1310a97f87
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/LICENSE
@@ -0,0 +1,201 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/v3-nodes/cg-use-everywhere/README-old.md b/v3-nodes/cg-use-everywhere/README-old.md
new file mode 100644
index 0000000000000000000000000000000000000000..6e5dedcd778b58490630ad67a70ef3631d8b70d5
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/README-old.md
@@ -0,0 +1,335 @@
+# UE Nodes
+
+Love this node? [Buy me a coffee!](https://www.buymeacoffee.com/chrisgoringe)
+
+Getting started? Download the test workflow below and see how it works.
+
+Problems? Jump down to [logging and debugging](https://github.com/chrisgoringe/cg-use-everywhere/blob/main/README.md#loggingdebugging)
+
+Ideas for how to improve the nodes (or bug reports) - [raise an issue](https://github.com/chrisgoringe/cg-use-everywhere/issues)
+
+Shameless plug for my other nodes -> Check out [Image Picker](https://github.com/chrisgoringe/cg-image-filter) for another way to make some workflows smoother. And leave a star if you like something!
+
+---
+
+# Important!
+
+I've merged v7 of UE to coincide with the update to ComfyUI, but the instructions below have not been updated.
+
+Please see [this discussion](https://github.com/chrisgoringe/cg-use-everywhere/discussions/334) (but ignore the bits about branches).
+
+---
+
+Over the next few releases I am aiming to reduce the UE zoo down to a single node that can do everything, but is as easy (or easier) to use than the current nodes.
+
+The first step in this process is merging the `AnythingEverywhere` and `AnythingEverywhere?` nodes by moving the regex restrictions into a separate dialog that can be used by either. In future iterations the group and color restrictions will also move to this dialog (so people find them!)
+
+---
+
+# 6.3 Update for ComfyUI front end (version TBC) - subgraphs
+
+With the addition of subgraphs, group nodes are no longer supported in UE from 6.3 onwards.
+
+Instead, UE 6.3 has the following support for subgraphs:
+
+- UE nodes can be used in the main graph and in subgraphs.
+- UE nodes only broadcast within the graph or subgraph they are in.
+- Subgraph nodes are just nodes. You can connect UE nodes to their outputs, and UE links will connect to their inputs.
+- Within a subgraph you can connect from the input panel to a UE node.
+- UE nodes within the subgraph will *not* broadcast to the output panel.
+- When you convert a set of nodes to a subgraph, UE links will work as long as:
+ - the UE node (Control) and the node feeding it (Source) are both in the subgraph, or
+ - neither the Source nor Control are in the subgraph, or
+ - the Source is in the subgraph, and the Control is not, and no Target nodes are in the subgraph
+
+
+## Subgraph creation
+
+There are three nodes involved in every UE link:
+- Source (the link sending the data),
+- Control (the UE node connected to the source),
+- Target (the node that is receiving the data as an input)
+
+This is how those cases are treated:
+
+|Support|Source|Control|Target||
+|-|-|-|-|-|
+|Yes|Graph|Graph|Graph|Nothing changes|
+|Yes|Graph|Graph|Subgraph|The subgraph will have inputs for the data; in the subgraph the input panel is connected to the Target with a real link|
+|No|Graph|Subgraph|Graph|Not supported|
+|No*|Graph|Subgraph|Subgraph|Not supported|
+|Yes|Subgraph|Graph|Graph|The subgraph will be connected to the Control|
+|No|Subgraph|Graph|Subgraph|Not supported|
+|Yes|Subgraph|Subgraph|Graph|The Source will be connected to the Control *and* the output panel in the subgraph, the output will be connected to the Target with a real link|
+|Yes|Subgraph|Subgraph|Subgraph|All nodes will be connected in the subgraph as they were in the graph|
+
+No* indicates a case that does not work, but might get implemented.
+
+No indicates a case I'm unlikely ever to support
+
+---
+
+## Test workflow
+
+|This workflow uses all five nodes, and can be used to test (and understand!) the nodes. You wouldn't build it like this, it's just an example...|Here's an image with the workflow in|
+|-|-|
+|||
+
+Or [the workflow as json](docs/test-workflow.json)
+
+## Current known limitations
+
+There are some situations that UE nodes can't cope with at present. Here are some I know about, and possible workarounds.
+
+### Pythonssss Preset Text
+
+[pythonsssss](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) custom nodes are great, but there are some limitations in using them with UE nodes. In particular, you can't feed the output of a Preset Text node directly into a UE node (see https://github.com/chrisgoringe/cg-use-everywhere/issues/154).
+
+### Group nodes
+
+UE nodes mostly work with group nodes. But there are a couple of important things to note:
+
+- when you create a group node the input names and node names can change. This might break UE? regex connections.
+
+## Latest updates
+
+6.3 June/July 2025
+- support for ComfyUI subgraphs
+
+6.1 (May 2025)
+- Mostly some bug-fixes (#216, #217, #300)
+- New option to show UE links differently without animation for better performance
+
+6.0 (April 2025)
+- Very major rebuild to deal with latest version of Comfy
+
+## Installing
+
+Use Comfy Manager. If you really want to do it manually, just clone this repository in your custom_nodes directory.
+
+## Anything Everywhere (start here!)
+
+The `Anything Everywhere` node has a single input, initially labelled 'anything'. Connect anything to it (directly - not via a reroute), and the input name changes to match the input type. Disconnect and it goes back to 'anything'.
+
+When you run the prompt, any unconnected input, anywhere in the workflow, which matches that type, will act as if it were connected to the same input.
+
+To visualise what it's being connected to, right-click on the background canvas and select `Toggle UE Link Visibility`.
+
+## Anything Everywhere? - control matching with regex rules
+
+This node adds two widgets - title_regex and input_regex. It will only send to inputs which match. So in the example, title_regex is 'Preview' so the image is sent to the Preview Image node but not the Save Image node. Note that you can rename node and input titles, which can help!
+
+(From 4.6 you can also specify a group regex to only match inputs on nodes which are in groups that match the regex.)
+
+
+
+*The matches are regular expressions, not string matches.* Most simple strings will work (matching any part of the title or input name), but some characters have special meanings (including various sorts of brackets, ^, $, /, and . in particular) so just avoid them if you aren't regex-inclined.
+
+Using regex means you can use `^prompt` to match `prompt` at the beginning of the title only, to avoid matching `negative_prompt`.
+
+Regex 101 - `^` means 'the start', `$` means 'the end', `.` matches any single character, `.*` matches anything of any length (including zero). For more than that, visit [regex101](https://regex101.com/) (the flavour you want is ECMAScript, though that probably won't matter).
+
+### Can I make the regex an input instead of a widget?
+
+Sort of.
+
+Because the regex needs to be known before the workflow is submitted (in order to calculate the links), you can't pass a string into the `Anything Everywhere?` node and expect it to work. The *only* thing that is supported is if the input comes *directly* from a node which sets it with a string widget. The `Simple String` node that is included in this pack will work.
+
+|This works|This doesn't. And never will.|
+|-|-|
+|||
+
+
+## Seed Everywhere
+
+Seed Everywhere connects to any unconnected INT input with `seed` in the input name (seed, noise_seed, etc), and it has the control_after_generate feature. So if you convert the seed widgets to inputs you can use the same seed everywhere.
+
+## Anything Everywhere3 - One node, three inputs.
+
+Really just three `Anything Everywhere` nodes packaged together. Designed for the outputs of Checkpoint Loader.
+
+
+
+## Prompts Everywhere - two strings or conditionings
+
+Prompt Everywhere has two inputs. They will be sent with regex matching rules of `(^prompt|^positive)` and `neg` respectively. These should match the various versions of names that get used for prompts and negative prompts or conditionings.
+
+|strings|conditionings|
+|-|-|
+||
+
+# Primitives and COMBOs and the like
+
+UE nodes don't work with primitives and COMBOs (the data type used for dropdown lists, which are also a type of primitive within Comfy). It's unlikely they ever will.
+
+If you want to use UE to control sampler or sigma, you can do this with the built in `SamplerCustom` nodes:
+
+
+
+For more on this, see [this discussion](https://github.com/chrisgoringe/cg-use-everywhere/issues/69)
+
+# Other features
+
+## Third Party Integration
+
+At the suggestion of [@fighting-tx](https://github.com/fighting-tx),
+I've added a method that third party nodes can use if they want to see the prompt as generated by UE.
+It's attached to the `app` object, so you can check if it is present and use it something like this:
+
+```js
+var prompt
+if (app.ue_modified_prompt) {
+ prompt = await app.ue_modified_prompt()
+} else {
+ prompt = await original_graphToPrompt.apply(app)
+}
+```
+
+Other methods could be exposed if there is interest - raise an issue if you'd like to see something.
+
+## Reject links
+
+Right click on a node and you can set it to reject UE links
+
+## Show links - visualisation and animation.
+
+If you want to see the UE links, you can turn them on and off by right-clicking on the canvas. For finer control, the main settings menu has options to show links when the mouse moves over the node at either end, or when one of those nodes is selected.
+
+The links can be animated to distinguish them from normal links - this animation can take the form of moving dots, a pulsing glow, or both. This may impact performance in some cases - note that the pulse animation requires less processing than the moving dots. Control this in the main settings menu.
+
+By default the animations turn off when the workflow is running to minimise impact on CPU/GPU - you can change this in the settings too.
+
+## Convert to real links
+
+If you want to share a workflow without UE nodes being required, or to save an API version of a workflow, you can replace the virtual links created by UE nodes with real links (and remove the UE nodes).
+
+This can be done for a single node by right-clicking on it and selecting `Convert to real links`, or for all UE nodes in a workflow by right-clicking the background and selecting `Convert all UEs to real links`.
+
+## Shift drag
+
+Shift click on an output node and drag then release to get an autocreate menu. This replaces the default behaviour (which gives you a search box), so you can disable it with the `Anything Everywhere replace search` setting.
+
+
+
+## Group and color restriction
+
+UE nodes can be restricted to send only to nodes of the same color, or only to nodes that *aren't* the same color.
+
+They can also be restricted to send only to nodes in the same group (any group in common), or only to nodes that aren't in the same group.
+
+Right-click on the node and select `Group restrictions` or `Color restrictions`. UE nodes which are restricted (in either or both ways) have a green circle in the top-left corner.
+
+## Highway nodes
+
+Trung 0246's [Highway nodes](https://github.com/Trung0246/ComfyUI-0246) are a pretty cool way of piping data around. You can target them with an `Anything Everywhere?` node by using an `input_regex` which matches the unconnected input name with the '+', like this:
+
+
+This is new, so please report any issues!
+
+## Loop checking
+
+By default workflows are checked for loops before they are submitted (because UE can introduce them, and a loop results in a bad python outcome). If a loop is detected you'll get a JavaScript warning showing you the node ids involved. However, especially if there are other custom nodes involved, it's possible that the check will miss a loop, or flag one that isn't real.
+
+If you get a warning and don't believe there is a loop (having checked the node ids listed!) you can turn loop checking off in the main settings menu. If something flagged as a loop runs fine, please [raise an issue](https://github.com/chrisgoringe/cg-use-everywhere/issues) and include the workflow in the report (save the json and zip it, because GitHub doesn't accept .json files). Likewise if a loop doesn't get caught.
+
+I've written code for the core Comfy backend to catch loops, maybe it'll be included - [PR for ComfyUI](https://github.com/comfyanonymous/ComfyUI/pull/1652) - or maybe they have another plan.
+
+## Priorities
+
+If there is more than one sending node that matches an input, the basic rules is that the more specific node wins. The order of priorities is:
+
+- `Anything Everywhere?`
+- `Seed Everywhere` and `Prompts Everywhere`
+- `Anything Everywhere`
+- `Anything Everywhere3`
+
+For nodes of the same time, those with colour restrictions and group restriction are prioritised (colour+group > colour > group > none).
+
+If two nodes with the same priority both match *neither will connect* - better to fail fast than have an ambiguous outcome. If there are ambiguous matches you can display them using `Show UE broadcast clashes` (right-click on background - the option only appears if there are clashes).
+
+## See what is sent
+
+The nodes which only have one output can also gain a text box showing exactly what passed through the node. You need to turn this on if you want it - it's in the main settings, 'Anything Everywhere node details'.
+
+## Logging/Debugging
+
+The JavaScript console (press f12 in some browsers) has logging information about what is being connected. You can change the level of detail by finding the file `[comfy_install]/custom_nodes/cg-use-everywhere/js/use_everywhre_utilities.js` and near the top finding this bit:
+```javascript
+ static ERROR = 0; // actual errors
+ static PROBLEM = 1; // things that stop the workflow working
+ static INFORMATION = 2; // record of good things
+ static DETAIL = 3; // details
+
+ static LEVEL = Logger.PROBLEM;
+ static TRACE = false; // most of the method calls
+```
+Change the `LEVEL` to `Logger.INFORMATION` for more, or `Logger.DETAIL` for even more; set `TRACE` to `true` for some other debugging information.
+
+If you have a problem, pressing f12 to see the JavaScript console can often help. The following steps are really helpful in making a good bug report:
+
+- update to the latest version
+- restart ComfyUI
+- clear the canvas
+- close the browser
+- open a new Comfy window (with no workflow), look in console (f12) to see if there were any errors as ComfyUI started up
+- load your workflow, and look again
+- run, and look again
+
+The other thing worth trying is clearing out all the custom node javascript from where it gets copied when ComfyUI starts:
+
+- stop Comfy
+- go to [comfy root]/web/extensions (*not* under custom_nodes)
+- remove everything there EXCEPT for `core`. Leave `core` (it's ComfyUI stuff)
+- restart Comfy (all custom nodes will reinstall their javascript at startup)
+
+If you find a bug, please [raise an issue](https://github.com/chrisgoringe/cg-use-everywhere/issues) - if you can include the workflow, that's a huge help (you'll need to save it as .txt, or zip the .json file, because GitHub doesn't accept .json).
+
+## Cautions
+
+Bypassing and disabling nodes works, but with one catch. If you have a UE nodes that does matching (`Anything Everywhere?` and `Prompt Everywhere`) and you bypass the node it matches to, the link won't be made. So
+
+|If you use a ? node to send to a node...|...and bypass the recipient, it doesn't get connected |
+|-|-|
+|||
+
+This is unlikely to be fixed, but should be fairly easy to avoid!
+
+
+---
+# Some older information
+
+# Update for ComfyUI front end 1.16 and above
+
+ComfyUI front end 1.16 made a major change to the way that inputs and widgets work, which had a significant impact on the UE nodes.
+
+A widget is no longer converted to an input in order to connect to it; instead, you just connect an output to it, and it converts.
+
+This is quite cool - except that it meant that there was no way for UE to tell if a widget was supposed to be an empty input for UE to feed.
+
+So there is a new mechanism for this. Here's a picture of a node that I have right-clicked:
+
+
+
+Things to notice:
+
+- There is a new menu `UE Connectable Widgets` which lists all the widgets. Those marked as connectable have a green bar to the left of the name.
+ - Select a widget to toggle its value
+- Next to the widget, where an input would be, the connectable widgets have a graphical indication of their state.
+ - The smaller dark circle (next to steps) indicate a connectable node which is not connected (widget value will be used)
+ - The light circle (next to cfg) and greying out the widget indicates that a UE node will provide the value
+ - No icon (all the other widgets) indicates that they are not UE connectable
+- Other inputs still have the indications of normal connection or UE connection (here model is a UE connection)
+
+You'll probably find that turning the `showlinks` option on helps, and the `highlight` option is required for those widget indicators which help a lot.
+
+
+
+Hopefully old workflows will be automatically converted when you load them. Hopefully.
+
+## Naming widgets
+
+The `Prompts Everywhere`, `Seed Everywhere`, and `Anything Everywhere?` all make use of the name of an input. On some occasions this might mean you want to rename an input.
+
+For an input that can't be a widget, that's trivial - right click on the input dot and select `Rename Slot`. The name will be updated, and the new name displayed next to the dot.
+
+If an input *can* be a widget, you can rename it in the same way, but the new name *does not get displayed*. The name has changed, and the UE nodes will use the new name, but the old name is still shown. I consider this to be a bug in the Comfy front-end, and have raised an issue [here](https://github.com/Comfy-Org/ComfyUI_frontend/issues/3654). Unfortunately it appears that Comfy UI is going to remove the ability to rename a widget entirely, so best to avoid using widget input names if at all possible.
diff --git a/v3-nodes/cg-use-everywhere/README.md b/v3-nodes/cg-use-everywhere/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6fb5d2789b95698c03f0dbe0d88e52b3a31dd2a6
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/README.md
@@ -0,0 +1,491 @@
+# UE Nodes
+
+## Love this node?
+
+[Buy me a coffee!](https://www.buymeacoffee.com/chrisgoringe)
+
+## Shameless plug
+
+Check out [Image Picker](https://github.com/chrisgoringe/cg-image-filter) for another way to make some workflows smoother.
+
+# Common Questions
+
+## How do I get positive and negative conditionings to work?
+
+Connect both to a single UE node, and then rename the inputs. See [here](https://github.com/chrisgoringe/cg-use-everywhere/issues/317#issuecomment-3663225670).
+
+## Why doesn't it work with reroutes / primitives?
+
+Nodes which dynamically work out their data type are a real problem that isn't worth the complexity to fix. See [here](https://github.com/chrisgoringe/cg-use-everywhere/tree/main#primitives-and-reroutes) for more info.
+
+## Can I broadcast a COMBO, like my choice of sampler?
+
+Yes. See Combo Clone node [here](https://github.com/chrisgoringe/cg-use-everywhere/tree/main#special-case-nodes).
+
+# Recent Changes
+
+## 7.7
+
+- Added "Apply to unrepeated output" options
+- Cleaned up restrictions dialog
+- Internal refactoring to conform to ComfyUI changes
+
+## 7.6
+
+- Added visual indicators to subgraph nodes
+- Added hover-over conflicted node to show conflict
+- Added "Send to Any" option in restrictions
+
+
+Older changes since 7.0
+
+## 7.5
+
+- Added per-output granularity to any-node broadcasting
+- Prompt Everywhere node deprecated and auto-replaced
+- Convert to real links now works for any-node broadcasting
+- Anything Everywhere nodes in a subgraph can broadcast to its output
+- Inputs which have a clash of two more possible UE sources are marked with a red cross
+- Couple of bug fixes, including one significant performance issue
+
+## 7.4
+
+- Added broadcasting from [any node](#any-node-broadcasting)
+- Added negative regex option
+- Seed Everywhere node deprecated and auto-replaced
+
+## 7.3
+
+Bugfixes:
+[361](https://github.com/chrisgoringe/cg-use-everywhere/issues/361),
+[379](https://github.com/chrisgoringe/cg-use-everywhere/issues/379),
+[381](https://github.com/chrisgoringe/cg-use-everywhere/issues/381)
+
+## 7.2.1 and 7.2.2
+
+Bugfixes:
+[383](https://github.com/chrisgoringe/cg-use-everywhere/issues/383),
+[384](https://github.com/chrisgoringe/cg-use-everywhere/issues/384),
+[388](https://github.com/chrisgoringe/cg-use-everywhere/issues/388)
+
+## 7.2
+
+- Added support for [Combo inputs](#combo-clone) via the `Combo Clone` helper node
+- Fixed a number of issues related to subgraphs
+- Improved handling of disconnect
+- Added new options for handling multiple inputs of same type
+- Added internationalisation options
+
+## 7.1
+
+- Allowed individual inputs to block UE connections
+- Added support for multiple inputs of the same type
+- Fixed a number of minor subgraph issues
+- Fixed a serious bug with UE on Safari [details](https://github.com/chrisgoringe/cg-use-everywhere/issues/359)
+
+
+
+# What is Anything Everywhere?
+
+The `Anything Everywhere` node takes one or more inputs and sends the data to other nodes that need it.
+You can connect as many inputs to an `Anything Everywhere` node as you like, and there are a range of ways
+to control where the data will be sent.
+
+Here's the standard ComfyUI template modified to use `AnythingEverywhere`
+
+|workflow|output image you can drop into Comfy|
+|-|-|
+|||
+
+The `MODEL`, `CLIP`, and `VAE` are automatically broadcast to all the places they are needed.
+
+Doesn't make much difference in this simple case, but with complex workflows it really does.
+This is what the default wan 2.2 s2v video workflow looks like:
+
+|before|after|
+|-|-|
+|||
+
+# Any node broadcasting
+
+
+New in version 7.4 - any node can broadcast it's outputs
+
+As of version `7.4`, any node can be set to broadcast with `Add UE broadcasting` in the right-click menu. I find this really helpful for subgraphs especially.
+
+When broadcasting, the node acts like all its outputs were connected to a single UE node, so
+
+|This...|...does the same as this|
+|-|-|
+|||
+
+From `7.5`, the outputs that will broadcast can also be set (like inputs):
+
+
+
+
+
+# Where will the data be sent?
+
+The key to using Anything Everywhere nodes is understanding where the data will be sent.
+
+By default the data will be sent to any input of the same data type which does not have a connection,
+and does not have a widget providing the value.
+
+
+
+At the sending end, you can constrain where the data gets send through restrictions applied to the `Anything Everywhere` node.
+
+
+Restrictions can be accessed by double-clicking the body of the node, or through the right-click menu.
+
+
+
+The first three entries are [regex](https://regex101.com/) patterns.
+The node will only send data to another node if the regex matches the receiving node title, the name of the input, or the name of a group the receiving node is in, respectively. Check the 'invert' checkbox to invert the regex (send only to things which _don't_ match).
+
+The Group and Colour restrictions will constrain the node to only send to nodes in (or not in) the same group, and of the same (or different) colour.
+
+`Repeated Types` determines behaviour when more than one input of the same type is connected to an `Anything Everywhere` node.
+In this case an additional constraint is used to disambiguate which inputs match
+by comparing the name of the input slot on the `Anything Everywhere` node with
+either the name of the input slot on the target node, or the name of the target node.
+The `match start` and `match end` options require that the input names match from the start (or end) for the full length of the shorter:
+so you can match `seed` to `seed` or `noise_seed` by naming the `Anything Everywhere` input `seed` and selecting `Match end of input names`.
+
+If `Apply to Unrepeated` is set to `Yes`, the `Repeated Types` rule will be applied to all outputs, whether or not they are repeated.
+
+`Send To Any`(default 'no') can be used to allow this node to send data to inputs with the `Any` type.
+
+`Priority` is used to resolve conflicts between multiple sending nodes. It has a default value (which generally follows the rule that the more restrictive the rules, the higher the priority), but you can override that value.
+
+If you select multiple restrictions, all must be satisfied for the node to send.
+
+If any restrictions are applied, the `Anything Everywhere` node gets a green circle in the top left hand corner, and a tooltip if you hover the mouse over it (as long as Show Links is not set to None).
+
+You can rename input slots by right-clicking on the input dot - but you can't rename widget inputs - this is a limitation imposed by ComfyUI ([discussion](https://github.com/Comfy-Org/ComfyUI_frontend/issues/3654)). The work-around is to rename the target node (or use multiple `Anything Everywhere` nodes with other constraints, especially color matching).
+
+
+
+
+
+At the receiving end: you can specify that an input should not accept data, or that one with a widget should.
+
+
+In the `UE Connectable Inputs` menu (right-click on a node):
+the green bar indicates an input is connectable. The `Reject UE links` option can be used to make this node completely reject UE links, regardless of other settings.
+
+
+
+
+
+
+What if two or more `Anything Everywhere` nodes can send to the same input? How are conflicts resolved?
+
+
+Each node has an automatically calculated priority - in general the more restrictive the node, the higher the priority.
+You can see this prority in the restrictions dialog, and you can choose to replace the automatically calculated value if you wish.
+
+If two more more `Anything Everywhere` nodes match the same input, the higher priority node is used. If there is a tie, _no connection is made_.
+The input is marked with a red cross to indicate that there is a conflict. If you hover over the node, the two (or more) conflicting UE inputs
+are shown as red lines.
+
+Alternatively, if you right-click on the canvas you will find an option to show which nodes are the problem.
+
+
+
+# Visual Clues
+
+Anything Everywhere has a number of ways to help you visualise what it is doing.
+
+
+Show Anything Everywhere links
+
+If you want to see the UE links, you can turn them on and off by right-clicking on the canvas. For finer control, the main settings menu has options to show links when the mouse moves over the node at either end, or when one of those nodes is selected.
+
+The links can be animated to distinguish them from normal links - this animation can take the form of moving dots, a pulsing glow, or both. This may impact performance in some cases - note that the pulse animation requires less processing than the moving dots. Control this in the main settings menu.
+
+Animation takes quite a lot of processing, so don't use it unless you really need to. By default the animations turn off when the workflow is running to minimise impact on CPU/GPU - you can change this in the settings too.
+
+
+
+Nodes that broadcast have a badge
+
+Any node that is capable of broadcasting data (a UE node, or another node to which broadcasting has been added) is marked with a circle in the top left hand corner.
+
+If the circle is green, the node has no additional restrictions on where data will be sent;
+if it is red, it has one or more restrictions (which you can see by hovering your mouse over it, or by editing restrictions
+with the option on the right click menu, or by double clicking the node).
+
+If the node is actually sending data, the circle (red or green) is bold; if the node is capable of sending but is not actually making
+and connections, it is muted.
+
+
+
+
+
+Inputs and outputs have a subtle visual clue
+
+The state of inputs is also represented visually: a black ring and a glow on the input dot indicates it is connectable.
+In the image below, `positive` has been set to not accept UE inputs, `steps` has been set to accept them, and `model` has a UE connection.
+
+
+
+If a widget is getting data from a UE connection, it is grayed out, like `steps` below:
+
+
+
+Similarly, if a node has had broadcasting added to it, the outputs that can broadcast are shown:
+here `merged` can broadcast, but isn't, `x` can broadcast and is, and `y` is set to not broadcast.
+
+
+
+
+
+# Special Case Nodes
+
+
+To broadcast to COMBO (dropdown menu) widgets, you can either use String to Combos (above) or a Combo Clone
+
+For each Combo type you want to use, add a Combo Clone node. Here we want to be able to broadcast `sampler_name` and `scheduler`.
+
+
+
+Connect the `Combo Clone` output to a widget you want it to replicate. The `Combo Clone` copies the options from that widget, and the widget name.
+
+
+
+Disconnect the `Combo Clone` and connect it to an `Anything Everywhere` node. The name copying means you can connect multiple Combos to a single `Anything Everywhere` node.
+
+
+
+It can now broadcast to any node with the same input type (but remember you will have to mark the Combo widget as UE Connectable, since widgets are not connectable by default).
+
+
+
+
+Prompts Everywhere handles a positive and a negative prompt
+
+
+Prompt Everywhere has two inputs. They will be sent with regex matching rules designed to match `prompt` or `positive`, and `neg`, respectively.
+
+The actual regexes used are `(_|\\b)pos(itive|_|\\b)|^prompt|正面` and `(_|\\b)neg(ative|_|\\b)|负面`
+
+|strings|conditionings|
+|-|-|
+||
+
+
+
+
+
+Seed Everywhere, Anything Everywhere? and Anything Everywhere3 are deprecated and will be automatically replaced
+
+The `Seed Everywhere` node will be replaced with a primitive int, set to broadcast, with an input regex restriction that matches the localised name of the `seed` input on the base `KSampler` node - `seed` or `随机种` being the most common.
+
+The `Anything Everywhere?` node will be replaced with an `Anything Everywhere` node with restrictions.
+
+The `Anything Everywhere3` node will be replaced with an `Anything Everywhere` node with multiple inputs and any appropriate restrictions.
+
+These replacements should not break any workflows. If they do... sorry.
+
+
+
+
+# Options
+
+
+
+In the main settings menu, you will find the Use Everywhere options:
+
+
+
+
+The top set, `Graphics`, modify the visual appearance only.
+
+The bottom set, `Options`, modify behaviour:
+
+- When connecting, use the output slot's name as the input name. When a new connection is made to a UE node, the default is to name the input with the type. Select this option to use the output name of the node the link is from.
+- Block workflow validation. This prevents other nodes from complaining about the lack of connections, or creating them. If you turn this off, there may be unexpected consequences.
+- Logging. Increase the logging level if you are asked to help debug.
+- Connect to bypassed nodes. When off, Use Everywhere will not connect to a bypassed node, and will attempt to work out whether an input is connected when upstream nodes are bypassed. I recommend turning this on.
+
+
+
+# Primitives and Reroutes
+
+
+
+UE nodes work with the new primitives added in more recent versions of Comfy (in the `primitive` submenu)
+
+
+
+
+UE nodes do not work with the old-style `Primitive` nodes (which automatically determined what data type they needed to be),
+nor do they work with reroute nodes.
+
+In both cases that is dues to some issues that are deep within ComfyUI, related to the way that these nodes work out
+the data type they represent, which makes it next to impossible for UE to correctly intereact with them.
+
+
+
+# Other features
+
+
+
+Third Party Integration - the UE API
+
+
+At the suggestion of [@fighting-tx](https://github.com/fighting-tx),
+I've added a method that third party nodes can use if they want to see the prompt as generated by UE.
+It's attached to the `app` object, so you can check if it is present and use it something like this:
+
+```js
+var prompt
+if (app.ue_modified_prompt) {
+ prompt = await app.ue_modified_prompt()
+} else {
+ prompt = await original_graphToPrompt.apply(app)
+}
+```
+
+Other methods could be exposed if there is interest - raise an issue if you'd like to see something.
+
+
+
+
+Convert to real links
+
+
+If you want to share a workflow without Anything Everywhere nodes being required, or to save an API version of a workflow, you can replace the virtual links created by Anything Everywhere nodes with real links (and remove the UE nodes).
+
+This can be done for a single node by right-clicking on it and selecting `Convert to real links`, or for all UE nodes in a graph or subgraph by right-clicking the background and selecting `Convert all UEs to real links`.
+
+
+
+# Subgraphs
+
+Anything Everywhere works fairly well with the new subgraphs (and setting a subgraph to broadcast its outputs is very powerful!).
+A couple of caveats:
+
+
+Data is only broadcast within a graph
+
+Anything Everywhere will not make links from nodes in one graph into a different graph. So you cannot broadcast data into a subgraph, nor can you broadcast it out.
+
+This is a deliberate design decision, reflecting the fundamental principle of subgraphs - that they are self contained.
+Data can only go in or out of a subgraph through its input and output panels.
+This sort of data isolation is a very good thing in terms of maintaining a workflow, and by working consistently with the expectated
+behaviour of subgraphs it is far less like that future changes in the front end will break Anything Everywhere.
+
+This is not open for discussion. There are plenty of better ways to achieve the same end.
+
+An Anything Everywhere node in your main graph can send to the input of a subgraph node;
+you can also connect as Anything Everywhere node to one of the inputs inside a subgraph.
+
+You can connect an output of the subgraph node to an Anything Everywhere node (or set the subgraph node to broadcast),
+and you can broadcast from within the subgraph to the output panel of the subgraph.
+
+
+
+
+
+When you create a subgraph, Anything Everywhere nodes do their best...
+
+There are three nodes involved in every UE link:
+- Source (the link sending the data),
+- Control (the UE node connected to the source),
+- Target (the node that is receiving the data as an input)
+
+This is how those cases are treated:
+
+|Support|Source|Control|Target||
+|-|-|-|-|-|
+|Yes|Graph|Graph|Graph|Nothing changes|
+|Yes|Graph|Graph|Subgraph|The subgraph will have inputs for the data; in the subgraph the input panel is connected to the Target with a real link|
+|No|Graph|Subgraph|Graph|Not supported|
+|No*|Graph|Subgraph|Subgraph|Not supported|
+|Yes|Subgraph|Graph|Graph|The subgraph will be connected to the Control|
+|No|Subgraph|Graph|Subgraph|Not supported|
+|Yes|Subgraph|Subgraph|Graph|The Source will be connected to the Control *and* the output panel in the subgraph, the output will be connected to the Target with a real link|
+|Yes|Subgraph|Subgraph|Subgraph|All nodes will be connected in the subgraph as they were in the graph|
+
+No* indicates a case that does not work, but might get implemented.
+
+No indicates a case I'm unlikely ever to support
+
+
+
+# Reporting a bug well
+
+If there is a bug in Anything Everywhere, I want to fix it.
+But my ability to do so depends on the quality of the information I have.
+
+Seriously, people submit bug reports which just say "It isn't working since I updated".
+Well, it's working for me, so I don't have any idea what your problem is.
+
+Give me good information, and if I can reproduce the problem, there's a good chance I'll fix it (or suggest a work-around).
+
+So if you have a problem, go to [the issues page](https://github.com/chrisgoringe/cg-use-everywhere/issues),
+click on `New Issue`, select, `Bug Report`, and fill in as much of the information as you can.
+
+# Suggesting new features
+
+Go to [the issues page](https://github.com/chrisgoringe/cg-use-everywhere/issues),
+click on `New Issue`, select, `Feature Request`, and tell me your idea.
+If you can explain why the feature would be helpful to you, and ideally to others, I'm
+much more likely to think about whether it's possible.
+
+# Thanks to
+
+The following people have contributed code or helpful discussions, without which these nodes would be less good!
+
+- [DrJKL](https://github.com/DrJKL)
+- [fighting-tx](https://github.com/fighting-tx)
+- [huchenlei](https://github.com/huchenlei)
+- [fichas](https://github.com/fichas)
+- [LukeG89](https://github.com/LukeG89)
+- [set-soft](https://github.com/set-soft)
+- [TinyTerra](https://github.com/TinyTerra)
+- [bananasss00](https://github.com/bananasss00)
+- [JorgeR81](https://github.com/JorgeR81)
+
+Feel free to [make suggestions, or implement features](https://github.com/chrisgoringe/cg-use-everywhere/issues) to get your name added here!
+
+---
+
+# Anything Everywhere v7
+
+Version 7 is a major update to the Anything Everywhere nodes, so the documentation below is all new. If you are looking for the old docs, you can find them [here](https://github.com/chrisgoringe/cg-use-everywhere/README-old).
+
+
+Major changes in v7
+
+If you used Anything Everywhere prior to v7, the major improvements are:
+
+- The `Anything Everywhere3` and `Anything Everywhere?` nodes are deprecated, as their features are now part of the standard `Anything Everywhere` node.
+- `Anything Everywhere` nodes now have dynamic inputs, so you can plug as many different things into them as you like.
+- All the restrictions on what nodes data will be sent to are now in a restrictions editor, that can be accessed through the right click menu of the node, or by double-clicking the body of the node.
+ - In the restrictions editor you can set title, input, and group regexes, color restrictions, group restrictions, and priority (for when two nodes both match)
+ - The green circle is used to indicate that _any_ restrictions are in place; if you hover over a node with restrictions they will appear in a tooltip
+- Subgraphs are supported (in the majority of cases). Yay subgraphs! Seriously, they are _so_ much better than group nodes.
+ - There are lots of odd cases with subgraphs, so if you find a case not covered properly, please [raise an issue](https://github.com/chrisgoringe/cg-use-everywhere/issues)
+
+There are a couple of features that have been removed:
+
+- Group nodes are no longer supported, as they are deprecated in ComfyUI in favour of the new subgraphs, which are supported (in most configurations)
+- The `Simple String` mechanism to provide an input to the regex of an `Anything Everywhere?` node is no longer supported
+ - Other UI mechanisms to address this need are under consideration
+
+## Upgrade considerations
+
+Other than the limitations noted, old workflows _should_ load and work out of the box,
+with `Anything Everywhere3` and `Anything Everywhere?` nodes automatically converted to `Anything Everywhere` nodes with the appropriate restrictions applied.
+
+However, there may be edge cases that don't work; if you have any problems, please [raise an issue](https://github.com/chrisgoringe/cg-use-everywhere/issues).
+
+You will _not_ be able to use workflows saved using v7 with older versions of ComfyUI or older versions of UE.
+
+**Group Nodes are no longer supported**
+
+
diff --git a/v3-nodes/cg-use-everywhere/__init__.py b/v3-nodes/cg-use-everywhere/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..00d3dcd5af95d07d37fb7c0624c718aef0f42e5e
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/__init__.py
@@ -0,0 +1,15 @@
+from comfy_api.latest import ComfyExtension, io
+from .use_everywhere import SeedEverywhere, AnythingEverywhere, AnythingSomewhere, AnythingEverywhereTriplet, SimpleString, ComboClone, AnythingEverywherePrompts
+
+UE_VERSION = "7.7"
+WEB_DIRECTORY = "./js"
+__all__ = [ "WEB_DIRECTORY"]
+
+async def comfy_entrypoint() -> ComfyExtension:
+ class UseEverywhereExtension(ComfyExtension):
+ async def get_node_list(self) -> list[type[io.ComfyNode]]:
+ return [
+ AnythingEverywhere, AnythingSomewhere, AnythingEverywhereTriplet, SimpleString, ComboClone, SeedEverywhere, AnythingEverywherePrompts
+ ]
+
+ return UseEverywhereExtension()
diff --git a/v3-nodes/cg-use-everywhere/docs/ComboClone1.png b/v3-nodes/cg-use-everywhere/docs/ComboClone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..937eb2825cd0bdc34ffbf7baeb5deb60ccf90a09
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/ComboClone1.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/ComboClone2.png b/v3-nodes/cg-use-everywhere/docs/ComboClone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..b04370882c68859b77df0aea3d4ec18a97b8ad44
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/ComboClone2.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/ComboClone3.png b/v3-nodes/cg-use-everywhere/docs/ComboClone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f490de664da230948c7c34c965f3784ebb91be2
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/ComboClone3.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/PE.png b/v3-nodes/cg-use-everywhere/docs/PE.png
new file mode 100644
index 0000000000000000000000000000000000000000..73b200e98ebb44a815e6a9df1a793065831639fe
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/PE.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/after.png b/v3-nodes/cg-use-everywhere/docs/after.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fd62e304a3ac16f7a4c826aaec7b4c12ae94497
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/after.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/before.png b/v3-nodes/cg-use-everywhere/docs/before.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ed7281857b89a48272b96452fb1247a3a2f4a0f
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/before.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/broadcastoff.png b/v3-nodes/cg-use-everywhere/docs/broadcastoff.png
new file mode 100644
index 0000000000000000000000000000000000000000..34af3b897784059440eb06d0a7f4085616681961
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/broadcastoff.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/broadcaston.png b/v3-nodes/cg-use-everywhere/docs/broadcaston.png
new file mode 100644
index 0000000000000000000000000000000000000000..df7cbf8f3bc2deed62db01db521f2f6ee0c625ef
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/broadcaston.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/conditioning.png b/v3-nodes/cg-use-everywhere/docs/conditioning.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fe8e19f094fdf7d5f2527b045f5a2c9cdb63498
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/conditioning.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/connectable.png b/v3-nodes/cg-use-everywhere/docs/connectable.png
new file mode 100644
index 0000000000000000000000000000000000000000..19cf11fc5e48f71c855ad1b04a3de4b7c81d5fcc
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/connectable.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/connectable2.png b/v3-nodes/cg-use-everywhere/docs/connectable2.png
new file mode 100644
index 0000000000000000000000000000000000000000..791a5ca08309efa589f340a765363d5e03775b7c
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/connectable2.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/connectable3.png b/v3-nodes/cg-use-everywhere/docs/connectable3.png
new file mode 100644
index 0000000000000000000000000000000000000000..721bd3ecd58d4b0c0199029ed1747e8c6b2d7cc4
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/connectable3.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/icon.png b/v3-nodes/cg-use-everywhere/docs/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..89995a772571290e560c0d3844ee32cfd7d73efa
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/icon.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/options.png b/v3-nodes/cg-use-everywhere/docs/options.png
new file mode 100644
index 0000000000000000000000000000000000000000..7220072b8524a0246a4d7cdc5761aa528b30205e
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/options.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/output_set.png b/v3-nodes/cg-use-everywhere/docs/output_set.png
new file mode 100644
index 0000000000000000000000000000000000000000..b368fca71c8485bb5fd514073edc5203378a25fb
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/output_set.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/outputs.png b/v3-nodes/cg-use-everywhere/docs/outputs.png
new file mode 100644
index 0000000000000000000000000000000000000000..63f57cba9f267b00f7f730edf341614543ced1a6
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/outputs.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/primitives.png b/v3-nodes/cg-use-everywhere/docs/primitives.png
new file mode 100644
index 0000000000000000000000000000000000000000..0dae0129672914be0b0ecb5994cfef613909ef02
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/primitives.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/redgreen.png b/v3-nodes/cg-use-everywhere/docs/redgreen.png
new file mode 100644
index 0000000000000000000000000000000000000000..95bd8cf5c3a9d43a56301c4405458436ae668b75
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/redgreen.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/restrictions.png b/v3-nodes/cg-use-everywhere/docs/restrictions.png
new file mode 100644
index 0000000000000000000000000000000000000000..29c50da056e950b86f81deabc50005b436b2e30d
Binary files /dev/null and b/v3-nodes/cg-use-everywhere/docs/restrictions.png differ
diff --git a/v3-nodes/cg-use-everywhere/docs/simple-example-image.png b/v3-nodes/cg-use-everywhere/docs/simple-example-image.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e60ba476ad5a6e546e98fdd0e53e76cca365870
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/docs/simple-example-image.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bcce28436629546d1d57c1e1bc531e1a5f8c80cf096604b9219f0de6a3645f35
+size 391062
diff --git a/v3-nodes/cg-use-everywhere/docs/simple-example.png b/v3-nodes/cg-use-everywhere/docs/simple-example.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff5c46d419938d8a0623c9246d11e674e7395656
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/docs/simple-example.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0b8c0248da5946f9c17b3dee67745dd11a2e6c1f1e983578d6df489bb3a5bb8
+size 101324
diff --git a/v3-nodes/cg-use-everywhere/js/combo_clone.js b/v3-nodes/cg-use-everywhere/js/combo_clone.js
new file mode 100644
index 0000000000000000000000000000000000000000..0edaea787e7f506ab65b33d2c6a9f576bac34bed
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/combo_clone.js
@@ -0,0 +1,42 @@
+import { shared } from "./shared.js"
+import { Logger } from "./use_everywhere_utilities.js"
+
+function update_me(node) {
+ if (node.properties.comboclone) {
+ Logger.log_problem(`Reseting combo clone node ${node.id}`)
+ node.widgets[0].options.values = [...node.properties.comboclone.options]
+ node.outputs[0].type = "COMBO"
+ node.outputs[0].label = node.properties.comboclone.name
+ }
+}
+
+export function is_combo_clone(node) {
+ return (node.type == "Combo Clone")
+}
+
+export function reset_comboclone_on_load(node) {
+ if (is_combo_clone(node)) update_me(node)
+}
+
+export function comboclone_on_connection(node, link_info, connect) {
+ if (!is_combo_clone(node)) return Logger.log_problem(`comboclone_on_connection called for node ${node.id} of type ${node.type}`)
+ if (shared.graph_being_configured) return
+ if (connect) {
+ if (!link_info) return
+ const target_node = node.graph.getNodeById(link_info.target_id)
+ const input_name = target_node?.inputs[link_info.target_slot].name
+ const widget = target_node?.widgets?.find((w)=>(w.name==input_name))
+
+ if (widget?.type=="combo" && widget?.options?.values) {
+ node.properties.comboclone = {
+ options : [...widget.options.values],
+ name : input_name
+ }
+ update_me(node)
+ node.widgets[0].value = widget.value
+ link_info.type = "COMBO"
+ }
+ } else {
+ // no action when we disconnect
+ }
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/connections.js b/v3-nodes/cg-use-everywhere/js/connections.js
new file mode 100644
index 0000000000000000000000000000000000000000..bbc6259f34b9be5db7b708f8c14213d9ece6948b
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/connections.js
@@ -0,0 +1,146 @@
+import { link_is_from_subgraph_input, get_subgraph_input_type } from "./use_everywhere_subgraph_utils.js"
+import { get_real_node, is_UEnode, Logger } from "./use_everywhere_utilities.js";
+import { app } from "../../scripts/app.js";
+import { i18n } from "./i18n.js";
+import { shared } from "./shared.js";
+import { reset_comboclone_on_load, is_combo_clone } from "./combo_clone.js";
+import { ue_callbacks } from "./recursive_callbacks.js";
+
+function get_type(graph, link_info) {
+ var type = null
+ if (link_info) {
+ if (link_is_from_subgraph_input(link_info)) { // input slot of subgraph
+ type = get_subgraph_input_type(graph, link_info.origin_slot)
+ } else {
+ type = get_real_node(link_info.origin_id, graph)?.outputs[link_info.origin_slot]?.type
+ }
+ if (type != link_info.type && link_info.type != '*') {
+ Logger.log_problem(`Type detected from upstream, ${type} != type on link_info ${link_info.type}`)
+ }
+ } else {
+ Logger.log_problem(`input_changed with connect true, graph_being_configured false, and no link_info`)
+ }
+ type = type || '*'
+ if (type!='*' && link_info) link_info.type = type
+ if (type=='*' && link_info) type = link_info.type
+ return type
+}
+
+
+/*
+Called by onConnectionsChange for a UE_Node when side == 1 (input).
+*/
+
+export function input_changed(node, slot, connect, link_info) {
+ if (shared.graph_being_configured) return
+ if (node.id==-1) return // this node isn't in the graph yet
+ if (!node?.inputs) return Logger.log_problem(`input_changed called but node.inputs not retrievable`)
+ const in_slot = node.inputs[slot]
+ if (!in_slot) return Logger.log_problem(`input_changed called for node #${node.id} slot ${slot} but that wasn't found`)
+ const graph = node.graph
+
+ if (connect) {
+ const type = get_type(graph, link_info)
+ if (type=='*') {
+ // no real idea what to do
+ } else {
+ if (in_slot.transient_label) {
+ in_slot.label = in_slot.transient_label
+ Logger.log("Restoring transient label")
+ } else if (in_slot.label && in_slot.label != i18n('anything')) {
+ // leave custom label alone
+ Logger.log("Leaving custom label")
+ } else if (app.ui.settings.getSettingValue("Use Everywhere.Options.use_output_name") && link_info) {
+ const out_slot = (link_info.origin_id==-10) ?
+ graph.inputNode?.allSlots[link_info.origin_slot] :
+ graph.getNodeById(link_info.origin_id)?.outputs[link_info.origin_slot]
+ in_slot.label = out_slot?.label || out_slot?.localized_name || out_slot?.name || i18n(type);
+ } else {
+ in_slot.label = i18n(type);
+ }
+ in_slot.color_on = app.canvas.default_connection_color_byType[type];
+ in_slot.type = type
+ }
+ } else {
+ in_slot.transient_label = in_slot.label
+ if (is_removable(node, in_slot, slot)) {
+ in_slot.label = i18n('anything');
+ in_slot.color_on = undefined;
+ in_slot.type = '*'
+ }
+ setTimeout(()=>{in_slot.transient_label=null}, 100)
+ }
+
+}
+
+function post_configure_fixes(node) {
+ if (is_combo_clone(node)) reset_comboclone_on_load(node)
+ if (is_UEnode(node)) fix_inputs(node, "post_configure_fixes")
+}
+ue_callbacks.register_allnode_callback('afterConfigureGraph', post_configure_fixes)
+
+function add_new_input(node) {
+ Logger.log_info(`Adding new anything input to node ${node.id} (${fix_call_message})`)
+ try {
+ node.properties.ue_properties.next_input_index = (node.properties.ue_properties.next_input_index || 10) + 1
+ node.addInput(`anything${node.properties.ue_properties.next_input_index}`, "*", {label:i18n('anything')})
+ return true
+ } catch (e) { Logger.log_error(e) }
+ return false
+}
+
+function remove_excess_input(node) {
+ const idx = node.inputs.findIndex((inputslot, i)=>(is_removable(node,inputslot,i)))
+ if (idx>=0) {
+ try {
+ Logger.log_info(`Removing excess anything input from node ${node.id} (${fix_call_message})`)
+ node.removeInput(idx)
+ return true
+ } catch (e) { Logger.log_error(e) }
+ } else { Logger.log_problem(`Something very odd happened in fix_inputs for ${node.id}`) }
+ return false
+}
+
+var fix_call_message;
+
+function to_keep(node, i) {
+ return (node.properties.ue_properties?.keep_inputs && node.properties.ue_properties?.keep_inputs.includes(i))
+}
+
+function is_removable(node, inputslot, i) {
+ return (inputslot.type=='*' && !to_keep(node,i))
+}
+
+function fix_unconnected_inputs(node) {
+ node.inputs.filter((input, i)=>(!input.link && !to_keep(node,i))).forEach((input)=>{
+ input.type = '*'
+ input.label = i18n('anything')
+ })
+}
+
+function fix_star_inputs(node) {
+ node.inputs.filter((input)=>(input.type=='*' && input.link)).forEach((input)=>{
+ const llink = node.graph.links[input.link]
+ if (llink.type) input.type = llink.type
+ })
+}
+
+export function fix_inputs(node, message) {
+ fix_call_message = message
+ if (!node.graph) return // node has been deleted prior to the fix
+ if (shared.graph_being_configured) return
+ if (node.properties.ue_properties.fixed_inputs) return
+
+ fix_unconnected_inputs(node)
+ fix_star_inputs(node)
+
+ const empty_removable_inputs = node.inputs.filter((inputslot, i)=>(is_removable(node, inputslot, i)))
+ var excess_inputs = empty_removable_inputs.length - 1
+
+ if (excess_inputs<0) {
+ if (add_new_input(node)) fix_inputs(node, message)
+ } else if (excess_inputs>0) {
+ if (remove_excess_input(node)) fix_inputs(node, message)
+ }
+
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/floating_window.js b/v3-nodes/cg-use-everywhere/js/floating_window.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e8918eaa75a3d47f0490b9a59e5ec5a580e4397
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/floating_window.js
@@ -0,0 +1,123 @@
+import { create } from "./use_everywhere_utilities.js"
+import { settingsCache } from "./use_everywhere_cache.js"
+
+
+export class FloatingWindow extends HTMLElement {
+ constructor() {
+ super()
+ this.classList.add('ue_editor')
+ this.header = create('div', 'ue_editor_header', this)
+ this.body = create('div', 'ue_editor_body', this)
+ this.footer = create('button', 'ue_editor_footer', this, { innerText: "Close" })
+
+ /** @type {HTMLElement | null} */
+ this.firstFocusable = null
+ /** @type {HTMLElement | null} */
+ this.lastFocusable = null
+
+ this.header.addEventListener('mousedown',this.start_dragging.bind(this))
+ this.addEventListener('mousemove', this.mousemovedover.bind(this))
+ document.addEventListener('mousemove', this.mousemoved.bind(this))
+ document.addEventListener('mouseup',this.stop_dragging.bind(this))
+ document.addEventListener('mouseleave',this.stop_dragging.bind(this))
+ this.footer.addEventListener('click', this.hide.bind(this))
+ this.addEventListener("keydown", this.handle_keydown.bind(this));
+
+ this.dragging = false
+ this.hide()
+ document.body.append(this)
+ }
+
+ show() {
+ this.style.display = 'flex';
+ const tt = document.getElementById('ue_tooltip')
+ if (tt) tt.style.display = 'none'
+ this.showing = true
+ }
+ hide() {
+ this.style.display = 'none'
+ this.showing = false
+ }
+ set_title(title) {
+ this.header.innerText = title
+ }
+ set_body(element) {
+ this.body.innerHTML = ""
+ this.body.appendChild(element)
+ this.setup_focus_trap()
+ }
+
+ maybe_move_to(x,y) {
+ if (this.position && settingsCache.getSettingValue("Use Everywhere.Graphics.preserve edit window position")) return
+ this.move_to(x,y)
+ }
+
+ move_to(x,y) {
+ this.position = {x:x,y:y}
+ this.style.left = `${this.position.x}px`
+ this.style.top = `${this.position.y}px`
+ }
+
+ swallow(e) {
+ e.stopPropagation()
+ e.preventDefault()
+ }
+
+ start_dragging(e) {
+ this.dragging = true
+ this.swallow(e)
+ }
+
+ stop_dragging(e) {
+ this.dragging = false
+ }
+
+ mousemovedover(e) {
+ this.mousemoved(e)
+ this.swallow(e)
+ }
+
+ mousemoved(e) {
+ if (this.dragging) this.move_to( this.position.x + e.movementX , this.position.y + e.movementY )
+ }
+
+ /** @param {KeyboardEvent} e */
+ handle_keydown(e) {
+ if (e.key === "Escape" || e.key === "Enter") {
+ this.hide()
+ return
+ }
+ if (e.key === "Tab") {
+ if (e.shiftKey && document.activeElement === this.firstFocusable) {
+ this.lastFocusable.focus()
+ e.preventDefault()
+ return
+ }
+ if (!e.shiftKey && document.activeElement === this.lastFocusable) {
+ this.firstFocusable.focus()
+ e.preventDefault()
+ }
+ }
+ }
+
+ setup_focus_trap() {
+ this.firstFocusable = null
+ this.lastFocusable = null
+
+ const focusableEls = this.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])')
+ if (focusableEls?.length == 0) return
+
+ this.firstFocusable = focusableEls[0] ?? null
+ this.lastFocusable = focusableEls[focusableEls.length - 1] ?? null
+
+ if (this.firstFocusable) {
+ setTimeout(() => {
+ this.firstFocusable.focus()
+ }, 0);
+ }
+ }
+}
+
+
+customElements.define('ue-floating', FloatingWindow)
+export const edit_window = new FloatingWindow()
diff --git a/v3-nodes/cg-use-everywhere/js/i18n.js b/v3-nodes/cg-use-everywhere/js/i18n.js
new file mode 100644
index 0000000000000000000000000000000000000000..1821a42216ea97ce57d4e09e76e008fd99d72de8
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/i18n.js
@@ -0,0 +1,106 @@
+import { app } from "../../scripts/app.js";
+
+export var REPEATED_TYPE_OPTIONS;
+export var GROUP_RESTRICTION_OPTIONS;
+export var COLOR_RESTRICTION_OPTIONS;
+
+
+var _FUNCTIONAL = null
+var _FUNCTIONAL_REGEX = null
+
+const _DISPLAY = {
+ 'en' : {
+
+ },
+ 'zn' : {
+
+ }
+}
+
+function DISPLAY(v, lang) { return _DISPLAY[lang || current_language()]?.[v] || _DISPLAY['en'][v] || v }
+
+function current_language() {
+ return app.ui.settings.getSettingValue('Comfy.Locale')
+}
+
+export function language_changed(is_now, was_before) {
+ _FUNCTIONAL = null
+ _FUNCTIONAL_REGEX = null
+
+ REPEATED_TYPE_OPTIONS = [
+ i18n("Exact match of input names"),
+ i18n("Match start of input names"),
+ i18n("Match end of input names"),
+ i18n("Inputs matches target node name"),
+ ]
+
+ GROUP_RESTRICTION_OPTIONS = [
+ i18n("No restrictions"),
+ i18n("Send only within group"),
+ i18n("Send only not within group")
+ ]
+
+ COLOR_RESTRICTION_OPTIONS = [
+ i18n("No restrictions"),
+ i18n("Send only to same color"),
+ i18n("Send only to different color")
+ ]
+}
+
+function get_functional() {
+ const nd = LiteGraph.createNode('KSampler')
+ if (nd) {
+ _FUNCTIONAL = {
+ seed_input_regex : `seed|${nd.inputs.find((i)=>(i.name=='seed')).localized_name}`,
+ prompt_regex : `(_|\\b)pos(itive|_|\\b)|${nd.inputs.find((i)=>(i.name=='positive')).localized_name}`,
+ negative_regex : `(_|\\b)neg(ative|_|\\b)|${nd.inputs.find((i)=>(i.name=='negative')).localized_name}`,
+ seed : nd.inputs.find((i)=>(i.name=='seed')).localized_name,
+ positive : nd.inputs.find((i)=>(i.name=='positive')).localized_name,
+ negative : nd.inputs.find((i)=>(i.name=='negative')).localized_name,
+
+ }
+ }
+}
+
+function get_functional_regex() {
+ if (!_FUNCTIONAL) get_functional()
+ _FUNCTIONAL_REGEX = {
+ prompt_regex : new RegExp(_FUNCTIONAL.prompt_regex),
+ negative_regex : new RegExp(_FUNCTIONAL.negative_regex),
+ }
+}
+
+export function i18n_functional(v) {
+ if (!_FUNCTIONAL) get_functional()
+ return _FUNCTIONAL?.[v] || v
+}
+
+export function i18n_functional_regex(v) {
+ if (!_FUNCTIONAL_REGEX) get_functional_regex()
+ return _FUNCTIONAL_REGEX[v]
+}
+
+const all_requested_stings = new Set()
+export function i18n(v, extras) {
+ all_requested_stings.add(v)
+ var r = DISPLAY(v, extras?.language) || LANGUAGES[v] || v
+ if (extras?.titlecase) r = toTitleCase(r)
+ return r
+}
+
+export function i18ify_settings(settings) {
+ settings.forEach((s)=>{
+ if (s.name) s.name = i18n(s.name)
+ if (s.tooltip) s.tooltip = i18n(s.tooltip)
+ if (s.options) s.options.forEach((o) => {o.text = i18n(o.text)})
+ })
+ return settings
+}
+
+const toTitleCase = (phrase) => {
+ return phrase
+ .toLowerCase()
+ .split(' ')
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+};
diff --git a/v3-nodes/cg-use-everywhere/js/recursive_callbacks.js b/v3-nodes/cg-use-everywhere/js/recursive_callbacks.js
new file mode 100644
index 0000000000000000000000000000000000000000..56ad15fd9297304379d3e8fe9ac93d3d44ccde35
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/recursive_callbacks.js
@@ -0,0 +1,64 @@
+import { app } from "../../scripts/app.js";
+
+class RecursiveCallbacks {
+ constructor() {
+ this.event_to_node_callbacklist = {}
+ this.event_to_graph_callbacklist = {}
+ }
+
+ /*
+ Register a callback for a named event.
+ */
+ register_allnode_callback(event, call, first) {
+ if (!this.event_to_node_callbacklist[event]) this.event_to_node_callbacklist[event] = []
+ if (first) this.event_to_node_callbacklist[event].unshift(call)
+ else this.event_to_node_callbacklist[event].push(call)
+ }
+ register_allgraph_callback(event, call, first) {
+ if (!this.event_to_graph_callbacklist[event]) this.event_to_graph_callbacklist[event] = []
+ if (first) this.event_to_graph_callbacklist[event].push(call)
+ else this.event_to_graph_callbacklist[event].push(call)
+ }
+
+
+
+ /*
+ For every callback registered to this event,
+ call allnode callback once for each node, with the node as argument
+ call allgraph callback once for each node, with the graph as argument
+ */
+ dispatch(event) {
+ if (this.event_to_node_callbacklist[event]) {
+ this.event_to_node_callbacklist[event].forEach((callback) => {
+ _node_dispatch(callback, app.graph)
+ });
+ }
+ if (this.event_to_graph_callbacklist[event]) {
+ this.event_to_graph_callbacklist[event].forEach((callback) => {
+ _graph_dispatch(callback, app.graph)
+ });
+ }
+ }
+}
+
+function _node_dispatch(callback, graph) {
+ graph.nodes.forEach((node) => {
+ callback(node)
+ if (node.subgraph) { _node_dispatch(callback, node.subgraph) }
+ })
+}
+
+function _graph_dispatch(callback, graph) {
+ callback(graph)
+ graph.nodes.filter((node)=>(node.subgraph)).forEach((node) => { _graph_dispatch(callback, node.subgraph) })
+}
+
+export const ue_callbacks = new RecursiveCallbacks()
+
+export function for_all_graphs(callback) {
+ _graph_dispatch(callback, app.graph)
+}
+
+export function for_all_nodes(callback) {
+ _node_dispatch(callback, app.graph)
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/shared.js b/v3-nodes/cg-use-everywhere/js/shared.js
new file mode 100644
index 0000000000000000000000000000000000000000..bcdccd3b5c3c1a75b2f80a5c79e396aa07b9fc69
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/shared.js
@@ -0,0 +1,36 @@
+export const VERSION = "7.7"
+
+class Shared {
+ constructor() {
+ this.graph_being_configured = 0
+ this.in_queuePrompt = 0
+ this.in_graphToPrompt = 0
+ this.in_midst_of_change = 0
+ this.graph_currently_modified = 0
+ this.graphAnalyser = undefined
+ this.linkRenderController = undefined
+ this.report_keys = [ 'graph_being_configured', 'in_queuePrompt', 'in_graphToPrompt', 'in_midst_of_change', 'graph_currently_modified' ]
+ }
+}
+
+class Deferred {
+ constructor() {
+ this.action_list = []
+ setInterval( this.execute.bind(this), 200 )
+ }
+ push(x) { this.action_list.push(x) } // add action of the form: { fn:function, args:array }
+ execute() {
+ if (shared.in_midst_of_change) return;
+ if (shared.graph_being_configured) return;
+ while (this.action_list.length>0) {
+ const action = this.action_list.pop()
+ try { action?.fn(...action?.args) }
+ catch (e) { Logger.log_error(e) }
+ }
+ }
+}
+
+export const deferred_actions = new Deferred()
+
+export const shared = new Shared()
+
diff --git a/v3-nodes/cg-use-everywhere/js/tooltip_window.js b/v3-nodes/cg-use-everywhere/js/tooltip_window.js
new file mode 100644
index 0000000000000000000000000000000000000000..b44428c3e18ba017e8f2b82c0a6a2b100a4699de
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/tooltip_window.js
@@ -0,0 +1,55 @@
+
+import { any_restrictions, describe_restrictions } from "./ue_properties.js";
+import { app } from "../../scripts/app.js";
+import { create } from "./use_everywhere_utilities.js";
+import { edit_window } from "./floating_window.js";
+import { settingsCache } from "./use_everywhere_cache.js";
+
+const HOVERTIME = 500
+var hover_node_id = null
+var mouse_pos = [0,0]
+
+const ue_tooltip_element = create('span', 'ue_tooltip', document.body, {id:'ue_tooltip'})
+
+function show_tooltip() {
+ ue_tooltip_element.style.display = "block"
+ ue_tooltip_element.style.left = `${mouse_pos[0]+10}px`
+ ue_tooltip_element.style.top = `${mouse_pos[1]+5}px`
+ ue_tooltip_element.innerHTML = ""
+ ue_tooltip_element.appendChild(describe_restrictions(app.canvas.node_over))
+ ue_tooltip_element.showing = true
+}
+
+function show_on_hover() {
+ if (!tooltipable()) return
+
+ if (mouse_pos[0]==app.canvas.mouse[0] && mouse_pos[1]==app.canvas.mouse[1]) {
+ show_tooltip()
+ } else {
+ maybe_show_tooltip()
+ }
+}
+
+function hide_tooltip() {
+ ue_tooltip_element.style.display = "none"
+ ue_tooltip_element.showing = false
+}
+
+function tooltipable() {
+ if (
+ (!app.canvas?.node_over) ||
+ ( edit_window.showing) ||
+ (!settingsCache.getSettingValue('Use Everywhere.Graphics.tooltips')) ||
+ (!any_restrictions(app.canvas.node_over))
+ ) return false
+
+ return true
+}
+
+export function maybe_show_tooltip() {
+ if (!tooltipable()) return hide_tooltip()
+
+ mouse_pos = [...app.canvas.mouse]
+ hover_node_id = app.canvas.node_over.id
+ setTimeout(show_on_hover, HOVERTIME)
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/ue.css b/v3-nodes/cg-use-everywhere/js/ue.css
new file mode 100644
index 0000000000000000000000000000000000000000..574e16d0b95cb108773a8b9330b3da0e8fa337d8
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/ue.css
@@ -0,0 +1,102 @@
+@layer ue {
+ #ue_tooltip {
+ z-index : 9000;
+ border: thin solid gray;
+ color:black;
+ position: absolute;
+ padding: 4px;
+ }
+ #ue_tooltip table {
+ border-collapse: collapse;
+
+ }
+ #ue_tooltip tr:nth-child(odd) {
+ background-color: rgba(234, 255, 234, 0.67);
+ }
+ #ue_tooltip tr:nth-child(even) {
+ background-color: rgba(255, 234, 234, 0.67);
+ }
+ #ue_tooltip th {
+ text-align: right;
+ }
+
+ .ue_editor {
+ z-index : 9000;
+ border: thin solid gray;
+ color:white;
+ position: absolute;
+ padding: 0px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: rgb(45 24 24 / 80%);
+ }
+
+ .ue_properties_table {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .ue_editor_header {
+ width: 100%;
+ text-align: center;
+ background-color: rgb(45 24 24 / 80%);
+ background-size: 100% 14px;
+ background-image: repeating-linear-gradient(0deg, #636363, black 1px, rgb(45 24 24 / 80%) 1px, rgb(45 24 24 / 80%) 2px);
+ padding-top: 4px;
+ border-bottom: thin dashed #ddd;
+ padding-bottom: 2px;
+ margin-bottom: 2px;
+ }
+
+ .ue_properties_row {
+ display: flex;
+ flex-direction: row;
+ }
+
+ .break_below {
+ border-bottom: thin dashed #ddd;
+ padding-bottom: 2px;
+ }
+
+ span.ue_properties_title {
+ text-align: right;
+ flex-grow: 1;
+ padding-right: 4px;
+ }
+
+ .ue_properties_title::after {
+ content: ':';
+ }
+
+ .ue_properties_cell {
+ flex-grow: 0;
+ width: 250px
+ }
+
+
+ .ue_editor_footer {
+ margin: 8px;
+ border: thin solid white;
+ padding: 2px 30px;
+ background-color: rgba(0, 0, 0, 0.75);
+ }
+
+ .regex_input_container {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ input {
+ flex-grow: 1;
+ }
+ span {
+ padding: 4px 8px 0px 0px;
+ font-size: 60%;
+ }
+ .checkbox {
+ flex-grow: 0;
+ margin-left: 0px;
+ }
+ }
+
+}
diff --git a/v3-nodes/cg-use-everywhere/js/ue_properties.js b/v3-nodes/cg-use-everywhere/js/ue_properties.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d00f6ba021fcd807f2a1f9055e15629001a2459
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/ue_properties.js
@@ -0,0 +1,158 @@
+import { version_at_least, create, is_UEnode, find_duplicate_broadcasted_types } from "./use_everywhere_utilities.js"
+import { i18n, i18n_functional, GROUP_RESTRICTION_OPTIONS, COLOR_RESTRICTION_OPTIONS } from "./i18n.js";
+import { shared } from "./shared.js";
+import { fix_inputs } from "./connections.js";
+import { VERSION } from "./shared.js";
+
+const ALL_REGEXES = ['title', 'input', 'prompt', 'negative', 'group']
+
+function any_regex_restrictions(node) {
+ if (!node.properties.ue_properties) return false
+ var restricted = false
+ ALL_REGEXES.forEach((r)=>{
+ const reg = node.properties.ue_properties[`${r}_regex`]
+ restricted = restricted || (reg && reg.length>0)
+ })
+ return restricted
+}
+
+export function any_restrictions(node) {
+ return (
+ node.properties.ue_properties?.group_restricted ||
+ node.properties.ue_properties?.color_restricted ||
+ node.properties.ue_properties?.priority ||
+ any_regex_restrictions(node)
+ )
+}
+
+export function describe_restrictions(node) {
+ const statements = []
+ if (node.properties.ue_properties) {
+ ALL_REGEXES.forEach((r)=>{
+ var reg = node.properties.ue_properties[`${r}_regex`]
+ if (reg && reg.length>0) {
+ const condition = i18n((node.properties.ue_properties[`${r}_regex_invert`]) ? i18n("not match"): i18n("match"))
+ statements.push([`${i18n(r)} regex`, `${condition} ${reg}`])
+ }
+ })
+ if (node.properties.ue_properties.group_restricted) statements.push([i18n('group'),i18n(GROUP_RESTRICTION_OPTIONS[node.properties.ue_properties.group_restricted])])
+ if (node.properties.ue_properties.color_restricted) statements.push([i18n('color'),i18n(COLOR_RESTRICTION_OPTIONS[node.properties.ue_properties.color_restricted])])
+ }
+ if (node.properties.ue_properties.priority !== undefined) statements.push([i18n('priority'), node.properties.ue_properties.priority])
+ const table = create('table')
+ statements.forEach((s)=>{
+ const row = create('tr', null, table)
+ create('th', null, row, {innerText:`${i18n(s[0], {titlecase:true})}:`})
+ create('td', null, row, {innerText:s[1]})
+ })
+ return table
+}
+
+export function default_priority(node) {
+ var p = 10
+ if (node.type === "Seed Everywhere" || node.type === "Prompts Everywhere") p += 10
+ if (any_regex_restrictions(node) || find_duplicate_broadcasted_types(node).size) p += 20
+ if (node.properties.ue_properties.group_restricted > 0) p += 3
+ if (node.properties.ue_properties.color_restricted > 0) p += 6
+ return p
+}
+
+const DEFAULT_PROPERTIES = {
+ version : VERSION,
+ group_restricted : 0,
+ color_restricted : 0,
+ widget_ue_connectable : {},
+ input_ue_unconnectable : {},
+ title_regex : null,
+ input_regex : null,
+ group_regex : null,
+ title_regex_invert : false,
+ input_regex_invert : false,
+ group_regex_invert : false,
+ priority : undefined,
+ repeated_type_rule : 0,
+ apply_to_unrepeated : 0,
+ string_to_combo : 0,
+ send_to_any : 0
+ }
+
+/*
+Called whenever any node is created.
+
+If the graph is still being configured, then that means the node is being created as part of a load.
+In that case the properties need to be set later, in setup_ue_properties_onload
+*/
+export function setup_ue_properties_oncreate(node) {
+ if (shared.graph_being_configured) return
+ if (!node.properties) node.properties = {}
+ node.properties.ue_properties = {...DEFAULT_PROPERTIES}
+ convert_node_types(node)
+}
+
+/*
+Convert node properties when loaded.
+*/
+export function setup_ue_properties_onload(node) {
+ if (!node.properties?.ue_properties) node.properties.ue_properties = {}
+ if ( !version_at_least(node.properties?.ue_properties?.version, "7.0") ) {
+ if (is_UEnode(node)) {
+ // convert a pre 7.0 UE node
+ node.properties.ue_properties = {
+ version : VERSION,
+ group_restricted : node.properties.group_restricted,
+ color_restricted : node.properties.color_restricted,
+ widget_ue_connectable : node.properties.widget_ue_connectable,
+ input_ue_unconnectable: {},
+ title_regex : node.widgets_values?.[0],
+ input_regex : node.widgets_values?.[1],
+ group_regex : node.widgets_values?.[2],
+ priority : undefined,
+ }
+ delete node.properties.group_restricted
+ delete node.properties.color_restricted
+ delete node.properties.widget_ue_connectable
+ } else {
+ node.properties.ue_properties.version = VERSION
+ node.properties.ue_properties.widget_ue_connectable = node.properties.ue_properties.widget_ue_connectable ||
+ node.properties.widget_ue_connectable ||
+ {}
+ node.properties.ue_properties.input_ue_unconnectable = {}
+ if (node.properties.widget_ue_connectable) delete node.properties.widget_ue_connectable
+ }
+ }
+ convert_node_types(node)
+
+}
+
+function convert_node_types(node) {
+ if (!is_UEnode(node)) return
+
+ if (node.type=="Anything Everywhere?") {
+ node.widgets.forEach((w)=>{w.hidden=true})
+ if (node.title=="Anything Everywhere?") node.title = "Anything Everywhere"
+ node.type = "Anything Everywhere"
+ } else if (node.type=="Anything Everywhere3") {
+ if (node.title=="Anything Everywhere3") node.title = "Anything Everywhere"
+ node.type = "Anything Everywhere"
+ } else if (node.type=="Seed Everywhere") {
+ if (node.title=="Anything Everywhere3") node.title = "PrimitiveInt"
+ node.type = "PrimitiveInt"
+ node.properties.ue_convert = true
+ node.properties.ue_properties.fixed_inputs = true
+ node.properties.ue_properties.seed_inputs = true
+ node.properties.ue_properties.input_regex = node.properties.ue_properties.input_regex || i18n_functional('seed_input_regex')
+ } else if (node.type=="Prompts Everywhere") {
+ if (node.title=="Prompts Everywhere") node.title = "Anything Everywhere"
+ node.properties.ue_properties.fixed_inputs = false
+ node.inputs[0].label = i18n_functional('positive')
+ node.inputs[1].label = i18n_functional('negative')
+ node.properties.ue_properties['keep_inputs'] = [0,1]
+ }
+
+ ALL_REGEXES.forEach((r)=>{
+ const rname = `${r}_regex`
+ if (node.properties.ue_properties[rname]==".*") node.properties.ue_properties[rname] = undefined
+ })
+
+ fix_inputs(node, "convert_node_types")
+}
diff --git a/v3-nodes/cg-use-everywhere/js/ue_properties_editor.js b/v3-nodes/cg-use-everywhere/js/ue_properties_editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f5b3da2c7abea5dbd7014bdd031dd6ffd9917d8
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/ue_properties_editor.js
@@ -0,0 +1,147 @@
+import { i18n, i18n_functional, GROUP_RESTRICTION_OPTIONS, COLOR_RESTRICTION_OPTIONS, REPEATED_TYPE_OPTIONS } from "./i18n.js";
+import { app } from "../../scripts/app.js";
+import { default_priority } from "./ue_properties.js";
+import { edit_window } from "./floating_window.js";
+import { shared } from "./shared.js";
+import { Logger } from "./use_everywhere_utilities.js";
+
+const REGEXES = ['title', 'input', 'group']
+const P_REGEXES = ['prompt', 'negative']
+
+function create_element(tag, parent, options={}) {
+ const elem = document.createElement(tag)
+ if (parent) parent.appendChild(elem)
+ Object.assign(elem, options)
+ return elem
+}
+
+export function edit_restrictions(a,b,c,d, node) { // a,b,c,d parameters are ignored - called from menu system
+ edit_window.set_body(create_editor_html(node))
+ edit_window.set_title(`Restrictions for node #${node.id}`)
+ edit_window.maybe_move_to(app.canvas.mouse[0]+10, app.canvas.mouse[1]+10)
+ edit_window.show()
+}
+
+function add_row(table, header) {
+ const row = document.createElement('div')
+ row.className = 'ue_properties_row'
+ table.appendChild(row)
+ const header_elem = document.createElement('span')
+ header_elem.className = 'ue_properties_title'
+ header_elem.innerText = header
+ row.appendChild(header_elem)
+ return row
+}
+
+function add_cell(row, cell) {
+ const td = document.createElement('span')
+ td.className = 'ue_properties_cell'
+ row.appendChild(td)
+ td.appendChild(cell)
+}
+
+function changed(node, property, value) {
+ node.properties.ue_properties[property] = value
+
+ if (!node.properties.ue_properties.priority) {
+ document.getElementById('priority_value').value = `${default_priority(node)}`
+ }
+
+ const elem = document.getElementById(`${property}_value`)
+ if (elem) elem.style.opacity = (value) ? "1" : "0.5"
+
+ shared.linkRenderController.mark_link_list_outdated()
+ app.canvas.setDirty(true,true)
+}
+
+function create_editor_html(node) {
+ const table = document.createElement('div')
+ table.className = 'ue_properties_table'
+
+ for (var i=0; i<=2; i++) {
+ const name = REGEXES[i]
+ const row = add_row(table, `${i18n(name)} regex`)
+
+ const contents = create_element('span', null, {'className':'regex_input_container'})
+
+ const checkbox_props = {
+ type:'checkbox',
+ id:`${name}_regex_invert`,
+ checked: (node.properties.ue_properties[`${name}_regex_invert`]) ? true : undefined,
+ className: 'checkbox'
+ }
+ create_element('input', contents, checkbox_props).
+ addEventListener('input', (e)=>{ changed(node, `${name}_regex_invert`, e.target.checked); } )
+
+ create_element('span', contents, {innerText:i18n('Invert'), className:'regex_checkbox_label'})
+
+ create_element('input', contents, {type:'text', value:node.properties.ue_properties[`${name}_regex`] || ''}).
+ addEventListener('input', (e)=>{ changed(node, `${name}_regex`, e.target.value)})
+
+ if (i==2) { row.classList.add('break_below') }
+
+ add_cell(row,contents)
+ }
+
+ const gr_row = add_row(table, i18n("Group"))
+ const gr_select = document.createElement('select')
+ add_cell(gr_row,gr_select)
+ add_select_options(node, gr_select, GROUP_RESTRICTION_OPTIONS, `group_restricted`)
+
+ const col_row = add_row(table, i18n("Color"))
+ const col_select = document.createElement('select')
+ add_cell(col_row,col_select)
+ add_select_options(node, col_select, COLOR_RESTRICTION_OPTIONS, `color_restricted` )
+ col_row.classList.add('break_below')
+
+ const repeated_type_row = add_row(table, i18n("Repeated Types"))
+ const repeated_type_select = document.createElement('select')
+ add_cell(repeated_type_row,repeated_type_select)
+ add_select_options(node, repeated_type_select, REPEATED_TYPE_OPTIONS, `repeated_type_rule`)
+
+ const apply_to_unrepeated_row = add_row(table, i18n("Apply to Unrepeated"))
+ const apply_to_unrepeated_select = document.createElement('select')
+ add_cell(apply_to_unrepeated_row, apply_to_unrepeated_select)
+ add_select_options(node, apply_to_unrepeated_select, ["no", "yes"], `apply_to_unrepeated`)
+ apply_to_unrepeated_row.classList.add('break_below')
+
+ if (node.inputs.find((i)=>(i.type=="STRING"))) {
+ const send_to_combos_row = add_row(table, i18n("String to Combos"))
+ const send_to_combos_select = document.createElement('select')
+ add_cell(send_to_combos_row, send_to_combos_select)
+ add_select_options(node, send_to_combos_select, ["no", "yes"], `string_to_combo`)
+ send_to_combos_row.classList.add('break_below')
+ }
+
+ const send_to_any_row = add_row(table, i18n("Send to Any"))
+ const send_to_any_select = document.createElement('select')
+ add_cell(send_to_any_row, send_to_any_select)
+ add_select_options(node, send_to_any_select, ["no", "yes"], `send_to_any`)
+ send_to_any_row.classList.add('break_below')
+
+ const priority_row = add_row(table, i18n("Priority"))
+ const priority_edit = document.createElement("input")
+ priority_edit.value = `${node.properties.ue_properties.priority || default_priority(node)}`
+ priority_edit.addEventListener('input', ()=>{
+ const p = parseInt(priority_edit.value)
+ if (p) changed(node, `priority`, p)
+ if (priority_edit.value=='') changed(node, `priority`, undefined)
+ })
+ priority_edit.id = 'priority_value'
+ if (!node.properties.ue_properties.priority) priority_edit.style.opacity = 0.5
+ add_cell(priority_row,priority_edit)
+ priority_row.classList.add('break_below')
+
+ return table
+}
+
+function add_select_options(node, select, OPTIONS, property) {
+ OPTIONS.forEach((txt, i)=>{
+ const option = document.createElement('option')
+ option.value = `${i}`
+ option.innerText = txt
+ select.appendChild(option)
+ })
+ select.value = `${node.properties.ue_properties[property] || 0}`
+ select.addEventListener('input', ()=>{ changed(node, property, parseInt(select.value))})
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere.js b/v3-nodes/cg-use-everywhere/js/use_everywhere.js
new file mode 100644
index 0000000000000000000000000000000000000000..172529e3193663b471c3b6d5d035e8f7b778ebcd
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere.js
@@ -0,0 +1,334 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+
+import { shared, deferred_actions } from "./shared.js";
+
+import { is_UEnode, inject, Logger, graphConverter, create, node_can_broadcast } from "./use_everywhere_utilities.js";
+import { title_bar_additions, LinkRenderController } from "./use_everywhere_ui.js";
+import { GraphAnalyser } from "./use_everywhere_graph_analysis.js";
+import { canvas_menu_settings, SETTINGS, add_extra_menu_items } from "./use_everywhere_settings.js";
+import { settingsCache } from "./use_everywhere_cache.js";
+import { convert_to_links } from "./use_everywhere_apply.js";
+import { visible_graph, fix_new_subgraph_node } from "./use_everywhere_subgraph_utils.js";
+import { setup_ue_properties_oncreate, setup_ue_properties_onload } from "./ue_properties.js";
+import { edit_restrictions } from "./ue_properties_editor.js";
+import { language_changed } from "./i18n.js";
+import { input_changed, fix_inputs } from "./connections.js";
+import { comboclone_on_connection, is_combo_clone } from "./combo_clone.js";
+import { ue_callbacks } from "./recursive_callbacks.js";
+
+/*
+All nodes need the onDrawTitleBar method so they can show if they are broadcasting UE data.
+*/
+function add_methods_to_all_nodes(node) {
+ if (node.ue_methods_added) return Logger.log_problem(`Node ${node.id} already has UE methods added`);
+
+ try {
+ add_extra_menu_items(node, inject_outdating_into_object_method) // right click menu additions
+
+ const original_onDrawTitleBar = node.onDrawTitleBar;
+ node.onDrawTitleBar = function(ctx, title_height) {
+ original_onDrawTitleBar?.apply(this, arguments);
+ title_bar_additions(node, ctx, title_height)
+ }
+
+ const original_onMouseEnter = node.onMouseEnter;
+ node.onMouseEnter = function(e) {
+ original_onMouseEnter?.apply(this, arguments)
+ shared.linkRenderController.node_over_changed()
+ }
+
+ const original_onMouseLeave = node.onMouseLeave;
+ node.onMouseLeave = function(e) {
+ original_onMouseLeave?.apply(this, arguments)
+ shared.linkRenderController.node_over_changed()
+ }
+
+ node.ue_methods_added = true;
+ } catch (e) {
+ Logger.log_error(e);
+ }
+
+}
+
+/*
+Inject a call to linkRenderController.mark_list_link_outdated into a method with name methodname on all objects in the array
+If object is undefined, do nothing.
+The injection is added at the end of the existing method (if the method didn't exist, it is created).
+*/
+function inject_outdating_into_objects(array, methodname, tracetext) {
+ if (array) {
+ array.forEach((object) => { inject_outdating_into_object_method(object, methodname, tracetext); })
+ }
+}
+function inject_outdating_into_object_method(object, methodname, tracetext) {
+ if (object) inject(object, methodname, tracetext, shared.linkRenderController.mark_link_list_outdated, shared.linkRenderController);
+}
+
+app.registerExtension({
+ name: "cg.customnodes.use_everywhere",
+ settings: SETTINGS,
+
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ /*
+ When a node is connected or unconnected, the link list is dirty.
+ If it is a UE node, we need to update it as well
+ */
+ const onConnectionsChange = nodeType.prototype.onConnectionsChange;
+ nodeType.prototype.onConnectionsChange = function (side,slot,connect,link_info,output) {
+ if (is_combo_clone(this) && !shared.in_queuePrompt) comboclone_on_connection(this, link_info, connect)
+ if (is_UEnode(this) && side==1) { // side 1 is input
+ input_changed(this, slot, connect, link_info)
+
+ if (!shared.graph_being_configured) {
+ // do the fix at the end of graph change
+ deferred_actions.push( { fn:fix_inputs, args:[this,"deferred onConnectionsChange",]} )
+ }
+ }
+ shared.linkRenderController?.mark_link_list_outdated();
+ onConnectionsChange?.apply(this, arguments);
+ };
+
+ /* Combo Clone can connect to COMBO or to UE nodes */
+ if (nodeData.name=="Combo Clone") {
+ const onConnectOutput = nodeType.prototype.onConnectOutput
+ nodeType.prototype.onConnectOutput = function(outputIndex, type, input, inputNode, inputIndex) {
+ if (!(type=="COMBO" || is_UEnode(inputNode))) return false;
+ return onConnectOutput?.apply(this,arguments)
+ }
+ }
+
+ },
+
+ async nodeCreated(node) { // the node isn't part of the graph yet, so can't do anything involving id or links.
+ add_methods_to_all_nodes(node)
+
+ if (shared.graph_being_configured) return
+
+ if (is_UEnode(node)) {
+ setup_ue_properties_oncreate(node)
+ } else if (!node.properties.ue_properties) {
+ node.properties.ue_properties = {
+ widget_ue_connectable : {},
+ input_ue_unconnectable : {}
+ }
+ }
+ },
+
+ // When a graph node is loaded convert it if needed
+ loadedGraphNode(node) {
+ graphConverter.convert_if_pre_116(node);
+ setup_ue_properties_onload(node)
+ },
+
+ async setup() {
+
+ create('link', null, document.getElementsByTagName('HEAD')[0],
+ {'rel':'stylesheet', 'type':'text/css', 'href': new URL("./ue.css", import.meta.url).href } )
+
+ api.addEventListener("status", ({detail}) => {
+ if (shared.linkRenderController) shared.linkRenderController.note_queue_size(detail ? detail.exec_info.queue_remaining : 0)
+ });
+
+ /* if we are on version 1.16 or later, stash input data to convert nodes when they are loaded */
+ //if (graphConverter.running_116_plus()) {
+ const original_loadGraphData = app.loadGraphData;
+ app.loadGraphData = async function (data) {
+ try {
+ graphConverter.store_node_input_map(data);
+ } catch (e) { Logger.log_error(e); }
+ const cvw_was = settingsCache.getSettingValue("Comfy.Validation.Workflows")
+ if (settingsCache.getSettingValue("Use Everywhere.Options.block_graph_validation")) {
+ app.ui.settings.setSettingValue("Comfy.Validation.Workflows", false);
+ }
+ await original_loadGraphData.apply(this, arguments);
+ app.ui.settings.setSettingValue("Comfy.Validation.Workflows", cvw_was);
+ //return v;
+ }
+ //}
+
+ /*
+ When we draw a node, render the virtual connection points
+ */
+ const original_drawNode = LGraphCanvas.prototype.drawNode;
+ LGraphCanvas.prototype.drawNode = function(node, ctx) {
+ try {
+ shared.linkRenderController.pause('drawFrontCanvas')
+ const v = original_drawNode.apply(this, arguments);
+ shared.linkRenderController.highlight_ue_connections(node, ctx);
+ if (node._last_seen_bg !== node.bgcolor) shared.linkRenderController.mark_link_list_outdated();
+ node._last_seen_bg = node.bgcolor
+ return v
+ } catch (e) {
+ Logger.log_error(e)
+ } finally {
+ shared.linkRenderController.unpause()
+ }
+ }
+
+ /*
+ Before drawing the canvas, temporarily disable all the ue connected widgets
+ so they get rendered as greyed out.
+ */
+ const original_drawFrontCanvas = LGraphCanvas.prototype.drawFrontCanvas
+ LGraphCanvas.prototype.drawFrontCanvas = function() {
+ try {
+ shared.linkRenderController.disable_all_connected_widgets()
+ return original_drawFrontCanvas.apply(this, arguments);
+ } catch (e) {
+ Logger.log_error(e)
+ } finally {
+ try {
+ shared.linkRenderController.enable_all_disabled_widgets()
+ } catch (e) {
+ Logger.log_error(e)
+ }
+
+ }
+ }
+
+ /*
+ When we draw connections, do the ue ones as well (logic for on/off is in lrc)
+ */
+ const drawConnections = LGraphCanvas.prototype.drawConnections;
+ LGraphCanvas.prototype.drawConnections = function(ctx) {
+ drawConnections?.apply(this, arguments);
+ try {
+ shared.linkRenderController.render_all_ue_links(ctx);
+ } catch (e) {
+ Logger.log_error(e)
+ }
+ }
+
+ },
+
+ init() {
+ shared.graphAnalyser = new GraphAnalyser();
+ shared.linkRenderController = new LinkRenderController();
+
+ /*
+ Modifications to the graph:
+ - track start and end of the graph being changed
+ - catch convertToSubgraph to try to fix UE links
+ */
+ const original_beforeChange = app.graph.beforeChange
+ app.graph.beforeChange = function () {
+ shared.in_midst_of_change += 1
+ original_beforeChange?.apply(this, arguments)
+ }
+
+ const original_afterChange = app.graph.afterChange
+ app.graph.afterChange = function () {
+ original_afterChange?.apply(this, arguments)
+ shared.in_midst_of_change = Math.max(0, shared.in_midst_of_change-1) // afterChange gets called without a beforeChange sometimes
+ }
+
+ const original_subgraph = app.graph.convertToSubgraph
+ app.graph.convertToSubgraph = function () {
+ const ctb_was = shared.graphAnalyser.connect_to_bypassed
+ shared.graphAnalyser.connect_to_bypassed = true
+ try {
+ const cur_list = shared.graphAnalyser.analyse_graph(visible_graph())
+ if (!cur_list) Logger.log_problem('convert to subgraph failed to get ues')
+ const mods = convert_to_links(cur_list, null, visible_graph());
+ const r = original_subgraph.apply(this, arguments);
+ mods.restorer()
+ fix_new_subgraph_node(r.node)
+ return r
+ } finally {
+ shared.graphAnalyser.connect_to_bypassed = ctb_was
+ }
+ }
+
+ /*
+ Modifications to app
+ - intercept graphToPrompt and queuePrompt so we know where we are
+ - provide API
+ */
+ const original_graphToPrompt = app.graphToPrompt;
+ app.graphToPrompt = async function () {
+ try {
+ shared.in_graphToPrompt += 1
+ if (shared.in_queuePrompt || app.ui.settings.getSettingValue("Use Everywhere.Options.always_modify_graph")) {
+ //Logger.log_shared('In graphToPrompt (going to modify graph):')
+ return await shared.graphAnalyser.call_function_with_modified_graph( original_graphToPrompt, arguments )
+ } else {
+ //Logger.log_shared('In graphToPrompt (not going to modify graph):')
+ return await original_graphToPrompt.apply(this, arguments)
+ }
+ } finally {
+ shared.in_graphToPrompt -= 1
+ }
+ }
+
+ const original_queuePrompt = app.queuePrompt;
+ app.queuePrompt = async function () {
+ try {
+ shared.in_queuePrompt += 1;
+ return await original_queuePrompt.apply(app, arguments);
+ } finally {
+ shared.in_queuePrompt -= 1;
+ }
+ }
+
+ app.ue_modified_prompt = async function () { // API function
+ return await shared.graphAnalyser.call_function_with_modified_graph( original_graphToPrompt )
+ }
+
+ /*
+ Modifications to the canvas
+ - listen for set-graph to mark the link list as out of date when we open or close a subgraph
+ - catch node-double-click to open the restrictions dialog
+ - onDrawForeground to highlight subgraph output links
+ */
+
+ app.canvas.canvas.addEventListener('litegraph:set-graph', ()=>{
+ shared.linkRenderController.mark_link_list_outdated()
+ setTimeout(()=>{app.canvas.setDirty(true,true)},200)
+ })
+
+ app.canvas.canvas.addEventListener('litegraph:canvas', (e)=>{
+ if (e?.detail?.subType=='node-double-click') {
+ const node = e.detail.node
+ if (node_can_broadcast(node)) {
+ if (app.ui.settings.getSettingValue('Comfy.Node.DoubleClickTitleToEdit') && e.detail.originalEvent.canvasY {
+ const input_node_id = st.node.id;
+ const input_node = get_real_node(input_node_id, ue.graph);
+ const input_index = st.input_index;
+ if (input_node.inputs?.[input_index].link) { // why would this be happening?
+ const llink = ue.graph.links[input_node.inputs[input_index].link]
+ if (llink) removed_links.push( {...llink} )
+ }
+ var new_link
+ if (input_node.io_node) {
+ new_link = input_node.io_node.slots[input_index].connect(output_node.outputs[output_index], output_node);
+ } else {
+ new_link = output_node.connect(output_index, input_node, input_index);
+ }
+
+ if (!new_link)
+ console.error("Failed to connect nodes: " +
+ `${output_node_id}[${output_index}] -> ` +
+ `${input_node_id}[${input_index}].`);
+ else { // Memorize the links we are adding to remove them later
+ if (added_links) {
+ new_link.control_node = ue.controller;
+ added_links.push(new_link);
+ }
+ Logger.log_info(` -> ${display_name(st.node)}, ${st.input.name} ` +
+ `(${st.node.id}.${st.input_index}) (ID: ${new_link.id})`);
+ }
+ });
+}
+
+export function convert_to_links(ues, control_node, graph) {
+ if (control_node) {
+ if (!graph) graph = control_node.graph
+ return _convert_graph_to_links(graph, ues, control_node.id );
+ } else {
+ if (!graph) graph = app.graph;
+ return _convert_graph_to_links(graph, ues, undefined);
+ }
+}
+
+function _convert_graph_to_links(graph, ues, control_node_id) {
+ if (!ues?.ues) {
+ Logger.log_detail('no ues in _convert_graph_to_links')
+ return {restorer:function(){}, added_links:[]}
+ }
+ const added_links = []
+ const removed_links = []
+ ues.ues.forEach((ue)=> {
+ if (control_node_id==undefined || ue.controller.id == control_node_id) _convert_to_links(ue, added_links, removed_links);
+ });
+
+ const restorer = function() {
+ const links_within_graph_ids = new Set()
+ const links_within_subgraph_ids = new Set()
+ const links_into_subgraph_ids = new Set()
+ const links_out_of_subgraph = new Set()
+ const problem_links = new Set()
+
+ const new_subgraph_node = graph.getNodeById(graph.last_node_id);
+ const new_subgraph = new_subgraph_node?.subgraph;
+
+ added_links.forEach(added_link => {
+ var id = added_link.id
+ try {
+ if (graph.links[id]) {
+ links_within_graph_ids.add(id);
+ } else {
+ const link_to_subgraph_node = new_subgraph_node?.inputs.find(input => input.linkIds.includes(id))?.link
+ if (link_to_subgraph_node) {
+ links_into_subgraph_ids.add(link_to_subgraph_node)
+ } else {
+ if (new_subgraph.links[id]) {
+ if (new_subgraph.links[id].target_id==-20) {
+ links_out_of_subgraph.add(added_link)
+ } else {
+ links_within_subgraph_ids.add(id)
+ }
+ } else problem_links.add(added_link)
+ }
+ }
+ } catch (e) {
+ problem_links.add(added_link)
+ Logger.log_error(e);
+ }
+ });
+
+ links_out_of_subgraph.forEach((llink)=>{
+ const control_node_id = llink.control_node?.id
+ const control_node_in_graph = graph.getNodeById(control_node_id)
+ if (control_node_in_graph) {
+ // the control node is outside, so we should disconnect anything else that is connected to the same output of the subgraph node
+ new_subgraph_node.outputs[new_subgraph.links[llink.id].target_slot].links.forEach((link_id)=>{
+ if (graph.links[link_id].target_id==control_node_id) {
+ // leave the link to the UE node
+ } else {
+ // remove others
+ links_within_graph_ids.add(link_id)
+ }
+ })
+ } else {
+ // control node is inside, leave the link, since we can't UE connect to the output panel
+ }
+ })
+
+ links_within_graph_ids.forEach((lid)=>{graph.removeLink(lid)})
+ links_into_subgraph_ids.forEach((lid)=>{graph.removeLink(lid)})
+ links_within_subgraph_ids.forEach((lid)=>{new_subgraph.removeLink(lid)})
+
+ if (problem_links.size>0) {
+ Logger.log_problem("Failed to work out how to remove some temporary links:", problem_links)
+ }
+
+ removed_links.forEach(llink => {
+ graph.getNodeById(llink.origin_id).connect(llink.origin_slot, graph.getNodeById(llink.target_id), llink.target_slot)
+ })
+ };
+
+ return {restorer:restorer, added_links:added_links}
+}
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_cache.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_cache.js
new file mode 100644
index 0000000000000000000000000000000000000000..425eaa3c1465ade852d9b7cc1129f4ffe2bf8be4
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_cache.js
@@ -0,0 +1,25 @@
+import { app } from "../../scripts/app.js";
+
+class LocalSettingsCache {
+ constructor() {
+ this.local_settings = {}
+ this.callbacks = {}
+ }
+ getSettingValue(id) {
+ if (this.local_settings[id]===undefined) this.local_settings[id] = app.ui.settings.getSettingValue(id)
+ return this.local_settings[id]
+ }
+ onSettingChange(new_value, old_value) {
+ const id = this.id // this references the setting that has been changed
+ settingsCache.local_settings[id] = new_value
+ if (settingsCache.callbacks[id]) {
+ settingsCache.callbacks[id].forEach((c) => {c(new_value, old_value, this)})
+ }
+ }
+ addCallback(id, callback) {
+ if (this.callbacks[id]===undefined) this.callbacks[id] = []
+ this.callbacks[id].push(callback)
+ }
+}
+
+export const settingsCache = new LocalSettingsCache()
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_classes.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_classes.js
new file mode 100644
index 0000000000000000000000000000000000000000..1df2bff450f315a77496f9ded6fa0bfb03214fc8
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_classes.js
@@ -0,0 +1,373 @@
+import { i18n_functional } from "./i18n.js";
+import { default_priority } from "./ue_properties.js";
+import { connection_from_output_as_input, visible_graph } from "./use_everywhere_subgraph_utils.js";
+import { nodes_in_my_group, nodes_not_in_my_group, nodes_my_color, nodes_not_my_color, nodes_in_groups_matching } from "./use_everywhere_ui.js";
+import { Logger, node_is_live, get_real_node, get_connection, find_duplicate_broadcasted_types, is_able_to_broadcast } from "./use_everywhere_utilities.js";
+
+
+export function display_name(node) {
+ if (node?.title) return node.title;
+ if (node?.type) return node.type;
+ if (node?.properties?.['Node name for S&R']) return node.properties['Node name for S&R'];
+ if (node?.id==-20) return "subgraph output node";
+ if (node?.id==-10) return "subgraph input node";
+ return "un-nameable node";
+}
+
+export function input_name(input) {
+ return input?.label || input?.localized_name || input?.name || "";
+}
+
+function regex_for(node, k) {
+ try {
+ const w0 = node.properties.ue_properties[`${k}_regex`]
+ return (w0 && w0!='.*') ? {regex:new RegExp(w0), invert:!!node.properties.ue_properties[`${k}_regex_invert`]} : null;
+ } catch (e) {
+ return null
+ }
+}
+
+
+const P_REGEXES = ['prompt', 'negative']
+const PROMPT_REGEXES = [new RegExp(i18n_functional('prompt_regex')), new RegExp(i18n_functional('negative_regex'))]
+
+/*
+The UseEverywhere object represents a single 'broadcast'. It generally contains
+ controller - the UE node that controls the broadcase
+ control_node_input_index - the input on that node
+ type - the data type
+ output - the output that is being rebroadcast as a list (node_id, output_index)
+ title_regex, input_regex - the UE? matching rules
+ priority - priorty :)
+ graph - the graph or subgraph
+*/
+class UseEverywhere {
+ constructor() {
+ this.sending_to = [];
+ Object.assign(this, arguments[0]);
+ if (this.priority === undefined) this.priority = 0;
+ if (this.graph === undefined) this.graph = visible_graph();
+ this.string_to_combo = (this.controller.properties.ue_properties.string_to_combo > 0)
+ this.send_to_any = (this.controller.properties.ue_properties.send_to_any > 0)
+
+ const from_node = get_real_node(this?.output[0], this.graph);
+ const to_node = get_real_node(this?.controller.id, this.graph)
+ try {
+ if (this.output[0]==-10) {
+ this.description = `source subgraph input slot ${this?.output[1]} ` +
+ `-> control "${display_name(to_node)}", ${to_node.inputs[this?.control_node_input_index]?.name} (${this?.controller?.id}.${this?.control_node_input_index}) ` +
+ `"${this.type}" <- (priority ${this.priority})`;
+ }
+ else if (this.control_node_input_index>=0) {
+ this.description = `source "${display_name(from_node)}", ${from_node.outputs[this?.output[1]]?.name} (${this?.output[0]}.${this?.output[1]}) ` +
+ `-> control "${display_name(to_node)}", ${to_node.inputs[this?.control_node_input_index]?.name} (${this?.controller.id}.${this?.control_node_input_index}) ` +
+ `"${this.type}" <- (priority ${this.priority})`;
+ } else {
+ this.description = `source "${display_name(from_node)}", ${from_node.outputs[this?.output[1]]?.name} (${this?.output[0]}.${this?.output[1]}) ` +
+ `"${this.type}" <- (priority ${this.priority})`;
+ }
+ } catch (e) {
+ // for breakpointing
+ throw e;
+ }
+ if (this.title_regex) this.description += ` - node title regex '${this.title_regex.regex?.source} ${(this.title_regex.invert) ? "(inverted)" : ""}'`;
+ if (this.input_regex) this.description += ` - input name regex '${this.input_regex.regex?.source} ${(this.input_regex.invert) ? "(inverted)" : ""}''`;
+ }
+
+ sending_differs_from(another_ue) {
+ if (this.sending_to.length != another_ue.sending_to.length) return true;
+ for (var i=0; i n.name==input.name);
+ else if (node.slots) input_index = node.slots.findIndex((n) => n.name==input.name);
+ if (input_index===undefined) {
+ Logger.log_problem(`UseEverywhere.note_sending_to could not find input index for node ${node.id} input ${input.name}`);
+ } else {
+ this.sending_to.push({node:node, input:input, input_index:input_index})
+ }
+ }
+ describe_sending(){
+ var description = " Linked to:";
+ this.sending_to.forEach((st) => description += `\n -> ${display_name(st.node)}, ${st.input.name}`);
+ if (this.sending_to.length===0) description += ' nothing';
+ return description;
+ }
+ describe() {
+ return this.description + "\n" + this.describe_sending();
+ }
+}
+
+function validity_errors(params) {
+ if (!node_is_live(params.controller)) return `UE node ${params.controller.id} is not alive`;
+ if (params.output[0]!=-10 && !node_is_live(get_real_node(params.output[0], params.graph))) return `upstream node ${params.output[0]} is not alive`;
+ return "";
+}
+
+export class Ambiguity {
+/*
+ name : display_name of node
+ id : node.id
+ input : input.name
+ matches : [
+ {
+ type : m[i].controller.type,
+ id : m[i].controller.id,
+ index : m[i].control_node_input_index
+ }
+ ]
+*/
+ toString() {
+ var m = `Node ${this.name} (${this.id}) input ${this.input} matches ${this.matches.length} sources:`
+ this.matches.forEach((match)=>{
+ m += `\n - ${match.type} (${match.id}) slot ${match.index}`
+ })
+ return m
+ }
+
+}
+
+export class UseEverywhereList {
+ constructor() { this.ues = []; this.unmatched_inputs = []; }
+
+ differs_from(another_uel) {
+ if (!another_uel || !another_uel.ues || !this.ues) return true;
+ if (this.ues.length != another_uel.ues.length) return true;
+ for (var i=0; i (
+ candidate.matches(node, input)
+ ));
+ if (matches.length==0) {
+ Logger.log_detail(`'${display_name(node)}' optional input '${input.name}' unmatched`)
+ return undefined;
+ }
+ if (matches.length>1) {
+ matches.sort((a,b) => b.priority-a.priority);
+ if(matches[0].priority == matches[1].priority) {
+ const msg = new Ambiguity()
+ Object.assign(msg, { name:display_name(node), id:node.id, input:input.name, matches:[] })
+
+ matches.filter((m)=>(m.priority == matches[0].priority)).forEach((m)=>{
+ msg.matches.push( {
+ type : m.controller.type,
+ id : m.controller.id,
+ index : m.control_node_input_index,
+ node : node,
+ node_index : node.inputs.findIndex((i)=>(i==input))
+ })
+ })
+
+ _ambiguities.push(msg);
+ return undefined;
+ }
+ }
+ matches[0].note_sending_to(node, input);
+ Logger.log_detail(`'${display_name(node)}' input '${input.name}' matched to ${matches[0].description}`);
+ return matches[0];
+ }
+
+ all_connected_inputs(for_node) {
+ const ue_connections = [];
+ this.ues.forEach((ue) => {
+ ue.sending_to.forEach((st) => {
+ if (st.node.id == for_node.id) {
+ ue_connections.push({
+ type : ue.type,
+ input_index : st.input_index,
+ control_node : get_real_node(ue.controller.id, ue.graph),
+ control_node_input_index : ue.control_node_input_index,
+ sending_to : st.node,
+ });
+ }
+ });
+ });
+ return ue_connections;
+ }
+
+ node_sending_anywhere(node) {
+ return this.ues.find((ue)=>(ue.controller?.id==node.id && ue.sending_to?.length>0)) ? true : false;
+ }
+
+ all_sending_slots(for_node) {
+ const sending_slots = new Set()
+ this.ues.filter((ue)=>(ue.controller.id == for_node.id) && (ue.sending_to.length>0)).forEach((ue) => {
+ sending_slots.add(ue.control_node_input_index)
+ });
+ return sending_slots;
+ }
+
+ all_ue_connections() {
+ const ue_connections = [];
+ this.ues.forEach((ue) => {
+ ue.sending_to.forEach((st) => {
+ ue_connections.push({
+ type : ue.type,
+ input_index : st.input_index,
+ control_node : get_real_node(ue.controller.id, ue.graph),
+ control_node_input_index : ue.control_node_input_index,
+ sending_to : st.node,
+ graph: ue.graph,
+ });
+ });
+ });
+ return ue_connections;
+ }
+
+ add_ue_from_node(node) {
+
+ if (node.properties.ue_properties.seed_inputs) {
+ this.add_ue(node, -1, "INT", [node.id.toString(),0], regex_for(node, 'input') );
+ } else {
+ var check_if_able_to_broadcast
+ var the_possibles
+ var connection_finder
+ if (node.properties.ue_convert) {
+ connection_finder = connection_from_output_as_input
+ the_possibles = node.outputs
+ check_if_able_to_broadcast = (node, i) => {
+ const name = node.outputs[i].name
+ return is_able_to_broadcast(node, name)
+ }
+ } else {
+ connection_finder = get_connection
+ the_possibles = node.inputs
+ check_if_able_to_broadcast = ()=>(true)
+ }
+
+ const duplicated_broadcasted_types = find_duplicate_broadcasted_types(node)
+
+ the_possibles.forEach((possible, i) => {
+ const connection = connection_finder(node, i);
+ if (connection.link && check_if_able_to_broadcast(node,i)) {
+ const input_regex = undefined
+ var additional_requirement = null
+ if (duplicated_broadcasted_types.has(connection.type) || node.properties.ue_properties.apply_to_unrepeated) {
+ const input_name = possible.label || possible.name
+ const rule = node.properties.ue_properties?.repeated_type_rule || 0
+ if (rule == 0) { // 0 is exact match of input name
+ additional_requirement = (target_input) => {
+ const target_name = target_input.label || target_input.name
+ return (target_name == input_name)
+ }
+ }
+ if (rule == 1) { // 1 is match start of input name
+ additional_requirement = (target_input) => {
+ const target_name = target_input.label || target_input.name
+ const chars = Math.min( input_name.length, target_name.length )
+ return (target_name.substring(0, chars) == input_name.substring(0, chars))
+ }
+ }
+ if (rule == 2) { // 2 is match end of input name
+ additional_requirement = (target_input) => {
+ const target_name = target_input.label || target_input.name
+ const chars = Math.min( input_name.length, target_name.length )
+ return (target_name.substring(target_name.length - chars) == input_name.substring(input_name.length - chars))
+ }
+ }
+ if (rule == 3) { // 3 input is match of target node name
+ additional_requirement = (_, target_node) => {
+ const target_name = target_node.title
+ return (target_name == input_name)
+ }
+ }
+ }
+ this.add_ue(node, i, connection.type, [connection.link.origin_id.toString(), connection.link.origin_slot], input_regex, additional_requirement);
+ }
+ })
+ }
+ }
+}
+
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_graph_analysis.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_graph_analysis.js
new file mode 100644
index 0000000000000000000000000000000000000000..686d77f8601894613b55001ff67a77d36d1b5f71
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_graph_analysis.js
@@ -0,0 +1,134 @@
+import { UseEverywhereList } from "./use_everywhere_classes.js";
+import { node_is_live, is_connected, is_UEnode, Logger, Pausable, node_can_broadcast } from "./use_everywhere_utilities.js";
+import { convert_to_links } from "./use_everywhere_apply.js";
+import { app } from "../../scripts/app.js";
+import { settingsCache } from "./use_everywhere_cache.js";
+import { is_connectable } from "./use_everywhere_settings.js";
+import { for_all_graphs } from "./recursive_callbacks.js";
+import { shared } from "./shared.js";
+
+class GraphAnalyser extends Pausable {
+
+ constructor() {
+ super('GraphAnalyser')
+ this.original_graphToPrompt = app.graphToPrompt;
+ this.ambiguities = [];
+ this.latest_ues = null
+ this.mods = []
+ }
+
+ modify_graph(graph) {
+ const ues = this.analyse_graph(graph, true)
+ if (ues===null) {
+ Logger.log_problem(`modify_graph called but no ues could be obtained for ${graph.id}`)
+ console.trace()
+ }
+ const modifications = convert_to_links( ues, null, graph )
+ this.mods.push( modifications );
+ if (!graph.extra) graph.extra = {}
+ graph.extra['links_added_by_ue'] = modifications.added_links.map(x=>x.id)
+ }
+
+ modify_all_graphs() {
+ for_all_graphs(this.modify_graph.bind(this))
+ }
+
+ async call_function_with_modified_graph( func, args ) {
+ var result
+ this.mods = []
+ try {
+ this.pause('call_function_with_modified_graph')
+ Logger.log_info("Modifying graphs")
+ this.modify_all_graphs()
+ shared.graph_currently_modified += 1
+ result = await (args ? func(...args) : func())
+ } catch (e) {
+ Logger.log_error(e)
+ }
+
+ Logger.log_info("Unmodifying graphs")
+ this.mods.forEach((mod)=>{
+ try {mod.restorer()}
+ catch (e) {Logger.log_error(e)}
+ })
+ this.mods = []
+ shared.graph_currently_modified -= 1
+ this.unpause()
+
+ return result
+ }
+
+
+ clean_slots(links, slots) {
+ slots?.forEach((slot)=>{
+ if (slot.linkIds.find((lid)=>(!links[lid]))) {
+ slot.linkIds = slot.linkIds.filter((lid)=>{ return links[lid] !== undefined })
+ }
+ })
+ }
+
+ analyse_graph(graph, ignore_pause) {
+ if (this.paused() && !ignore_pause) return null
+
+ /* work around known bug in ComfyUI front end that doesn't clean up the linkIds
+ https://github.com/Comfy-Org/ComfyUI_frontend/issues/5673#issuecomment-3314310014
+
+ Fixed? Doesn't seems to be.
+ https://github.com/Comfy-Org/ComfyUI_frontend/pull/6258 */
+
+
+ this.clean_slots(graph.links, graph.inputNode?.slots)
+ this.clean_slots(graph.links, graph.outputNode?.slots)
+
+ this.ambiguities = [];
+ const treat_bypassed_as_live = settingsCache.getSettingValue("Use Everywhere.Options.connect_to_bypassed") || this.connect_to_bypassed
+ const live_nodes = graph.nodes.filter((node) => node_is_live(node, treat_bypassed_as_live))
+
+ // Create a UseEverywhereList and populate it from all live (not bypassed) UE nodes
+ const ues = new UseEverywhereList();
+ live_nodes.filter((node) => node_can_broadcast(node)).filter((node)=>node_is_live(node,false)).forEach(node => { ues.add_ue_from_node(node); })
+
+ // List all unconnected inputs on non-UE nodes which are connectable
+ const connectable = []
+ live_nodes.filter((node) => !is_UEnode(node)).forEach(node => {
+ if (node && !node.properties.rejects_ue_links) {
+ //if (!real_node._widget_name_map) real_node._widget_name_map = real_node.widgets?.map(w => w.name) || [];
+ node.inputs?.forEach((input,index) => {
+ if (!input) return; // NoteNode has input = [undefined,] !
+ if (is_connected(input, treat_bypassed_as_live, node.graph)) return;
+ if (node.reject_ue_connection && node.reject_ue_connection(input)) return;
+ if (is_connectable(node, input.name)) connectable.push({node, input, index});
+ })
+ }
+ })
+
+ if (graph.outputNode) {
+ graph.outputNode.slots.filter((slot)=>(slot.linkIds.length==0)).forEach((slot,index)=>{
+ connectable.push({node:graph.outputNode, input:slot, index});
+ }
+ )}
+
+ // see if we can connect them
+ const links_added = new Set();
+ connectable.forEach(({node, input, index}) => {
+ var ue = ues.find_best_match(node, input, this.ambiguities);
+ if (ue) {
+ links_added.add({
+ "downstream":node.id, "downstream_slot":index,
+ "upstream":ue.output[0], "upstream_slot":ue.output[1],
+ "controller":ue.controller.id,
+ "type":ue.type
+ });
+ }
+ });
+
+ graph.extra['ue_links'] = Array.from(links_added)
+
+ if (this.ambiguities.length) Logger.log_info("Ambiguous connections", this.ambiguities, true);
+
+ this.latest_ues = ues;
+ return this.latest_ues;
+ }
+}
+
+export { GraphAnalyser }
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_settings.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..0dd3d8983e48feb870b69dfd9080fc2e3eba2e71
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_settings.js
@@ -0,0 +1,366 @@
+import { app } from "../../scripts/app.js";
+import { convert_to_links } from "./use_everywhere_apply.js";
+import { Logger, is_UEnode, node_can_broadcast, is_able_to_broadcast } from "./use_everywhere_utilities.js";
+import { settingsCache } from "./use_everywhere_cache.js";
+import { visible_graph } from "./use_everywhere_subgraph_utils.js";
+import { edit_restrictions } from "./ue_properties_editor.js";
+import { i18ify_settings } from "./i18n.js";
+import { VERSION, shared } from "./shared.js";
+import { for_all_graphs } from "./recursive_callbacks.js";
+
+const _SETTINGS = [
+ {
+ id: "Use Everywhere.About",
+ name: `Version ${VERSION}`,
+ type: () => {return document.createElement('span')},
+ },
+ {
+ id: "Use Everywhere.Graphics.showlinks",
+ name: "Show links",
+ type: "combo",
+ options: [ {value:0, text:"All off"}, {value:1, text:"Selected nodes"}, {value:2, text:"Mouseover node"}, {value:3, text:"Selected and mouseover nodes"}, {value:4, text:"All on"}],
+ defaultValue: 3,
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Graphics.fuzzlinks",
+ name: "Statically distinguish UE links",
+ type: "boolean",
+ tooltip: "Render UE links, when shown, differently from normal links. Much lower performance cost than animation.",
+ defaultValue: true,
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Graphics.animate",
+ name: "Animate UE links",
+ type: "combo",
+ options: [ {value:0, text:"Off"}, {value:1, text:"Dots"}, {value:2, text:"Pulse"}, {value:3, text:"Both"}, ],
+ defaultValue: 0,
+ onChange: settingsCache.onSettingChange,
+ tooltip: "Animating links may have a negative impact on UI performance. Consider using Statically distinguish UE links instead."
+ },
+ {
+ id: "Use Everywhere.Graphics.stop_animation_when_running",
+ name: "Turn animation off when workflow is running",
+ type: "boolean",
+ defaultValue: true,
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Graphics.highlight",
+ name: "Highlight connected and connectable inputs",
+ type: "boolean",
+ defaultValue: true,
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Graphics.preserve edit window position",
+ name: "Save restrictions edit window position",
+ type: "boolean",
+ defaultValue: false,
+ onChange: settingsCache.onSettingChange,
+ tooltip: "If off, the edit window appears where the mouse is"
+ },
+ {
+ id: "Use Everywhere.Graphics.tooltips",
+ name: "Show restrictions as tooltip",
+ type: "boolean",
+ defaultValue: true,
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Options.connect_to_bypassed",
+ name: "Connect to bypassed nodes",
+ type: "boolean",
+ defaultValue: true,
+ onChange: settingsCache.onSettingChange,
+ tooltip: "By default UE links are made to the node downstream of bypassed nodes."
+ },
+ {
+ id: "Use Everywhere.Options.logging",
+ name: "Logging",
+ type: "combo",
+ options: [ {value:0, text:"Errors Only"}, {value:1, text:"Problems"}, {value:2, text:"Information"}, {value:3, text:"Detail"}, ],
+ defaultValue: 1,
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Options.block_graph_validation",
+ name: "Block workflow validation",
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "Turn off workflow validation (which tends to replace UE links with real ones)",
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Options.always_modify_graph",
+ name: "Always modify graph",
+ type: "boolean",
+ defaultValue: false,
+ tooltip: "If turned on, the UE modifications will apply even if the trigger is unknown. This will enable API exports, but may cause incompatibility with other nodes.",
+ onChange: settingsCache.onSettingChange,
+ },
+ {
+ id: "Use Everywhere.Options.use_output_name",
+ name: "When connecting, use the output slot's name as the input name",
+ type: "boolean",
+ defaultValue: false,
+ tooltip: "By default the link type is used as the name",
+ onChange: settingsCache.onSettingChange,
+ },
+]
+
+export const SETTINGS = i18ify_settings(_SETTINGS)
+
+const ui_update_settings = [
+ "Use Everywhere.Graphics.showlinks",
+ "Use Everywhere.Graphics.fuzzlinks",
+ "Use Everywhere.Graphics.animate",
+ "Use Everywhere.Graphics.stop_animation_when_running",
+ "Use Everywhere.Graphics.highlight",
+ "Use Everywhere.Language.language",
+]
+ui_update_settings.forEach((id) => {
+ settingsCache.addCallback(id, ()=>{app.graph?.change.bind(app.graph)})
+})
+
+function show_connectable(submenu_root, node) {
+ node.inputs.forEach((input, i) => {
+ const current_element = submenu_root?.querySelector(`:nth-child(${i+1})`);
+ if (current_element) current_element.style.borderLeft = (is_connectable(node,input.name)) ? "2px solid #484" : "";
+ })
+}
+
+export function is_connectable(node, input_name){
+ if (node.properties.rejects_ue_links) return false
+ const input = node.inputs.find(i => i.name==input_name);
+ if (!input) {
+ Logger.log_error(`Can't find input ${input_name} on node ${node.title}`);
+ return false;
+ }
+ if (input.widget) {
+ return !!(node.properties?.ue_properties?.widget_ue_connectable?.[input_name])
+ } else {
+ return ! (node.properties?.ue_properties?.input_ue_unconnectable?.[input_name])
+ }
+}
+
+function toggle_connectable(node, input_name){
+ const input = node.inputs.find(i => i.name==input_name);
+ const p = node.properties.ue_properties
+ if (input.widget) {
+ p.widget_ue_connectable[input_name] = !!!p.widget_ue_connectable?.[input_name];
+ } else {
+ p.input_ue_unconnectable[input_name] = !!!p.input_ue_unconnectable?.[input_name];
+ }
+}
+
+function widget_ue_submenu(value, options, e, menu, node) {
+ if (!(node.properties.ue_properties)) node.properties.ue_properties = {}
+ if (!(node.properties.ue_properties.widget_ue_connectable)) node.properties.ue_properties.widget_ue_connectable = {};
+ if (!(node.properties.ue_properties.input_ue_unconnectable)) node.properties.ue_properties.input_ue_unconnectable = {};
+
+ const label_to_name = {}
+ node.inputs
+ .filter(i => !i.hidden)
+ .filter(i => !i.name?.includes('$$'))
+ .forEach((input) => { label_to_name[input.label || input.name] = input.name });
+
+ const submenu = new LiteGraph.ContextMenu(
+ Object.keys(label_to_name),
+ { event: e, callback: function (label) {
+ const name = label_to_name[label];
+ toggle_connectable(node, name);
+ shared.linkRenderController.mark_link_list_outdated();
+ show_connectable(this.parentElement, node)
+ return true; // keep open
+ },
+ parentMenu: menu, node:node}
+ )
+ show_connectable(submenu.root, node)
+}
+
+function show_broadcasting(submenu_root, node) {
+ node.outputs.forEach((input, i) => {
+ const current_element = submenu_root?.querySelector(`:nth-child(${i+1})`);
+ if (current_element) current_element.style.borderLeft = (is_able_to_broadcast(node,input.name)) ? "2px solid #484" : "";
+ })
+}
+
+function toggle_broadcasting(node, output_name){
+ const p = node.properties.ue_properties
+ p.output_not_broadcasting[output_name] = !!!p.output_not_broadcasting[output_name]
+}
+
+function output_ue_submenu(value, options, e, menu, node) {
+ if (!(node.properties.ue_properties)) node.properties.ue_properties = {}
+ if (!(node.properties.ue_properties.output_not_broadcasting)) node.properties.ue_properties.output_not_broadcasting = {};
+
+ const label_to_name = {}
+ node.outputs.forEach((output) => {label_to_name[output.label || output.name] = output.name})
+ const submenu = new LiteGraph.ContextMenu(
+ Object.keys(label_to_name),
+ { event: e, callback: function (label) {
+ const name = label_to_name[label];
+ toggle_broadcasting(node, name);
+ shared.linkRenderController.mark_link_list_outdated();
+ show_broadcasting(this.parentElement, node)
+ return true; // keep open
+ },
+ parentMenu: menu, node:node}
+ )
+ show_broadcasting(submenu.root, node)
+}
+
+
+
+export function add_extra_menu_items(node, ioio) {
+ if (node.ue_extra_menu_items_added) return
+ const getExtraMenuOptions = node.getExtraMenuOptions;
+ node.getExtraMenuOptions = function(_, options) {
+ getExtraMenuOptions?.apply(this, arguments);
+ options.push(null);
+ if (node_can_broadcast(this)) {
+ add_restrictions_and_convert(options, this);
+ }
+ if (!is_UEnode(node)) {
+ add_input_and_output_settings(options, node)
+ }
+ options.push(null);
+ ioio(options,'callback',`menu option on ${this.id}`);
+ }
+ node.ue_extra_menu_items_added = true
+}
+
+
+function add_restrictions_and_convert(options, node) {
+ options.push(
+ {
+ content: "Edit restrictions",
+ callback: edit_restrictions,
+ }
+ )
+
+ options.push(
+ {
+ content: "Convert to real links",
+ callback: async () => {
+ const ues = shared.graphAnalyser.analyse_graph(visible_graph());
+ if (ues===null) {
+ Logger.log_problem("Convert to real links failed to get ues")
+ alert("Convert failed - press f12 to see the console log")
+ } else {
+ convert_to_links(ues, node);
+ if (is_UEnode(node)) {
+ visible_graph().remove(node);
+ } else {
+ node.properties.ue_convert = false
+ }
+ }
+ }
+ }
+ )
+}
+
+function add_input_and_output_settings(options, node) {
+ if (node.inputs?.length) {
+ options.push(
+ {
+ content: node.properties.rejects_ue_links ? "Allow UE Links" : "Reject UE Links",
+ has_submenu: false,
+ callback: () => { node.properties.rejects_ue_links = !!!node.properties.rejects_ue_links },
+ }
+ )
+
+ if (!node.properties.rejects_ue_links) {
+ options.push(
+ {
+ content: "UE Connectable Inputs",
+ has_submenu: true,
+ callback: widget_ue_submenu,
+ }
+ )
+ }
+ }
+
+ if (node.outputs.length) {
+ options.push(
+ {
+ content: node.properties.ue_convert ? "Remove UE broadcasting" : "Add UE broadcasting",
+ has_submenu: false,
+ callback: () => { node.properties.ue_convert = !!!node.properties.ue_convert },
+ }
+ )
+ if (node.properties.ue_convert) {
+ options.push(
+ {
+ content: "Broadcasting Outputs",
+ has_submenu: true,
+ callback: output_ue_submenu,
+ }
+ )
+ }
+ }
+}
+
+function remove_ue_nodes(graph) {
+ graph._nodes.filter((node)=>is_UEnode(node)).forEach((node)=>{graph.remove(node)})
+}
+
+export function canvas_menu_settings() {
+ const options = []
+ options.push(null); // divider
+ options.push(
+ {
+ content: (app.ui.settings.getSettingValue('Use Everywhere.Graphics.showlinks')>0) ? "Hide UE links" : "Show UE links",
+ callback: () => {
+ const setTo = (app.ui.settings.getSettingValue('Use Everywhere.Graphics.showlinks')>0) ? 0 : 4;
+ app.ui.settings.setSettingValue('Use Everywhere.Graphics.showlinks', setTo);
+ app.graph.change();
+ }
+ },
+ {
+ content: "Convert all UEs (in this graph/subgraph) to real links",
+ callback: async () => {
+ if (window.confirm("This will convert all links (in this graph/subgraph) created by Use Everywhere to real links, and delete all the Use Everywhere nodes. Is that what you want?")) {
+ shared.linkRenderController.pause("convert");
+ try {
+ const graph = visible_graph()
+ shared.graphAnalyser.modify_graph( graph )
+ remove_ue_nodes( graph )
+ } finally {
+ app.graph.change();
+ shared.linkRenderController.unpause()
+ }
+ }
+ }
+ },
+ {
+ content: "Convert all UEs to real links",
+ callback: async () => {
+ if (window.confirm("This will convert all links created by Use Everywhere to real links, and delete all the Use Everywhere nodes. Is that what you want?")) {
+ shared.linkRenderController.pause("convert");
+ try {
+ for_all_graphs(shared.graphAnalyser.modify_graph.bind(shared.graphAnalyser))
+ for_all_graphs(remove_ue_nodes)
+ } finally {
+ app.graph.change();
+ shared.linkRenderController.unpause()
+ }
+
+ }
+ }
+ },
+ );
+ if (shared.graphAnalyser.ambiguities.length) {
+ options.push({
+ content: "Show UE broadcast clashes",
+ callback: async () => {
+ alert(shared.graphAnalyser.ambiguities.join("\n"))
+ }
+ })
+ }
+ options.push(null); // divider
+ return options
+}
+
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_subgraph_utils.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_subgraph_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..3a7db4c1a52ef41d66b368549bcd84e1695faf99
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_subgraph_utils.js
@@ -0,0 +1,86 @@
+import { app } from "../../scripts/app.js";
+import { ue_callbacks } from "./recursive_callbacks.js";
+import { Logger } from "./use_everywhere_utilities.js";
+import { shared } from "./shared.js";
+
+export function visible_graph() { return app.canvas.graph }
+
+export function in_visible_graph(node) {
+ try {
+ if (node.graph) return node.graph.id == app.canvas.graph.id
+ else if (node.subgraph) return node.subgraph.id == app.canvas.graph.id
+ else Logger.log_problem(`in_visible_graph: ${node.id} has no graph or subgraph`)
+ } catch (e) {
+ Logger.log_error(e)
+ return false
+ }
+}
+
+export function get_subgraph_input_type(graph, slot) { return graph.inputNode.slots[slot].type }
+export function link_is_from_subgraph_input(link) { return link.origin_id==-10 }
+
+export function connection_from_output_as_input(node, slot) {
+ try {
+ return {
+ type : node.outputs[slot].type,
+ link : {
+ origin_id : node.id,
+ origin_slot : slot,
+ },
+ }
+ } catch (e) {
+ console.error(e)
+ return false
+ }
+}
+
+export function fix_new_subgraph_node(node) {
+ try {
+ const subgraph = node.subgraph
+ subgraph.inputNode.slots.forEach((slot, i)=>{
+ slot.linkIds.forEach((lid) => {
+ const link = subgraph.links[lid]
+ const target = subgraph.getNodeById(link.target_id)
+ const slot_name = target.inputs[link.target_slot].name
+ const ue_connectable = target.properties.ue_properties.widget_ue_connectable[slot_name]
+ if (ue_connectable) {
+ const input_name = node.inputs[i]?.name
+ if (slot_name!=input_name) {
+ Logger.log_problem("In fix_new_subgraph names don't match")
+ } else {
+ node.properties.ue_properties.widget_ue_connectable[input_name] = true
+ }
+ }
+ })
+ })
+ } catch (e) {
+ Logger.log_error(e, "in fix_new_subgraph")
+ }
+}
+
+class WrappedIONode {
+ constructor(io_node) {
+ this.io_node = io_node;
+ this.graph = io_node.subgraph;
+ }
+
+ connect(output_index, input_node, input_index) {
+ this.graph.last_link_id += 1
+ this.graph.links[this.graph.last_link_id] = new LLink(this.graph.last_link_id, this.io_node.slots[output_index].type, -10, output_index, input_node.id, input_index)
+ input_node.inputs[input_index].link = this.graph.last_link_id;
+ this.io_node.slots[output_index].linkIds.push(this.graph.last_link_id)
+ return this.graph.links[this.graph.last_link_id]
+ }
+
+ getConnectionPos(is_input, slot_number) {
+ if (is_input) {
+ return [...this.io_node.slots[slot_number].pos]
+ } else {
+ Logger.log_problem("WrappedIONode.getConnectionPos called for output - not supported");
+ }
+ }
+
+}
+export function wrap_input(io_node) {
+ return new WrappedIONode(io_node);
+}
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_ui.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_ui.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d9aef7b73a4527fccc4ff14dbef3900e7a9b887
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_ui.js
@@ -0,0 +1,532 @@
+import { Logger, get_real_node, Pausable, node_can_broadcast, is_able_to_broadcast } from "./use_everywhere_utilities.js";
+import { app } from "../../scripts/app.js";
+import { settingsCache } from "./use_everywhere_cache.js";
+import { in_visible_graph, visible_graph } from "./use_everywhere_subgraph_utils.js";
+import { maybe_show_tooltip } from "./tooltip_window.js";
+import { is_connectable } from "./use_everywhere_settings.js";
+import { shared } from "./shared.js";
+import { any_restrictions } from "./ue_properties.js";
+
+export function nodes_in_my_group(node) {
+ const nodes_in = new Set();
+ node.graph._groups.forEach((group) => {
+ if (!app.canvas.selected_group_moving) group.recomputeInsideNodes();
+ if (group._nodes?.find((nd) => { return (nd.id===node.id) } )) {
+ group._nodes.forEach((nd) => { nodes_in.add(nd.id) } )
+ }
+ });
+ return [...nodes_in];
+}
+
+export function nodes_not_in_my_group(node) {
+ const nid = nodes_in_my_group(node);
+ const nodes_not_in = [];
+ node.graph._nodes.forEach((nd) => {
+ if (!nid.includes(nd.id)) nodes_not_in.push(nd.id);
+ });
+ return nodes_not_in;
+}
+
+export function nodes_in_groups_matching(regex, already_limited_to) {
+ const nodes_in = new Set();
+ app.graph._groups.forEach((group) => {
+ if (regex.regex.test(group.title) != regex.invert) {
+ if (!app.canvas.selected_group_moving) group.recomputeInsideNodes();
+ /*
+ Note for optimisation - it would be more efficient to calculate what nodes are in what groups
+ once at the start of analyse_graph() rather than for every group for every UE? with a group regex.
+ */
+ group._nodes.forEach((node) => {
+ if (!already_limited_to || already_limited_to.includes(node.id)) {
+ nodes_in.add(node.id)
+ }
+ } );
+ }
+ });
+ return [...nodes_in];
+}
+
+
+export function nodes_my_color(node, already_limited_to) {
+ const nodes_in = new Set();
+ const color = get_real_node(node.id, node.graph)?.color
+ if (already_limited_to) {
+ already_limited_to.forEach((nid) => {
+ if (get_real_node(nid, node.graph)?.color==color) nodes_in.add(nid)
+ })
+ } else {
+ node.graph._nodes.forEach((nd) => {
+ if (nd.color==color) nodes_in.add(nd.id)
+ })
+ }
+ return [...nodes_in];
+}
+
+export function nodes_not_my_color(node, already_limited_to) {
+ const nodes_in = new Set();
+ const color = get_real_node(node.id)?.color;
+ if (already_limited_to) {
+ already_limited_to.forEach((nid) => {
+ if (get_real_node(nid, node.graph)?.color!=color) nodes_in.add(nid)
+ })
+ } else {
+ node.graph._nodes.forEach((nd) => {
+ if (nd.color!=color) nodes_in.add(nd.id)
+ })
+ }
+ return [...nodes_in];
+}
+
+export function title_bar_additions(node, ctx, title_height) {
+ if (node_can_broadcast(node)) {
+ const restricted = any_restrictions(node);
+ const sending = shared.linkRenderController.node_sending_anywhere(node);
+
+ const color = restricted ? ( sending ? "rgba(255, 255, 72, 1)" : "rgba(255, 255, 72, 0.35)" ) :
+ ( sending ? "rgba(72, 255, 72, 1)" : "rgba(72, 255, 72, 0.35)" );
+
+ const offset_x = (node.subgraph) ? 5 : 5
+ const offset_y = (node.subgraph) ? 4 : 5
+ const width = (node.subgraph) ? 24 : 20
+ const height = (node.subgraph) ? 23 : 20
+ const radius = (node.subgraph) ? 6 : 6
+
+ const line_w = (node.subgraph) ? 1 : 2
+
+ ctx.save();
+ ctx.lineWidth = line_w;
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.roundRect(offset_x, offset_y-title_height, width, height, radius);
+ ctx.stroke();
+ ctx.restore();
+ }
+}
+
+export class LinkRenderController extends Pausable {
+
+ constructor() {
+ super('LinkRenderController')
+ this.ue_list = undefined; // the most current ue list - set to undefined if we know it is out of date
+ this.last_used_ue_list = undefined; // the last ue list we actually used to generate graphics
+ this.link_list_outdated = false;
+ this.widgets_disabled = []
+ setInterval(this.try_to_update_link_list.bind(this), 100);
+ setInterval(this.mark_link_list_outdated.bind(this), 2000);
+ }
+
+ queue_size = null;
+ note_queue_size(x) { this.queue_size = x; }
+
+
+ //on_unpause() {app.graph.change();}
+
+ node_over_changed() {
+ const mode = settingsCache.getSettingValue('Use Everywhere.Graphics.showlinks');
+ if (mode==2 || mode==3) app.canvas.setDirty(true,true)
+ }
+
+ // memory reuse
+ slot_pos1 = new Float32Array(2); //to reuse
+ slot_pos2 = new Float32Array(2); //to reuse
+
+
+ /*
+ Outdating.
+
+ Convention is that methods starting _ should only be called inside a pause()/unpause()
+ */
+
+ mark_link_list_outdated() {
+ this.link_list_outdated = true
+ }
+
+ try_to_update_link_list() {
+ if (!this.link_list_outdated) return;
+ if (this.paused()) return;
+ try {
+ this.pause('try_to_update_link_list')
+ this.ue_list = undefined;
+ if (this._request_link_list_update()) this.link_list_outdated = false;
+ } finally {
+ this.unpause()
+ }
+ }
+
+ _request_link_list_update() {
+ try {
+ const ues = shared.graphAnalyser.analyse_graph(visible_graph())
+ if (ues==null) return false // graph analyser was paused
+ this.ue_list = ues;
+ if (this.ue_list.differs_from(this.last_used_ue_list)) app.graph.change();
+ return true
+ } catch (e) {
+ Logger.log_error(e);
+ return false
+ }
+ }
+
+ disable_all_connected_widgets( ) {
+ this.widgets_disabled = []
+ app.canvas.graph.extra['ue_links']?.forEach((uel) => {
+ const node = app.canvas.graph.getNodeById(uel.downstream)
+ if (node) {
+ const name = node.inputs[uel.downstream_slot]?.name;
+ if (name) {
+ const widget = node.widgets?.find((w)=>(w.name==name))
+ if (widget && !widget.disabled) {
+ try {
+ widget._disabled = widget.disabled;
+ widget.disabled = true;
+ this.widgets_disabled.push(widget)
+ } catch (e) { Logger.log_error(e, `Error disabling widget ${name} on node ${node.id}`) }
+ }
+ }
+ }
+ })
+ }
+
+ enable_all_disabled_widgets() {
+ this.widgets_disabled.forEach((w)=>{
+ try { w.disabled=w._disabled } catch (e) { Logger.log_error(e, `Error enabling widget ${w}`) }
+ })
+ }
+
+ highlight_subgraph_node_connections(subgraph, ctx) {
+ if (!settingsCache.getSettingValue('Use Everywhere.Graphics.highlight')) return;
+ this.ue_list.all_connected_inputs(subgraph.outputNode).forEach((ue_connection)=>{
+ const pos2 = subgraph?.outputNode?.slots[ue_connection.input_index]?.pos;
+ if (pos2) {
+ drawcircle(ctx, pos2, CONNECTED_1, 5, "first", {strokeStyle:LGraphCanvas.link_type_colors[ue_connection.type]})
+ drawcircle(ctx, pos2, CONNECTED_2, 4, "last")
+ }
+ })
+ }
+
+ highlight_ue_connections(node, ctx) {
+ if (!settingsCache.getSettingValue('Use Everywhere.Graphics.highlight')) return;
+ if (!node.inputs) return;
+
+ try {
+ this.pause('highlight_ue_connections')
+ if (!this._list_ready()) return;
+
+ // get all the inputs that can be connected and aren't
+ // connected with a 'real' link
+ const unconnected_connectable_names = new Set(node.inputs
+ .filter((input)=>is_connectable(node,input.name))
+ .filter((input)=>(!input.link))
+ .map((input)=>input.name) || [])
+
+ if (this.ue_list.all_connected_inputs) {
+ this.ue_list.all_connected_inputs(node).forEach((ue_connection) => {
+ if (!ue_connection.control_node) return;
+ if (!node.inputs[ue_connection.input_index]) return
+
+ const name_sent_to = node.inputs[ue_connection.input_index].name;
+ unconnected_connectable_names.delete(name_sent_to); // remove the name from the list of connectables
+ const pos2 = this._relative_connection_pos(node, true, ue_connection.input_index);
+
+ drawcircle(ctx, pos2, CONNECTED_1, 6, "first", {strokeStyle:LGraphCanvas.link_type_colors[ue_connection.type]} )
+ drawcircle(ctx, pos2, CONNECTED_2, 5, "last" )
+ });
+ }
+
+ unconnected_connectable_names.forEach((name) => {
+ const index = node.inputs.findIndex((i) => i.name == name);
+ const pos2 = this._relative_connection_pos(node, true, index);
+ drawcircle(ctx, pos2, {...UNCONNECTED_CONNECTABLE}, 3)
+ })
+
+ if (node.properties.ue_convert) {
+ const sending_slots = this.ue_list.all_sending_slots(node)
+
+ node.outputs.forEach((output,i) => {
+ if (is_able_to_broadcast(node, output.name)) {
+ const pos2 = this._relative_connection_pos(node, false, i);
+ if (sending_slots.has(i)) {
+ drawcircle(ctx, pos2, CONNECTED_1, 6, "first", {strokeStyle:LGraphCanvas.link_type_colors[node.outputs[i].type]})
+ drawcircle(ctx, pos2, CONNECTED_2, 5, "last")
+ } else {
+ drawcircle(ctx, pos2, UNCONNECTED_CONNECTABLE, 3)
+ }
+
+ }
+ })
+ }
+
+ shared.graphAnalyser.ambiguities.filter((ambiguity)=>(ambiguity.id==node.id)).forEach((ambiguity)=>{
+ const index = node.inputs.findIndex((input)=>(input.name==ambiguity.input))
+ if (index>=0) {
+ const pos2 = this._relative_connection_pos(node, true, index);
+ drawcross(ctx, pos2, AMBIGUITY, 7)
+ }
+ })
+
+ } catch (e) {
+ Logger.log_error(e);
+ } finally {
+ this.unpause()
+ }
+ }
+
+ _relative_connection_pos(node, input, slot) {
+ var pos2;
+ if (node.getSlotPosition) {
+ pos2 = node.getSlotPosition(slot, input)
+ } else {
+ pos2 = node.getConnectionPos?.(input, slot, this.slot_pos1) || node.slots[slot].pos;
+ }
+ pos2[0] -= node.pos[0];
+ pos2[1] -= node.pos[1];
+ return pos2
+ }
+
+ _list_ready() {
+ if (!shared.graphAnalyser) return false; // we don't have the analyser yet (still loading)
+ if (!this.ue_list) {
+ this.mark_link_list_outdated();
+ return false;
+ }
+ return true;
+ }
+
+ node_sending_anywhere(node) {
+ if (!this._list_ready()) return false;
+ return this.ue_list.node_sending_anywhere(node);
+ }
+
+ node_in_ueconnection(ue_connection, id) {
+ if (ue_connection.control_node?.id == id) return true
+ if (ue_connection.sending_to?.id == id) return true
+ }
+
+ any_node_in_ueconnection(ue_connection, list_of_nodes) {
+ return (Object.values(list_of_nodes).find((node) => (this.node_in_ueconnection(ue_connection, node.id))))?true:false;
+ }
+
+ render_all_ue_links(ctx) {
+ maybe_show_tooltip()
+ if (this.paused()) return;
+ try {
+ this.pause('render_all_ue_links')
+ this._render_all_ue_links(ctx);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.unpause()
+ }
+ }
+
+ _render_all_ue_links(ctx) {
+ if (!this._list_ready()) return;
+ this.last_used_ue_list = this.ue_list;
+
+ ctx.save();
+ const orig_hqr = app.canvas.highquality_render;
+ app.canvas.highquality_render = false;
+
+ const mode = settingsCache.getSettingValue('Use Everywhere.Graphics.showlinks');
+ var animate = settingsCache.getSettingValue('Use Everywhere.Graphics.animate');
+ if (settingsCache.getSettingValue('Use Everywhere.Graphics.stop_animation_when_running') && this.queue_size>0) animate = 0;
+ if (animate==2 || animate==3) this.animate_step(ctx);
+
+ var any_links_shown = false;
+ var any_links = false;
+
+ shared.graphAnalyser.ambiguities.forEach((ambiguity)=>{
+ ambiguity.matches.forEach((m)=>{
+ if (app.canvas.node_over == m.node) {
+ this._render_ue_link(
+ {
+ graph : m.node.graph,
+ sending_to : m.node,
+ input_index : m.node_index,
+ control_node : m.node.graph.getNodeById(m.id),
+ control_node_input_index : m.index,
+ type : m.type,
+ }, ctx, 0, "#F00")
+ }
+
+ })
+ })
+
+
+ this.ue_list.all_ue_connections().forEach((ue_connection) => {
+ any_links = true;
+ var show = false;
+ if ( mode==4 ) show = true;
+ if ( (mode==2 || mode==3) && app.canvas.node_over && this.node_in_ueconnection(ue_connection, app.canvas.node_over.id) ) show = true;
+ if ( (mode==1 || mode==3) && this.any_node_in_ueconnection(ue_connection, app.canvas.selected_nodes)) show = true;
+
+ show = show && in_visible_graph(ue_connection.control_node) && in_visible_graph(ue_connection.sending_to);
+ if ( show ) {
+ this._render_ue_link(ue_connection, ctx, animate);
+ any_links_shown = true;
+ }
+ });
+
+ if (animate>0) {
+ /*
+ If animating, we want to mark the visuals as changed so the animation updates - but not often!
+ If links shown:
+ - If showing dots, wait 30ms
+ - Otherwise, wait 100ms
+ If no links are shown
+ - If there are links, and our mode is mouseover, wait 200ms
+ - Otherwise don't request an update (there are no links that could be shown without something else requesting a redraw)
+ */
+ const timeout = (any_links_shown) ? ((animate%2 == 1) ? 30 : 100) : ((mode==2 || mode==3) && any_links) ? 200 : -1;
+ if (timeout>0) setTimeout( app.graph.change.bind(app.graph), timeout );
+ }
+
+ app.canvas.highquality_render = orig_hqr;
+ ctx.restore();
+
+ }
+
+ _render_ue_link(ue_connection, ctx, animate, color) {
+ try {
+ const graph = ue_connection.graph
+ const node = get_real_node(ue_connection.sending_to.id, graph);
+
+ /* this is the end node; get the position of the input */
+ var pos2;
+
+ if (node.getSlotPosition) {
+ pos2 = node.getSlotPosition(ue_connection.input_index, true)
+ } else {
+ pos2 = node.getConnectionPos(true, ue_connection.input_index, this.slot_pos1);
+ }
+
+ const control_node = graph.getNodeById(ue_connection.control_node.id)
+ if (!control_node) {
+ Logger.problem(`Couldn't find position for UE link ${ue_connection}.`,null,true)
+ return;
+ }
+
+
+ /*
+ get the position of the *input* that is being echoed except for:
+ - the Seed Anywhere node: the class records control_node_input_index as -ve (-1 => 0, -2 => 1...)
+ - subgraph converts
+ those link to outputs
+ */
+ const input_source = (ue_connection.control_node_input_index >= 0 && !control_node.properties.ue_convert);
+ const source_index = (ue_connection.control_node_input_index >= 0 ) ? ue_connection.control_node_input_index : -1-ue_connection.control_node_input_index;
+ var pos1;
+
+ if (control_node.getSlotPosition) {
+ pos1 = control_node.getSlotPosition(source_index, input_source)
+ } else {
+ pos1 = control_node.getConnectionPos(input_source, source_index, this.slot_pos2);
+ }
+
+ /* get the direction that we start and end */
+ const delta_x = pos2[0] - pos1[0];
+ const delta_y = pos2[1] - pos1[1];
+ const end_direction = LiteGraph.LEFT; // always end going into an input
+ const sta_direction = ((Math.abs(delta_y) > Math.abs(delta_x)) && input_source) ?
+ ((delta_y>0) ? LiteGraph.DOWN : LiteGraph.UP) :
+ ((input_source && delta_x<0) ? LiteGraph.LEFT : LiteGraph.RIGHT)
+
+ color = color || LGraphCanvas.link_type_colors[ue_connection.type] || app.canvas.default_link_color;
+
+ ctx.save()
+ const rcb = app.canvas.render_connections_border;
+ try {
+ var skip_border = false
+ if (settingsCache.getSettingValue( "Use Everywhere.Graphics.fuzzlinks" )) {
+ app.canvas.render_connections_border = false
+ ctx.shadowColor = color;
+ ctx.shadowBlur = 6;
+ skip_border = true
+ }
+ app.canvas.renderLink(ctx, pos1, pos2, undefined, skip_border, animate%2, modify(color), sta_direction, end_direction, undefined);
+ } catch(e) {
+ Logger.log_error(e);
+ } finally {
+ ctx.restore()
+ app.canvas.render_connections_border = rcb;
+ }
+
+ } catch (e) {
+ Logger.log_error(e, `Couldn't render UE link ${ue_connection}. That's ok if something just got deleted.`);
+ }
+ }
+
+ animate_step(ctx) {
+ const max_blur = 8;
+ const speed = 0.75;
+ var f = (LiteGraph.getTime()*0.001*speed) % 1;
+ const step = Math.ceil(f*2*max_blur) % (2*max_blur);
+ ctx.shadowBlur = (step{
+ if (DEFAULTS[k]!==undefined) ctx[k] = DEFAULTS[k]
+ if (properties[k]!==undefined) ctx[k] = properties[k]
+ if (additional && additional[k]!==undefined) ctx[k] = additional[k]
+ })
+ ctx.beginPath();
+ ctx.roundRect(position[0]-radius,position[1]-radius,2*radius,2*radius,radius);
+ ctx.stroke();
+ if (!first_last || first_last=='last') ctx.restore()
+}
+
+function drawcross(ctx, position, properties, radius) {
+ ctx.save()
+ PROPS.forEach((k)=>{
+ if (DEFAULTS[k]) ctx[k] = DEFAULTS[k]
+ })
+ ctx.lineWidth = properties.lineWidth
+ ctx.strokeStyle = properties.strokeStyle
+ ctx.beginPath();
+ ctx.moveTo(position[0]-radius,position[1]-radius)
+ ctx.lineTo(position[0]+radius,position[1]+radius)
+ ctx.moveTo(position[0]-radius,position[1]+radius)
+ ctx.lineTo(position[0]+radius,position[1]-radius)
+ ctx.stroke();
+ ctx.restore()
+}
+
+function modify(c) {
+ if (c.length==4) return c + "6"
+ if (c.length==7) return c + "66"
+ return c
+}
diff --git a/v3-nodes/cg-use-everywhere/js/use_everywhere_utilities.js b/v3-nodes/cg-use-everywhere/js/use_everywhere_utilities.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc1637864dbb9b4fbc0d5b9fbf9663a8135aa502
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/js/use_everywhere_utilities.js
@@ -0,0 +1,390 @@
+import { app } from "../../scripts/app.js";
+import { settingsCache } from "./use_everywhere_cache.js";
+import { link_is_from_subgraph_input, visible_graph, wrap_input, connection_from_output_as_input } from "./use_everywhere_subgraph_utils.js";
+import { i18n } from "./i18n.js";
+import { ue_callbacks } from "./recursive_callbacks.js";
+import { shared } from "./shared.js";
+
+export function create( tag, clss, parent, properties ) {
+ const nd = document.createElement(tag);
+ if (clss) clss.split(" ").forEach((s) => nd.classList.add(s))
+ if (parent) parent.appendChild(nd);
+ if (properties) Object.assign(nd, properties);
+ return nd;
+}
+
+/*
+Return 1 if x is a later version than y (or y is not defined)
+Return -1 if x is an earlier version than y (or x is not defined)
+Return 0 if they are the same version
+*/
+function version_compare(x,y) {
+ if (x==y) return 0
+ if (!y) return 1
+ if (!x) return -1
+ const xbits = x.split('.')
+ const ybits = y.split('.')
+ var result = 0
+ for (var i=0; result!=0 && i parseInt(ybits[i])) result = 1
+ }
+ if (result==0) {
+ if (xbits.length < ybits.length) result = -1
+ if (xbits.length > ybits.length) result = 1
+ }
+ return result
+}
+
+export function version_at_least(x,y) {
+ return (version_compare(x,y) >= 0)
+}
+
+/*
+Return the node object for this node_id.
+*/
+export function get_real_node(node_id, graph) {
+ if (!graph) graph = visible_graph()
+ const nid = node_id.toString();
+ if (nid==-10) return wrap_input(graph.inputNode); // special case for subgraph input
+ if (nid==-20) return wrap_input(graph.outputNode); // special case for subgraph input
+ return graph.getNodeById(nid);
+}
+
+export class Logger {
+ static LIMITED_LOG_BLOCKED = false;
+ static LIMITED_LOG_MS = 5000;
+ static level; // 0 for errors only, 1 activates 'log_problem', 2 activates 'log_info', 3 activates 'log_detail'
+
+ static log_shared(message, trace, limited) {
+ if (limited && Logger.check_limited()) return false
+ console.log(message)
+ if (trace) console.trace()
+ shared.report_keys.forEach((key)=>{
+ console.log( `${key.padStart(40,' ')} = ${shared[key]}` )
+ })
+ }
+
+ static log_arguments(a) {
+ Object.keys(arguments).forEach((k)=>{console.log(arguments[k])})
+ }
+
+ static log_error(message, more) {
+ if (more) console.log(more)
+ console.error(message)
+ }
+
+ static log(message, foreachable, limited) {
+ if (limited && Logger.check_limited()) return false
+ console.log(message);
+ foreachable?.forEach((x)=>{console.log(x)})
+ return true
+ }
+
+ static log_with_trace() {
+ if (Logger.log(arguments)) console.trace()
+ }
+
+ static check_limited() {
+ if (Logger.LIMITED_LOG_BLOCKED) return true
+ Logger.LIMITED_LOG_BLOCKED = true
+ setTimeout( ()=>{Logger.LIMITED_LOG_BLOCKED = false}, Logger.LIMITED_LOG_MS )
+ return false
+ }
+
+ static null() {}
+
+ static level_changed(new_level) {
+ Logger.level = new_level
+ Logger.log_detail = (Logger.level>=3) ? Logger.log : Logger.null
+ Logger.log_info = (Logger.level>=2) ? Logger.log : Logger.null
+ Logger.log_problem = (Logger.level>=1) ? Logger.log_with_trace : Logger.null
+ }
+
+ static log_detail(){}
+ static log_info(){}
+ static log_problem(){}
+}
+
+Logger.level_changed(settingsCache.getSettingValue('Use Everywhere.Options.logging'))
+settingsCache.addCallback('Use Everywhere.Options.logging', Logger.level_changed)
+
+class GraphConverter {
+ static _instance;
+ static instance() {
+ if (!GraphConverter._instance) GraphConverter._instance = new GraphConverter();
+ return GraphConverter._instance;
+ }
+
+ constructor() {
+ this.node_input_map = {};
+ this.given_message = false;
+ this.did_conversion = false;
+ }
+
+ //running_116_plus() {
+ // const version = __COMFYUI_FRONTEND_VERSION__.split('.')
+ // return (parseInt(version[0])>=1 && (parseInt(version[0])>1 || parseInt(version[1])>=16))
+ //}
+
+ store_node_input_map(data) {
+ this.node_input_map = {};
+ data?.nodes.filter((node)=>(node.inputs)).forEach((node) => { this.node_input_map[node.id] = node.inputs.map((input) => input.name); })
+ Logger.log_detail("stored node_input_map", this.node_input_map);
+ }
+
+
+ clean_ue_node(node) {
+ var expected_inputs = 1
+ if (node.type == "Seed Everywhere") expected_inputs = 0
+ if (node.type == "Prompts Everywhere") expected_inputs = 2
+ if (node.type == "Anything Everywhere3") expected_inputs = 3
+ if (node.type == "Anything Everywhere?") expected_inputs = 4
+
+ // remove all the 'anything' inputs (because they may be duplicated)
+ const removed = node.inputs.filter(i=>(i.label==i18n('anything') || i.label=='*'))
+ node.inputs = node.inputs.filter(i=>(i.label!=i18n('anything') && i.label!='*'))
+ // add them back as required
+ while (node.inputs.length < expected_inputs) { node.inputs.push(removed.pop()) }
+ // the input comes before the regex widgets in UE?
+ if (expected_inputs==4) {
+ while(node.inputs[0].name.includes('regex')) {
+ node.inputs.unshift(node.inputs.pop())
+ }
+ }
+ // fix the localized names
+ node.inputs = node.inputs.forEach((input) => {
+ if (input) {
+ if (!input.localized_name || input.localized_name.startsWith(i18n('anything'))) input.localized_name = input.name
+ } else {
+ let brteakpoint;
+ }
+ })
+
+ // set types to match
+ if (node.inputs) {
+ node.inputs.forEach((input) => {
+ if (input.type=='*') {
+ const graph = node.graph;
+ if (input.link) {
+ const llink = graph.links[input.link];
+ if (link_is_from_subgraph_input(llink)) {
+ input.type = get_subgraph_input_type(graph, llink.origin_slot);
+ } else {
+ input.type = llink.type;
+ }
+ } else {
+ input.type = (input.label && input.label!=i18n('anything')) ? input.label : input.name
+ }
+ }
+ });
+
+ Logger.log_detail(`clean_ue_node ${node.id} (${node.type})`, node.inputs);
+ }
+ }
+
+ convert_if_pre_116(node) {
+ if (!node) return;
+
+ if (node.properties?.ue_properties?.widget_ue_connectable) return
+ if (node.properties?.widget_ue_connectable) return // pre 7.0 node which will be converted
+
+ if (is_UEnode(node)) {
+ if (node.properties?.ue_properties?.version) return
+ this.clean_ue_node(node)
+ }
+
+ if (!this.given_message) {
+ Logger.log_info(`Graph was saved with a version of ComfyUI before 1.16, so Anything Everywhere will try to work out which widgets are connectable`);
+ this.given_message = true;
+ }
+
+ if (!node.properties.ue_properties) node.properties.ue_properties = {}
+ node.properties.ue_properties['widget_ue_connectable'] = {}
+ const widget_names = node.widgets?.map(w => w.name) || [];
+
+ if (!(this.node_input_map[node.id])) {
+ Logger.log_detail(`node ${node.id} (${node.type} has no node_input_map`);
+ } else {
+ this.node_input_map[node.id].filter((input_name)=>widget_names.includes(input_name)).forEach((input_name) => {
+ node.properties.ue_properties['widget_ue_connectable'][input_name] = true;
+ this.did_conversion = true;
+ Logger.log_info(`node ${node.id} widget ${input_name} marked as accepting UE because it was an input when saved`);
+ });
+ }
+ }
+
+ remove_saved_ue_links(graph) {
+ if (graph.extra?.links_added_by_ue) {
+ graph.extra.links_added_by_ue.forEach((link_id) => { app.graph.links.delete(link_id); })
+ }
+ }
+}
+
+export const graphConverter = GraphConverter.instance();
+ue_callbacks.register_allgraph_callback('afterConfigureGraph', graphConverter.remove_saved_ue_links)
+
+/*
+Is a node alive (ie not bypassed or set to never)
+*/
+export function node_is_live(node, treat_bypassed_as_live){
+ if (!node) return false;
+ if (node.mode===0) return true;
+ if (node.mode===2 || node.mode===4) return !!treat_bypassed_as_live;
+ Logger.log_error(`node ${node.id} has mode ${node.mode} - I only understand modes 0, 2 and 4`);
+ return true;
+}
+
+function node_is_bypassed(node) {
+ return (node.mode===4);
+}
+
+/*
+Given a link object, and the type of the link,
+go upstream, following links with the same type, until you find a parent node which isn't bypassed.
+If either type or original link is null, or if the upstream thread ends, return null
+*/
+function handle_bypass(original_link, type, graph) {
+ if (!type || !original_link) return null;
+ var link = original_link;
+ if (link_is_from_subgraph_input(link)) return link
+ var parent = get_real_node(link.origin_id, graph);
+ if (!parent) return null;
+ while (node_is_bypassed(parent)) {
+ if (!parent.inputs) return null;
+ var link_id;
+ if (parent?.inputs[link.origin_slot]?.type == type) link_id = parent.inputs[link.origin_slot].link; // try matching number first
+ else link_id = parent.inputs.find((input)=>input.type==type)?.link;
+ if (!link_id) { return null; }
+ link = graph.links[link_id];
+ parent = get_real_node(link.origin_id, graph);
+ }
+ return link;
+}
+
+
+/*
+Does this input connect upstream to a live node?
+*/
+export function is_connected(input, treat_bypassed_as_live, graph) {
+ const link_id = input.link;
+ if (link_id === null) return false; // no connection
+ var the_link = graph.links[link_id];
+ if (!the_link) return false;
+ if (treat_bypassed_as_live) return true;
+ the_link = handle_bypass(the_link, the_link.type, graph); // find the link upstream of bypasses
+ if (!the_link) return false; // no source for data.
+ return true;
+}
+
+/*
+Is this a UE node?
+*/
+export function is_UEnode(node) {
+ const type = node.comfyClass;
+ if (type == "Anything Everywhere") return true;
+ return ((type) && (type.startsWith("Anything Everywhere") || type==="Seed Everywhere" || type==="Prompts Everywhere"))
+}
+
+export function node_can_broadcast(node) {
+ return (node.properties?.ue_convert || is_UEnode(node))
+}
+
+
+
+/*
+Inject a call into a method on object with name methodname.
+The injection is added at the end of the existing method (if the method didn't exist, it is created)
+injectionthis and injectionarguments are passed into the apply call (as the this and the arguments)
+*/
+export function inject(object, methodname, tracetext, injection, injectionthis, injectionarguments) {
+ const original = object[methodname];
+ object[methodname] = function() {
+ original?.apply(this, arguments);
+ injection.apply(injectionthis, injectionarguments);
+ }
+}
+
+export class Pausable {
+ constructor(name) {
+ this.name = name
+ this.pause_depth = 0
+ }
+ pause(note, ms) {
+ this.pause_depth += 1;
+ if (this.pause_depth>10) {
+ Logger.log_error(`${this.name} Over pausing`)
+ }
+ Logger.log_detail(`${this.name} pause ${note} with ${ms}`)
+ if (ms) setTimeout( this.unpause.bind(this), ms );
+ }
+ unpause() {
+ this.pause_depth -= 1
+ Logger.log_detail(`${this.name} unpause`)
+ if (this.pause_depth<0) {
+ Logger.log_error(`${this.name} Over unpausing`)
+ this.pause_depth = 0
+ }
+ this.on_unpause()
+ }
+ paused() {
+ return (this.pause_depth>0)
+ }
+ on_unpause(){}
+}
+
+export function get_connection(node, i) {
+ const graph = node.graph
+ const in_link = node?.inputs[i]?.link;
+ if (in_link) {
+ var llink = graph.links[in_link]
+ llink = handle_bypass(llink, llink.type, graph)
+ if (!llink) {
+ Logger.log(`handle_bypass failing - subgraph issue?`, null, true)
+ llink = graph.links[in_link]
+ }
+ return { link:llink, type:llink.type }
+ } else {
+ return { link:undefined, type:undefined }
+ }
+}
+
+export function is_able_to_broadcast(node, output_name) {
+ if (!node.properties.ue_convert) return false
+ const output = node.outputs.find(i => i.name==output_name);
+ if (!output) {
+ Logger.log_error(`Can't find output ${output_name} on node ${node.title}`);
+ return false;
+ }
+ return ! (node.properties?.ue_properties?.output_not_broadcasting?.[output_name])
+}
+
+export function find_duplicate_broadcasted_types(node) {
+ var the_possibles
+ var connection_finder
+ var check_if_able_to_broadcast
+
+ if (node.properties.ue_convert) {
+ the_possibles = node.outputs
+ connection_finder = connection_from_output_as_input
+ check_if_able_to_broadcast = (node, i) => {
+ const name = node.outputs[i].name
+ return is_able_to_broadcast(node, name)
+ }
+ } else {
+ check_if_able_to_broadcast = ()=>(true)
+ the_possibles = node.inputs
+ connection_finder = get_connection
+ }
+
+ const broadcasted_types = new Set()
+ const duplicated_broadcasted_types = new Set()
+ for (var i=0; i=1.25.0"
+]
+classifiers = [
+ "Operating System :: OS Independent"
+]
+
+[project.urls]
+Repository = "https://github.com/chrisgoringe/cg-use-everywhere"
+Documentation = "https://github.com/chrisgoringe/cg-use-everywhere/blob/main/README.md"
+
+[tool.comfy]
+PublisherId = "chrisgoringe"
+DisplayName = "CG Use Everywhere"
+Icon = "https://raw.githubusercontent.com/chrisgoringe/cg-use-everywhere/refs/heads/main/docs/icon.png"
diff --git a/v3-nodes/cg-use-everywhere/tests/compare.png b/v3-nodes/cg-use-everywhere/tests/compare.png
new file mode 100644
index 0000000000000000000000000000000000000000..5dee9acff70d76e11ca1e005a5fd13621f3294c8
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/tests/compare.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9a7f444af75e4ee892eccaeb44aa0dce884202b77edabff18cd420aa73fc1d9
+size 317837
diff --git a/v3-nodes/cg-use-everywhere/tests/test.md b/v3-nodes/cg-use-everywhere/tests/test.md
new file mode 100644
index 0000000000000000000000000000000000000000..a40be5bf6ec22c0fcf7e1afb5aac057d618782bf
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/tests/test.md
@@ -0,0 +1,3 @@
+# Testing
+
+Any image in this folder should have it's workflow saved with it, and that workflow should generate the same image.
\ No newline at end of file
diff --git a/v3-nodes/cg-use-everywhere/tests/test.png b/v3-nodes/cg-use-everywhere/tests/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..712d40f44ba0e0175dedd9e0deeef065cfead8f5
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/tests/test.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9ef7839143574b3eeacc71ef7f9657665a0615c011cfb49f5577d4eb36d9861d
+size 321804
diff --git a/v3-nodes/cg-use-everywhere/tests/test2.png b/v3-nodes/cg-use-everywhere/tests/test2.png
new file mode 100644
index 0000000000000000000000000000000000000000..85d46ab1b4db9ab6b7c3f4d6db69d3720319b882
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/tests/test2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:622201e7faf63da4bbed5a36a30deb7c5044ecf3cb16406df6d916a77102370c
+size 318043
diff --git a/v3-nodes/cg-use-everywhere/use_everywhere.py b/v3-nodes/cg-use-everywhere/use_everywhere.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc0d2ae8e25d27c326b4ca0478caf7ceb7d2d85a
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/use_everywhere.py
@@ -0,0 +1,153 @@
+
+from comfy.comfy_types.node_typing import IO
+from comfy_api.latest import io
+
+anything = io.Custom(IO.ANY)
+
+class ComboClone(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Combo Clone",
+ category = "everywhere",
+ display_name = "Combo Clone",
+ description = "The combo on this node will replicate whatever the output is connected to",
+ inputs = [
+ io.Combo.Input("combo", options=['connect me to a combo widget']),
+ ],
+ outputs = [
+ anything.Output("comboout"),
+ ],
+ )
+
+ @classmethod
+ def validate_inputs(cls, **kwargs) -> bool:
+ return isinstance(kwargs.get('combo', None),str)
+
+ @classmethod
+ def execute(cls, combo): # type: ignore
+ return io.NodeOutput(combo)
+
+class SimpleString(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Simple String",
+ category = "everywhere/deprecated",
+ display_name = "Simple String",
+ description = "Deprecated - use the core comfy string",
+ is_deprecated = True,
+ inputs = [
+ io.String.Input("string", default=""),
+ ],
+ outputs = [
+ io.String.Output("stringout"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, string): # type: ignore
+ return io.NodeOutput(string)
+
+class SeedEverywhere(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Seed Everywhere",
+ category = "everywhere/deprecated",
+ display_name = "Seed Everywhere",
+ description = "Deprecated - should automatically be replaced",
+ is_deprecated = True,
+ inputs = [
+ io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
+ ],
+ outputs = [
+ io.Int.Output("int"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, seed): # type: ignore
+ return io.NodeOutput(seed)
+
+class AnythingEverywhere(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Anything Everywhere",
+ category = "everywhere",
+ display_name = "Anything Everywhere",
+ inputs = [
+ anything.Input("anything", optional=True),
+ ],
+ outputs = [ ],
+ )
+
+ @classmethod
+ def execute(cls, **kwargs):
+ return io.NodeOutput()
+
+class AnythingEverywherePrompts(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Prompts Everywhere",
+ category = "everywhere/deprecated",
+ display_name = "Anything Everywhere Prompts",
+ description = "Deprecated - should automatically be replaced",
+ is_deprecated = True,
+ inputs = [
+ anything.Input("positive", display_name="+ve", optional=True),
+ anything.Input("negative", display_name="-ve", optional=True),
+ ],
+ outputs = [ ],
+ )
+
+ @classmethod
+ def execute(cls, **kwargs):
+ return io.NodeOutput()
+
+class AnythingEverywhereTriplet(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Anything Everywhere3",
+ category = "everywhere/deprecated",
+ display_name = "Anything Everywhere Triplet",
+ description = "Deprecated - should automatically be replaced",
+ is_deprecated = True,
+ inputs = [
+ anything.Input("anything", display_name="anything", optional=True),
+ anything.Input("anything2", display_name="anything2", optional=True),
+ anything.Input("anything3", display_name="anything3", optional=True),
+ ],
+ outputs = [ ],
+ )
+
+ @classmethod
+ def execute(cls, **kwargs):
+ return io.NodeOutput()
+
+class AnythingSomewhere(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id = "Anything Everywhere?",
+ category = "everywhere/deprecated",
+ display_name = "Anything Somewhere",
+ description = "Deprecated - should automatically be replaced",
+ is_deprecated = True,
+ inputs = [
+ anything.Input("anything", display_name="anything", optional=True),
+ io.String.Input("title_regex", default="", optional=True),
+ io.String.Input("input_regex", default="", optional=True),
+ io.String.Input("group_regex", default="", optional=True),
+ ],
+ outputs = [ ],
+ )
+
+ @classmethod
+ def execute(cls, **kwargs):
+ return io.NodeOutput()
+
+
diff --git a/v3-nodes/cg-use-everywhere/workflow_fixer.py b/v3-nodes/cg-use-everywhere/workflow_fixer.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2fd7ea42f82070816bde52877fda5c151b1a3e7
--- /dev/null
+++ b/v3-nodes/cg-use-everywhere/workflow_fixer.py
@@ -0,0 +1,22 @@
+import json, sys
+
+INFO = '''
+If you saved a json workflow using 'Anything Everywhere?' nodes before the third regex was added, then you may find that when you load it, the Group Regex widget doesn't correctly default to '.*'.
+
+If so, run python workflow_fixer.py filename.json newname.json to fix it.
+'''
+
+def convert(oldname, newname):
+ with open(oldname) as f: workflow = json.load(f)
+ for node in workflow['nodes']:
+ if node['type'] == "Anything Everywhere?":
+ print(f"Fixing {node['title'] if 'title' in node else 'Untitled AE? node'}...")
+ node['widgets_values'][2] = '.*'
+ with open(newname,'w') as f: print(json.dumps(workflow, indent=2), file=f)
+
+if __name__=='__main__':
+ if len(sys.argv)!=3:
+ print(INFO)
+ else:
+ convert(sys.argv[1], sys.argv[2])
+