diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/.gitattributes b/ComfyUI/custom_nodes/cg-use-everywhere/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..f13e053bf0ebf99d69b8e28c0f02eb346dcfe15e --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/.github/workflows/publish_action.yml b/ComfyUI/custom_nodes/cg-use-everywhere/.github/workflows/publish_action.yml new file mode 100644 index 0000000000000000000000000000000000000000..abda881760b91dcbe7c8cfd5b347f452938537ad --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/.github/workflows/publish_action.yml @@ -0,0 +1,20 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/.gitignore b/ComfyUI/custom_nodes/cg-use-everywhere/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..7d3fc697da9414cdb86778980808463c73175870 --- /dev/null +++ b/ComfyUI/custom_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/ComfyUI/custom_nodes/cg-use-everywhere/LICENSE b/ComfyUI/custom_nodes/cg-use-everywhere/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..deeea2d8ccdb1354f351a6ea02ed456849d51422 --- /dev/null +++ b/ComfyUI/custom_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/ComfyUI/custom_nodes/cg-use-everywhere/README.md b/ComfyUI/custom_nodes/cg-use-everywhere/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4ef6f6c77cb9e688e8399336131cc1e865c9e34f --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/README.md @@ -0,0 +1,251 @@ +# 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-picker) for another way to make some workflows smoother. And leave a star if you like something! + +--- + +## 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| +|-|-| +|![screen](docs/test-workflow-screenshot.png)|![image](docs/test-workflow.png)| + +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 + +4.9 (2nd May 2024) +- Fix incompatibility with Efficiency Nodes (#182) + +4.8 (18th March 2024) +- Group and color sending have a `send to unmatched` mode +- UE link animations can be the classic dots, or a pulsing glow (or both, or neither) +- Show UE links can now be on, off, mouseover, selected nodes, or mouseover and selected nodes + +4.7 (1st March 2024) +- UE now works in group nodes +- Autocomplete on `Anything Everywhere?` nodes + +4.6 +- add Group Regex to `Anything Everywhere?` node +- if you have workflow json files saved that now don't work, try 'workflow_fixer.py' + +4.5 +- add support for Comfy UI Group Nodes (UE nodes can be used to connect to group node inputs and outputs, but not within a group node) +- add `convert to real links` + +4.4 +- add (limited) support for converting regex in the `Anything Everywhere?` node with inputs (only works if the link is from a node that is a simple string source) + +4.3 +- added support for targetting [Highway nodes](https://github.com/chrisgoringe/cg-use-everywhere#highway-nodes) + +4.2 +- improved performance of loop detection, especially with [highway nodes](https://github.com/Trung0246/ComfyUI-0246) +- updated docs to not use other custom nodes in examples + +4.1.2 +- tweaks to improve handling of bypass +- fixed connecting to Seed Everywhere + +4.1.1 +- added option to turn animation off + +4.1 + +- added [loop detection](https://github.com/chrisgoringe/cg-use-everywhere#loop-checking) +- added [group restriction](https://github.com/chrisgoringe/cg-use-everywhere#group-restriction). + +The v1 nodes have been fully removed. If you were using one, you can just replace it with an `Anything Everywhere` node. + +## 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.) + +![regex](docs/regex.png) + +*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.| +|-|-| +|![Alt text](docs/image.png)|![no](docs/imagex.png)| + + +## 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. + +![UE3](docs/UE3.png) + +## 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| +|-|-| +|![pe](docs/PE.png)|![pe](docs/conditioning.png) + +# 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: + +![sample and sigma](docs/sampler%20and%20sigma.png) + +For more on this, see [this discussion](https://github.com/chrisgoringe/cg-use-everywhere/issues/69) + +# Other features + +## 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. + +![auto](docs/auto.gif) + +## 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: +![highway](docs/highway.png) + +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 | +|-|-| +|![1](docs/bypass_catch1.png)|![2](docs/bypass_catch2.png)| + +This is unlikely to be fixed, but should be fairly easy to avoid! diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/__init__.py b/ComfyUI/custom_nodes/cg-use-everywhere/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..001408c3ec41845c39d66240bf07deefc509c1a1 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/__init__.py @@ -0,0 +1,30 @@ +from .use_everywhere import SeedEverywhere, AnythingEverywherePrompts + +UE_VERSION = "4.8.5" + +NODE_CLASS_MAPPINGS = { "Seed Everywhere": SeedEverywhere } + +from .use_everywhere import AnythingEverywhere, AnythingSomewhere, AnythingEverywhereTriplet, SimpleString +NODE_CLASS_MAPPINGS["Anything Everywhere"] = AnythingEverywhere +NODE_CLASS_MAPPINGS["Anything Everywhere3"] = AnythingEverywhereTriplet +NODE_CLASS_MAPPINGS["Anything Everywhere?"] = AnythingSomewhere +NODE_CLASS_MAPPINGS["Prompts Everywhere"] = AnythingEverywherePrompts +NODE_CLASS_MAPPINGS["Simple String"] = SimpleString + +import os, shutil +import folder_paths + +# temporary code to remove old javascript installs +module_js_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "js") +application_root_directory = os.path.dirname(folder_paths.__file__) +old_code_location = os.path.join(application_root_directory, "web", "extensions", "use_everywhere") +if os.path.exists(old_code_location): + shutil.rmtree(old_code_location) + +old_code_location = os.path.join(application_root_directory, "web", "extensions", "cg-nodes", "use_everywhere.js") +if os.path.exists(old_code_location): + os.remove(old_code_location) +# end of temporary code + +WEB_DIRECTORY = "./js" +__all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/ComfyUI_temp_zbfdv_00012_.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/ComfyUI_temp_zbfdv_00012_.png new file mode 100644 index 0000000000000000000000000000000000000000..cecee2f81404336daea74a67dd58a9716bbe7507 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/ComfyUI_temp_zbfdv_00012_.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/PE.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/PE.png new file mode 100644 index 0000000000000000000000000000000000000000..73b200e98ebb44a815e6a9df1a793065831639fe Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/PE.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/UE3.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/UE3.png new file mode 100644 index 0000000000000000000000000000000000000000..2a02f0299638794c063611cc3cfef2c7e941a92d Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/UE3.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/UEQ.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/UEQ.png new file mode 100644 index 0000000000000000000000000000000000000000..7f418043cfc1402cbfebaa2081c34e301d44d887 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/UEQ.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/UEQportrait.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/UEQportrait.png new file mode 100644 index 0000000000000000000000000000000000000000..29ee87ba6e7f2c8a73e9ea2eea5ec03a74e2e0fc Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/UEQportrait.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/auto.gif b/ComfyUI/custom_nodes/cg-use-everywhere/docs/auto.gif new file mode 100644 index 0000000000000000000000000000000000000000..83fe03d493e3f2b45ee5cac2ab84d595ab3234e3 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/auto.gif differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/bypass_catch1.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/bypass_catch1.png new file mode 100644 index 0000000000000000000000000000000000000000..e0a1c5b9b1bc7f1c844267c3ecdf18168eed17a1 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/bypass_catch1.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/bypass_catch2.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/bypass_catch2.png new file mode 100644 index 0000000000000000000000000000000000000000..526f500fb52ceffe3aa2f523281c601a4f090a0f Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/bypass_catch2.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/clashes.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/clashes.png new file mode 100644 index 0000000000000000000000000000000000000000..150d749dbded4fb2c179f792993c99d755d69f3d Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/clashes.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/conditioning.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/conditioning.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe8e19f094fdf7d5f2527b045f5a2c9cdb63498 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/conditioning.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/connected.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..c2188a8bd53feb2b2e3c2e635c00af8e8988ffe1 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/connected.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/connection-ui.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/connection-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..7231ef9d70146e80359373c74a0b713c3fd26859 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/connection-ui.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/deprecated.md b/ComfyUI/custom_nodes/cg-use-everywhere/docs/deprecated.md new file mode 100644 index 0000000000000000000000000000000000000000..92a06462be4ec02101704388cd40f859106180ed --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/docs/deprecated.md @@ -0,0 +1,33 @@ + +# Deprecated Nodes + +This is the old documentation, in case you have a workflow still using the deprecated nodes. + + +UE nodes are "Use Everywhere". Put a UE node into your workflow, connect its input, and every node with an unconnected input of the same type will act as if connected to it. + +CLIP, IMAGE, MODEL, VAE, CONDITIONING, or LATENT (want something else? Edit `__init__.py` line 3.) + +Update: added INT, MASK, and CHECKPOIMNT - which combines MODEL, CLIP, and VAE, and a special node for SEEDs. + +| Model, clip, vae, latent and image are all being automagically connected. | Drop this image into ComfyUI to get a working workflow. | +|-|-| +|![workflow](./workflow.png)|![portrait](./portrait.png)| + +## UE? Nodes + +UE? nodes are like UE Nodes, but add two widgets, 'title' and 'input'. These are Regular Expressions, and the node will only send to nodes where the node Title and the unconnected input name match. + +It doesn't need to be a complete match - the logic is `regex.match(name) || regex.match(title)`, so if you want to match the exact name `seed`, you'll need something like `^seed$` as your regex. + +Regex 101 - ^ means 'the start', $ means 'the end', '.' matches anything, '.*' matches any number of anything. For more than that, visit [regex101](https://regex101.com/) (the flavour you want is ECMAScript, though that probably won't matter). + +| So you can do things like: | Drop this image into ComfyUI to get a working workflow. | +|-|-| +|![this](./UEQ.png)|![drop](./UEQportrait.png)| + +## Widget? + +A UE or UE? node with just one output can have the output converted to a widget. But the combination ones can't. Also note that if you convert it to a widget, you can't then change the title + +Why not? because the code gets the data type from the input (weirdly the prompt doesn't contain the data type on outputs), and it's not available if it's a widget, because reasons, so the hack is to get the data type from what comes after `UE ` in the title... diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/group.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/group.png new file mode 100644 index 0000000000000000000000000000000000000000..268a23e4b672fea148fc9346325fa20016eb7bd9 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/group.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/highway.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/highway.png new file mode 100644 index 0000000000000000000000000000000000000000..ba907c5bf311c997b413699423955cf42b6ed3f1 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/highway.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/image.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/image.png new file mode 100644 index 0000000000000000000000000000000000000000..c517527837cf95f5ecca76a587e8e4719b5417be Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/image.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/imagex.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/imagex.png new file mode 100644 index 0000000000000000000000000000000000000000..5e6d230d5a4b1a18ca4e395f0f3f45f6953258c2 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/imagex.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/mouseOver.gif b/ComfyUI/custom_nodes/cg-use-everywhere/docs/mouseOver.gif new file mode 100644 index 0000000000000000000000000000000000000000..e722be47fe72be1af321614987f573caf6c28127 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/mouseOver.gif differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/off.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/off.png new file mode 100644 index 0000000000000000000000000000000000000000..aec0f52155972341fe1f3f6bfadc101a9eebd872 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/off.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/on.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/on.png new file mode 100644 index 0000000000000000000000000000000000000000..40eecc25ceb41110f1be8ed76f31b81c039d89fa Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/on.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/portrait.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/portrait.png new file mode 100644 index 0000000000000000000000000000000000000000..74ab43c92402bbdee01a9600a36f7d878ef63d4e Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/portrait.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/priority.gif b/ComfyUI/custom_nodes/cg-use-everywhere/docs/priority.gif new file mode 100644 index 0000000000000000000000000000000000000000..8ba2e192fab9df475beeb971d38ead37da5aa88f Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/priority.gif differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/regex.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/regex.png new file mode 100644 index 0000000000000000000000000000000000000000..2001d10b06043709a3baea0d70f9f8598a2d9c60 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/regex.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/run.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/run.png new file mode 100644 index 0000000000000000000000000000000000000000..c741d44b8fa774063779e92e50de100a319b61ff Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/run.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/sampler and sigma.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/sampler and sigma.png new file mode 100644 index 0000000000000000000000000000000000000000..a7a8e4c5a80dbe0da3f34c446f8ff1d04bd3dc43 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/sampler and sigma.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/separate.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/separate.png new file mode 100644 index 0000000000000000000000000000000000000000..717062f66fde9e5bef8b84302e2761d3637b0074 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/separate.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow-screenshot.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f0d4547c0699e69035e5dab1fe2d2b31410bb1aa Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow-screenshot.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow.json b/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow.json new file mode 100644 index 0000000000000000000000000000000000000000..eac87d7c2f27f1d49afe05ae588e02176840d497 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow.json @@ -0,0 +1,775 @@ +{ + "last_node_id": 185, + "last_link_id": 555, + "nodes": [ + { + "id": 144, + "type": "PreviewImage", + "pos": [ + 928, + -39 + ], + "size": { + "0": 430.8935546875, + "1": 533.0433349609375 + }, + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": null + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 180, + "type": "Prompts Everywhere", + "pos": [ + 1189, + -256 + ], + "size": { + "0": 177.46200561523438, + "1": 46 + }, + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "CONDITIONING", + "type": "*", + "link": 535, + "color_on": "#FFA931" + }, + { + "name": "CONDITIONING", + "type": "*", + "link": 536, + "color_on": "#FFA931" + } + ], + "properties": { + "Node name for S&R": "Prompts Everywhere", + "group_restricted": false + } + }, + { + "id": 148, + "type": "CheckpointLoaderSimple", + "pos": [ + -356, + -204 + ], + "size": { + "0": 308.89697265625, + "1": 98 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 540 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 542 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 539 + ], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "copaxVividXL_v2.safetensors" + ] + }, + { + "id": 181, + "type": "Anything Everywhere3", + "pos": [ + 332, + -204 + ], + "size": { + "0": 210, + "1": 66 + }, + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "MODEL", + "type": "*", + "link": 541, + "color_on": "#B39DDB" + }, + { + "name": "CLIP", + "type": "*", + "link": 543, + "color_on": "#FFD500" + }, + { + "name": "VAE", + "type": "*", + "link": 539, + "color_on": "#FF6E6E" + } + ], + "properties": { + "Node name for S&R": "Anything Everywhere3", + "group_restricted": false + } + }, + { + "id": 178, + "type": "EmptyLatentImage", + "pos": [ + -350, + 1 + ], + "size": { + "0": 269.2752990722656, + "1": 106 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 544 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 768, + 1024, + 1 + ] + }, + { + "id": 183, + "type": "Anything Everywhere", + "pos": [ + -316, + 179 + ], + "size": { + "0": 210, + "1": 26 + }, + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "LATENT", + "type": "*", + "link": 544, + "color_on": "#FF9CF9" + } + ], + "properties": { + "Node name for S&R": "Anything Everywhere", + "group_restricted": false + } + }, + { + "id": 5, + "type": "KSampler", + "pos": [ + 51, + -1 + ], + "size": { + "0": 260.72747802734375, + "1": 249.28138732910156 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": null + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": null + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": null + }, + { + "name": "latent_image", + "type": "LATENT", + "link": null + }, + { + "name": "seed", + "type": "INT", + "link": null, + "widget": { + "name": "seed" + } + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 545 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 1125899906842624, + "increment", + 35, + 8, + "dpmpp_3m_sde", + "karras", + 1 + ], + "color": "#57571a", + "bgcolor": "#6b6b2e" + }, + { + "id": 184, + "type": "Anything Everywhere?", + "pos": [ + 339, + 0 + ], + "size": { + "0": 210, + "1": 82 + }, + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "LATENT", + "type": "*", + "link": 545, + "color_on": "#FF9CF9" + } + ], + "properties": { + "Node name for S&R": "Anything Everywhere?", + "group_restricted": false + }, + "widgets_values": [ + ".*", + "samples" + ] + }, + { + "id": 179, + "type": "Anything Everywhere", + "pos": [ + 624, + 185 + ], + "size": { + "0": 181.96005249023438, + "1": 26 + }, + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "IMAGE", + "type": "*", + "link": 534, + "color_on": "#64B5F6" + } + ], + "properties": { + "Node name for S&R": "Anything Everywhere", + "group_restricted": false + } + }, + { + "id": 7, + "type": "VAEDecode", + "pos": [ + 637, + 74 + ], + "size": { + "0": 140, + "1": 46 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 534 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "color": "#2e571a", + "bgcolor": "#426b2e" + }, + { + "id": 182, + "type": "LoraLoader", + "pos": [ + 15, + -290 + ], + "size": { + "0": 273.7867126464844, + "1": 126 + }, + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 540 + }, + { + "name": "clip", + "type": "CLIP", + "link": 542 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 541 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 543 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoraLoader" + }, + "widgets_values": [ + "sd_xl_offset_example-lora_1.0.safetensors", + 1, + 1 + ] + }, + { + "id": 185, + "type": "Note", + "pos": [ + 396, + 326 + ], + "size": { + "0": 437.6109619140625, + "1": 131.43035888671875 + }, + "flags": {}, + "order": 5, + "mode": 0, + "properties": { + "text": "" + }, + "widgets_values": [ + "This workflow uses all the UE nodes, and can also test bypass (load LoRA)" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 169, + "type": "Seed Everywhere", + "pos": [ + 81, + 345 + ], + "size": { + "0": 210, + "1": 82 + }, + "flags": {}, + "order": 6, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Seed Everywhere", + "group_restricted": false + }, + "widgets_values": [ + 356735678581, + "fixed" + ] + }, + { + "id": 162, + "type": "CLIPTextEncode", + "pos": [ + 599, + -303 + ], + "size": { + "0": 247.4329071044922, + "1": 96 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": null + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 535 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "atmospheric photo of woman at night" + ] + }, + { + "id": 163, + "type": "CLIPTextEncode", + "pos": [ + 873, + -210 + ], + "size": { + "0": 247.4329071044922, + "1": 96 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": null + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 536 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "blue" + ] + } + ], + "links": [ + [ + 534, + 7, + 0, + 179, + 0, + "*" + ], + [ + 535, + 162, + 0, + 180, + 0, + "*" + ], + [ + 536, + 163, + 0, + 180, + 1, + "*" + ], + [ + 539, + 148, + 2, + 181, + 2, + "*" + ], + [ + 540, + 148, + 0, + 182, + 0, + "MODEL" + ], + [ + 541, + 182, + 0, + 181, + 0, + "*" + ], + [ + 542, + 148, + 1, + 182, + 1, + "CLIP" + ], + [ + 543, + 182, + 1, + 181, + 1, + "*" + ], + [ + 544, + 178, + 0, + 183, + 0, + "*" + ], + [ + 545, + 5, + 0, + 184, + 0, + "*" + ], + [ + 546, + 7, + 0, + 144, + 0, + "IMAGE" + ], + [ + 547, + 182, + 0, + 5, + 0, + "MODEL" + ], + [ + 548, + 162, + 0, + 5, + 1, + "CONDITIONING" + ], + [ + 549, + 163, + 0, + 5, + 2, + "CONDITIONING" + ], + [ + 550, + 178, + 0, + 5, + 3, + "LATENT" + ], + [ + 551, + 169, + 0, + 5, + 4, + "INT" + ], + [ + 552, + 5, + 0, + 7, + 0, + "LATENT" + ], + [ + 553, + 148, + 2, + 7, + 1, + "VAE" + ], + [ + 554, + 182, + 1, + 162, + 0, + "CLIP" + ], + [ + 555, + 182, + 1, + 163, + 0, + "CLIP" + ] + ], + "groups": [ + { + "title": "Model", + "bounding": [ + -371, + -387, + 926, + 294 + ], + "color": "#3f789e", + "font_size": 24, + "locked": false + }, + { + "title": "Conditioning", + "bounding": [ + 571, + -391, + 836, + 294 + ], + "color": "#a1309b", + "font_size": 24, + "locked": false + }, + { + "title": "Sampling", + "bounding": [ + -372, + -74, + 1198, + 343 + ], + "color": "#b06634", + "font_size": 24, + "locked": false + } + ], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..81f067dc5d58d1118806b48dd7f8ef7bd3872cba Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/test-workflow.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/unconnected.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/unconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..525ef8d4b8f050a31134ced4f73b407f56295e4d Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/unconnected.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/docs/workflow.png b/ComfyUI/custom_nodes/cg-use-everywhere/docs/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..e7005fd88cf38c466a9af327df8dd0db53204c1d Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/docs/workflow.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/ue.css b/ComfyUI/custom_nodes/cg-use-everywhere/js/ue.css new file mode 100644 index 0000000000000000000000000000000000000000..c5b812de2caae36f26256c106cda53c13a0e084f --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/ue.css @@ -0,0 +1,39 @@ + +.litegraph label.ueprompt { + padding:0px; + font-size: 12px; + border-radius: 0; + border: 0; + box-shadow: none !important; + margin:0px; + height:30px; + background-color: rgba(0,0,0,0); +} + +.litegraph span.ueprompttext { + margin: 0px; + min-width: 30px; + font-family: Arial, sans-serif; + color: var(--descrip-text); + text-align: right; + padding: 2px 2px 4px 0px; + background-color: inherit; +} + +.litegraph span.uepromptspan { + margin: 0px; + width: 100%; + padding-left:12px; + background-color: inherit; +} + +.litegraph input.uepromptinput { + padding: 0 0 0 6px; + font-size: 12px; + border-radius: 12px; + border: 2px solid var(--border-color); + color: var(--input-text); + margin: 0px; + width: 100%; + float: right; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/ue_debug.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/ue_debug.js new file mode 100644 index 0000000000000000000000000000000000000000..02b331effea28d2e4da06f683d29a58214656896 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/ue_debug.js @@ -0,0 +1,18 @@ +import { app } from "../../scripts/app.js"; + +/* +Things that can be useful (generally as breakpoints) when debugging +*/ +export function add_debug() { + var dirty_canvas = true; + Object.defineProperty(app.canvas, 'dirty_canvas', { + get : () => { return dirty_canvas }, + set : (v) => { dirty_canvas = v;} // a breakpoint here catches the calls that mark the canvas as dirty + }) + + var dirty_bg_canvas = true; + Object.defineProperty(app.canvas, 'dirty_bg_canvas', { + get : () => { return dirty_bg_canvas }, + set : (v) => { dirty_bg_canvas = v;} // a breakpoint here catches the calls that mark the background canvas as dirty + }) +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere.js new file mode 100644 index 0000000000000000000000000000000000000000..4787fce6550cdf2b414a396c13a0ccca309b5198 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere.js @@ -0,0 +1,272 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +import { is_UEnode, is_helper, inject, Logger, get_real_node } from "./use_everywhere_utilities.js"; +import { displayMessage, update_input_label, indicate_restriction } from "./use_everywhere_ui.js"; +import { LinkRenderController } from "./use_everywhere_ui.js"; +import { autoCreateMenu } from "./use_everywhere_autocreate.js"; +import { add_autoprompts } from "./use_everywhere_autoprompt.js"; +import { GraphAnalyser } from "./use_everywhere_graph_analysis.js"; +import { main_menu_settings, node_menu_settings, canvas_menu_settings } from "./use_everywhere_settings.js"; +import { add_debug } from "./ue_debug.js"; + +/* +The ui component that looks after the link rendering +*/ +var linkRenderController; +var graphAnalyser; + +/* +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). +A Logger.trace call is added at the start with 'tracetext' +*/ +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, linkRenderController.mark_link_list_outdated, linkRenderController); +} + +const nodeHandler = { + set: function(obj, property, value) { + const oldValue = Reflect.get(obj, property, this); + const result = Reflect.set(...arguments); + if (oldValue!=value) { + if (property==='bgcolor') { + if (obj.mode!=4) linkRenderController.mark_link_list_outdated(); + } + if (property==='mode') { + linkRenderController.mark_link_list_outdated(); + obj.widgets?.forEach((widget) => {widget.onModeChange?.(value)}); + } + } + return result; + }, +} + +app.registerExtension({ + name: "cg.customnodes.use_everywhere", + + 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) { + Logger.trace("onConnectionsChange", arguments, this); + if (this.IS_UE && side==1) { // side 1 is input + if (this.type=="Anything Everywhere?" && slot!=0) { + // don't do anything for the regexs + } else { + const type = (connect && link_info) ? get_real_node(link_info?.origin_id)?.outputs[link_info?.origin_slot]?.type : undefined; + this.input_type[slot] = type; + if (link_info) link_info.type = type ? type : "*"; + update_input_label(this, slot, app); + } + } + linkRenderController.mark_link_list_outdated(); + onConnectionsChange?.apply(this, arguments); + }; + + /* + Toggle the group restriction. + Any right click action on a node might make the link list dirty. + */ + const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function(_, options) { + Logger.trace("getExtraMenuOptions", arguments, this); + getExtraMenuOptions?.apply(this, arguments); + if (is_UEnode(this)) { + node_menu_settings(options, this); + + } + // any right click action can make the list dirty + inject_outdating_into_objects(options,'callback',`menu option on ${this.id}`); + } + + if (is_UEnode(nodeType)) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + if (!this.properties) this.properties = {} + this.properties.group_restricted = 0; + this.properties.color_restricted = 0; + if (this.inputs) { + if (!this.widgets) this.widgets = []; + for (const input of this.inputs) { + if (input.widget && !this.widgets.find((w) => w.name === input.widget.name)) this.widgets.push(input.widget) + } + } + return r; + } + } + }, + + async nodeCreated(node) { + node.IS_UE = is_UEnode(node); + if (node.IS_UE) { + node.input_type = [undefined, undefined, undefined]; // for dynamic input types + node.displayMessage = displayMessage; // receive messages from the python code + + // If a widget on a UE node is edited, link list is dirty + inject_outdating_into_objects(node.widgets,'callback',`widget callback on ${node.id}`); + + // draw the indication of group restrictions + const original_onDrawTitleBar = node.onDrawTitleBar; + node.onDrawTitleBar = function(ctx, title_height) { + original_onDrawTitleBar?.apply(this, arguments); + if (node.properties.group_restricted || node.properties.color_restricted) indicate_restriction(ctx, title_height); + } + } + + if (is_helper(node)) { // editing a helper node makes the list dirty + inject_outdating_into_objects(node.widgets,'callback',`widget callback on ${this.id}`); + } + + // removing a node makes the list dirty + inject_outdating_into_object_method(node, 'onRemoved', `node ${node.id} removed`) + + // creating a node makes the link list dirty - but give the system a moment to finish + setTimeout( ()=>{linkRenderController.mark_link_list_outdated()}, 100 ); + }, + + loadedGraphNode(node) { if (node.flags.collapsed && node.loaded_when_collapsed) node.loaded_when_collapsed(); }, + + async setup() { + const head = document.getElementsByTagName('HEAD')[0]; + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = 'extensions/cg-use-everywhere/ue.css'; + head.appendChild(link); + + /* + Listen for message-handler event from python code + */ + function messageHandler(event) { + const id = event.detail.id; + const message = event.detail.message; + const node = get_real_node(id); + if (node && node.displayMessage) node.displayMessage(id, message); + else (console.log(`node ${id} couldn't handle a message`)); + } + api.addEventListener("ue-message-handler", messageHandler); + + api.addEventListener("status", ({detail}) => { + if (linkRenderController) linkRenderController.note_queue_size(detail ? detail.exec_info.queue_remaining : 0) + }); + + /* + We don't want to do that if we are saving the workflow or api: + */ + const _original_save_onclick = document.getElementById('comfy-save-button').onclick; + document.getElementById('comfy-save-button').onclick = function() { + graphAnalyser.pause(); + _original_save_onclick(); + graphAnalyser.unpause() + } + const _original_save_api_onclick = document.getElementById('comfy-dev-save-api-button').onclick; + document.getElementById('comfy-dev-save-api-button').onclick = function() { + graphAnalyser.pause(); + _original_save_api_onclick(); + graphAnalyser.unpause(); + } + + /* + Hijack drawNode to render the virtual connection points + and links to node with mouseOver + */ + const original_drawNode = LGraphCanvas.prototype.drawNode; + LGraphCanvas.prototype.drawNode = function(node, ctx) { + original_drawNode.apply(this, arguments); + linkRenderController.highlight_ue_connections(node, ctx); + } + + /* + When we draw connections, do the ue ones as well + */ + const drawConnections = LGraphCanvas.prototype.drawConnections; + LGraphCanvas.prototype.drawConnections = function(ctx) { + drawConnections?.apply(this, arguments); + linkRenderController.render_all_ue_links(ctx); + } + + main_menu_settings(); + + /* + Canvas menu is the right click on backdrop. + We need to add our option, and hijack the others. + */ + const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + // Add our items to the canvas menu + const options = original_getCanvasMenuOptions.apply(this, arguments); + canvas_menu_settings(options); + + // every menu item makes our list dirty + inject_outdating_into_objects(options,'callback',`menu option on canvas`); + + return options; + } + + /* + When you drag from a node, showConnectionMenu is called. If shift key is pressed call ours + */ + const original_showConnectionMenu = LGraphCanvas.prototype.showConnectionMenu; + LGraphCanvas.prototype.showConnectionMenu = function (optPass) { + if (optPass.e.shiftKey) { + autoCreateMenu.apply(this, arguments); + } else { + this.use_original_menu = true; + original_showConnectionMenu.apply(this, arguments); + this.use_original_menu = false; + } + } + + /* + To allow us to use the shift drag above, we need to intercept 'allow_searchbox' sometimes + (because searchbox is the default behaviour when shift dragging) + */ + var original_allow_searchbox = app.canvas.allow_searchbox; + Object.defineProperty(app.canvas, 'allow_searchbox', { + get : function() { + if (this.use_original_menu) { return original_allow_searchbox; } + if(app.ui.settings.getSettingValue('AE.replacesearch', true) && this.connecting_output) { + return false; + } else { return original_allow_searchbox; } + }, + set : function(v) { original_allow_searchbox = v; } + }); + + + }, + + init() { + graphAnalyser = GraphAnalyser.instance(); + app.graphToPrompt = async function () { + return graphAnalyser.analyse_graph(true, true, false); + } + + linkRenderController = LinkRenderController.instance(graphAnalyser); + + add_autoprompts(); + const createNode = LiteGraph.createNode; + LiteGraph.createNode = function() { + const nd = createNode.apply(this,arguments); + if (nd && nd.IS_UE) { + return new Proxy( nd, nodeHandler ); + } else { + return nd; + } + } + + if (false) add_debug(); + + } + +}); diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_apply.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_apply.js new file mode 100644 index 0000000000000000000000000000000000000000..1199167220a8694a85756d342389e97a9c205600 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_apply.js @@ -0,0 +1,31 @@ +import { app } from "../../scripts/app.js"; +import { is_UEnode, get_real_node } from "./use_everywhere_utilities.js"; + + +function _convert_to_links(ue) { + const output_node_id = ue.output[0]; + const output_index = ue.output[1]; + const output_node = get_real_node(output_node_id); + ue.sending_to.forEach((st) => { + const input_node_id = st.node.id; + const input_node = get_real_node(input_node_id); + const input_index = st.input_index; + output_node.connect(output_index, input_node, input_index); + }); +} + +function convert_to_links(ues, control_node_id) { + ues.ues.forEach((ue)=> { + if (control_node_id==-1 || ue.controller.id == control_node_id) _convert_to_links(ue); + }); +} + +function remove_all_ues() { + var match = app.graph._nodes.find((node)=>is_UEnode(node)); + while (match) { + app.graph.remove(match); + match = app.graph._nodes.find((node)=>is_UEnode(node)); + } +} + +export {convert_to_links, remove_all_ues} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_autocreate.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_autocreate.js new file mode 100644 index 0000000000000000000000000000000000000000..e885f0e9b3ad5e13595735932f4e4423c81038f3 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_autocreate.js @@ -0,0 +1,46 @@ +import { app } from "../../scripts/app.js"; + +function autoCreateMenu(opts) { + //opts.e.stopPropagation(); + var options = ["Search",]; + var search_opts; + if (opts.nodeFrom && opts.slotFrom) { + options.push(null); + options.push("Anything Everywhere"); + options.push("Anything Everywhere?"); + if (opts.nodeFrom?.outputs?.length==3 && + opts.nodeFrom.outputs[0].name=='MODEL' && + opts.nodeFrom.outputs[1].name=='CLIP' && + opts.nodeFrom.outputs[2].name=='VAE') options.push("Anything Everywhere3"); + search_opts = {node_from: opts.nodeFrom, slot_from: opts.slotFrom, type_filter_in: opts.slotFrom.type}; + } else { + search_opts = {node_to: opts.nodeTo, slot_from: opts.slotTo, type_filter_out: slotTo.type}; + } + + var menu = new LiteGraph.ContextMenu(options, { + event: opts.e, + title: "UE Node", + callback: inner_clicked + }); + + const p = [ opts.e.canvasX, opts.e.canvasY ]; + + function inner_clicked(v,options,e) { + if (!v) return; + if (v=="Search") { + app.canvas.showSearchBox(opts.e,search_opts); + return; + } + var newNode = LiteGraph.createNode(v); + app.graph.add(newNode); + newNode.pos = p; + if (v=="Anything Everywhere3") { + for (var i=0; i<3; i++) {opts.nodeFrom.connect( i, newNode, i );} + } else { + opts.nodeFrom.connect( opts.nodeFrom.findOutputSlot(opts.slotFrom.name), newNode, 0 ); + } + app.graph.change(); + } +} + +export {autoCreateMenu} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_autoprompt.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_autoprompt.js new file mode 100644 index 0000000000000000000000000000000000000000..f9031e94438fba67c9ba3ba0002f713ee3bf821a --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_autoprompt.js @@ -0,0 +1,181 @@ +import { is_UEnode } from "./use_everywhere_utilities.js"; +import { ComfyWidgets} from "../../scripts/widgets.js"; +import { app } from "../../scripts/app.js"; +import { LinkRenderController } from "./use_everywhere_ui.js"; + +function update_picklist(node, inputname) { + const d = document.getElementById("uedynamiclist"); + while (d.firstChild) { d.removeChild(d.lastChild); }; + let options = []; + if (inputname=="title_regex") { options = LinkRenderController.instance().ue_list?.all_nodes_with_unmatched_input(node.input_type[0]); } + else if (inputname=="input_regex") { options = LinkRenderController.instance().ue_list?.all_unmatched_input_names(node.input_type[0]); } + else if (inputname=="group_regex") { options = LinkRenderController.instance().ue_list?.all_group_names(node.input_type[0]); } + options.forEach((option) => { + const theOption = document.createElement("option"); + theOption.setAttribute("value", option); + d.appendChild(theOption) + }) +} + +function intersect(a, b) { + const x = Math.max(a.x, b.x); + const num1 = Math.min(a.x + a.width, b.x + b.width); + const y = Math.max(a.y, b.y); + const num2 = Math.min(a.y + a.height, b.y + b.height); + if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]; + else return null; +} + +function union(a,b) { + if (!b) return a; + if (!a) return b; + const x = Math.min(a.x, b.x); + const y = Math.min(a.y,b.y); + const width = Math.max(a.x+a.width, b.x+b.width) - x; + const height = Math.max(a.y+a.height, b.y+b.height) - x; + return { x:x, y:y, width:width, height:height }; +} + +function getClipPath(node, element) { + const scale = app.canvas.ds.scale; + const widgetRect = element.getBoundingClientRect(); + var onTopOfMe = false; + var clip = null; + app.graph._nodes.forEach((other_node) => { + if (other_node.id == node.id) { + onTopOfMe = true; + } + else if (onTopOfMe) { + const MARGIN = other_node.is_selected ? 7 : 2; + const bounding = other_node.getBounding(); + const intersection = intersect( + { x: widgetRect.x / scale, y: widgetRect.y / scale, width: widgetRect.width / scale, height: widgetRect.height / scale }, + { + x: other_node.pos[0] + app.canvas.ds.offset[0] - MARGIN, + y: other_node.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN, + width: bounding[2] + MARGIN + MARGIN, + height: bounding[3] + MARGIN + MARGIN, + } + ); + if (intersection) { + clip = union(clip, { + x : intersection[0] - widgetRect.x / scale, + y : intersection[1] - widgetRect.y / scale, + width : intersection[2], + height : intersection[3] + }) + //const newpath = `0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%`; + //path = path != '' ? `${path}, ${newpath}` : newpath; + } + } + }) + const path = clip ? `polygon(0% 0%, 0% 100%, ${clip.x}px 100%, ${clip.x}px ${clip.y}px, ${clip.x + clip.width}px ${clip.y}px, ${clip.x + clip.width}px ${clip.y + clip.height}px, ${clip.x}px ${clip.y + clip.height}px, ${clip.x}px 100%, 100% 100%, 100% 0%)` : ''; + return path; +} + +function active_text_widget(node, inputname) { + const label = document.createElement("label"); + label.className = "graphdialog ueprompt"; + label.style.display = "none"; + + const label_text = document.createElement("span"); + label_text.innerText = `${inputname.substring(0,5)} `; + label_text.className = "ueprompttext"; + label.appendChild(label_text); + + const span = document.createElement("span"); + span.className = "uepromptspan"; + label.appendChild(span); + + const inputEl = document.createElement("input"); + inputEl.setAttribute("type", "text"); + inputEl.className = "uepromptinput"; + span.appendChild(inputEl); + + const widget = node.addDOMWidget(inputname, "input", label, { + getValue() { return inputEl.value; }, + setValue(v) { inputEl.value = v; }, + onDraw(w) { + // are we the most recently selected node? + if (Object.values(app.canvas.selected_nodes)[0]?.id == node.id) { + // if so, turn off DOM clipping + w.element.style.clipPath = null; w.element.style.willChange = null; + } else { + w.element.style.zIndex = 0; + const p = getClipPath(node, w.element); + w.element.style.clipPath = p; + let a; + } + } + }); + widget.element.hidden = true; + + inputEl.onmousedown = function(e) { + const x = app.canvas.prompt("Value",widget.value,function(v) { this.value = v; }.bind(widget), e, false ); + const input = x.getElementsByClassName("value")[0]; + input.setAttribute("list", "uedynamiclist"); + input.parentNode.style.zIndex = `${parseInt(label.style.zIndex ? label.style.zIndex : '0')+1}`; + input.addEventListener("input", function (v) { + widget.value = this.value; + LinkRenderController.instance().mark_link_list_outdated(); + app.graph.setDirtyCanvas(true,true); + }.bind(input)); + update_picklist(node, inputname); + e.stopImmediatePropagation(); + } + + widget.computeSize = function (parent_width) { + return parent_width ? [parent_width, 27] : [400, 20]; + } + + inputEl.addEventListener("focus", () => { + if (inputEl.value==".*") inputEl.value = ""; + }); + + widget.onModeChange = function (mode) { + label.style.opacity = mode==4 ? 0.2 : 1.0; + } + + node.loaded_when_collapsed = function() { + node.widgets?.forEach((widget) => { + if (widget.element) { + widget.element.hidden = true; + widget.element.style.display = "none"; + } + }) + } + + return { widget }; +} + +function activate(node, widget) { + if (node.flags?.collapsed) return; + widget.element.hidden = false; + widget.element.style.display = ""; +} + +function add_autoprompts() { + const STRING = ComfyWidgets.STRING; + ComfyWidgets.STRING = function (node, inputName, inputData, app) { + if (!is_UEnode(node) || !inputName?.includes("regex") || !app.ui.settings.getSettingValue('AE.autoprompt', true)) { + return STRING.apply(this, arguments); + } + const atw = active_text_widget(node, inputName); + const orig_onAdded = node.onAdded; + node.onAdded = function () { + orig_onAdded?.apply(this, arguments); + activate(node, atw.widget); + } + return atw; + } + const datalist = document.createElement("datalist"); + datalist.id = "uedynamiclist"; + document.body.append(datalist); +} + +function node_added(node) { + const a = 1; +} + + +export { add_autoprompts } \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_classes.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_classes.js new file mode 100644 index 0000000000000000000000000000000000000000..97a27c2eabd1fc2211c08e6b45f99cea26441334 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_classes.js @@ -0,0 +1,262 @@ +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 } from "./use_everywhere_utilities.js"; + +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']; + return "un-nameable node"; +} + +/* +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 :) +*/ +class UseEverywhere { + constructor() { + this.sending_to = []; + Object.assign(this, arguments[0]); + if (this.priority === undefined) this.priority = 0; + this.description = `source ${this?.output[0]}.${this?.output[1]} -> control ${this?.controller.id}.${this?.control_node_input_index} "${this.type}" <- (priority ${this.priority})`; + if (this.title_regex) this.description += ` - node title regex '${this.title_regex.source}'`; + if (this.input_regex) this.description += ` - input name regex '${this.input_regex.source}'`; + } + + 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); + 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.output[0]} is not alive`; + if (!node_is_live(get_real_node(params.output[0]))) return `upstream node ${params.output[0]} is not alive`; + return ""; +} + +class UseEverywhereList { + constructor() { this.ues = []; this.unmatched_inputs = []; } + + differs_from(another_uel) { + if (!another_uel) 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(Logger.INFORMATION, `'${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 = `'${display_name(node)}' (${node.id}) input '${input.name}' matches multiple Use Everwhere sources:`; + _ambiguity_messages.push(msg); + for (var i=0; i { console.log(ue.describe()); }); + } + + all_unmatched_inputs(type) { + return this.unmatched_inputs.filter((ui)=>ui.input.type==type); + } + + all_nodes_with_unmatched_input(type) { + const result = new Set(); + this.all_unmatched_inputs(type).forEach((ui) => { + result.add(display_name(ui.node)); + }) + return result; + } + + all_unmatched_input_names(type) { + const result = new Set(); + this.all_unmatched_inputs(type).forEach((ui) => { + result.add(ui.input.label ? ui.input.label : ui.input.name); + }) + return result; + } + + all_group_names() { + const result = new Set(); + app.graph._groups.forEach((group) => { + result.add(group.title); + }) + return result; + } + + 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), + control_node_input_index : ue.control_node_input_index, + sending_to : st.node, + }); + } + }); + }); + return ue_connections; + } + + 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), + control_node_input_index : ue.control_node_input_index, + sending_to : st.node, + }); + }); + }); + return ue_connections; + } + + all_ue_connections_for(node_id) { + const ue_connections = []; + this.ues.forEach((ue) => { + ue.sending_to.forEach((st) => { + if (get_real_node(st.node.id).id==node_id || get_real_node(ue.controller.id).id==node_id) { + ue_connections.push({ + type : ue.type, + input_index : st.input_index, + control_node : get_real_node(ue.controller.id), + control_node_input_index : ue.control_node_input_index, + sending_to : st.node, + }); + } + }); + }); + return ue_connections; + } +} + +export {UseEverywhereList} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_graph_analysis.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_graph_analysis.js new file mode 100644 index 0000000000000000000000000000000000000000..357c930139fd4d188346f84101b85a9623d76bbf --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_graph_analysis.js @@ -0,0 +1,152 @@ +import { GroupNodeHandler } from "../core/groupNode.js"; +import { UseEverywhereList } from "./use_everywhere_classes.js"; +import { add_ue_from_node, add_ue_from_node_in_group } from "./use_everywhere_nodes.js"; +import { node_in_loop, node_is_live, is_connected, is_UEnode, Logger, get_real_node } from "./use_everywhere_utilities.js"; +import { app } from "../../scripts/app.js"; + +class GraphAnalyser { + static _instance; + static instance() { + if (!this._instance) this._instance = new GraphAnalyser(); + return this._instance; + } + + constructor() { + this.original_graphToPrompt = app.graphToPrompt; + this.ambiguity_messages = []; + this.pause_depth = 0; + } + + pause() { this.pause_depth += 1; } + unpause() { this.pause_depth -= 1; } + + + async analyse_graph(modify_and_return_prompt=false, check_for_loops=false, supress_before_queued=true) { + //try { + /*if (supress_before_queued) { + app.graph._nodes.forEach((node) => { + node.widgets?.forEach((widget) => { + if (widget.beforeQueued) { + widget.__beforeQueued = widget.beforeQueued; + widget.beforeQueued = null; + } + }) + if(node.seedControl && node.seedControl.lastSeedButton){ // for efficiency nodes seedControl + node.seedControl.lastSeedButton.__disabled = node.seedControl.lastSeedButton.disabled + node.seedControl.lastSeedButton.disabled = true + } + }) + }*/ + //return this._analyse_graph(modify_and_return_prompt, check_for_loops); + /*} finally { + if (supress_before_queued) { + app.graph._nodes.forEach((node) => { + node.widgets?.forEach((widget) => { + if (widget.__beforeQueued) { + widget.beforeQueued = widget.__beforeQueued; + widget.__beforeQueued = null; + } + }) + if(node.seedControl && node.seedControl.lastSeedButton){ // for efficiency nodes seedControl + node.seedControl.lastSeedButton.disabled = node.seedControl.lastSeedButton.__disabled + } + }) + } + }*/ + //} + //async _analyse_graph(modify_and_return_prompt=false, check_for_loops=false) { + if (this.pause_depth > 0) { return this.original_graphToPrompt.apply(app) } + this.ambiguity_messages = []; + var p; + if (modify_and_return_prompt) { + p = await this.original_graphToPrompt.apply(app); + p = structuredClone(p); + } else { + p = { workflow:app.graph.serialize() } + } + + // Create a UseEverywhereList and populate it from all live (not bypassed) nodes + const ues = new UseEverywhereList(); + const live_nodes = p.workflow.nodes.filter((node) => node_is_live(node)) + live_nodes.filter((node) => is_UEnode(node)).forEach(node => { add_ue_from_node(ues, node); }) + live_nodes.filter((node) => (get_real_node(node.id, Logger.INFORMATION) && GroupNodeHandler.isGroupNode(get_real_node(node.id)))).forEach( groupNode => { + const group_data = GroupNodeHandler.getGroupData(get_real_node(groupNode.id)); + group_data.nodeData.nodes.filter((node) => is_UEnode(node)).forEach(node => { + add_ue_from_node_in_group(ues, node, groupNode.id, group_data); + }) + }) + + const links_added = new Set(); + // Look for unconnected inputs and see if we can connect them + live_nodes.filter((node) => !is_UEnode(node)).forEach(node => { + const nd = get_real_node(node.id, Logger.INFORMATION); + if (nd) { + var gpData = GroupNodeHandler.getGroupData(nd); + const isGrp = !!gpData; + const o2n = isGrp ? Object.entries(gpData.oldToNewInputMap) : null; + node.inputs?.forEach(input => { + if (!is_connected(input) && !(node.reject_ue_connection && node.reject_ue_connection(input))) { + var ue = ues.find_best_match(node, input, this.ambiguity_messages); + if (ue && modify_and_return_prompt) { + var effective_node = node; + var effective_node_slot = -1; + if (isGrp) { // the node we are looking at is a group node + const in_index = node.inputs.findIndex((i)=>i==input); + const inner_node_index = o2n.findIndex((l)=>Object.values(l[1]).includes(in_index)); + const inner_node_slot_index = Object.values(o2n[inner_node_index][1]).findIndex((l)=>l==in_index); + effective_node_slot = Object.keys(o2n[inner_node_index][1])[inner_node_slot_index]; + effective_node = nd.getInnerNodes()[o2n[inner_node_index][0]]; + } + const upNode = get_real_node(ue.output[0]); + var effective_output = [ue.output[0], ue.output[1]]; + if (GroupNodeHandler.isGroupNode(upNode)) { // the upstream node is a group node + const upGpData = GroupNodeHandler.getGroupData(upNode); + const up_inner_node = upGpData.newToOldOutputMap[ue.output[1]].node; + const up_inner_node_index = up_inner_node.index; + const up_inner_node_id = upNode.getInnerNodes()[up_inner_node_index].id; + const up_inner_node_slot = upGpData.newToOldOutputMap[ue.output[1]].slot; + effective_output = [`${up_inner_node_id}`, up_inner_node_slot]; + } + if (effective_node_slot==-1) effective_node_slot = effective_node.inputs.findIndex((i)=>(i.label ? i.label : i.name)===(input.label ? input.label : input.name)); + p.output[effective_node.id].inputs[effective_node.inputs[effective_node_slot].name] = effective_output; + links_added.add({ + "downstream":effective_node.id, "downstream_slot":effective_node_slot, + "upstream":effective_output[0], "upstream_slot":effective_output[1], + "controller":ue.controller.id, + "type":ue.type + }); + } + } + }); + } + }); + + if (this.ambiguity_messages.length) Logger.log(Logger.PROBLEM, "Ambiguous connections", this.ambiguity_messages, Logger.CAT_AMBIGUITY); + + // if there are loops report them and raise an exception + if (check_for_loops && app.ui.settings.getSettingValue('AE.checkloops', true)) { + try { + node_in_loop(live_nodes, links_added); + } catch (e) { + if (!e.stack) throw e; + if (e.ues && e.ues.length > 0){ + alert(`Loop (${e.stack}) with broadcast (${e.ues}) - not submitting workflow`); + } else { + alert(`Loop (${e.stack}) - not submitting workflow`); + } + throw new Error(`Loop Detected ${e.stack}, ${e.ues}`, {"cause":e}); + } + } + + if (modify_and_return_prompt) { + [...links_added].forEach((l)=>{ + p.workflow.last_link_id += 1; + p.workflow.links.push([p.workflow.last_link_id, parseInt(l.upstream), l.upstream_slot, l.downstream, l.downstream_slot, l.type]) + }) + return p; + } + else return ues; + } +} + +export { GraphAnalyser } diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_nodes.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_nodes.js new file mode 100644 index 0000000000000000000000000000000000000000..94711c367794b6cc1b3777190c06a64d1924d6e7 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_nodes.js @@ -0,0 +1,113 @@ +import { handle_bypass, get_real_node, get_group_node } from "./use_everywhere_utilities.js"; +import { app } from "../../scripts/app.js"; + +const CONVERTED_TYPE = "converted-widget"; +// import {CONVERTED_TYPE} from "../../extensions/core/widgetInputs.js" + +/* +If a widget hasn't been converted, just get it's value +If it has, *try* to go upstream +*/ +function get_widget_or_input_values(node_obj, widget_id) { + if (node_obj.widgets[widget_id]?.type.startsWith(CONVERTED_TYPE)) { + try { + const name = node_obj.widgets[widget_id].name; + const input_id = node_obj.inputs.findIndex((input) => input.name==name); + const connection = get_connection(node_obj, input_id, "STRING"); + const upstream_node_obj = get_real_node(connection.link.origin_id.toString()); + const widget = upstream_node_obj.widgets.find((w) => w.name.toLowerCase() == upstream_node_obj.outputs[connection.link.origin_slot].name.toLowerCase()); + return widget.value; + } catch (error) { + return "NOT CONNECTED DONT MATCH"; + } + } + return node_obj.widgets[widget_id].value; +} + +function add_ue_from_node_in_group(ues, node, group_node_id, group_data) { + const group_node = get_real_node(group_node_id); + const ue_node = group_node.getInnerNodes()[node.index]; + ue_node.in_group_with_data = group_data; + ue_node.getInnerNodesOfGroup = group_node.getInnerNodes; + add_ue_from_node(ues, ue_node) +} + +function get_available_input_name(inputs, the_input, type) { + const used_names = []; + inputs.forEach((input) => { if (input!=the_input) used_names.push(input.name); }); + const base = `UE ${type.toLowerCase()}`; + if (!used_names.includes(base)) return base; + for (var i=2; ;i++) { + if (!used_names.includes(`${base}${i}`)) return `${base}${i}`; + } +} + +function get_connection(node, i, override_type) { + const in_link = node?.inputs[i].link; + var type = override_type; + var link = undefined; + if (in_link) { + if (!override_type) type = get_real_node(node.id.toString())?.input_type[i]; + link = handle_bypass(app.graph.links[in_link],type); + } else if (node.in_group_with_data) { + if (node.in_group_with_data.linksTo[node.index] && node.in_group_with_data.linksTo[node.index][i]) { + const group_style_link = node.in_group_with_data.linksTo[node.index][i]; + link = { "origin_id":node.getInnerNodesOfGroup()[group_style_link[0]].id, "origin_slot" : group_style_link[1] }; + if (!override_type) type = group_style_link[5]; + } else { // group external input + const group_node = get_group_node(node.id); + const group_node_input = group_node.inputs[node.in_group_with_data.oldToNewInputMap[node.index][i]]; + const link_n = group_node_input.link; + if (link_n) { + link = app.graph.links[link_n]; + if (!override_type) type = app.graph._nodes_by_id[link.origin_id].outputs[link.origin_slot].type; + // update the group input node... and the link type + group_node_input.type = type; + group_node_input.name = get_available_input_name(group_node.inputs, group_node_input, type); + link.type = type; + } + } + } + return { link:link, type:type } +} + +/* +Add UseEverywhere broadcasts from this node to the list +*/ +function add_ue_from_node(ues, node) { + if (node.type === "Seed Everywhere") ues.add_ue(node, -1, "INT", [node.id.toString(),0], + undefined, new RegExp("seed|随机种"), undefined, 5); + + if (node.type === "Anything Everywhere?") { + const connection = get_connection(node, 0); + if (connection.link) { + const node_obj = get_real_node(node.id.toString()); + const w0 = get_widget_or_input_values(node_obj,0); + const r0 = new RegExp(w0); + const w1 = get_widget_or_input_values(node_obj,1); + const r1 = (w1.startsWith('+')) ? w1 : new RegExp(w1); + const w2 = get_widget_or_input_values(node_obj,2); + const r2 = (w2 && w2!=".*") ? new RegExp(w2) : null; + ues.add_ue(node, 0, connection.type, [connection.link.origin_id.toString(), connection.link.origin_slot], r0, r1, r2, 10); + } + } + if (node.type === "Prompts Everywhere") { + for (var i=0; i<2; i++) { + const connection = get_connection(node, i); + if (connection.link) ues.add_ue(node, i, connection.type, [connection.link.origin_id.toString(), connection.link.origin_slot], + undefined, new RegExp(["(_|\\b)pos(itive|_|\\b)|^prompt|正面","(_|\\b)neg(ative|_|\\b)|负面"][i]), undefined, 5); + } + } + if (node.type === "Anything Everywhere") { + const connection = get_connection(node, 0); + if (connection.link) ues.add_ue(node, 0, connection.type, [connection.link.origin_id.toString(),connection. link.origin_slot], undefined, undefined, undefined, 2); + } + if (node.type === "Anything Everywhere3") { + for (var i=0; i<3; i++) { + const connection = get_connection(node, i); + if (connection.link) ues.add_ue(node, i, connection.type, [connection.link.origin_id.toString(), connection.link.origin_slot]); + } + } +} + +export {add_ue_from_node, add_ue_from_node_in_group} diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_settings.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_settings.js new file mode 100644 index 0000000000000000000000000000000000000000..0a6a24886ccad531f3fbafd6b6c6d42a9aabfdb3 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_settings.js @@ -0,0 +1,171 @@ +import { app } from "../../scripts/app.js"; +import { GraphAnalyser } from "./use_everywhere_graph_analysis.js"; +import { LinkRenderController } from "./use_everywhere_ui.js"; +import { convert_to_links, remove_all_ues } from "./use_everywhere_apply.js"; +import { has_priority_boost } from "./use_everywhere_utilities.js"; + +function main_menu_settings() { + + app.ui.settings.addSetting({ + id: "AE.details", + name: "Anything Everywhere show node details", + type: "boolean", + defaultValue: false, + }); + app.ui.settings.addSetting({ + id: "AE.autoprompt", + name: "Anything Everywhere? autocomplete (may require page reload)", + type: "boolean", + defaultValue: true, + }); + app.ui.settings.addSetting({ + id: "AE.checkloops", + name: "Anything Everywhere check loops", + type: "boolean", + defaultValue: true, + }); + app.ui.settings.addSetting({ + id: "AE.showlinks", + name: "Anything Everywhere 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: 0, + onChange: app.graph.change.bind(app.graph), + }); + app.ui.settings.addSetting({ + id: "AE.animate", + name: "Anything Everywhere animate UE links", + type: "combo", + options: [ {value:0, text:"Off"}, {value:1, text:"Dots"}, {value:2, text:"Pulse"}, {value:3, text:"Both"}, ], + defaultValue: 3, + onChange: app.graph.change.bind(app.graph), + }); + app.ui.settings.addSetting({ + id: "AE.stop.animation.running", + name: "Anything Everywhere turn animation off when running", + type: "boolean", + defaultValue: true, + onChange: app.graph.change.bind(app.graph), + }); + app.ui.settings.addSetting({ + id: "AE.highlight", + name: "Anything Everywhere highlight connected nodes", + type: "boolean", + defaultValue: true, + onChange: app.graph.change.bind(app.graph), + }); + app.ui.settings.addSetting({ + id: "AE.replacesearch", + name: "Anything Everywhere replace search", + type: "boolean", + defaultValue: true, + }); +} + +function submenu(properties, property, options, e, menu, node) { + const current = properties[property] ? (properties[property]==2 ? 3 : 2 ) : 1; + const submenu = new LiteGraph.ContextMenu( + options, + { event: e, callback: inner_function, parentMenu: menu, node: node } + ); + const current_element = submenu.root.querySelector(`:nth-child(${current})`); + if (current_element) current_element.style.borderLeft = "2px solid #484"; + function inner_function(v) { + if (node) { + const choice = Object.values(options).indexOf(v); + properties[property] = choice; + LinkRenderController.instance().mark_link_list_outdated(); + } + } +} + +const GROUP_RESTRICTION_OPTIONS = ["No restrictions", "Send only within group", "Send only not within group"] +function group_restriction_submenu(value, options, e, menu, node) { + submenu(node.properties, "group_restricted", GROUP_RESTRICTION_OPTIONS, e, menu, node); +} + +const COLOR_RESTRICTION_OPTIONS = ["No restrictions", "Send only to same color", "Send only to different color"] +function color_restriction_submenu(value, options, e, menu, node) { + submenu(node.properties, "color_restricted", COLOR_RESTRICTION_OPTIONS, e, menu, node); +} + +function priority_boost_submenu(value, options, e, menu, node) { + const current = (node.properties["priority_boost"] ? node.properties["priority_boost"] : 0) + 1; + const submenu = new LiteGraph.ContextMenu( + [0,1,2,3,4,5,6,7,8,9], + { event: e, callback: function (v) { + node.properties["priority_boost"] = parseInt(v); + LinkRenderController.instance().mark_link_list_outdated(); + }, + parentMenu: menu, node:node} + ) + const current_element = submenu.root.querySelector(`:nth-child(${current})`); + if (current_element) current_element.style.borderLeft = "2px solid #484"; +} + +function node_menu_settings(options, node) { + options.push(null); + if (has_priority_boost(node)) options.push( + { + content: "Priority Boost", + has_submenu: true, + callback: priority_boost_submenu, + } + ) + options.push( + { + content: "Group Restrictions", + has_submenu: true, + callback: group_restriction_submenu, + }, + { + content: "Color Restrictions", + has_submenu: true, + callback: color_restriction_submenu, + }, + { + content: "Convert to real links", + callback: async () => { + const ues = await GraphAnalyser.instance().analyse_graph(); + convert_to_links(ues, node.id); + app.graph.remove(node); + } + } + ) + options.push(null); +} + +function canvas_menu_settings(options) { + options.push(null); // divider + options.push({ + content: (app.ui.settings.getSettingValue('AE.showlinks', 0)>0) ? "Hide UE links" : "Show UE links", + callback: () => { + const setTo = (app.ui.settings.getSettingValue('AE.showlinks', 0)>0) ? 0 : 4; + app.ui.settings.setSettingValue('AE.showlinks', setTo); + app.graph.change(); + } + }, + { + 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?")) { + const ues = await GraphAnalyser.instance().analyse_graph(); + LinkRenderController.instance().pause(); + convert_to_links(ues, -1); + remove_all_ues(); + app.graph.change(); + } + } + }); + if (GraphAnalyser.instance().ambiguity_messages.length) { + options.push({ + content: "Show UE broadcast clashes", + callback: async () => { + alert(GraphAnalyser.instance().ambiguity_messages.join("\n")) + } + }) + } + options.push(null); // divider +} + +export { main_menu_settings, node_menu_settings, canvas_menu_settings } \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_ui.js b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_ui.js new file mode 100644 index 0000000000000000000000000000000000000000..895f1d98212580837ac4634e89ba09505d8d48dd --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/js/use_everywhere_ui.js @@ -0,0 +1,347 @@ +import { Logger, get_real_node, get_group_node, get_all_nodes_within } from "./use_everywhere_utilities.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; +import { app } from "../../scripts/app.js"; + +function nodes_in_my_group(node_id) { + const nodes_in = new Set(); + app.graph._groups.forEach((group) => { + if (!app.canvas.selected_group_moving) group.recomputeInsideNodes(); + if (group._nodes?.find((node) => { return (node.id===node_id) } )) { + group._nodes.forEach((node) => { nodes_in.add(node.id) } ) + } + }); + return [...nodes_in]; +} + +function nodes_not_in_my_group(node_id) { + const nid = nodes_in_my_group(node_id); + const nodes_not_in = []; + app.graph._nodes.forEach((node) => { + if (!nid.includes(node.id)) nodes_not_in.push(node.id); + }); + return nodes_not_in; +} + +function nodes_in_groups_matching(regex, already_limited_to) { + const nodes_in = new Set(); + app.graph._groups.forEach((group) => { + if (regex.test(group.title)) { + 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]; +} + + +function nodes_my_color(node_id, 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).color==color) nodes_in.add(nid) + }) + } else { + app.graph._nodes.forEach((node) => { + if (node.color==color) nodes_in.add(node.id) + }) + } + return [...nodes_in]; +} + +function nodes_not_my_color(node_id, 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).color!=color) nodes_in.add(nid) + }) + } else { + app.graph._nodes.forEach((node) => { + if (node.color!=color) nodes_in.add(node.id) + }) + } + return [...nodes_in]; +} + +function indicate_restriction(ctx, title_height) { + ctx.save(); + ctx.lineWidth = 2; + ctx.strokeStyle = "#6F6"; + ctx.beginPath(); + ctx.roundRect(5,5-title_height,20,20,8); + ctx.stroke(); + ctx.restore(); +} + +function displayMessage(id, message) { + const node = get_real_node(id); + if (!node) return; + var w = node.widgets?.find((w) => w.name === "display_text_widget"); + if (app.ui.settings.getSettingValue('AE.details', false) || w) { + if (!w) { + w = ComfyWidgets["STRING"](this, "display_text_widget", ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.6; + w.inputEl.style.fontSize = "9pt"; + } + w.value = message; + this.onResize?.(this.size); + } +} + +function update_input_label(node, slot, app) { + if (node.input_type[slot]) { + node.inputs[slot].name = node.input_type[slot]; + node.inputs[slot].color_on = app.canvas.default_connection_color_byType[node.input_type[slot]]; + } else { + node.inputs[slot].name = "anything"; + node.inputs[slot].color_on = undefined; + } +} + +class LinkRenderController { + static _instance; + static instance(tga) { + if (!this._instance) this._instance = new LinkRenderController(); + if (tga && !this._instance.the_graph_analyser) this._instance.the_graph_analyser = tga; + return this._instance + } + constructor() { + this.the_graph_analyser = null; + this.periodically_mark_link_list_outdated(); + } + + ue_list = undefined; // the most current ue list - set to undefined if we know it is out of date + ue_list_reloading = false; // true when a reload has been requested but not completed + last_used_ue_list = undefined; // the last ue list we actually used to generate graphics + paused = false; + reading_list = false; // don't outdate the list while we read it (because reading it can trigger outdates!) + + queue_size = null; + note_queue_size(x) { this.queue_size = x; } + + pause(ms) { + this.paused = true; + if (!ms) ms = 100; + setTimeout( this.unpause.bind(this), ms ); + } + unpause() { + this.paused = false; + app.graph.change(); + } + + // memory reuse + slot_pos1 = new Float32Array(2); //to reuse + slot_pos2 = new Float32Array(2); //to reuse + + mark_link_list_outdated() { + if (this.reading_list) return; + if (this.ue_list) { + this.ue_list = undefined; + this.request_link_list_update(); + Logger.log(Logger.INFORMATION, "link_list marked outdated"); + } else { + Logger.log(Logger.INFORMATION, "link_list was already outdated"); + } + } + + periodically_mark_link_list_outdated() { + this.mark_link_list_outdated(); + setTimeout(this.periodically_mark_link_list_outdated.bind(this), 1000); + } + + // callback when the_graph_analyser finishes - store the result and note reloading is false + reload_resolve = function (value) { + this.ue_list = value; + this.ue_list_reloading = false; + if (this.ue_list.differs_from(this.last_used_ue_list)) app.graph.change(); + Logger.log(Logger.INFORMATION, "link list update completed"); + Logger.log_call(Logger.DETAIL, this.ue_list.print_all); + }.bind(this) + + // callback for when the_graph_analyser fails - note reloading is false and log + reload_reject = function(reason) { + this.ue_list_reloading=false; + Logger.log(Logger.ERROR, "link list update failed"); + Logger.log_error(Logger.ERROR, reason); + }.bind(this) + + // request an update to the ue_list. + request_link_list_update() { + if (this.ue_list_reloading) return; // already doing it + this.ue_list_reloading = true; // stop any more requests + this.the_graph_analyser.analyse_graph().then(this.reload_resolve, this.reload_reject); // an async call is a promise; pass it two callbacks + Logger.log(Logger.INFORMATION, "link list update started"); + } + + highlight_ue_connections(node, ctx) { + try { + this._highlight_ue_connections(node, ctx); + } catch (e) { + console.error(e); + } + } + + _highlight_ue_connections(node, ctx) { + this.reading_list = true; + if (!app.ui.settings.getSettingValue('AE.highlight', true)) return; + //if (this._ue_links_visible) return; + if (!this.list_ready()) return; + + this.ue_list.all_connected_inputs(node).forEach((ue_connection) => { + if (!ue_connection.control_node) { // control node deleted... + this.mark_link_list_outdated(); + return; + } + var pos2 = node.getConnectionPos(true, ue_connection.input_index, this.slot_pos1); + pos2[0] -= node.pos[0]; + pos2[1] -= node.pos[1]; + ctx.save(); + ctx.lineWidth = 1; + var radius=5 + ctx.strokeStyle = LGraphCanvas.link_type_colors[ue_connection.type]; + ctx.shadowColor = "white"; + ctx.shadowBlur = 10; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.beginPath(); + ctx.roundRect(pos2[0]-radius,pos2[1]-radius,2*radius,2*radius,radius); + ctx.stroke(); + ctx.beginPath(); + ctx.strokeStyle = "black"; + ctx.shadowBlur = 0; + radius = radius - 1; + ctx.roundRect(pos2[0]-radius,pos2[1]-radius,2*radius,2*radius,radius); + ctx.stroke(); + + ctx.restore(); + }); + this.reading_list = false; + } + + list_ready(make_latest) { + if (this.paused) return false; + if (!this.the_graph_analyser) return false; // we don't have the analyser yet (still loading) + if (!this.ue_list) this.request_link_list_update(); + if (!this.ue_list) return false; + if (make_latest) this.last_used_ue_list = this.ue_list; + return true; + } + + node_in_ueconnection(ue_connection, id) { + return (get_group_node(ue_connection.control_node.id).id == id || get_group_node(ue_connection.sending_to.id).id == id) + } + + 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) { + try { + this._render_all_ue_links(ctx); + } catch (e) { + console.error(e); + } + } + + _render_all_ue_links(ctx) { + if (!this.list_ready(true)) return; + + this.reading_list = true; + ctx.save(); + const orig_hqr = app.canvas.highquality_render; + app.canvas.highquality_render = false; + + const mode = app.ui.settings.getSettingValue('AE.showlinks', 0); + var animate = app.ui.settings.getSettingValue('AE.animate', 3); + if (app.ui.settings.getSettingValue('AE.stop.animation.running', true) && this.queue_size>0) animate = 0; + if (animate==2 || animate==3) this.animate_step(ctx); + + var any_links_shown = false; + var any_links = false; + + 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; + + 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(); + this.reading_list = false; + } + + + _render_ue_link(ue_connection, ctx, animate) { + try { + const node = get_real_node(ue_connection.sending_to.id); + + /* this is the end node; get the position of the input */ + var pos2 = node.getConnectionPos(true, ue_connection.input_index, this.slot_pos1); + + /* get the position of the *input* that is being echoed - except for the Seed Anywhere node, + which is displayed with an output: the class records control_node_input_index as -ve (-1 => 0, -2 => 1...) */ + const input_source = (ue_connection.control_node_input_index >= 0); + const source_index = input_source ? ue_connection.control_node_input_index : -1-ue_connection.control_node_input_index; + const pos1 = get_group_node(ue_connection.control_node.id).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))) ? + ((delta_y>0) ? LiteGraph.DOWN : LiteGraph.UP) : + ((input_source && delta_x<0) ? LiteGraph.LEFT : LiteGraph.RIGHT) + + var color = LGraphCanvas.link_type_colors[ue_connection.type]; + if (color=="") color = app.canvas.default_link_color; + ctx.shadowColor = color; + + app.canvas.renderLink(ctx, pos1, pos2, undefined, true, animate%2, color, sta_direction, end_direction, undefined); + } catch (e) { + Logger.log(Logger.PROBLEM, `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=Logger.INFORMATION) for (var i=0; i { // normal links + const link_id = input.link; + if (link_id) { + const link = app.graph.links[link_id]; + if (link) all_upstream.push({id:link.origin_id, slot:link.origin_slot}); + } + }); + links_added.forEach((la)=>{ // UE links + if (get_real_node(la.downstream).id==node.id) { + all_upstream.push({id:la.upstream, slot:la.upstream_slot, ue:la.controller.toString()}) + } + }); + if (node.id != get_group_node(node.id).id) { // node is in group + const grp_nd = get_group_node(node.id).id; + const group_data = GroupNodeHandler.getGroupData(get_group_node(node.id)); + const indx = group_data.nodeData.nodes.findIndex((n)=>n.pos[0]==node.pos[0] && n.pos[1]==node.pos[1]); + if (indx>=0) { + if (GroupNodeHandler.getGroupData(app.graph._nodes_by_id[grp_nd])?.linksTo?.[indx] ) { // links within group + Object.values(GroupNodeHandler.getGroupData(app.graph._nodes_by_id[grp_nd]).linksTo[indx]).forEach((internal_link) => { + all_upstream.push({id:`${grp_nd}:${internal_link[0]}`, slot:internal_link[1]}); + }); + } + if (GroupNodeHandler.getGroupData(app.graph._nodes_by_id[grp_nd]).oldToNewInputMap?.[indx]) { // links out of group + Object.values(GroupNodeHandler.getGroupData(app.graph._nodes_by_id[grp_nd]).oldToNewInputMap?.[indx]).forEach((groupInput) => { + const link_id = get_group_node(node.id).inputs?.[groupInput]?.link; + if (link_id) { + const link = app.graph.links[link_id]; + if (link) all_upstream.push({id:link.origin_id, slot:link.origin_slot}); + } + }) + } + } + } + return all_upstream; +} + +function recursive_follow(node_id, start_node_id, links_added, stack, nodes_cleared, ues, count, slot) { + const node = get_real_node(node_id); + if (slot>=0 && GroupNodeHandler.isGroupNode(node)) { // link into group + const mapped = GroupNodeHandler.getGroupData(node).newToOldOutputMap[slot]; + return recursive_follow(`${node.id}:${mapped.node.index}`, start_node_id, links_added, stack, nodes_cleared, ues, count, mapped.slot); + } + count += 1; + if (stack.includes(node.id.toString())) throw new LoopError(node.id, new Set(stack), new Set(ues)); + if (nodes_cleared.has(node.id.toString())) return; + stack.push(node.id.toString()); + + find_all_upstream(node.id, links_added).forEach((upstream) => { + if (upstream.ue) ues.push(upstream.ue); + count = recursive_follow(upstream.id, start_node_id, links_added, stack, nodes_cleared, ues, count, upstream.slot); + if (upstream.ue) ues.pop(); + }) + + nodes_cleared.add(node.id.toString()); + stack.pop(); + return count; +} + +/* +Throw a LoopError if there is a loop. +live_nodes is a list of all live (ie not bypassed) nodes in the graph +links_added is a list of the UE virtuals links +*/ +function node_in_loop(live_nodes, links_added) { + var nodes_to_check = []; + const nodes_cleared = new Set(); + live_nodes.forEach((n)=>nodes_to_check.push(get_real_node(n.id).id)); + var count = 0; + while (nodes_to_check.length>0) { + const node_id = nodes_to_check.pop(); + count += recursive_follow(node_id, node_id, links_added, [], nodes_cleared, [], 0, -1); + nodes_to_check = nodes_to_check.filter((nid)=>!nodes_cleared.has(nid.toString())); + } + console.log(`node_in_loop made ${count} checks`) +} + +/* +Is a node alive (ie not bypassed or set to never) +*/ +function node_is_live(node){ + if (!node) return false; + if (node.mode===0) return true; + if (node.mode===2 || node.mode===4) return false; + Logger.log(Logger.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) { + if (!type || !original_link) return null; + var link = original_link; + var parent = get_real_node(link.origin_id); + 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 = app.graph.links[link_id]; + parent = get_real_node(link.origin_id); + } + return link; +} + +function all_group_nodes() { + return app.graph._nodes.filter((node) => GroupNodeHandler.isGroupNode(node)); +} + +function is_in_group(node_id, group_node) { + return group_node.getInnerNodes().find((inner_node) => (inner_node.id==node_id)); +} + +/* +Return the group node if this node_id is part of a group, else return the node itself. +Returns a full node object +*/ +function get_group_node(node_id, level=Logger.ERROR) { + const nid = node_id.toString(); + var gn = app.graph._nodes_by_id[nid]; + if (!gn && nid.includes(':')) gn = app.graph._nodes_by_id[nid.split(':')[0]]; + if (!gn) gn = all_group_nodes().find((group_node) => is_in_group(nid, group_node)); + if (!gn) Logger.log(level, `get_group node couldn't find ${nid}`) + return gn; +} + +/* +Return the node object for this node_id. +- if it's in _nodes_by_id return it +- if it is of the form x:y find it in group node x +- if it is the real node number of something in a group, get it from the group +*/ +function get_real_node(node_id, level=Logger.INFORMATION) { + const nid = node_id.toString(); + var rn = app.graph._nodes_by_id[nid]; + if (!rn && nid.includes(':')) rn = app.graph._nodes_by_id[nid.split(':')[0]]?.getInnerNodes()[nid.split(':')[1]] + if (!rn) { + all_group_nodes().forEach((node) => { + if (!rn) rn = node.getInnerNodes().find((inner_node) => (inner_node.id==nid)); + }) + } + if (!rn) Logger.log(level, `get_real_node couldn't find ${node_id} - ok during loading, shortly after node deletion etc.`) + return rn; +} + +function get_all_nodes_within(node_id) { + const node = get_group_node(node_id); + if (GroupNodeHandler.isGroupNode(node)) return node.getInnerNodes(); + return []; +} + + +/* +Does this input connect upstream to a live node? +*/ +function is_connected(input) { + const link_id = input.link; + if (link_id === null) return false; // no connection + var the_link = app.graph.links[link_id]; + if (!the_link) return false; + the_link = handle_bypass(the_link, the_link.type); // find the link upstream of bypasses + if (!the_link) return false; // no source for data. + return true; +} + +/* +Is this a UE node? +*/ +function is_UEnode(node_or_nodeType) { + const title = node_or_nodeType.type ?? node_or_nodeType.comfyClass; + return ((title) && (title.startsWith("Anything Everywhere") || title==="Seed Everywhere" || title==="Prompts Everywhere")) +} +function is_helper(node_or_nodeType) { + const title = node_or_nodeType.type ?? node_or_nodeType.comfyClass; + return ((title) && (title.startsWith("Simple String"))) +} +function has_priority_boost(node_or_nodeType) { + const title = node_or_nodeType.type ?? node_or_nodeType.comfyClass; + return ((title) && (title == "Anything Everywhere?")) +} + +/* +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) +*/ +function inject(object, methodname, tracetext, injection, injectionthis, injectionarguments) { + const original = object[methodname]; + object[methodname] = function() { + Logger.trace(`${tracetext} hijack`, arguments); + original?.apply(this, arguments); + injection.apply(injectionthis, injectionarguments); + } +} + + +export { node_in_loop, handle_bypass, node_is_live, is_connected, is_UEnode, is_helper, inject, Logger, get_real_node, get_group_node, get_all_nodes_within, has_priority_boost} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/pyproject.toml b/ComfyUI/custom_nodes/cg-use-everywhere/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b6df4a6ad3d8ac04df4bf3f3428cfb945242dd71 --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "cg-use-everywhere" +description = "A set of nodes that allow data to be 'broadcast' to some or all unconnected inputs. Greatly reduces link spaghetti." +version = "4.8.5" +license = "LICENSE" + +[project.urls] +Repository = "https://github.com/chrisgoringe/cg-use-everywhere" + +[tool.comfy] +PublisherId = "chrisgoringe" +DisplayName = "cg-use-everywhere" +Icon = "" diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/tests/compare.png b/ComfyUI/custom_nodes/cg-use-everywhere/tests/compare.png new file mode 100644 index 0000000000000000000000000000000000000000..9e374d06099a9fa3085b1411a88a978b5cae9ce6 Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/tests/compare.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/tests/test.md b/ComfyUI/custom_nodes/cg-use-everywhere/tests/test.md new file mode 100644 index 0000000000000000000000000000000000000000..2db2a4211aa7526f6612dc90b64e48d99d6d9227 --- /dev/null +++ b/ComfyUI/custom_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/ComfyUI/custom_nodes/cg-use-everywhere/tests/test.png b/ComfyUI/custom_nodes/cg-use-everywhere/tests/test.png new file mode 100644 index 0000000000000000000000000000000000000000..b642916dcbefde67253b7b0c17c205b10ed023dc Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/tests/test.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/tests/test2.png b/ComfyUI/custom_nodes/cg-use-everywhere/tests/test2.png new file mode 100644 index 0000000000000000000000000000000000000000..0ddd6017ef71bbecab8412bd4a2cd17ae59ac0ec Binary files /dev/null and b/ComfyUI/custom_nodes/cg-use-everywhere/tests/test2.png differ diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/use_everywhere.py b/ComfyUI/custom_nodes/cg-use-everywhere/use_everywhere.py new file mode 100644 index 0000000000000000000000000000000000000000..ded6ef75f2f3711b1e32aa4a281461e02600401f --- /dev/null +++ b/ComfyUI/custom_nodes/cg-use-everywhere/use_everywhere.py @@ -0,0 +1,86 @@ +from server import PromptServer +import torch + +def message(id,message): + if isinstance(message, torch.Tensor): + string = f"Tensor shape {message.shape}" + elif isinstance(message, dict) and "samples" in message and isinstance(message["samples"], torch.Tensor): + string = f"Latent shape {message['samples'].shape}" + else: + string = f"{message}" + PromptServer.instance.send_sync("ue-message-handler", {"id": id, "message":string}) + +class Base(): + OUTPUT_NODE = True + FUNCTION = "func" + CATEGORY = "everywhere" + RETURN_TYPES = () + +class SimpleString(Base): + OUTPUT_NODE = False + @classmethod + def INPUT_TYPES(s): + return {"required":{ "string": ("STRING", {"default": ""}) }} + RETURN_TYPES = ("STRING",) + + def func(self,string): + return (string,) + +class SeedEverywhere(Base): + @classmethod + def INPUT_TYPES(s): + return {"required":{ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}) }, + "hidden": {"id":"UNIQUE_ID"} } + + RETURN_TYPES = ("INT",) + + def func(self, seed, id): + message(id, seed) + return (seed,) + +class AnythingEverywhere(Base): + @classmethod + def INPUT_TYPES(s): + return {"required":{}, + "optional": { "anything" : ("*", {}), }, + "hidden": {"id":"UNIQUE_ID"} } + + def func(self, id, **kwargs): + for key in kwargs: + message(id, kwargs[key],) + return () + +class AnythingEverywherePrompts(Base): + @classmethod + def INPUT_TYPES(s): + return {"required":{}, + "optional": { "+ve" : ("*", {}), "-ve" : ("*", {}), } } + + def func(self, **kwargs): + return () + +class AnythingEverywhereTriplet(Base): + @classmethod + def INPUT_TYPES(s): + return {"required":{}, + "optional": { "anything" : ("*", {}), "anything2" : ("*", {}), "anything3" : ("*", {}),} } + + def func(self, **kwargs): + return () + +class AnythingSomewhere(Base): + @classmethod + def INPUT_TYPES(s): + return {"required":{}, + "optional": { + "anything" : ("*", {}), + "title_regex" : ("STRING", {"default":".*"}), + "input_regex" : ("STRING", {"default":".*"}), + "group_regex" : ("STRING", {"default":".*"}), + }, + "hidden": {"id":"UNIQUE_ID"} } + + def func(self, id, title_regex=None, input_regex=None, group_regex=None, **kwargs): + for key in kwargs: + message(id, kwargs[key],) + return () diff --git a/ComfyUI/custom_nodes/cg-use-everywhere/workflow_fixer.py b/ComfyUI/custom_nodes/cg-use-everywhere/workflow_fixer.py new file mode 100644 index 0000000000000000000000000000000000000000..48812442387d182efb2662764a7effe9dea16131 --- /dev/null +++ b/ComfyUI/custom_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]) +