remove old comfyUI

This commit is contained in:
salt
2024-02-29 14:11:16 -08:00
parent d44ea6619d
commit 9aad2f3d72
2932 changed files with 0 additions and 932769 deletions
@@ -1,10 +0,0 @@
__pycache__
*.ini
wildcards/**
.vscode/
.idea/
node_modules/
rgthree_config.json
web/rgthree_config.js
web/comfyui/rgthree_config.js
userdata/**
@@ -1,3 +0,0 @@
{
"printWidth": 100
}
@@ -1,633 +0,0 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=^\.#
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.10
# Discover python modules and packages in the file system subtree.
recursive=no
# Add paths to the list of the source roots. Supports globbing patterns. The
# source root is an absolute path or a path relative to the current working
# directory used to determine a package namespace for modules located under the
# source root.
source-roots=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
C0103, # invalid-name :: constant case
W0603 # global-statements
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work..
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
@@ -1,8 +0,0 @@
# https://github.com/google/yapf
[style]
based_on_style = google
indent_width = 2
CONTINUATION_INDENT_WIDTH = 2
COLUMN_LIMIT = 100
EACH_DICT_ENTRY_ON_SEPARATE_LINE = true
INDENT_DICTIONARY_VALUE = false
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Regis Gaughan, III (rgthree)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,344 +0,0 @@
<h1 align="center">
rgthree-comfy
<br>
<sub><sup><i>Making ComfyUI more comfortable!</i></sup></sub>
<br>
</h1>
<p align="center">
<a href="#️-the-nodes">The Nodes</a> &nbsp; | &nbsp; <a href="#-improvements--features">Improvements & Features</a> &nbsp; | &nbsp; <a href="#-link-fixer">Link Fixer</a>
</p>
<hr>
A collection of nodes and improvements created while messing around with ComfyUI. I made them for myself to make my workflow cleaner, easier, and faster. You're welcome to try them out. But remember, I made them for my own use cases :)
![Context Node](./docs/rgthree_advanced.png)
# Get Started
## Install
1. Install the great [ComfyUi](https://github.com/comfyanonymous/ComfyUI).
2. Clone this repo into `custom_modules`:
```
cd ComfyUI/custom_nodes
git clone https://github.com/rgthree/rgthree-comfy.git
```
3. Start up ComfyUI.
## Settings
You can configure certain aspect of rgthree-comfy. For instance, perhaps a future ComfyUI change breaks rgthree-comfy, or you already have another extension that does something similar and you want to turn it off for rgthree-comfy.
You can get to rgthree-settings by right-clicking on the empty part of the graph, and selecting `rgthree-comfy > Settings (rgthree-comfy)` or by clicking the `rgthree-comfy settings` in the ComfyUI settings dialog.
_(Note, settings are stored in an `rgthree_config.json` in the `rgthree-comfy` directory. There are other advanced settings that can only be configured there; You can copy default settings from `rgthree_config.json.default` before `rgthree_config.json` before modifying)_.
<br>
# ✴️ The Nodes
Note, you can right-click on a bunch of the rgthree-comfy nodes and select `🛟 Node Help` menu item for in-app help when available.
## Seed
> An intuitive seed control node for ComfyUI that works very much like Automatic1111's seed control.
> <details>
> <summary>ℹ️ <i>See More Information</i></summary>
>
> - Set the seed value to "-1" to use a random seed every time
> - Set any other number in there to use as a static/fixed seed
> - Quick actions to randomize, or (re-)use the last queued seed.
> - Images metadata will store the seed value _(so dragging an image in, will have the seed field already fixed to its seed)_.
> - _Secret Features_: You can manually set the seed value to "-2" or "-3" to increment or decrement the last seed value. If there was not last seed value, it will randomly use on first.
>
> ![Router Node](./docs/rgthree_seed.png)
> </details>
## Reroute
> Keep your workflow neat with this much improved Reroute node with, like, actual rerouting with multiple directions and sizes.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Use the right-click context menu to change the width, height and connection layout
> - Also toggle resizability (min size is 40x43 if resizing though), and title/type display.
>
> ![Router Node](./docs/rgthree_router.png)
> </details>
## Bookmark (🔖)
> Place the bookmark node anywhere on screen to quickly navigate to that with a shortcut key.
> <details>
> <summary>ℹ️ <i>See More Information</i></summary>
>
> - Define the `sahortvut_key` to press to go right to that bookmark node, anchored in the top left
> - You can also define the zoom level as well!
> </details>
## Context / Context Big
> Pass along in general flow properties, and merge in new data. Similar to some other node suites "pipes" but easier merging, is more easily interoperable with standard nodes by both combining and exploding all in a single node.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Context and Context Big are backwards compatible with each other. That is, an input connected to a Context Big will be passed through the CONTEXT outputs through normal Context nodes and available as an output on either (or, Context Big if the output is only on that node, like "steps").
> - Pro Tip: When dragging a Context output over a nother node, hold down "ctrl" and release to automatically connect the other Context outputs to the hovered node.
> - Pro Tip: You can change between Context and Context Big nodes from the menu.
>
> ![Context Node](./docs/rgthree_context.png)
> </details>
## Image Comparer
> The Image Comparer node compares two images on top of each other.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - **Note:** The right-click menu may show image options (Open Image, Save Image, etc.) which will correspond to the first image (image_a) if clicked on the left-half of the node, or the second image if on the right half of the node.
> - **Inputs:**
> - `image_a` _Required._ The first image to use to compare. If image_b is not supplied and image_a is a batch, the comparer will use the first two images of image_a.
> - `image_b` _Optional._ The second image to use to compare. Optional only if image_a is a batch with two images.
> - **Properties:** You can change the following properties (by right-clicking on the node, and select "Properties" or "Properties Panel" from the menu):
> - `comparer_mode` - Choose between "Slide" and "Click". Defaults to "Slide".
## Image Inset Crop
> The node that lets you crop an input image by either pixel value, or percentage value.
## Display Any
> Displays most any piece of text data from the backend _after execution_.
## Lora Loader Stack
> A simplified Lora Loader stack. Much like other suites, but more interoperable with standard inputs/outputs.
## Power Prompt
> Power up your prompt and get drop downs for adding your embeddings, loras, and even have saved prompt snippets.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - At the core, you can use Power Prompt almost as a String Primitive node with additional features of dropdowns for choosing your embeddings, and even loras, with no further processing. This will output just the raw `TEXT` to another node for any lora processing, CLIP Encoding, etc.
> - Connect a `CLIP` to the input to encode the text, with both the `CLIP` and `CONDITIONING` output right from the node.
> - Connect a `MODEL` to the input to parse and load any `<lora:...>` tags in the text automatically, without
> needing a separate Lora Loaders
> </details>
## Power Prompt - Simple
> Same as Power Prompt above, but without LORA support; made for a slightly cleaner negative prompt _(since negative prompts do not support loras)_.
## SDXL Power Prompt - Positive
> The SDXL sibling to the Power Prompt above. It contains the text_g and text_l as separate text inputs, as well a couple more input slots necessary to ensure proper clipe encoding. Combine with
## SDXL Power Prompt - Simple
> Like the non-SDXL `Power Prompt - Simple` node, this one is essentially the same as the SDXL Power Prompt but without lora support for either non-lora positive prompts or SDXL negative prompts _(since negative prompts do not support loras)_.
## SDXL Config
> Just some configuration fields for SDXL prompting. Honestly, could be used for non SDXL too.
## Context Switch / Context Switch Big
> A powerful node to branch your workflow. Works by choosing the first Context input that is not null/empty.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Pass in several context nodes and the Context Switch will automatically choose the first non-null context to continue onward with.
> - Wondering how to toggle contexts to null? Use in conjuction with the **Fast Muter** or **Fast Groups Muter**
>
> </details>
## Any Switch
> A powerful node to similar to the Context Switch above, that chooses the first input that is not null/empty.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Pass in several inmputs of the same type and the Any Switch will automatically choose the first non-null value to continue onward with.
> - Wondering how to toggle contexts to null? Use in conjuction with the **Fast Muter** or **Fast Groups Muter**
>
> </details>
## Fast Groups Muter
> The Fast Groups Muter is an input-less node that automatically collects all groups in your current workflow and allows you to quickly mute and unmute all nodes within the group.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Groups will automatically be shown, though you can filter, sort and more from the **node Properties** _(by right-clicking on the node, and select "Properties" or "Properties Panel" from the menu)_. Properties include:
> - `matchColors` - Only add groups that match the provided colors. Can be ComfyUI colors (red, pale_blue) or hex codes (#a4d399). Multiple can be added, comma delimited.
> - `matchTitle` - Filter the list of toggles by title match (string match, or regular expression).
> - `showNav` - Add / remove a quick navigation arrow to take you to the group. (default: true)
> - `sort` - Sort the toggles' order by "alphanumeric", graph "position", or "custom alphabet". (default: "position")
> - `customSortAlphabet` - When the sort property is "custom alphabet" you can define the alphabet to use here, which will match the beginning of each group name and sort against it. If group titles do not match any custom alphabet entry, then they will be put after groups that do, ordered alphanumerically.
>
> This can be a list of single characters, like "zyxw..." or comma delimited strings for more control, like "sdxl,pro,sd,n,p".
>
> Note, when two group title match the same custom alphabet entry, the normal alphanumeric alphabet breaks the tie. For instance, a custom alphabet of "e,s,d" will order groups names like "SDXL, SEGS, Detailer" eventhough the custom alphabet has an "e" before "d" (where one may expect "SE" to be before "SD").
>
> To have "SEGS" appear before "SDXL" you can use longer strings. For instance, the custom alphabet value of "se,s,f" would work here.
> - `toggleRestriction` - Optionally, attempt to restrict the number of widgets that can be enabled to a maximum of one, or always one.
>
> _Note: If using "max one" or "always one" then this is only enforced when clicking a toggle on this node; if nodes within groups are changed outside of the initial toggle click, then these restriction will not be enforced, and could result in a state where more than one toggle is enabled. This could also happen if nodes are overlapped with multiple groups._
> </details>
## Fast Groups Bypasser
> _Same as **Fast Groups Muter** above, but sets the connected nodes to "Bypass" instead of "Mute"_
## Fast Muter
> A powerful 'control panel' node to quickly toggle connected nodes allowing them to quickly be muted or enabled
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Add a collection of all connected nodes allowing a single-spot as a "dashboard" to quickly enable and disable nodes. Two distinct nodes; one for "Muting" connected nodes, and one for "Bypassing" connected nodes.
> </details>
## Fast Bypasser
> Same as Fast Muter but sets the connected nodes to "Bypass"
## Fast Actions Button
> Oh boy, this node allows you to semi-automate connected nodes and/or ConfyUI.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Connect nodes and, at the least, mute, bypass or enable them when the button is pressed.
> - Certain nodes expose additional actions. For instance, the `Seed` node you can set `Randomize Each Time` or `Use Last Queued Seed` when the button is pressed.
> - Also, from the node properties, set a shortcut key to toggle the button actions, without needing a click!
> </details>
## Node Collector
> Used to cleanup noodles, this will accept any number of input nodes and passes it along to another node.
>
> ⚠️ *Currently, this should really only be connected to **Fast Muter**, **Fast Bypasser**, or **Mute / Bypass Relay**.*
## Mute / Bypass Repeater
> A powerful node that will dispatch its Mute/Bypass/Active mode to all connected input nodes or, if in a group w/o any connected inputs, will dispatch its Mute/Bypass/Active mode to all nodes in that group.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - 💡 Pro Tip #1: Connect this node's output to a **Fast Muter** or **Fast Bypasser** to have a single toggle there that can mute/bypass/enable many nodes with one click.
>
> - 💡 Pro Tip #2: Connect a **Mute / Bypass Relay** node to this node's inputs to have the relay automatically dispatch a mute/bypass/enable change to the repeater.
> </details>
## Mute / Bypass Relay
> An advanced node that, when working with a **Mute / Bypass Repeater** will relay a mute/bypass/activate signal to the repeater
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
> - Useful when you want a specific node or set of nodes to be muted when a different set of nodes are also muted.
> </details>
## Random Unmuter
> An advanced node used to unmute one of its inputs randomly when the graph is queued (and, immediately mute it back).
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - **Note:** All input nodes MUST be muted to start; if not this node will not randomly unmute another. (This is powerful, as the generated image can be dragged in and the chosen input will already by unmuted and work w/o any further action.)
> - **Tip:** Connect a Repeater's output to this nodes input and place that Repeater on a group without any other inputs, and it will mute/unmute the entire group.
> </details>
# Advanced Techniques
## First, a word on muting
A lot of the power of these nodes comes from *Muting*. Muting is the basis of correctly implementing multiple paths for a workflow utlizing the Context Switch node.
While other extensions may provide switches, they often get it wrong causing your workflow to do more work than is needed. While other switches may have a selector to choose which input to pass along, they don't stop the execution of the other inputs, which will result in wasted work. Instead, Context Switch works by choosing the first non-empty context to pass along and correctly Muting is one way to make a previous node empty, and causes no extra work to be done when set up correctly.
### To understand muting, is to understand the graph flow
Muting, and therefore using Switches, can often confuse people at first because it _feels_ like muting a node, or using a switch, should be able to stop or direct the _forward_ flow of the graph. However, this is not the case and, in fact, the graph actually starts working backwards.
If you have a workflow that has a path like `... > Context > KSampler > VAE Decode > Save Image` it may initially _feel_ like you should be able to mute that first Context node and the graph would stop there when moving forward and skip the rest of that workflow.
But you'll quickly find that will cause an error, becase the graph doesn't actually move forward. When a workflow is processed, it _first moves backwards_ starting at each "Output Node" (Preview Image, Save Image, even "Display String" etc.) and then walking backwards to all possible paths to get there.
So, with that `... > Context > KSampler > VAE Decode > Save Image` example from above, we actually want to mute the `Save Image` node to stop this path. Once we do, since the output node is gone, none of these nodes will be run.
Let's take a look at an example.
### A powerful combination: Using Context, Context Switch, & Fast Muter
![Context Node](./docs/rgthree_advanced.png)
1. Using the **Context Switch** (aqua colored in screenshot) feed context inputs in order of preference. In the workflow above, the `Upscale Out` context is first so, if that one is enabled, it will be chosen for the output. If not, the second input slot which comes from the context rerouted from above (before the Upscaler booth) will be chosen.
- Notice the `Upscale Preview` is _after_ the `Upscale Out` context node, using the image from it instead of the image from the upscale `VAE Decoder`. This is on purpose so, when we disable the `Upscale Out` context, none of the Upscaler nodes will run, saving precious GPU cycles. If we had the preview hooked up directly to the `VAE Decoder` the upscaler would always run to generate the preview, even if we had the `Upscale Out` context node disabled.
2. We can now disable the `Upscale Out` context node by _muting_ it. Highlighting it and pressing `ctrl + m` will work. By doing so, it's output will be None, and it will not pass anthing onto the further nodes. In the diagram you can see the `Upscale Preview` is red, but that's OK; there are no actual errors to stop execution.
3. Now, let's hook it up to the `Fast Muter` node. `The Fast Muter` node works as dashboard by adding quick toggles for any connected node (ignoring reroutes). In the diagram, we have both the `Upscaler Out` context node, and the `Save File` context node hooked up. So, we can quickly enable and disable those.
- The workflow seen here would be a common one where we can generate a handful of base previews cheaply with a random seed, and then choose one to upscale and save to disk.
4. Lastly, and optionally, you can see the `Node Collector`. Use it to clean up noodles if you want and connect it to the muter. You can connect anything to it, but doing so may break your workflow's execution.
<br>
# ⚡ Improvements & Features
rgthree-comfy adds several improvements, features, and optimizations to ComfyUI that are not directly tied to nodes.
## Progress Bar
> A minimal progress bar that run alongs the top of the app window that shows the queue size, the current progress of the a prompt execution (within the same window), and the progress of multi-step nodes as well.
>
> <i>You can remove/enable from rgthree-comfy settings, as well as configure the height/size.</i>
## ComfyUI Recursive Optimization
> An optimization to ComfyUI's recursive execution. Because rgthree-comfy nodes make it easy to build larger, more complex workflows, I (and others) started to hit a wall of poor execution times.
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Until [ComfyUI/issues/1502](https://github.com/comfyanonymous/ComfyUI/issues/1502) is resolved and/or [ComfyUI/pull/1503](https://github.com/comfyanonymous/ComfyUI/pull/1503) is pulled in, then know that you're benefiting from hundreds of millions of saved cycles each run.
>
> - Specifically, for a rather complex test workflow, the patch reduces iterations of `recursive_will_execute` from 113,292,566 to just 135 (and 116.32 seconds to 69.84 seconds on my machine) on a fresh queue, and reduces recursive calls of `recursive_output_delete_if_changed` from 250,496,808 to 142 (and 158.13 seconds to 0.0 seconds on my machine).
>
> - ⚠️ *However,* there is a chance ComfyUI changes something in/around the code I patched which could break. If that's the case, you should disable the optimization from rgthree-comfy settings.
>
> </details>
## "Queue Selected Output Nodes" in right-click menu
> Sometimes you want to just queue one or two paths to specific output node(s) without executing the entire workflow. Well, now you can do just that by right-clicking on an output node and selecting `Queue Selected Output Nodes (rgthree)`.
>
> <details>
> <summary>ℹ️ <i>More Information</i></summary>
>
> - Select the _output_ nodes you want to execute.
>
> - Note: Only output nodes are captured and traversed, not all selected nodes. So if you select an output AND a node from a different path, only the path connected to the output will be executed and not non-output nodes, even if they were selected.
>
> - Note: The whole workflow is serialized, and then we trim what we don't want for the backend. So things like all seed random/increment/decrement will run even if that node isn't being sent in the end, etc.
>
> </details>
## Auto-Nest Subdirectories in long Combos
> _(Off by default while experimenting, turn on in rgthree-comfy settings)_.
>
> Automatically detect top-level subdirectories in long combo lists (like, Load Checkpoint) and break out into sub directories.
## "Copy Image" in right-click menu
> Right clicking on a node that has an image should have a context-menu item of "Copy Image" will allow you to copy the image right to your clipboard
>
> <i>🎓 I believe this has graduated, with ComfyUI recently adding this setting too. You won't get two menu items; my code checks that there isn't already a "Copy Image" item there before adding it.</i>
## Other/Smaller Fixes
- Fixed the width of ultra-wide node chooser on double click.
- Fixed z-indexes for textareas that would overlap above other elements, like Properties Panel, or @pythongosssss's image viewer.
- Check for bad links when loading a workflow and log to console, by default. _(See Link Fixer below)._
<br>
# 📄 Link Fixer
If your workflows sometimes have missing connections, or even errors on load, start up ComfyUI and go to http://127.0.0.1:8188/rgthree/link_fixer which will allow you to drop in an image or workflow json file and check for and fix any bad links.
You can also enable a link fixer check in the rgthree-comfy settings to give you an alert if you load a workflow with bad linking data to start.
@@ -1,82 +0,0 @@
#!/usr/bin/env python3
import subprocess
import os
from shutil import rmtree, copytree, ignore_patterns
from glob import glob
import time
import re
from py.log import COLORS
from py.config import RGTHREE_CONFIG
start = time.time()
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
DIR_SRC_WEB = os.path.abspath(f'{THIS_DIR}/src_web/')
DIR_WEB = os.path.abspath(f'{THIS_DIR}/web/')
DIR_WEB_COMFYUI = os.path.abspath(f'{DIR_WEB}/comfyui/')
rmtree(DIR_WEB)
def log_step(msg=None, status=None):
""" Logs a step keeping track of timing and initial msg. """
global step_msg # pylint: disable=W0601
global step_start # pylint: disable=W0601
if msg:
step_msg = f'▻ [Starting] {msg}...'
step_start = time.time()
print(step_msg, end="\r")
elif status:
step_time = round(time.time() - step_start, 3)
if status == 'Error':
status_msg=f'{COLORS["RED"]}⤫ {status}{COLORS["RESET"]}'
else:
status_msg=f'{COLORS["BRIGHT_GREEN"]}🗸 {status}{COLORS["RESET"]}'
print(
f'{step_msg.ljust(50, ".")} {COLORS["BRIGHT_GREEN"]}🗸 {status}{COLORS["RESET"]} ({step_time}s)'
)
log_step(msg='Copying web directory')
copytree(DIR_SRC_WEB, DIR_WEB, ignore=ignore_patterns("typings*", "*.ts", "*.scss"))
log_step(status="Done")
log_step(msg='TypeScript')
checked = subprocess.run(["./node_modules/typescript/bin/tsc"], check=True)
log_step(status="Done")
scsss = glob(os.path.join(DIR_SRC_WEB, "**", "*.scss"), recursive=True)
log_step(msg=f'SASS for {len(scsss)} files')
scsss = [i.replace(THIS_DIR, '.') for i in scsss]
cmds = ["node", "./node_modules/sass/sass"]
for scss in scsss:
out = scss.replace('src_web', 'web').replace('.scss', '.css')
cmds.append(f'{scss}:{out}')
cmds.append('--no-source-map')
checked = subprocess.run(cmds, check=True)
log_step(status="Done")
# Handle the common directories. Because ComfyUI loads under /extensions/rgthree-comfy we can't
# easily share sources outside of the `DIR_WEB_COMFYUI` _and_ allow typescript to resolve them in
# src view, so we set the path in the tsconfig to map an import of "rgthree/common" to the
# "src_web/common" directory, but then need to rewrite the comfyui JS files to load from
# "../../rgthree/common" (which we map correctly in rgthree_server.py).
log_step(msg='Cleaning Imports')
print('▻ [Starting] Cleaning Imports...', end="\r")
web_subfolders = [f.name for f in os.scandir(DIR_WEB) if f.is_dir()]
for subfolder in web_subfolders:
js_files = glob(os.path.join(DIR_WEB, subfolder, '*.js'), recursive=True)
for file in js_files:
with open(file, 'r', encoding="utf-8") as f:
filedata = f.read()
if subfolder == 'comfyui':
filedata = re.sub(r'(from\s+["\'])rgthree/', '\\1../../rgthree/', filedata)
else:
filedata = re.sub(r'(from\s+["\'])rgthree/', '\\1../', filedata)
with open(file, 'w', encoding="utf-8") as f:
f.write(filedata)
log_step(status="Done")
print(f'Finished all in {round(time.time() - start, 3)}s')
@@ -1,313 +0,0 @@
"""
@author: rgthree
@title: Comfy Nodes
@nickname: rgthree
@description: A bunch of nodes I created that I also find useful.
"""
from glob import glob
import json
import os
import shutil
import re
import random
import execution
from .py.log import log
from .py.config import get_config_value
from .py.rgthree_server import *
from .py.context import RgthreeContext
from .py.context_switch import RgthreeContextSwitch
from .py.context_switch_big import RgthreeContextSwitchBig
from .py.display_any import RgthreeDisplayAny, RgthreeDisplayInt
from .py.lora_stack import RgthreeLoraLoaderStack
from .py.seed import RgthreeSeed
from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage
from .py.power_prompt import RgthreePowerPrompt
from .py.power_prompt_simple import RgthreePowerPromptSimple
from .py.image_inset_crop import RgthreeImageInsetCrop
from .py.context_big import RgthreeBigContext
from .py.ksampler_config import RgthreeKSamplerConfig
from .py.sdxl_power_prompt_postive import RgthreeSDXLPowerPromptPositive
from .py.sdxl_power_prompt_simple import RgthreeSDXLPowerPromptSimple
from .py.any_switch import RgthreeAnySwitch
from .py.context_merge import RgthreeContextMerge
from .py.context_merge_big import RgthreeContextMergeBig
from .py.image_comparer import RgthreeImageComparer
NODE_CLASS_MAPPINGS = {
RgthreeBigContext.NAME: RgthreeBigContext,
RgthreeContext.NAME: RgthreeContext,
RgthreeContextSwitch.NAME: RgthreeContextSwitch,
RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig,
RgthreeContextMerge.NAME: RgthreeContextMerge,
RgthreeContextMergeBig.NAME: RgthreeContextMergeBig,
RgthreeDisplayInt.NAME: RgthreeDisplayInt,
RgthreeDisplayAny.NAME: RgthreeDisplayAny,
RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack,
RgthreeSeed.NAME: RgthreeSeed,
RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop,
RgthreePowerPrompt.NAME: RgthreePowerPrompt,
RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple,
RgthreeKSamplerConfig.NAME: RgthreeKSamplerConfig,
RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage,
RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive,
RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple,
RgthreeAnySwitch.NAME: RgthreeAnySwitch,
RgthreeImageComparer.NAME: RgthreeImageComparer,
}
# WEB_DIRECTORY is the comfyui nodes directory that ComfyUI will link and auto-load.
WEB_DIRECTORY = "./web/comfyui"
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
DIR_WEB = os.path.abspath(f'{THIS_DIR}/{WEB_DIRECTORY}')
DIR_PY = os.path.abspath(f'{THIS_DIR}/py')
# remove old directories
OLD_DIRS = [
os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree'),
os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy'),
]
for old_dir in OLD_DIRS:
if os.path.exists(old_dir):
shutil.rmtree(old_dir)
__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
NOT_NODES = ['constants', 'log', 'utils', 'rgthree', 'rgthree_server', 'image_clipbaord', 'config']
nodes = []
for file in glob(os.path.join(DIR_PY, '*.py')) + glob(os.path.join(DIR_WEB, '*.js')):
name = os.path.splitext(os.path.basename(file))[0]
if name in NOT_NODES or name in nodes:
continue
if name.startswith('_') or name.startswith('base') or 'utils' in name:
continue
nodes.append(name)
if name == 'display_any':
nodes.append('display_int')
print()
adjs = ['exciting', 'extraordinary', 'epic', 'fantastic', 'magnificent']
log(f'Loaded {len(nodes)} {random.choice(adjs)} nodes.', color='BRIGHT_GREEN')
# Alright, I don't like doing this, but until https://github.com/comfyanonymous/ComfyUI/issues/1502
# and/or https://github.com/comfyanonymous/ComfyUI/pull/1503 is pulled into ComfyUI, we need a way
# to optimize the recursion that happens on prompt eval. This is particularly important for
# rgthree nodes because workflows can contain many context nodes, but the problem would exist for
# other nodes' (like "pipe" nodes, efficieny nodes). With `Context Big` nodes being
# introduced, the number of input recursion that happens in these methods is exponential with a
# saving of 1000's of percentage points over.
# We'll use this to check if we _can_ patch execution. Other work to change the execution may
# remove these methods, and we want to ensure people's apps do not break.
could_patch_execution = (hasattr(execution, 'recursive_output_delete_if_changed') and
hasattr(execution, 'recursive_will_execute') and
hasattr(execution.PromptExecutor, 'execute'))
if get_config_value('features.patch_recursive_execution') is True:
if not could_patch_execution:
log("NOTE: Will NOT use rgthree's optimized recursive execution as ComfyUI has changed.",
color='YELLOW')
else:
log("Will use rgthree's optimized recursive execution.", color='BRIGHT_GREEN')
class RgthreePatchRecursiveExecute_Set_patch_recursive_execution_to_false_if_not_working:
"""A fake 'list' that the caller for recursive_will_execute expects but we override such that
`len(inst)` will return the count number, and `inst[-1]` will return the unique_id. Since that
all the caller cares about, we can save several minutes and many MB of ram by simply counting
numbers instead of concatenating a list of millions (only to count it). However the caller
expects such a list, so we fake it with this.
This mimics the enhancement from https://github.com/rgthree/ComfyUI/commit/50b3fb1 but without
modifying the execution.py
"""
def __init__(self, unique_id):
self.unique_id = unique_id
self.count = 0
def add(self, value):
self.count += value
def __getitem__(self, key):
"""Returns the `unique_id` with '-1' since that's what the caller expects."""
if key == -1:
return self.unique_id
# This one would future proof the proposed changes, in that case "0" is the count
if key == 0:
return self.count
else:
return -1
def __len__(self):
"""Returns the "count" of the "list" as if we were building up a list instea of just
incrementing `count`.
"""
return self.count
# The following (hopefully) future proofs if https://github.com/rgthree/ComfyUI/commit/50b3fb1
# goes in, which changes from using `len` on a list, to sort directly (and, thus "<" and ">").
def __gt__(self, other):
return self.count > other
def __lt__(self, other):
return self.count < other
def __str__(self):
return str((
self.count,
self.unique_id,
))
# Caches which will be cleared on each run
execution.rgthree_cache_recursive_output_delete_if_changed_output = {}
execution.rgthree_cache_recursive_will_execute = {}
execution.rgthree_is_currently_optimized = False
def rgthree_execute(self, *args, **kwargs):
""" A patch of ComfyUI's default execution for optimization (or un-optimization) via config."""
if get_config_value('features.patch_recursive_execution') is True:
if could_patch_execution:
log("Using rgthree's optimized recursive execution.", color='GREEN')
# When we execute, we'll reset our global cache here.
execution.rgthree_cache_recursive_output_delete_if_changed_output = {}
execution.rgthree_cache_recursive_will_execute = {}
if not execution.rgthree_is_currently_optimized:
log("First run patching recursive_output_delete_if_changed and recursive_will_execute.",
color='GREEN',
msg_color='RESET')
log(
"Note: \33[0mIf execution seems broken due to forward ComfyUI changes, you can disable " +
"the optimization from rgthree settings in ComfyUI.",
color='YELLOW')
execution.rgthree_old_recursive_output_delete_if_changed = execution.recursive_output_delete_if_changed
execution.recursive_output_delete_if_changed = rgthree_recursive_output_delete_if_changed
execution.rgthree_old_recursive_will_execute = execution.recursive_will_execute
execution.recursive_will_execute = rgthree_recursive_will_execute
execution.rgthree_is_currently_optimized = True
elif execution.rgthree_is_currently_optimized:
log("Removing optimizations to recursive_output_delete_if_changed and recursive_will_execute.",
color='YELLOW',
msg_color='RESET')
log("You can enable optimization in the rgthree settings in ComfyUI.", color='CYAN')
execution.recursive_output_delete_if_changed = execution.rgthree_old_recursive_output_delete_if_changed
execution.recursive_will_execute = execution.rgthree_old_recursive_will_execute
execution.rgthree_is_currently_optimized = False
# We always call the original execute, it's just whether we patch or unpacth first.
return self.rgthree_old_execute(*args, **kwargs)
# We always patch execute, so we can check if we want to do work. Up in rgthree_execute we will
# either patch or unpatch recursive_will_execute recursive_output_delete_if_changed at runtime when
# config changes.
execution.PromptExecutor.rgthree_old_execute = execution.PromptExecutor.execute
execution.PromptExecutor.execute = rgthree_execute
def rgthree_recursive_will_execute(prompt, outputs, current_item, *args, **kwargs):
"""Patches recursive_will_execute function to cache the result of each output."""
unique_id = current_item
inputs = prompt[unique_id]['inputs']
will_execute = RgthreePatchRecursiveExecute_Set_patch_recursive_execution_to_false_if_not_working(
unique_id)
if unique_id in outputs:
return will_execute
will_execute.add(1)
for x in inputs:
input_data = inputs[x]
if isinstance(input_data, list):
input_unique_id = input_data[0]
output_index = input_data[1]
node_output_cache_key = f'{input_unique_id}.{output_index}'
will_execute_value = None
# If this node's output has already been recursively evaluated, then we can reuse.
if node_output_cache_key in execution.rgthree_cache_recursive_will_execute:
will_execute_value = execution.rgthree_cache_recursive_will_execute[node_output_cache_key]
elif input_unique_id not in outputs:
will_execute_value = execution.recursive_will_execute(prompt, outputs, input_unique_id,
*args, **kwargs)
execution.rgthree_cache_recursive_will_execute[node_output_cache_key] = will_execute_value
if will_execute_value is not None:
will_execute.add(len(will_execute_value))
return will_execute
def rgthree_recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item, *args,
**kwargs):
"""Patches recursive_output_delete_if_changed function to cache the result of each output."""
unique_id = current_item
inputs = prompt[unique_id]['inputs']
class_type = prompt[unique_id]['class_type']
class_def = execution.nodes.NODE_CLASS_MAPPINGS[class_type]
is_changed_old = ''
is_changed = ''
to_delete = False
if hasattr(class_def, 'IS_CHANGED'):
if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]:
is_changed_old = old_prompt[unique_id]['is_changed']
if 'is_changed' not in prompt[unique_id]:
input_data_all = execution.get_input_data(inputs, class_def, unique_id, outputs)
if input_data_all is not None:
try:
#is_changed = class_def.IS_CHANGED(**input_data_all)
is_changed = execution.map_node_over_list(class_def, input_data_all, "IS_CHANGED")
prompt[unique_id]['is_changed'] = is_changed
except:
to_delete = True
else:
is_changed = prompt[unique_id]['is_changed']
if unique_id not in outputs:
return True
if not to_delete:
if is_changed != is_changed_old:
to_delete = True
elif unique_id not in old_prompt:
to_delete = True
elif inputs == old_prompt[unique_id]['inputs']:
for x in inputs:
input_data = inputs[x]
if isinstance(input_data, list):
input_unique_id = input_data[0]
output_index = input_data[1]
node_output_cache_key = f'{input_unique_id}.{output_index}'
# If this node's output has already been recursively evaluated, then we can stop.
if node_output_cache_key in execution.rgthree_cache_recursive_output_delete_if_changed_output:
to_delete = execution.rgthree_cache_recursive_output_delete_if_changed_output[
node_output_cache_key]
elif input_unique_id in outputs:
to_delete = execution.recursive_output_delete_if_changed(prompt, old_prompt, outputs,
input_unique_id, *args,
**kwargs)
execution.rgthree_cache_recursive_output_delete_if_changed_output[
node_output_cache_key] = to_delete
else:
to_delete = True
if to_delete:
break
else:
to_delete = True
if to_delete:
d = outputs.pop(unique_id)
del d
return to_delete
print()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

@@ -1,260 +0,0 @@
{
"name": "rgthree-comfy",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"prettier": "3.0.3",
"sass": "^1.70.0",
"typescript": "^5.2.2"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/immutable": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
"integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
"dev": true
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prettier": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/sass": {
"version": "1.70.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
"integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
@@ -1,10 +0,0 @@
{
"devDependencies": {
"prettier": "3.0.3",
"typescript": "^5.2.2",
"sass": "^1.70.0"
},
"scripts": {
"build": "./__build__.py"
}
}
@@ -1,4 +0,0 @@
import folder_paths
# Add 'saved_prompts' as a folder for Power Prompt node.
folder_paths.folder_names_and_paths['saved_prompts'] = ([], set(['.txt']))
@@ -1,52 +0,0 @@
import json
from .context_utils import is_context_empty
from .constants import get_category, get_name
from .utils import any_type
def is_none(value):
"""Checks if a value is none. Pulled out in case we want to expand what 'None' means."""
if value is not None:
if isinstance(value, dict) and 'model' in value and 'clip' in value:
return is_context_empty(value)
return value is None
class RgthreeAnySwitch:
"""The any switch. """
NAME = get_name("Any Switch")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {},
"optional": {
"any_01": (any_type,),
"any_02": (any_type,),
"any_03": (any_type,),
"any_04": (any_type,),
"any_05": (any_type,),
},
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ('*',)
FUNCTION = "switch"
def switch(self, any_01=None, any_02=None, any_03=None, any_04=None, any_05=None):
"""Chooses the first non-empty item to output."""
any_value = None
if not is_none(any_01):
any_value = any_01
elif not is_none(any_02):
any_value = any_02
elif not is_none(any_03):
any_value = any_03
elif not is_none(any_04):
any_value = any_04
elif not is_none(any_05):
any_value = any_05
return (any_value,)
@@ -1,91 +0,0 @@
import os
import json
import re
from .utils import get_dict_value, set_dict_value, dict_has_key
def get_config_value(key):
return get_dict_value(RGTHREE_CONFIG, key)
def extend_config(default_config, user_config):
""" Returns a new config dict combining user_config into defined keys for default_config."""
cfg = {}
for key, value in default_config.items():
if key not in user_config:
cfg[key] = value
elif isinstance(value, dict):
cfg[key] = extend_config(value, user_config[key])
else:
cfg[key] = user_config[key] if key in user_config else value
return cfg
def set_user_config(data: dict):
""" Sets the user configuration."""
count = 0
for key, value in data.items():
if dict_has_key(DEFAULT_CONFIG, key):
set_dict_value(USER_CONFIG, key, value)
set_dict_value(RGTHREE_CONFIG, key, value)
count+=1
if count > 0:
write_user_config()
def get_rgthree_default_config():
""" Gets the default configuration."""
with open(DEFAULT_CONFIG_FILE, 'r', encoding = 'UTF-8') as file:
config = re.sub(r"(?:^|\s)//.*", "", file.read(), flags=re.MULTILINE)
return json.loads(config)
def get_rgthree_user_config():
""" Gets the user configuration."""
if os.path.exists(USER_CONFIG_FILE):
with open(USER_CONFIG_FILE, 'r', encoding = 'UTF-8') as file:
config = re.sub(r"(?:^|\s)//.*", "", file.read(), flags=re.MULTILINE)
return json.loads(config)
else:
return {}
def write_user_config():
""" Writes the user configuration."""
with open(USER_CONFIG_FILE, 'w+', encoding = 'UTF-8') as file:
json.dump(USER_CONFIG, file, sort_keys=True, indent=2, separators=(",", ": "))
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_CONFIG_FILE = os.path.join(THIS_DIR, '..', 'rgthree_config.json.default')
USER_CONFIG_FILE = os.path.join(THIS_DIR, '..', 'rgthree_config.json')
DEFAULT_CONFIG = get_rgthree_default_config()
# Shim in unreleased features.
if 'progress_bar' not in DEFAULT_CONFIG["features"]:
DEFAULT_CONFIG["features"]["progress_bar"] = {"enabled": False}
USER_CONFIG = get_rgthree_user_config()
# Migrate old config options into "features"
needs_to_write_user_config = False
if 'patch_recursive_execution' in USER_CONFIG:
if 'features' not in USER_CONFIG:
USER_CONFIG['features'] = {}
USER_CONFIG['features']['patch_recursive_execution'] = USER_CONFIG['patch_recursive_execution']
del USER_CONFIG['patch_recursive_execution']
needs_to_write_user_config = True
if 'show_alerts_for_corrupt_workflows' in USER_CONFIG:
if 'features' not in USER_CONFIG:
USER_CONFIG['features'] = {}
USER_CONFIG['features']['show_alerts_for_corrupt_workflows'] = USER_CONFIG['show_alerts_for_corrupt_workflows']
del USER_CONFIG['show_alerts_for_corrupt_workflows']
needs_to_write_user_config = True
if 'monitor_for_corrupt_links' in USER_CONFIG:
if 'features' not in USER_CONFIG:
USER_CONFIG['features'] = {}
USER_CONFIG['features']['monitor_for_corrupt_links'] = USER_CONFIG['monitor_for_corrupt_links']
del USER_CONFIG['monitor_for_corrupt_links']
needs_to_write_user_config = True
if needs_to_write_user_config is True:
print('writing new user config.')
write_user_config()
RGTHREE_CONFIG = extend_config(DEFAULT_CONFIG, USER_CONFIG)
@@ -1,11 +0,0 @@
NAMESPACE='rgthree'
def get_name(name):
return '{} ({})'.format(name, NAMESPACE)
def get_category(sub_dirs = None):
if sub_dirs is None:
return NAMESPACE
else:
return "{}/utils".format(NAMESPACE)
@@ -1,33 +0,0 @@
"""The Context node."""
from .context_utils import (ORIG_CTX_OPTIONAL_INPUTS, ORIG_CTX_RETURN_NAMES, ORIG_CTX_RETURN_TYPES,
get_orig_context_return_tuple, new_context)
from .constants import get_category, get_name
class RgthreeContext:
"""The initial Context node.
For now, this nodes' outputs will remain as-is, as they are perfect for most 1.5 application, but
is also backwards compatible with other Context nodes.
"""
NAME = get_name("Context")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {},
"optional": ORIG_CTX_OPTIONAL_INPUTS,
"hidden": {
"version": "FLOAT"
},
}
RETURN_TYPES = ORIG_CTX_RETURN_TYPES
RETURN_NAMES = ORIG_CTX_RETURN_NAMES
FUNCTION = "convert"
def convert(self, base_ctx=None, **kwargs): # pylint: disable = missing-function-docstring
ctx = new_context(base_ctx, **kwargs)
return get_orig_context_return_tuple(ctx)
@@ -1,31 +0,0 @@
"""The Conmtext big node."""
from .constants import get_category, get_name
from .context_utils import (ALL_CTX_OPTIONAL_INPUTS, ALL_CTX_RETURN_NAMES, ALL_CTX_RETURN_TYPES,
new_context, get_context_return_tuple)
class RgthreeBigContext:
"""The Context Big node.
This context node will expose all context fields as inputs and outputs. It is backwards compatible
with other context nodes and can be intertwined with them.
"""
NAME = get_name("Context Big")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name,missing-function-docstring
return {
"required": {},
"optional": ALL_CTX_OPTIONAL_INPUTS,
"hidden": {},
}
RETURN_TYPES = ALL_CTX_RETURN_TYPES
RETURN_NAMES = ALL_CTX_RETURN_NAMES
FUNCTION = "convert"
def convert(self, base_ctx=None, **kwargs): # pylint: disable = missing-function-docstring
ctx = new_context(base_ctx, **kwargs)
return get_context_return_tuple(ctx)
@@ -1,35 +0,0 @@
"""The Context Switch (Big)."""
from .constants import get_category, get_name
from .context_utils import (ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES,
merge_new_context, get_orig_context_return_tuple)
class RgthreeContextMerge:
"""The Context Merge Big node."""
NAME = get_name("Context Merge")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {},
"optional": {
"ctx_01": ("RGTHREE_CONTEXT",),
"ctx_02": ("RGTHREE_CONTEXT",),
"ctx_03": ("RGTHREE_CONTEXT",),
"ctx_04": ("RGTHREE_CONTEXT",),
"ctx_05": ("RGTHREE_CONTEXT",),
},
}
RETURN_TYPES = ORIG_CTX_RETURN_TYPES
RETURN_NAMES = ORIG_CTX_RETURN_NAMES
FUNCTION = "merge"
def merge(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None):
"""Merges any non-null passed contexts; later ones overriding earlier.
"""
ctx = merge_new_context(ctx_01, ctx_02, ctx_03, ctx_04, ctx_05)
return get_orig_context_return_tuple(ctx)
@@ -1,35 +0,0 @@
"""The Context Switch (Big)."""
from .constants import get_category, get_name
from .context_utils import (ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES, is_context_empty,
merge_new_context, get_context_return_tuple)
class RgthreeContextMergeBig:
"""The Context Merge Big node."""
NAME = get_name("Context Merge Big")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {},
"optional": {
"ctx_01": ("RGTHREE_CONTEXT",),
"ctx_02": ("RGTHREE_CONTEXT",),
"ctx_03": ("RGTHREE_CONTEXT",),
"ctx_04": ("RGTHREE_CONTEXT",),
"ctx_05": ("RGTHREE_CONTEXT",),
},
}
RETURN_TYPES = ALL_CTX_RETURN_TYPES
RETURN_NAMES = ALL_CTX_RETURN_NAMES
FUNCTION = "merge"
def merge(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None):
"""Merges any non-null passed contexts; later ones overriding earlier.
"""
ctx = merge_new_context(ctx_01, ctx_02, ctx_03, ctx_04, ctx_05)
return get_context_return_tuple(ctx)
@@ -1,51 +0,0 @@
"""The original Context Switch."""
from .constants import get_category, get_name
from .context_utils import (ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES, is_context_empty,
get_orig_context_return_tuple)
class RgthreeContextSwitch:
"""The initial Context Switch node.
For now, this will remain as-is but is otherwise backwards compatible with other Context nodes
outputs.
"""
NAME = get_name("Context Switch")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {},
"optional": {
"ctx_01": ("RGTHREE_CONTEXT",),
"ctx_02": ("RGTHREE_CONTEXT",),
"ctx_03": ("RGTHREE_CONTEXT",),
"ctx_04": ("RGTHREE_CONTEXT",),
"ctx_05": ("RGTHREE_CONTEXT",),
},
}
RETURN_TYPES = ORIG_CTX_RETURN_TYPES
RETURN_NAMES = ORIG_CTX_RETURN_NAMES
FUNCTION = "switch"
def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None):
"""Chooses the first non-empty Context to output.
As of right now, this returns the "original" context. We could expand it, or create another
"Context Big Switch" and have all the outputs...
"""
ctx = None
if not is_context_empty(ctx_01):
ctx = ctx_01
elif not is_context_empty(ctx_02):
ctx = ctx_02
elif not is_context_empty(ctx_03):
ctx = ctx_03
elif not is_context_empty(ctx_04):
ctx = ctx_04
elif not is_context_empty(ctx_05):
ctx = ctx_05
return get_orig_context_return_tuple(ctx)
@@ -1,44 +0,0 @@
"""The Context Switch (Big)."""
from .constants import get_category, get_name
from .context_utils import (ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES, is_context_empty,
get_context_return_tuple)
class RgthreeContextSwitchBig:
"""The Context Switch Big node."""
NAME = get_name("Context Switch Big")
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {},
"optional": {
"ctx_01": ("RGTHREE_CONTEXT",),
"ctx_02": ("RGTHREE_CONTEXT",),
"ctx_03": ("RGTHREE_CONTEXT",),
"ctx_04": ("RGTHREE_CONTEXT",),
"ctx_05": ("RGTHREE_CONTEXT",),
},
}
RETURN_TYPES = ALL_CTX_RETURN_TYPES
RETURN_NAMES = ALL_CTX_RETURN_NAMES
FUNCTION = "switch"
def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None):
"""Chooses the first non-empty Context to output.
"""
ctx = None
if not is_context_empty(ctx_01):
ctx = ctx_01
elif not is_context_empty(ctx_02):
ctx = ctx_02
elif not is_context_empty(ctx_03):
ctx = ctx_03
elif not is_context_empty(ctx_04):
ctx = ctx_04
elif not is_context_empty(ctx_05):
ctx = ctx_05
return get_context_return_tuple(ctx)
@@ -1,117 +0,0 @@
"""A set of constants and utilities for handling contexts.
Sets up the inputs and outputs for the Context going forward, with additional functions for
creating and exporting context objects.
"""
import comfy.samplers
import folder_paths
_all_context_input_output_data = {
"base_ctx": ("base_ctx", "RGTHREE_CONTEXT", "CONTEXT"),
"model": ("model", "MODEL", "MODEL"),
"clip": ("clip", "CLIP", "CLIP"),
"vae": ("vae", "VAE", "VAE"),
"positive": ("positive", "CONDITIONING", "POSITIVE"),
"negative": ("negative", "CONDITIONING", "NEGATIVE"),
"latent": ("latent", "LATENT", "LATENT"),
"images": ("images", "IMAGE", "IMAGE"),
"seed": ("seed", "INT", "SEED"),
"steps": ("steps", "INT", "STEPS"),
"step_refiner": ("step_refiner", "INT", "STEP_REFINER"),
"cfg": ("cfg", "FLOAT", "CFG"),
"ckpt_name": ("ckpt_name", folder_paths.get_filename_list("checkpoints"), "CKPT_NAME"),
"sampler": ("sampler", comfy.samplers.KSampler.SAMPLERS, "SAMPLER"),
"scheduler": ("scheduler", comfy.samplers.KSampler.SCHEDULERS, "SCHEDULER"),
"clip_width": ("clip_width", "INT", "CLIP_WIDTH"),
"clip_height": ("clip_height", "INT", "CLIP_HEIGHT"),
"text_pos_g": ("text_pos_g", "STRING", "TEXT_POS_G"),
"text_pos_l": ("text_pos_l", "STRING", "TEXT_POS_L"),
"text_neg_g": ("text_neg_g", "STRING", "TEXT_NEG_G"),
"text_neg_l": ("text_neg_l", "STRING", "TEXT_NEG_L"),
"mask": ("mask", "MASK", "MASK"),
"control_net": ("control_net", "CONTROL_NET", "CONTROL_NET"),
}
force_input_types = ["INT", "STRING", "FLOAT"]
force_input_names = ["sampler", "scheduler", "ckpt_name"]
def _create_context_data(input_list=None):
"""Returns a tuple of context inputs, return types, and return names to use in a node"s def"""
if input_list is None:
input_list = _all_context_input_output_data.keys()
list_ctx_return_types = []
list_ctx_return_names = []
ctx_optional_inputs = {}
for inp in input_list:
data = _all_context_input_output_data[inp]
list_ctx_return_types.append(data[1])
list_ctx_return_names.append(data[2])
ctx_optional_inputs[data[0]] = tuple([data[1]] + ([{
"forceInput": True
}] if data[1] in force_input_types or data[0] in force_input_names else []))
ctx_return_types = tuple(list_ctx_return_types)
ctx_return_names = tuple(list_ctx_return_names)
return (ctx_optional_inputs, ctx_return_types, ctx_return_names)
ALL_CTX_OPTIONAL_INPUTS, ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES = _create_context_data()
_original_ctx_inputs_list = [
"base_ctx", "model", "clip", "vae", "positive", "negative", "latent", "images", "seed"
]
ORIG_CTX_OPTIONAL_INPUTS, ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES = _create_context_data(
_original_ctx_inputs_list)
def new_context(base_ctx, **kwargs):
"""Creates a new context from the provided data, with an optional base ctx to start."""
context = base_ctx if base_ctx is not None else None
new_ctx = {}
for key in _all_context_input_output_data:
if key == "base_ctx":
continue
v = kwargs[key] if key in kwargs else None
new_ctx[key] = v if v is not None else context[
key] if context is not None and key in context else None
return new_ctx
def merge_new_context(ctx_01, ctx_02, ctx_03, ctx_04, ctx_05):
"""Creates a new context by merging provided contexts with the latter overriding same fields."""
new_ctx = {}
for key in _all_context_input_output_data:
if key == "base_ctx":
continue
v = None
for ctx in [ctx_05, ctx_04, ctx_03, ctx_02, ctx_01]:
v = ctx[key] if ctx is not None and key in ctx else None
if v is not None:
break
new_ctx[key] = v
return new_ctx
def get_context_return_tuple(ctx, inputs_list=None):
"""Returns a tuple for returning in the order of the inputs list."""
if inputs_list is None:
inputs_list = _all_context_input_output_data.keys()
tup_list = [
ctx,
]
for key in inputs_list:
if key == "base_ctx":
continue
tup_list.append(ctx[key] if ctx is not None and key in ctx else None)
return tuple(tup_list)
def get_orig_context_return_tuple(ctx):
"""Returns a tuple for returning from a node with only the original context keys."""
return get_context_return_tuple(ctx, _original_ctx_inputs_list)
def is_context_empty(ctx):
"""Checks if the provided ctx is None or contains just None values."""
return not ctx or all(v is None for v in ctx.values())
@@ -1,71 +0,0 @@
import json
from .constants import get_category, get_name
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any = AnyType("*")
class RgthreeDisplayAny:
"""Display any data node."""
NAME = get_name('Display Any')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"source": (any, {}),
},
}
RETURN_TYPES = ()
FUNCTION = "main"
OUTPUT_NODE = True
def main(self, source=None):
value = 'None'
if source is not None:
try:
value = json.dumps(source)
except Exception:
try:
value = str(source)
except Exception:
value = 'source exists, but could not be serialized.'
return {"ui": {"text": (value,)}}
class RgthreeDisplayInt:
"""Old DisplayInt node.
Can be ported over to DisplayAny if https://github.com/comfyanonymous/ComfyUI/issues/1527 fixed.
"""
NAME = get_name('Display Int')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input": ("INT", {
"forceInput": True
}),
},
}
RETURN_TYPES = ()
FUNCTION = "main"
OUTPUT_NODE = True
def main(self, input=None):
return {"ui": {"text": (input,)}}
@@ -1,44 +0,0 @@
from nodes import PreviewImage
from .constants import get_category, get_name
class RgthreeImageComparer(PreviewImage):
"""A node that compares two images in the UI."""
NAME = get_name('Image Comparer')
CATEGORY = get_category()
FUNCTION = "compare_images"
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"image_a": ("IMAGE",),
},
"optional": {
"image_b": ("IMAGE",),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
},
}
def compare_images(self,
image_a,
image_b=None,
filename_prefix="rgthree.compare.",
prompt=None,
extra_pnginfo=None):
images = []
images.append(image_a[0])
if image_b is not None and len(image_b) > 0:
images.append(image_b[0])
elif len(image_a) > 1:
images.append(image_b[1])
else:
raise ValueError(
"You must supply two images; either both image_a & image_b, or two batch images in image_a")
return self.save_images(images, filename_prefix, prompt, extra_pnginfo)
@@ -1,93 +0,0 @@
"""Image Inset Crop, with percentages."""
from .log import log_node_info
from .constants import get_category, get_name
from nodes import MAX_RESOLUTION
def get_new_bounds(width, height, left, right, top, bottom):
"""Returns the new bounds for an image with inset crop data."""
left = 0 + left
right = width - right
top = 0 + top
bottom = height - bottom
return (left, right, top, bottom)
class RgthreeImageInsetCrop:
"""Image Inset Crop, with percentages."""
NAME = get_name('Image Inset Crop')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"image": ("IMAGE",),
"measurement": (['Pixels', 'Percentage'],),
"left": ("INT", {
"default": 0,
"min": 0,
"max": MAX_RESOLUTION,
"step": 8
}),
"right": ("INT", {
"default": 0,
"min": 0,
"max": MAX_RESOLUTION,
"step": 8
}),
"top": ("INT", {
"default": 0,
"min": 0,
"max": MAX_RESOLUTION,
"step": 8
}),
"bottom": ("INT", {
"default": 0,
"min": 0,
"max": MAX_RESOLUTION,
"step": 8
}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "crop"
# pylint: disable = too-many-arguments
def crop(self, measurement, left, right, top, bottom, image=None):
"""Does the crop."""
_, height, width, _ = image.shape
if measurement == 'Percentage':
left = int(width - (width * (100 - left) / 100))
right = int(width - (width * (100 - right) / 100))
top = int(height - (height * (100 - top) / 100))
bottom = int(height - (height * (100 - bottom) / 100))
# Snap to 8 pixels
left = left // 8 * 8
right = right // 8 * 8
top = top // 8 * 8
bottom = bottom // 8 * 8
if left == 0 and right == 0 and bottom == 0 and top == 0:
return (image,)
inset_left, inset_right, inset_top, inset_bottom = get_new_bounds(width, height, left, right,
top, bottom)
if inset_top > inset_bottom:
raise ValueError(
f"Invalid cropping dimensions top ({inset_top}) exceeds bottom ({inset_bottom})")
if inset_left > inset_right:
raise ValueError(
f"Invalid cropping dimensions left ({inset_left}) exceeds right ({inset_right})")
log_node_info(
self.NAME, f'Cropping image {width}x{height} width inset by {inset_left},{inset_right}, ' +
f'and height inset by {inset_top}, {inset_bottom}')
image = image[:, inset_top:inset_bottom, inset_left:inset_right, :]
return (image,)
@@ -1,56 +0,0 @@
"""Some basic config stuff I use for SDXL."""
from .constants import get_category, get_name
from nodes import MAX_RESOLUTION
import comfy.samplers
class RgthreeKSamplerConfig:
"""Some basic config stuff I started using for SDXL, but useful in other spots too."""
NAME = get_name('KSampler Config')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"steps_total": ("INT", {
"default": 30,
"min": 1,
"max": MAX_RESOLUTION,
"step": 1,
}),
"refiner_step": ("INT", {
"default": 24,
"min": 1,
"max": MAX_RESOLUTION,
"step": 1,
}),
"cfg": ("FLOAT", {
"default": 8.0,
"min": 0.0,
"max": 100.0,
"step": 0.5,
}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
#"refiner_ascore_pos": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
#"refiner_ascore_neg": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
},
}
RETURN_TYPES = ("INT", "INT", "FLOAT", comfy.samplers.KSampler.SAMPLERS,
comfy.samplers.KSampler.SCHEDULERS)
RETURN_NAMES = ("STEPS", "REFINER_STEP", "CFG", "SAMPLER", "SCHEDULER")
FUNCTION = "main"
def main(self, steps_total, refiner_step, cfg, sampler_name, scheduler):
"""main"""
return (
steps_total,
refiner_step,
cfg,
sampler_name,
scheduler,
)
@@ -1,85 +0,0 @@
# https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
# https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
COLORS = {
'BLACK': '\33[30m',
'RED': '\33[31m',
'GREEN': '\33[32m',
'YELLOW': '\33[33m',
'BLUE': '\33[34m',
'MAGENTA': '\33[35m',
'CYAN': '\33[36m',
'WHITE': '\33[37m',
'GREY': '\33[90m',
'BRIGHT_RED': '\33[91m',
'BRIGHT_GREEN': '\33[92m',
'BRIGHT_YELLOW': '\33[93m',
'BRIGHT_BLUE': '\33[94m',
'BRIGHT_MAGENTA': '\33[95m',
'BRIGHT_CYAN': '\33[96m',
'BRIGHT_WHITE': '\33[97m',
# Styles.
'RESET': '\33[0m',
'BOLD': '\33[1m',
'NORMAL': '\33[22m',
'ITALIC': '\33[3m',
'UNDERLINE': '\33[4m',
'BLINK': '\33[5m',
'BLINK2': '\33[6m',
'SELECTED': '\33[7m',
# Backgrounds
'BG_BLACK': '\33[40m',
'BG_RED': '\33[41m',
'BG_GREEN': '\33[42m',
'BG_YELLOW': '\33[43m',
'BG_BLUE': '\33[44m',
'BG_MAGENTA': '\33[45m',
'BG_CYAN': '\33[46m',
'BG_WHITE': '\33[47m',
'BG_GREY': '\33[100m',
'BG_BRIGHT_RED': '\33[101m',
'BG_BRIGHT_GREEN': '\33[102m',
'BG_BRIGHT_YELLOW': '\33[103m',
'BG_BRIGHT_BLUE': '\33[104m',
'BG_BRIGHT_MAGENTA': '\33[105m',
'BG_BRIGHT_CYAN': '\33[106m',
'BG_BRIGHT_WHITE': '\33[107m',
}
def log_node_success(node_name, message):
"""Logs a success message."""
_log_node(COLORS["GREEN"], node_name, message)
def log_node_info(node_name, message):
"""Logs an info message."""
_log_node(COLORS["CYAN"], node_name, message)
def log_node_warn(node_name, message):
"""Logs an warn message."""
_log_node(COLORS["YELLOW"], node_name, message)
def log_node(node_name, message):
"""Logs a message."""
_log_node(COLORS["CYAN"], node_name, message)
def _log_node(color, node_name, message, prefix=''):
print(_get_log_msg(color, node_name, message, prefix=prefix))
def _get_log_msg(color, node_name, message, prefix=''):
msg = f'{COLORS["BOLD"]}{color}{prefix}[rgthree] {node_name.replace(" (rgthree)", "")}'
msg += f':{COLORS["RESET"]} {message}'
return msg
def log(message, color=None, msg_color=None, prefix=None):
color = COLORS[color] if color is not None and color in COLORS else COLORS["BRIGHT_GREEN"]
msg_color = COLORS[msg_color] if msg_color is not None and msg_color in COLORS else ''
prefix = f'[{prefix}]' if prefix is not None else ''
msg = f'{color}[rgthree]{prefix}'
msg += f'{msg_color} {message}{COLORS["RESET"]}'
print(msg)
@@ -1,46 +0,0 @@
from .constants import get_category, get_name
from nodes import LoraLoader
import folder_paths
class RgthreeLoraLoaderStack:
NAME = get_name('Lora Loader Stack')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP", ),
"lora_01": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_01":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_02": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_02":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_03": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_03":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_04": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_04":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "load_lora"
def load_lora(self, model, clip, lora_01, strength_01, lora_02, strength_02, lora_03, strength_03, lora_04, strength_04):
if lora_01 != "None" and strength_01 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_01, strength_01, strength_01)
if lora_02 != "None" and strength_02 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_02, strength_02, strength_02)
if lora_03 != "None" and strength_03 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_03, strength_03, strength_03)
if lora_04 != "None" and strength_04 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_04, strength_04, strength_04)
return (model, clip)
@@ -1,94 +0,0 @@
import os
from .log import log_node_warn, log_node_info, log_node_success
from .constants import get_category, get_name
from .power_prompt_utils import get_and_strip_loras
from nodes import LoraLoader, CLIPTextEncode
import folder_paths
NODE_NAME = get_name('Power Prompt')
class RgthreePowerPrompt:
NAME = NODE_NAME
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
SAVED_PROMPTS_FILES = folder_paths.get_filename_list('saved_prompts')
SAVED_PROMPTS_CONTENT = []
for filename in SAVED_PROMPTS_FILES:
with open(folder_paths.get_full_path('saved_prompts', filename), 'r') as f:
SAVED_PROMPTS_CONTENT.append(f.read())
return {
'required': {
'prompt': ('STRING', {
'multiline': True
}),
},
'optional': {
"opt_model": ("MODEL",),
"opt_clip": ("CLIP",),
'insert_lora': (['CHOOSE', 'DISABLE LORAS'] +
[os.path.splitext(x)[0] for x in folder_paths.get_filename_list('loras')],),
'insert_embedding': ([
'CHOOSE',
] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],),
'insert_saved': ([
'CHOOSE',
] + SAVED_PROMPTS_FILES,),
},
'hidden': {
'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,),
}
}
RETURN_TYPES = (
'CONDITIONING',
'MODEL',
'CLIP',
'STRING',
)
RETURN_NAMES = (
'CONDITIONING',
'MODEL',
'CLIP',
'TEXT',
)
FUNCTION = 'main'
def main(self,
prompt,
opt_model=None,
opt_clip=None,
insert_lora=None,
insert_embedding=None,
insert_saved=None,
values_insert_saved=None):
if insert_lora == 'DISABLE LORAS':
prompt, loras = get_and_strip_loras(prompt, log_node=NODE_NAME, silent=True)
log_node_info(
NODE_NAME,
f'Disabling all found loras ({len(loras)}) and stripping lora tags for TEXT output.')
elif opt_model != None and opt_clip != None:
prompt, loras = get_and_strip_loras(prompt, log_node=NODE_NAME)
if len(loras):
for lora in loras:
opt_model, opt_clip = LoraLoader().load_lora(opt_model, opt_clip, lora['lora'],
lora['strength'], lora['strength'])
log_node_success(NODE_NAME, f'Loaded "{lora["lora"]}" from prompt')
log_node_info(NODE_NAME, f'{len(loras)} Loras processed; stripping tags for TEXT output.')
elif '<lora:' in prompt:
_prompt_stripped, loras = get_and_strip_loras(prompt, log_node=NODE_NAME, silent=True)
if len(loras):
log_node_warn(
NODE_NAME, f'Found {len(loras)} lora tags in prompt but model & clip were not supplied!')
log_node_info(NODE_NAME, 'Loras not processed, keeping for TEXT output.')
conditioning = None
if opt_clip != None:
conditioning = CLIPTextEncode().encode(opt_clip, prompt)[0]
return (conditioning, opt_model, opt_clip, prompt)
@@ -1,43 +0,0 @@
import os
import folder_paths
from nodes import CLIPTextEncode
from .constants import get_category, get_name
from .power_prompt import RgthreePowerPrompt
class RgthreePowerPromptSimple(RgthreePowerPrompt):
NAME=get_name('Power Prompt - Simple')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
SAVED_PROMPTS_FILES=folder_paths.get_filename_list('saved_prompts')
SAVED_PROMPTS_CONTENT=[]
for filename in SAVED_PROMPTS_FILES:
with open(folder_paths.get_full_path('saved_prompts', filename), 'r') as f:
SAVED_PROMPTS_CONTENT.append(f.read())
return {
'required': {
'prompt': ('STRING', {'multiline': True}),
},
'optional': {
"opt_clip": ("CLIP", ),
'insert_embedding': (['CHOOSE',] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],),
'insert_saved': (['CHOOSE',] + SAVED_PROMPTS_FILES,),
},
'hidden': {
'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,),
}
}
RETURN_TYPES = ('CONDITIONING', 'STRING',)
RETURN_NAMES = ('CONDITIONING', 'TEXT',)
FUNCTION = 'main'
def main(self, prompt, opt_clip=None, insert_embedding=None, insert_saved=None, values_insert_saved=None):
conditioning=None
if opt_clip != None:
conditioning = CLIPTextEncode().encode(opt_clip, prompt)[0]
return (conditioning, prompt)
@@ -1,56 +0,0 @@
"""Utilities for Power Prompt nodes."""
import re
import os
import folder_paths
from .log import log_node_warn, log_node_info
def get_and_strip_loras(prompt, silent=False, log_node="Power Prompt"):
"""Collects and strips lora tags from a prompt."""
pattern = '<lora:([^:>]*?)(?::(-?\d*(?:\.\d*)?))?>'
lora_paths = folder_paths.get_filename_list('loras')
lora_paths_no_ext = [os.path.splitext(x)[0] for x in lora_paths]
lora_filenames_no_ext = [os.path.splitext(os.path.basename(x))[0] for x in lora_paths]
matches = re.findall(pattern, prompt)
loras = []
for match in matches:
tag_path = match[0]
tag_path_no_ext = os.path.splitext(match[0])[0]
tag_filename_no_ext = os.path.splitext(os.path.basename(match[0]))[0]
strength = float(match[1] if len(match) > 1 and len(match[1]) else 1.0)
if strength == 0 and not silent:
log_node_info(log_node, f'Skipping "{tag_path}" with strength of zero')
continue
if tag_path not in lora_paths:
if tag_path_no_ext in lora_paths_no_ext:
# See if we've entered the exact path, but without the extension
tag_path = lora_paths[lora_paths_no_ext.index(tag_path_no_ext)]
elif tag_path_no_ext == tag_filename_no_ext and tag_filename_no_ext in lora_filenames_no_ext:
# See if we've entered only a file, that is in only the files
tag_path = lora_paths[lora_filenames_no_ext.index(tag_filename_no_ext)]
else:
# Let's be flexible; see if the entered lora overlaps at all with any of the loras
found_tag_filename = None
for index, lora_path in enumerate(lora_paths_no_ext):
if tag_path_no_ext in lora_path:
found_tag_filename = lora_paths[index]
break
if found_tag_filename:
if not silent:
log_node_info(log_node, f'Found "{found_tag_filename}" for "{tag_path}" in prompt')
tag_path = found_tag_filename
else:
if not silent:
log_node_warn(log_node, f'Lora "{tag_path}" not found, skipping.')
continue
loras.append({'lora': tag_path, 'strength': strength})
return (re.sub(pattern, '', prompt), loras)
@@ -1,81 +0,0 @@
from server import PromptServer
from aiohttp import web
import comfy.samplers
import folder_paths
import os
import time
import json
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
DIR_WEB = os.path.abspath(f'{THIS_DIR}/../web/')
routes = PromptServer.instance.routes
def set_default_page_resources(path):
""" Sets up routes for handling static files under a path."""
@routes.get(f'/rgthree/{path}/{{file}}')
async def get_resource(request):
""" Returns a resource file. """
return web.FileResponse(os.path.join(DIR_WEB, path, request.match_info['file']))
@routes.get(f'/rgthree/{path}/{{subdir}}/{{file}}')
async def get_resource_subdir(request):
""" Returns a resource file. """
return web.FileResponse(
os.path.join(DIR_WEB, path, request.match_info['subdir'], request.match_info['file']))
def set_default_page_routes(path):
""" Sets default path handling for a hosted rgthree page. """
@routes.get(f'/rgthree/{path}')
async def get_path_redir(request):
""" Redirects to the path adding a trailing slash. """
raise web.HTTPFound(f'{request.path}/')
@routes.get(f'/rgthree/{path}/')
async def get_path_index(request):
""" Handles the page's index loading. """
html = ''
with open(os.path.join(DIR_WEB, path, 'index.html'), 'r', encoding='UTF-8') as file:
html = file.read()
return web.Response(text=html, content_type='text/html')
set_default_page_resources(path)
# Sometimes other pages (link_fixer, etc.) may want to import JS from the comfyui
# directory. To allows TS to resolve like '../comfyui/file.js', we'll also resolve any module HTTP
# to these routes.
set_default_page_resources("comfyui")
set_default_page_resources("common")
set_default_page_routes("link_fixer")
# Configuration
from .config import RGTHREE_CONFIG, set_user_config
@routes.get('/rgthree/config.js')
def api_get_user_config_file(request):
""" Returns the user configuration as a jsavascript file. """
text=f'export const rgthreeConfig = {json.dumps(RGTHREE_CONFIG, sort_keys=True, indent=2, separators=(",", ": "))}'
return web.Response(text=text, content_type='application/javascript')
@routes.get('/rgthree/api/config')
def api_get_user_config(request):
""" Returns the user configuration. """
return web.json_response(json.dumps(RGTHREE_CONFIG))
@routes.post('/rgthree/api/config')
async def api_set_user_config(request):
""" Returns the user configuration. """
post = await request.post()
data = json.loads(post.get("json"))
set_user_config(data)
return web.json_response({"status": "ok"})
@@ -1,63 +0,0 @@
from nodes import EmptyLatentImage
from .constants import get_category, get_name
class RgthreeSDXLEmptyLatentImage:
NAME = get_name('SDXL Empty Latent Image')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"dimensions": (
[
# 'Custom',
'1536 x 640 (landscape)',
'1344 x 768 (landscape)',
'1216 x 832 (landscape)',
'1152 x 896 (landscape)',
'1024 x 1024 (square)',
' 896 x 1152 (portrait)',
' 832 x 1216 (portrait)',
' 768 x 1344 (portrait)',
' 640 x 1536 (portrait)',
],
{
"default": '1024 x 1024 (square)'
}),
"clip_scale": ("FLOAT", {
"default": 2.0,
"min": 1.0,
"max": 10.0,
"step": .5
}),
"batch_size": ("INT", {
"default": 1,
"min": 1,
"max": 64
}),
},
# "optional": {
# "custom_width": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 64}),
# "custom_height": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 64}),
# }
}
RETURN_TYPES = ("LATENT", "INT", "INT")
RETURN_NAMES = ("LATENT", "CLIP_WIDTH", "CLIP_HEIGHT")
FUNCTION = "generate"
def generate(self, dimensions, clip_scale, batch_size):
"""Generates the latent and exposes the clip_width and clip_height"""
if True:
result = [x.strip() for x in dimensions.split('x')]
width = int(result[0])
height = int(result[1].split(' ')[0])
latent = EmptyLatentImage().generate(width, height, batch_size)[0]
return (
latent,
int(width * clip_scale),
int(height * clip_scale),
)
@@ -1,160 +0,0 @@
import os
import re
from nodes import MAX_RESOLUTION
from comfy_extras.nodes_clip_sdxl import CLIPTextEncodeSDXL
from .log import log_node_warn, log_node_info, log_node_success
from .constants import get_category, get_name
from .power_prompt_utils import get_and_strip_loras
from nodes import LoraLoader, CLIPTextEncode
import folder_paths
NODE_NAME = get_name('SDXL Power Prompt - Positive')
class RgthreeSDXLPowerPromptPositive:
"""The Power Prompt for positive conditioning."""
NAME = NODE_NAME
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
SAVED_PROMPTS_FILES = folder_paths.get_filename_list('saved_prompts')
SAVED_PROMPTS_CONTENT = []
for filename in SAVED_PROMPTS_FILES:
with open(folder_paths.get_full_path('saved_prompts', filename), 'r') as f:
SAVED_PROMPTS_CONTENT.append(f.read())
return {
'required': {
'prompt_g': ('STRING', {
'multiline': True
}),
'prompt_l': ('STRING', {
'multiline': True
}),
},
'optional': {
"opt_model": ("MODEL",),
"opt_clip": ("CLIP",),
"opt_clip_width": ("INT", {
"forceInput": True,
"default": 1024.0,
"min": 0,
"max": MAX_RESOLUTION
}),
"opt_clip_height": ("INT", {
"forceInput": True,
"default": 1024.0,
"min": 0,
"max": MAX_RESOLUTION
}),
'insert_lora': (['CHOOSE', 'DISABLE LORAS'] +
[os.path.splitext(x)[0] for x in folder_paths.get_filename_list('loras')],),
'insert_embedding': ([
'CHOOSE',
] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],),
'insert_saved': ([
'CHOOSE',
] + SAVED_PROMPTS_FILES,),
# We'll hide these in the UI for now.
"target_width": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
"target_height": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
"crop_width": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
"crop_height": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
},
'hidden': {
'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,),
}
}
RETURN_TYPES = ('CONDITIONING', 'MODEL', 'CLIP', 'STRING', 'STRING')
RETURN_NAMES = ('CONDITIONING', 'MODEL', 'CLIP', 'TEXT_G', 'TEXT_L')
FUNCTION = 'main'
def main(self,
prompt_g,
prompt_l,
opt_model=None,
opt_clip=None,
opt_clip_width=None,
opt_clip_height=None,
insert_lora=None,
insert_embedding=None,
insert_saved=None,
target_width=-1,
target_height=-1,
crop_width=-1,
crop_height=-1,
values_insert_saved=None):
if insert_lora == 'DISABLE LORAS':
prompt_g, loras_g = get_and_strip_loras(prompt_g, True)
prompt_l, loras_l = get_and_strip_loras(prompt_l, True)
loras = loras_g + loras_l
log_node_info(
NODE_NAME,
f'Disabling all found loras ({len(loras)}) and stripping lora tags for TEXT output.')
elif opt_model != None and opt_clip != None:
prompt_g, loras_g = get_and_strip_loras(prompt_g)
prompt_l, loras_l = get_and_strip_loras(prompt_l)
loras = loras_g + loras_l
if len(loras):
for lora in loras:
opt_model, opt_clip = LoraLoader().load_lora(opt_model, opt_clip, lora['lora'],
lora['strength'], lora['strength'])
log_node_success(NODE_NAME, f'Loaded "{lora["lora"]}" from prompt')
log_node_info(NODE_NAME, f'{len(loras)} Loras processed; stripping tags for TEXT output.')
elif '<lora:' in prompt_g or '<lora:' in prompt_l:
_prompt_stripped_g, loras_g = get_and_strip_loras(prompt_g, True)
_prompt_stripped_l, loras_l = get_and_strip_loras(prompt_l, True)
loras = loras_g + loras_l
if len(loras):
log_node_warn(
NODE_NAME, f'Found {len(loras)} lora tags in prompt but model & clip were not supplied!')
log_node_info(NODE_NAME, 'Loras not processed, keeping for TEXT output.')
conditioning = self.get_conditioning(prompt_g, prompt_l, opt_clip, opt_clip_width,
opt_clip_height, target_width, target_height, crop_width,
crop_height)
return (conditioning, opt_model, opt_clip, prompt_g, prompt_l)
def get_conditioning(self, prompt_g, prompt_l, opt_clip, opt_clip_width, opt_clip_height,
target_width, target_height, crop_width, crop_height):
"""Checks the inputs and gets the conditioning."""
conditioning = None
if opt_clip is not None:
if opt_clip_width and opt_clip_height:
target_width = target_width if target_width and target_width > 0 else opt_clip_width
target_height = target_height if target_height and target_height > 0 else opt_clip_height
crop_width = crop_width if crop_width and crop_width > 0 else 0
crop_height = crop_height if crop_height and crop_height > 0 else 0
conditioning = CLIPTextEncodeSDXL().encode(opt_clip, opt_clip_width, opt_clip_height,
crop_width, crop_height, target_width,
target_height, prompt_g, prompt_l)[0]
else:
# If we got an opt_clip, but no clip_width or _height, then use normal CLIPTextEncode
log_node_info(
self.NAME,
'CLIP supplied, but not CLIP_WIDTH and CLIP_HEIGHT. Text encoding will use standard encoding with prompt_g and prompt_l concatenated.'
)
conditioning = CLIPTextEncode().encode(
opt_clip, f'{prompt_g if prompt_g else ""}\n{prompt_l if prompt_l else ""}')[0]
return conditioning
@@ -1,106 +0,0 @@
"""A simpler SDXL Power Prompt that doesn't load Loras, like for negative."""
import os
import re
import folder_paths
from nodes import MAX_RESOLUTION, LoraLoader
from comfy_extras.nodes_clip_sdxl import CLIPTextEncodeSDXL
from .sdxl_power_prompt_postive import RgthreeSDXLPowerPromptPositive
from .log import log_node_warn, log_node_info, log_node_success
from .constants import get_category, get_name
NODE_NAME = get_name('SDXL Power Prompt - Simple / Negative')
class RgthreeSDXLPowerPromptSimple(RgthreeSDXLPowerPromptPositive):
"""A simpler SDXL Power Prompt that doesn't handle Loras."""
NAME = NODE_NAME
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
saved_prompts_files = folder_paths.get_filename_list('saved_prompts')
saved_promptes_content = []
for fname in saved_prompts_files:
with open(folder_paths.get_full_path('saved_prompts', fname), 'r', encoding="utf-8") as file:
saved_promptes_content.append(file.read())
return {
'required': {
'prompt_g': ('STRING', {
'multiline': True
}),
'prompt_l': ('STRING', {
'multiline': True
}),
},
'optional': {
"opt_clip": ("CLIP",),
"opt_clip_width": ("INT", {
"forceInput": True,
"default": 1024.0,
"min": 0,
"max": MAX_RESOLUTION
}),
"opt_clip_height": ("INT", {
"forceInput": True,
"default": 1024.0,
"min": 0,
"max": MAX_RESOLUTION
}),
'insert_embedding': ([
'CHOOSE',
] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],),
'insert_saved': ([
'CHOOSE',
] + saved_prompts_files,),
# We'll hide these in the UI for now.
"target_width": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
"target_height": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
"crop_width": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
"crop_height": ("INT", {
"default": -1,
"min": -1,
"max": MAX_RESOLUTION
}),
},
'hidden': {
'values_insert_saved': (['CHOOSE'] + saved_promptes_content,),
}
}
RETURN_TYPES = ('CONDITIONING', 'STRING', 'STRING')
RETURN_NAMES = ('CONDITIONING', 'TEXT_G', 'TEXT_L')
FUNCTION = 'main'
def main(self,
prompt_g,
prompt_l,
opt_clip=None,
opt_clip_width=None,
opt_clip_height=None,
insert_embedding=None,
insert_saved=None,
target_width=-1,
target_height=-1,
crop_width=-1,
crop_height=-1,
values_insert_saved=None):
conditioning = self.get_conditioning(prompt_g, prompt_l, opt_clip, opt_clip_width,
opt_clip_height, target_width, target_height, crop_width, crop_height)
return (conditioning, prompt_g, prompt_l)
@@ -1,29 +0,0 @@
"""See node."""
from .constants import get_category, get_name
class RgthreeSeed:
"""See node."""
NAME = get_name('Seed')
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"seed": ("INT", {
"default": 0,
"min": -1125899906842624,
"max": 1125899906842624
}),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("SEED",)
FUNCTION = "main"
def main(self, seed=0):
"""Returns the passed seed on execution."""
return (seed,)
@@ -1,45 +0,0 @@
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
def get_dict_value(data: dict, dict_key: str, default = None):
""" Gets a deeply nested value given a dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
found = data[key] if key in data else None
if found is not None and len(keys) > 0:
return get_dict_value(found, '.'.join(keys), default)
return found if found is not None else default
def set_dict_value(data: dict, dict_key: str, value, create_missing_objects = True):
""" Sets a deeply nested value given a dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
if key not in data:
if create_missing_objects == False:
return
data[key] = {}
if len(keys) == 0:
data[key] = value
else:
set_dict_value(data[key], '.'.join(keys), value, create_missing_objects)
return data
def dict_has_key(data: dict, dict_key):
""" Checks if a dict has a deeply nested dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
if key is None or key not in data:
return False
if len(keys) == 0:
return True
return dict_has_key(data[key], '.'.join(keys))
@@ -1,36 +0,0 @@
// COPY THIS FILE BEFORE MAKING CHANGES TO: rgthree_config.json
{
"log_level": "WARN",
"features": {
"patch_recursive_execution": true,
"show_alerts_for_corrupt_workflows": false,
"monitor_for_corrupt_links": false,
"menu_queue_selected_nodes": true,
"menu_auto_nest": {
"subdirs": null,
"threshold": 20
},
"progress_bar": {
"enabled": true,
"height": 16,
"position": "top"
}
},
"nodes": {
"reroute": {
"default_width": 40,
"default_height": 30,
"default_resizable": false,
"default_layout": ["Left", "Right"],
"fast_reroute": {
"enabled": true,
"key_create_while_dragging_link" : "Shift + R",
"key_rotate": "Shift + A",
"key_resize": "Shift + X",
"key_move": "Shift + Z",
"key_connections_input": "Shift + S",
"key_connections_output": "Shift + D"
}
}
}
}
@@ -1,110 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {
INodeInputSlot,
INodeOutputSlot,
LGraphNodeConstructor,
LLink,
SerializedLGraphNode,
LGraphNode as TLGraphNode,
LiteGraph as TLiteGraph,
} from "typings/litegraph.js";
import type { ComfyApp, ComfyObjectInfo } from "typings/comfy.js";
import {
IoDirection,
addConnectionLayoutSupport,
applyMixins,
followConnectionUntilType,
replaceNode,
} from "./utils.js";
import { RgthreeBaseNode } from "./base_node.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
let hasShownAlertForUpdatingInt = false;
class AnySwitchforMixin extends RgthreeBaseNode {
static comfyClass? = "";
private scheduleStabilizePromise: Promise<void> | null = null;
private nodeType: string | string[] | null = null;
override onConnectionsChange(
type: number,
slotIndex: number,
isConnected: boolean,
linkInfo: LLink,
ioSlot: INodeOutputSlot | INodeInputSlot,
) {
super.onConnectionsChange?.(type, slotIndex, isConnected, linkInfo, ioSlot);
this.scheduleStabilize();
}
onConnectionsChainChange() {
this.scheduleStabilize();
}
scheduleStabilize(ms = 64) {
if (!this.scheduleStabilizePromise) {
this.scheduleStabilizePromise = new Promise((resolve) => {
setTimeout(() => {
this.scheduleStabilizePromise = null;
this.stabilize();
resolve();
}, ms);
});
}
return this.scheduleStabilizePromise;
}
stabilize() {
// We prefer the inputs, then the output.
let connectedType = followConnectionUntilType(this, IoDirection.INPUT, undefined, true);
if (!connectedType) {
connectedType = followConnectionUntilType(this, IoDirection.OUTPUT, undefined, true);
}
// TODO: What this doesn't do is broadcast to other nodes when its type changes. Reroute node
// does, but, for now, if this was connected to another Any Switch, say, the second one wouldn't
// change its type when the first does. The user would need to change the connections.
this.nodeType = connectedType?.type || "*";
for (const input of this.inputs) {
input.type = this.nodeType as string; // So, types can indeed be arrays,,
}
for (const output of this.outputs) {
output.type = this.nodeType as string; // So, types can indeed be arrays,,
output.label =
output.type === 'RGTHREE_CONTEXT' ? 'CONTEXT' :
Array.isArray(this.nodeType) || this.nodeType.includes(",")
? connectedType?.label || String(this.nodeType)
: String(this.nodeType);
}
}
static override setUp<T extends RgthreeBaseNode>(nodeType: new(title?: any) => T) {
AnySwitchforMixin.title = (nodeType as any).title;
AnySwitchforMixin.type = (nodeType as any).type || (nodeType as any).title;
AnySwitchforMixin.comfyClass = (nodeType as any).comfyClass;
setTimeout(() => {
AnySwitchforMixin.category = (nodeType as any).category;
});
applyMixins(nodeType, [RgthreeBaseNode, AnySwitchforMixin]);
addConnectionLayoutSupport(nodeType, app, [["Left", "Right"], ["Right", "Left"]]);
}
}
app.registerExtension({
name: "rgthree.AnySwitch",
async beforeRegisterNodeDef(
nodeType: LGraphNodeConstructor,
nodeData: ComfyObjectInfo,
app: ComfyApp,
) {
if (nodeData.name === "Any Switch (rgthree)") {
AnySwitchforMixin.setUp(nodeType as any);
}
},
});
@@ -1,315 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import {rgthree} from "./rgthree.js"
import type {Vector2, LLink, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget, SerializedLGraphNode} from 'typings/litegraph.js';
import { PassThroughFollowing, addConnectionLayoutSupport, addMenuItem, filterOutPassthroughNodes, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodes, getConnectedOutputNodesAndFilterPassThroughs} from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
/**
* A Virtual Node that allows any node's output to connect to it.
*/
export class BaseAnyInputConnectedNode extends RgthreeBaseNode {
override isVirtualNode = true;
/**
* Whether inputs show the immediate nodes, or follow and show connected nodes through
* passthrough nodes.
*/
readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.NONE;
debouncerTempWidth: number = 0;
schedulePromise: Promise<void> | null = null;
constructor(title = BaseAnyInputConnectedNode.title) {
super(title);
this.addInput("", "*");
}
/** Schedules a promise to run a stabilization. */
scheduleStabilizeWidgets(ms = 100) {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
this.schedulePromise = null
this.doStablization();
resolve();
}, ms);
});
}
return this.schedulePromise;
}
override clone() {
const cloned = super.clone();
// Copying to clipboard (and also, creating node templates) work by cloning nodes and, for some
// reason, it manually manipulates the cloned data. So, we want to keep the present input slots
// so if it's pasted/templatized the data is correct. Otherwise, clear the inputs and so the new
// node is ready to go, fresh.
if (!rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes) {
while (cloned.inputs.length > 1) {
cloned.removeInput(cloned.inputs.length - 1);
}
if (cloned.inputs[0]) {
cloned.inputs[0].label = '';
}
}
return cloned;
}
/**
* Ensures we have at least one empty input at the end.
*/
stabilizeInputsOutputs() {
const hasEmptyInput = !this.inputs[this.inputs.length - 1]?.link;
if (!hasEmptyInput) {
this.addInput("", "*");
}
for (let index = this.inputs.length - 2; index >= 0; index--) {
const input = this.inputs[index]!;
if (!input.link) {
this.removeInput(index);
} else {
const node = getConnectedInputNodesAndFilterPassThroughs(this, this, index, this.inputsPassThroughFollowing)[0];
input.name = node?.title || '';
}
}
}
/**
* Stabilizes the node's inputs and widgets.
*/
private doStablization() {
if (!this.graph) {
return;
}
// When we add/remove widgets, litegraph is going to mess up the size, so we
// store it so we can retrieve it in computeSize. Hacky..
(this as any)._tempWidth = this.size[0];
const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this);
this.stabilizeInputsOutputs();
this.handleLinkedNodesStabilization(linkedNodes);
app.graph.setDirtyCanvas(true, true);
// Schedule another stabilization in the future.
this.scheduleStabilizeWidgets(500);
}
handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
linkedNodes; // No-op, but makes overridding in VSCode cleaner.
throw new Error('handleLinkedNodesStabilization should be overridden.');
}
onConnectionsChainChange() {
this.scheduleStabilizeWidgets();
}
override onConnectionsChange(type: number, index: number, connected: boolean, linkInfo: LLink, ioSlot: (INodeOutputSlot | INodeInputSlot)) {
super.onConnectionsChange && super.onConnectionsChange(type, index, connected, linkInfo, ioSlot);
if (!linkInfo) return;
// Follow outputs to see if we need to trigger an onConnectionChange.
const connectedNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
for (const node of connectedNodes) {
if ((node as BaseAnyInputConnectedNode).onConnectionsChainChange) {
(node as BaseAnyInputConnectedNode).onConnectionsChainChange();
}
}
this.scheduleStabilizeWidgets();
}
override removeInput(slot: number) {
(this as any)._tempWidth = this.size[0];
return super.removeInput(slot);
}
override addInput(name: string, type: string|-1, extra_info?: Partial<INodeInputSlot>) {
(this as any)._tempWidth = this.size[0];
return super.addInput(name, type, extra_info);
}
override addWidget<T extends IWidget>(type: T["type"], name: string, value: T["value"], callback?: T["callback"] | string, options?: T["options"]) {
(this as any)._tempWidth = this.size[0];
return super.addWidget(type, name, value, callback, options);
}
/**
* Guess this doesn't exist in Litegraph...
*/
override removeWidget(widgetOrSlot?: IWidget | number) {
(this as any)._tempWidth = this.size[0];
super.removeWidget(widgetOrSlot);
}
override computeSize(out: Vector2) {
let size = super.computeSize(out);
if ((this as any)._tempWidth) {
size[0] = (this as any)._tempWidth;
// We sometimes get repeated calls to compute size, so debounce before clearing.
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
this.debouncerTempWidth = setTimeout(() => {
(this as any)._tempWidth = null;
}, 32);
}
// If we're collapsed, then subtract the total calculated height of the other input slots.
if (this.properties['collapse_connections']) {
const rows = Math.max(this.inputs?.length || 0, this.outputs?.length || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
/**
* When we connect our output, check our inputs and make sure we're not trying to connect a loop.
*/
override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: TLGraphNode, inputIndex: number): boolean {
let canConnect = true;
if (super.onConnectOutput) {
canConnect = super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
if (canConnect) {
const nodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop.
if (nodes.includes(inputNode)) {
alert(`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, `
+ `an situation that could create a time paradox, the results of which could cause a `
+ `chain reaction that would unravel the very fabric of the space time continuum, `
+ `and destroy the entire universe!`);
canConnect = false;
}
}
return canConnect;
}
override onConnectInput(inputIndex: number, outputType: string | -1, outputSlot: INodeOutputSlot, outputNode: TLGraphNode, outputIndex: number): boolean {
let canConnect = true;
if (super.onConnectInput) {
canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex);
}
if (canConnect) {
const nodes = getConnectedOutputNodes(this); // We want passthrough nodes, since they will loop.
if (nodes.includes(outputNode)) {
alert(`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, `
+ `an situation that could create a time paradox, the results of which could cause a `
+ `chain reaction that would unravel the very fabric of the space time continuum, `
+ `and destroy the entire universe!`);
canConnect = false;
}
}
return canConnect;
}
/**
* If something is dropped on us, just add it to the bottom. onConnectInput should already cancel
* if it's disallowed.
*/
override connectByTypeOutput<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string,
): T | null {
const lastInput = this.inputs[this.inputs.length - 1];
if (!lastInput?.link && lastInput?.type === '*') {
var sourceSlot = sourceNode.findOutputSlotByType(sourceSlotType, false, true);
return sourceNode.connect(sourceSlot, this, slot);
}
return super.connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn);
// return null;
// if (!super.connectByType) {
// canConnect = LGraphNode.prototype.connectByType.call(
// this,
// slot,
// sourceNode,
// sourceSlotType,
// optsIn,
// );
// }
// if (!canConnect && slot === 0) {
// const ctrlKey = rgthree.ctrlKey;
// // Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
// // Unfortunately, we don't know which are null now, so we'll just connect any that are
// // not already connected.
// for (const [index, input] of (sourceNode.inputs || []).entries()) {
// if (input.link && !ctrlKey) {
// continue;
// }
// const inputType = input.type as string;
// const inputName = input.name.toUpperCase();
// let thisOutputSlot = -1;
// if (["CONDITIONING", "INT"].includes(inputType)) {
// thisOutputSlot = this.outputs.findIndex(
// (o) =>
// o.type === inputType &&
// (o.name.toUpperCase() === inputName ||
// (o.name.toUpperCase() === "SEED" &&
// inputName.includes("SEED")) ||
// (o.name.toUpperCase() === "STEP_REFINER" &&
// inputName.includes("AT_STEP"))),
// );
// } else {
// thisOutputSlot = this.outputs.map((s) => s.type).indexOf(input.type);
// }
// if (thisOutputSlot > -1) {
// thisOutputSlot;
// this.connect(thisOutputSlot, sourceNode, index);
// }
// }
// }
// return null;
}
static override setUp<T extends RgthreeBaseNode>(clazz: new(title?: string) => T) {
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(clazz, app, [['Left', 'Right'],['Right', 'Left']]);
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(clazz, app, {
name: (node) => (`${node.properties?.['collapse_connections'] ? 'Show' : 'Collapse'} Connections`),
property: 'collapse_connections',
prepareValue: (_value, node) => !node.properties?.['collapse_connections'],
callback: (_node) => {app.graph.setDirtyCanvas(true, true)}
});
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
}
}
// Ok, hack time! LGraphNode's connectByType is powerful, but for our nodes, that have multiple "*"
// input types, it seems it just takes the first one, and disconnects it. I'd rather we don't do
// that and instead take the next free one. If that doesn't work, then we'll give it to the old
// method.
const oldLGraphNodeConnectByType = LGraphNode.prototype.connectByType;
LGraphNode.prototype.connectByType = function connectByType<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string): T | null {
// If we're droppiong on a node, and the last input is free and an "*" type, then connect there
// first...
if (sourceNode.inputs) {
for (const [index, input] of sourceNode.inputs.entries()) {
if (!input.link && input.type === '*') {
this.connect(slot, sourceNode, index);
return null;
}
}
}
return (oldLGraphNodeConnectByType && oldLGraphNodeConnectByType.call(this, slot, sourceNode, sourceSlotType, optsIn) || null) as T;
}
@@ -1,333 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
import { NodeMode } from "typings/comfy.js";
import type {
IWidget,
SerializedLGraphNode,
LiteGraph as TLiteGraph,
LGraphNode as TLGraphNode,
LGraphCanvas,
ContextMenuItem,
} from "typings/litegraph.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
// @ts-ignore
import { app } from "../../scripts/app.js";
import { rgthree } from "./rgthree.js";
import { addHelpMenuItem } from "./utils.js";
import { RgthreeHelpDialog } from "rgthree/common/dialog.js";
declare const LGraphNode: typeof TLGraphNode;
declare const LiteGraph: typeof TLiteGraph;
/**
* A base node with standard methods, extending the LGraphNode.
* This can be used for ui-nodes and a further base for server nodes.
*/
export class RgthreeBaseNode extends LGraphNode {
/**
* Action strings that can be exposed and triggered from other nodes, like Fast Actions Button.
*/
static exposedActions: string[] = [];
static override title = "__NEED_NAME__";
// `category` seems to get reset at register, so we'll
// re-reset it after the register call. ¯\_(ツ)_/¯
static category = "rgthree";
static _category = "rgthree";
/** A temporary width value that can be used to ensure compute size operates correctly. */
_tempWidth = 0;
/** Private Mode member so we can override the setter/getter and call an `onModeChange`. */
private mode_: NodeMode;
isVirtualNode = false;
removed = false;
configuring = false;
helpDialog: RgthreeHelpDialog | null = null;
constructor(title = RgthreeBaseNode.title) {
super(title);
if (title == "__NEED_NAME__") {
throw new Error("RgthreeBaseNode needs overrides.");
}
this.properties = this.properties || {};
}
override configure(info: SerializedLGraphNode<TLGraphNode>): void {
this.configuring = true;
super.configure(info);
// Fix https://github.com/comfyanonymous/ComfyUI/issues/1448 locally.
// Can removed when fixed and adopted.
for (const w of this.widgets || []) {
w.last_y = w.last_y || 0;
}
this.configuring = false;
}
/**
* Override clone for, at the least, deep-copying properties.
*/
override clone() {
const cloned = super.clone();
// This is whild, but LiteGraph clone doesn't deep clone data, so we will. We'll use structured
// clone, which most browsers in 2022 support, but but we'll check.
if (cloned.properties && !!window.structuredClone) {
cloned.properties = structuredClone(cloned.properties);
}
return cloned;
}
// @ts-ignore - Changing the property to an accessor here seems to work, but ts compiler complains.
override set mode(mode: NodeMode) {
if (this.mode_ != mode) {
this.mode_ = mode;
this.onModeChange();
}
}
override get mode() {
return this.mode_;
}
/** When a mode change, we want all connected nodes to match. */
onModeChange() {
// Override
}
/**
* Given a string, do something. At the least, handle any `exposedActions` that may be called and
* passed into from other nodes, like Fast Actions Button
*/
async handleAction(action: string) {
action; // No-op. Should be overridden but OK if not.
}
/**
* Guess this doesn't exist in Litegraph...
*/
removeWidget(widgetOrSlot?: IWidget | number) {
if (typeof widgetOrSlot === "number") {
this.widgets.splice(widgetOrSlot, 1);
} else if (widgetOrSlot) {
const index = this.widgets.indexOf(widgetOrSlot);
if (index > -1) {
this.widgets.splice(index, 1);
}
}
}
override onRemoved(): void {
super.onRemoved?.();
this.removed = true;
}
static setUp<T extends RgthreeBaseNode>(...args: any[]) {
// No-op.
}
/**
* A function to provide help text to be overridden.
*/
getHelp() {
return "";
}
showHelp() {
const help = this.getHelp() || (this.constructor as any).help;
if (help) {
this.helpDialog = new RgthreeHelpDialog(this, help).show();
this.helpDialog.addEventListener("close", (e) => {
console.log("close", e);
this.helpDialog = null;
});
}
}
override onKeyDown(event: KeyboardEvent): void {
rgthree.handleKeydown(event);
if (event.key == "?" && !this.helpDialog) {
this.showHelp();
}
}
override onKeyUp(event: KeyboardEvent): void {
rgthree.handleKeyup(event);
}
override getExtraMenuOptions(canvas: LGraphCanvas, options: ContextMenuItem[]): void {
// Some other extensions override getExtraMenuOptions on the nodeType as it comes through from
// the server, so we can call out to that if we don't have our own.
if (super.getExtraMenuOptions) {
super.getExtraMenuOptions?.apply(this, [canvas, options]);
} else if ((this.constructor as any).nodeType?.prototype?.getExtraMenuOptions) {
(this.constructor as any).nodeType?.prototype?.getExtraMenuOptions?.apply(this, [
canvas,
options,
]);
}
// If we have help content, then add a menu item.
const help = this.getHelp() || (this.constructor as any).help;
if (help) {
addHelpMenuItem(this, help, options);
}
}
}
const overriddenServerNodes = new Map<any, any>();
/**
* A base node with standard methods, extending the LGraphNode.
* This is somewhat experimental, but if comfyui is going to keep breaking widgets and inputs, it
* seems safer than NOT overriding.
*/
export class RgthreeBaseServerNode extends RgthreeBaseNode {
static nodeData: any | null = null;
static nodeType: any | null = null;
comfyClass!: string;
constructor(title: string) {
super(title);
this.serialize_widgets = true;
this.setupFromServerNodeData();
}
getWidgets() {
return ComfyWidgets;
}
override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: LGraphCanvas): void {
const nodeType = (this.constructor as any).nodeType;
// This is specifically for ComfyUi-Manager to draw the badge... though could have other
// side-effects if other extensions override. If it gets messy, may have to remove.
nodeType?.prototype?.onDrawForeground?.apply(this, [ctx, canvas]);
super.onDrawForeground && super.onDrawForeground(ctx, canvas);
}
/**
* This takes the server data and builds out the inputs, outputs and widgets. It's similar to the
* ComfyNode constructor in registerNodes in ComfyUI's app.js, but is more stable and thus
* shouldn't break as often when it modifyies widgets and types.
*/
async setupFromServerNodeData() {
const nodeData = (this.constructor as any).nodeData;
if (!nodeData) {
throw Error("No node data");
}
// Necessary for serialization so Comfy backend can check types.
// Serialized as `class_type`. See app.js#graphToPrompt
this.comfyClass = nodeData.name;
let inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined) {
inputs = Object.assign({}, inputs, nodeData["input"]["optional"]);
}
const WIDGETS = this.getWidgets();
const config: { minWidth: number; minHeight: number; widget?: null | { options: any } } = {
minWidth: 1,
minHeight: 1,
widget: null,
};
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
// If we're forcing the input, just do it now and forget all that widget stuff.
// This is one of the differences from ComfyNode and provides smoother experience for inputs
// that are going to remain inputs anyway.
// Also, it fixes https://github.com/comfyanonymous/ComfyUI/issues/1404 (for rgthree nodes)
if (inputData[1]?.forceInput) {
this.addInput(inputName, type);
} else {
let widgetCreated = true;
if (Array.isArray(type)) {
// Enums
Object.assign(config, WIDGETS.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in WIDGETS) {
// Support custom widgets by Type:Name
Object.assign(
config,
WIDGETS[`${type}:${inputName}`](this, inputName, inputData, app) || {},
);
} else if (type in WIDGETS) {
// Standard type widgets
Object.assign(config, WIDGETS[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
widgetCreated = false;
}
// Don't actually need this right now, but ported it over from ComfyWidget.
if (widgetCreated && inputData[1]?.forceInput && config?.widget) {
if (!config.widget.options) config.widget.options = {};
config.widget.options.forceInput = inputData[1].forceInput;
}
if (widgetCreated && inputData[1]?.defaultInput && config?.widget) {
if (!config.widget.options) config.widget.options = {};
config.widget.options.defaultInput = inputData[1].defaultInput;
}
}
}
for (const o in nodeData["output"]) {
let output = nodeData["output"][o];
if (output instanceof Array) output = "COMBO";
const outputName = nodeData["output_name"][o] || output;
const outputShape = nodeData["output_is_list"][o]
? LiteGraph.GRID_SHAPE
: LiteGraph.CIRCLE_SHAPE;
this.addOutput(outputName, output, { shape: outputShape });
}
const s = this.computeSize();
s[0] = Math.max(config.minWidth, s[0] * 1.5);
s[1] = Math.max(config.minHeight, s[1]);
this.size = s;
this.serialize_widgets = true;
}
static __registeredForOverride__: boolean = false;
static registerForOverride(comfyClass: any, rgthreeClass: any) {
if (overriddenServerNodes.has(comfyClass)) {
throw Error(
`Already have a class to overridde ${
comfyClass.type || comfyClass.name || comfyClass.title
}`,
);
}
overriddenServerNodes.set(comfyClass, rgthreeClass);
// Mark the rgthreeClass as `__registeredForOverride__` because ComfyUI will repeatedly call
// this and certain setups will only want to setup once (like adding context menus, etc).
if (!rgthreeClass.__registeredForOverride__) {
rgthreeClass.__registeredForOverride__ = true;
rgthreeClass.onRegisteredForOverride(comfyClass, rgthreeClass);
}
}
static onRegisteredForOverride(comfyClass: any, rgthreeClass: any) {
// To be overridden
}
}
const oldregisterNodeType = LiteGraph.registerNodeType;
/**
* ComfyUI calls registerNodeType with its ComfyNode, but we don't trust that will remain stable, so
* we need to identify it, intercept it, and supply our own class for the node.
*/
LiteGraph.registerNodeType = function (nodeId: string, baseClass: any) {
const clazz = overriddenServerNodes.get(baseClass) || baseClass;
if (clazz !== baseClass) {
const classLabel = clazz.type || clazz.name || clazz.title;
const [n, v] = rgthree.logger.debugParts(
`${nodeId}: replacing default ComfyNode implementation with custom ${classLabel} class.`,
);
console[n]?.(...v);
}
return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
};
@@ -1,65 +0,0 @@
import type { LLink, INodeOutputSlot, LGraphNode } from "typings/litegraph.js";
// @ts-ignore
import { app } from "../../scripts/app.js";
import { rgthree } from "./rgthree.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { PassThroughFollowing, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodes, getOriginNodeByLink, shouldPassThrough } from "./utils.js";
/**
* Base collector node that monitors changing inputs and outputs.
*/
export class BaseCollectorNode extends BaseAnyInputConnectedNode {
/**
* We only want to show nodes through re_route nodes, other pass through nodes show each input.
*/
override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.REROUTE_ONLY;
constructor(title?: string) {
super(title);
this.addOutput("Output", "*");
}
override clone() {
const cloned = super.clone();
return cloned;
}
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]): void {
// No-op, no widgets.
}
/**
* When we connect an input, check to see if it's already connected and cancel it.
*/
override onConnectInput(inputIndex: number, outputType: string | -1, outputSlot: INodeOutputSlot, outputNode: LGraphNode, outputIndex: number): boolean {
let canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex);
if (canConnect) {
const allConnectedNodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop.
const nodesAlreadyInSlot = getConnectedInputNodes(this, undefined, inputIndex);
if (allConnectedNodes.includes(outputNode)) {
// If we're connecting to the same slot, then allow it by replacing the one we have.
// const slotsOriginNode = getOriginNodeByLink(this.inputs[inputIndex]?.link);
rgthree.logger.debug(`BaseCollectorNode: ${outputNode.title} is already connected to ${this.title}.`);
if (nodesAlreadyInSlot.includes(outputNode)) {
rgthree.logger.debug(`... but letting it slide since it's for the same slot.`);
} else {
canConnect = false;
}
}
if (canConnect && shouldPassThrough(outputNode, PassThroughFollowing.REROUTE_ONLY)) {
const connectedNode = getConnectedInputNodesAndFilterPassThroughs(outputNode, undefined, undefined, PassThroughFollowing.REROUTE_ONLY)[0];
if (connectedNode && allConnectedNodes.includes(connectedNode)) {
// If we're connecting to the same slot, then allow it by replacing the one we have.
rgthree.logger.debug(`BaseCollectorNode: ${connectedNode.title} is already connected to ${this.title}.`);
if (nodesAlreadyInSlot.includes(connectedNode)) {
rgthree.logger.debug(`... but letting it slide since it's for the same slot.`);
} else {
canConnect = false;
}
}
}
}
return canConnect;
}
}
@@ -1,102 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { RgthreeBaseNode } from "./base_node.js";
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget, INodeInputSlot, INodeOutputSlot, LLink} from 'typings/litegraph.js';
import { PassThroughFollowing } from "./utils.js";
import { wait } from "rgthree/common/shared_utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
export class BaseNodeModeChanger extends BaseAnyInputConnectedNode {
override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL;
static collapsible = false;
override isVirtualNode = true;
// These Must be overriden
readonly modeOn: number = -1;
readonly modeOff: number = -1;
static "@toggleRestriction" = {
type: "combo",
values: ["default", "max one", "always one"],
};
constructor(title?: string) {
super(title);
this.properties = this.properties || {};
this.properties['toggleRestriction'] = 'default';
wait(10).then(() => {
if (this.modeOn < 0 || this.modeOff < 0) {
throw new Error('modeOn and modeOff must be overridden.');
}
});
this.addOutput("OPT_CONNECTION", "*");
}
override handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
for (const [index, node] of linkedNodes.entries()) {
let widget = this.widgets && this.widgets[index];
if (!widget) {
// When we add a widget, litegraph is going to mess up the size, so we
// store it so we can retrieve it in computeSize. Hacky..
(this as any)._tempWidth = this.size[0];
widget = this.addWidget('toggle', '', false, '', {"on": 'yes', "off": 'no'});
}
node && this.setWidget(widget, node);
}
if (this.widgets && this.widgets.length > linkedNodes.length) {
this.widgets.length = linkedNodes.length
}
}
protected setWidget(widget: IWidget, linkedNode: TLGraphNode, forceValue?: boolean) {
const value = forceValue == null ? linkedNode.mode === this.modeOn : forceValue;
widget.name = `Enable ${linkedNode.title}`;
widget.options = {'on': 'yes', 'off': 'no'}
widget.value = value;
(widget as any).doModeChange = (forceValue?: boolean, skipOtherNodeCheck?: boolean) => {
let newValue = forceValue == null ? linkedNode.mode === this.modeOff : forceValue;
if (skipOtherNodeCheck !== true) {
if (newValue && this.properties?.['toggleRestriction']?.includes(' one')) {
for (const widget of this.widgets) {
(widget as any).doModeChange(false, true);
}
} else if (!newValue && this.properties?.['toggleRestriction'] === 'always one') {
newValue = this.widgets.every(w => !w.value || w === widget);
}
}
linkedNode.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
widget.value = newValue;
}
widget.callback = () => {
(widget as any).doModeChange();
}
if (forceValue != null) {
linkedNode.mode = (forceValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
}
}
forceWidgetOff(widget: IWidget, skipOtherNodeCheck?: boolean) {
(widget as any).doModeChange(false, skipOtherNodeCheck);
}
forceWidgetOn(widget: IWidget, skipOtherNodeCheck?: boolean) {
(widget as any).doModeChange(true, skipOtherNodeCheck);
}
forceWidgetToggle(widget: IWidget, skipOtherNodeCheck?: boolean) {
(widget as any).doModeChange(!widget.value, skipOtherNodeCheck);
}
static override setUp<T extends RgthreeBaseNode>(clazz: new(title?: string) => T) {
BaseAnyInputConnectedNode.setUp(clazz);
}
}
@@ -1,298 +0,0 @@
// / <reference path='../node_modules/litegraph.js/src/litegraph.d.ts' />
// @ts-ignore
import {app} from '../../scripts/app.js';
// @ts-ignore
import {api} from '../../scripts/api.js';
// @ts-ignore
import { ComfyWidgets } from '../../scripts/widgets.js';
import type {LLink, IComboWidget, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, INodeOutputSlot, INodeInputSlot, IWidget, SerializedLGraphNode} from 'typings/litegraph.js';
import type {ComfyObjectInfo, ComfyGraphNode} from 'typings/comfy.js';
import { wait } from "rgthree/common/shared_utils.js";
import { rgthree } from "./rgthree.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
/** Wraps a node instance keeping closure without mucking the finicky types. */
export class PowerPrompt {
readonly isSimple: boolean;
readonly node: ComfyGraphNode;
readonly promptEl: HTMLTextAreaElement;
nodeData: ComfyObjectInfo;
readonly combos: {[key:string]: IComboWidget} = {};
readonly combosValues: {[key:string]: string[]} = {};
boundOnFreshNodeDefs!: (event: CustomEvent) => void;
private configuring = false;
constructor(node: ComfyGraphNode, nodeData: ComfyObjectInfo) {
this.node = node;
this.node.properties = this.node.properties || {};
this.node.properties['combos_filter'] = '';
this.nodeData = nodeData;
this.isSimple = this.nodeData.name.includes('Simple');
this.promptEl = (node.widgets[0]! as any).inputEl;
this.addAndHandleKeyboardLoraEditWeight();
this.patchNodeRefresh();
const oldConfigure = this.node.configure;
this.node.configure = (info: SerializedLGraphNode) => {
this.configuring = true;
oldConfigure?.apply(this.node, [info]);
this.configuring = false;
}
const oldOnConnectionsChange = this.node.onConnectionsChange;
this.node.onConnectionsChange = (type: number, slotIndex: number, isConnected: boolean, link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) => {
oldOnConnectionsChange?.apply(this.node, [type, slotIndex, isConnected, link_info,_ioSlot]);
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info,_ioSlot);
}
const oldOnConnectInput = this.node.onConnectInput;
this.node.onConnectInput = (inputIndex: number, outputType: INodeOutputSlot["type"], outputSlot: INodeOutputSlot, outputNode: TLGraphNode, outputIndex: number) => {
let canConnect = true;
if (oldOnConnectInput) {
canConnect = oldOnConnectInput.apply(this.node, [inputIndex, outputType, outputSlot, outputNode,outputIndex]);
}
return this.configuring || rgthree.loadingApiJson || (canConnect && !this.node.inputs[inputIndex]!.disabled);
}
const oldOnConnectOutput = this.node.onConnectOutput;
this.node.onConnectOutput = (outputIndex: number, inputType: INodeInputSlot["type"], inputSlot: INodeInputSlot, inputNode: TLGraphNode, inputIndex: number) => {
let canConnect = true;
if (oldOnConnectOutput) {
canConnect = oldOnConnectOutput?.apply(this.node, [outputIndex, inputType, inputSlot, inputNode, inputIndex]);
}
return this.configuring || rgthree.loadingApiJson || (canConnect && !this.node.outputs[outputIndex]!.disabled);
}
const onPropertyChanged = this.node.onPropertyChanged;
this.node.onPropertyChanged = (property: string, value: any, prevValue: any) => {
onPropertyChanged && onPropertyChanged.call(this, property, value, prevValue);
if (property === 'combos_filter') {
this.refreshCombos(this.nodeData);
}
}
// Strip all widgets but prompt (we'll re-add them in refreshCombos)
// this.node.widgets.splice(1);
for (let i = this.node.widgets.length-1; i >= 0; i--) {
if (this.shouldRemoveServerWidget(this.node.widgets[i]!)) {
this.node.widgets.splice(i, 1);
}
}
this.refreshCombos(nodeData);
setTimeout(()=> {
this.stabilizeInputsOutputs();
}, 32);
}
/**
* Cleans up optional out puts when we don't have the optional input. Purely a vanity function.
*/
onNodeConnectionsChange(_type: number, _slotIndex: number, _isConnected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
this.stabilizeInputsOutputs();
}
private stabilizeInputsOutputs() {
// If we are currently "configuring" then skip this stabilization. The connected nodes may
// not yet be configured.
if (this.configuring || rgthree.loadingApiJson) {
return;
}
// If our first input is connected, then we can show the proper output.
const clipLinked = this.node.inputs.some(i=>i.name.includes('clip') && !!i.link);
const modelLinked = this.node.inputs.some(i=>i.name.includes('model') && !!i.link);
for (const output of this.node.outputs) {
const type = (output.type as string).toLowerCase();
if (type.includes('model')) {
output.disabled = !modelLinked;
} else if (type.includes('conditioning')) {
output.disabled = !clipLinked;
} else if (type.includes('clip')) {
output.disabled = !clipLinked;
} else if (type.includes('string')) {
// Our text prompt is always enabled, but let's color it so it stands out
// if the others are disabled. #7F7 is Litegraph's default.
output.color_off = '#7F7';
output.color_on = '#7F7';
}
if (output.disabled) {
// this.node.disconnectOutput(index);
}
}
}
onFreshNodeDefs(event: CustomEvent) {
this.refreshCombos(event.detail[this.nodeData.name]);
}
shouldRemoveServerWidget(widget: IWidget) {
return widget.name?.startsWith('insert_') || widget.name?.startsWith('target_') || widget.name?.startsWith('crop_') || widget.name?.startsWith('values_');
}
refreshCombos(nodeData: ComfyObjectInfo) {
this.nodeData = nodeData;
let filter: RegExp|null = null;
if (this.node.properties['combos_filter']?.trim()) {
try {
filter = new RegExp(this.node.properties['combos_filter'].trim(), 'i');
} catch(e) {
console.error(`Could not parse "${filter}" for Regular Expression`, e);
filter = null;
}
}
// Add the combo for hidden inputs of nodeData
let data = Object.assign({}, this.nodeData.input?.optional || {}, this.nodeData.input?.hidden || {});
for (const [key, value] of Object.entries(data)) {//Object.entries(this.nodeData.input?.hidden || {})) {
if (Array.isArray(value[0])) {
let values = value[0] as string[];
if (key.startsWith('insert')) {
values = filter ? values.filter((v, i) => i < 1 || (i == 1 && v.match(/^disable\s[a-z]/i)) || filter?.test(v)) : values;
const shouldShow = values.length > 2 || (values.length > 1 && !values[1]!.match(/^disable\s[a-z]/i));
if (shouldShow) {
if (!this.combos[key]) {
this.combos[key] = this.node.addWidget('combo', key, values, (selected) => {
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
// We wait a frame because if we use a keydown event to call, it'll wipe out
// the selection.
wait().then(() => {
if (key.includes('embedding')) {
this.insertSelectionText(`embedding:${selected}`);
} else if (key.includes('saved')) {
this.insertSelectionText(this.combosValues[`values_${key}`]![values.indexOf(selected)]!);
} else if (key.includes('lora')) {
this.insertSelectionText(`<lora:${selected}:1.0>`);
}
this.combos[key]!.value = values[0];
});
}
}, {
values,
serialize: true, // Don't include this in prompt.
});
(this.combos[key]! as any).oldComputeSize = this.combos[key]!.computeSize;
let node = this.node;
this.combos[key]!.computeSize = function(width: number) {
const size = (this as any).oldComputeSize?.(width) || [width, LiteGraph.NODE_WIDGET_HEIGHT];
if (this === node.widgets[node.widgets.length- 1]) {
size[1] += 10;
}
return size;
};
}
this.combos[key]!.options!.values = values;
this.combos[key]!.value = values[0];
} else if (!shouldShow && this.combos[key]) {
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]!), 1);
delete this.combos[key];
}
} else if (key.startsWith('values')) {
this.combosValues[key] = values;
}
}
}
}
insertSelectionText(text: string) {
if (!this.promptEl) {
console.error('Asked to insert text, but no textbox found.');
return;
}
let prompt = this.promptEl.value;
// Use selectionEnd as the split; if we have highlighted text, then we likely don't want to
// overwrite it (we could have just deleted it more easily).
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, '');
first = first + (['\n'].includes(first[first.length-1]!) ? '' : first.length ? ' ' : '');
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, '');
second = (['\n'].includes(second[0]!) ? '' : second.length ? ' ' : '') + second;
this.promptEl.value = first + text + second;
this.promptEl.focus();
this.promptEl.selectionStart = first.length;
this.promptEl.selectionEnd = first.length + text.length;
}
/**
* Adds a keydown event listener to our prompt so we can see if we're using the
* ctrl/cmd + up/down arrows shortcut. This kind of competes with the core extension
* "Comfy.EditAttention" but since that only handles parenthesis and listens on window, we should
* be able to intercept and cancel the bubble if we're doing the same action within the lora tag.
*/
addAndHandleKeyboardLoraEditWeight() {
this.promptEl.addEventListener('keydown', (event: KeyboardEvent)=> {
// If we're not doing a ctrl/cmd + arrow key, then bail.
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
if (!event.ctrlKey && !event.metaKey) return;
// Unfortunately, we can't see Comfy.EditAttention delta in settings, so we hardcode to 0.01.
// We can acutally do better too, let's make it .1 by default, and .01 if also holding shift.
const delta = event.shiftKey ? .01 : .1;
let start = this.promptEl.selectionStart;
let end = this.promptEl.selectionEnd;
let fullText = this.promptEl.value;
let selectedText = fullText.substring(start, end);
// We don't care about fully rewriting Comfy.EditAttention, we just want to see if our
// selected text is a lora, which will always start with "<lora:". So work backwards until we
// find something that we know can't be a lora, or a "<".
if (!selectedText) {
const stopOn = "<>()\r\n\t"; // Allow spaces, since they can be in the filename
if (fullText[start] == '>') {
start-=2;
end-=2;
}
if (fullText[end-1] == '<') {
start+=2;
end+=2;
}
while (!stopOn.includes(fullText[start]!) && start > 0) {
start--;
}
while (!stopOn.includes(fullText[end-1]!) && end < fullText.length) {
end++;
}
selectedText = fullText.substring(start, end);
}
// Bail if this isn't a lora.
if (!selectedText.startsWith('<lora:') || !selectedText.endsWith('>')) {
return;
}
let weight = Number(selectedText.match(/:(-?\d*(\.\d*)?)>$/)?.[1]) ?? 1;
weight += event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
// Handle the new value and cancel the bubble so Comfy.EditAttention doesn't also try.
this.promptEl.setRangeText(updatedText, start, end, 'select');
event.preventDefault();
event.stopPropagation();
});
}
/**
* Patches over api.getNodeDefs in comfy's api.js to fire a custom event that we can listen to
* here and manually refresh our combos when a request comes in to fetch the node data; which
* only happens once at startup (but before custom nodes js runs), and then after clicking
* the "Refresh" button in the floating menu, which is what we care about.
*/
patchNodeRefresh() {
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
api.addEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
const oldNodeRemoved = this.node.onRemoved;
this.node.onRemoved = () => {
oldNodeRemoved?.call(this.node);
api.removeEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
}
}
}
@@ -1,122 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import {
type LGraph as TLGraph,
type LiteGraph as TLiteGraph,
LGraphCanvas as TLGraphCanvas,
ISliderWidget,
INumberWidget,
Vector2,
} from "typings/litegraph.js";
declare const LiteGraph: typeof TLiteGraph;
/**
* A bookmark node. Can be placed anywhere in the workflow, and given a shortcut key that will
* navigate to that node, with it in the top-left corner.
*/
export class Bookmark extends RgthreeBaseNode {
static override type = NodeTypesString.BOOKMARK;
static override title = "🔖";
// Really silly, but Litegraph assumes we have at least one input/output... so we need to
// counteract it's computeSize calculation by offsetting the start.
static slot_start_y = -20;
// LiteGraph adds mroe spacing than we want when calculating a nodes' `_collapsed_width`, so we'll
// override it with a setter and re-set it measured exactly as we want.
___collapsed_width: number = 0;
override isVirtualNode = true;
override serialize_widgets = true;
//@ts-ignore - TS Doesn't like us overriding a property with accessors but, too bad.
override get _collapsed_width() {
return this.___collapsed_width;
}
override set _collapsed_width(width: number) {
const canvas = app.canvas as TLGraphCanvas;
const ctx = canvas.canvas.getContext('2d')!;
const oldFont = ctx.font;
ctx.font = canvas.title_text_font;
this.___collapsed_width = 40 + ctx.measureText(this.title).width;
ctx.font = oldFont;
}
readonly keypressBound;
constructor(title = Bookmark.title) {
super(title);
this.addWidget('text', 'shortcut_key', '1', (value: string, ...args) => {
value = value.trim()[0] || '1';
},{
y: 8,
});
this.addWidget<INumberWidget>('number', 'zoom', 1, (value: number) => {
}, {
y: 8 + LiteGraph.NODE_WIDGET_HEIGHT + 4,
max: 2,
min: 0.5,
precision: 2,
});
this.keypressBound = this.onKeypress.bind(this);
}
// override computeSize(out?: Vector2 | undefined): Vector2 {
// super.computeSize(out);
// const minHeight = (this.widgets?.length || 0) * (LiteGraph.NODE_WIDGET_HEIGHT + 4) + 16;
// this.size[1] = Math.max(minHeight, this.size[1]);
// }
static override setUp<T extends RgthreeBaseNode>(clazz: new (title?: string) => T) {
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
}
override onAdded(graph: TLGraph): void {
window.addEventListener("keydown", this.keypressBound);
}
override onRemoved(): void {
window.removeEventListener("keydown", this.keypressBound);
}
async onKeypress(event: KeyboardEvent) {
const target = (event.target as HTMLElement)!;
if (['input','textarea'].includes(target.localName)) {
return;
}
if (event.key.toLocaleLowerCase() === this.widgets[0]!.value.toLocaleLowerCase()) {
this.canvasToBookmark();
}
}
canvasToBookmark() {
const canvas = app.canvas as TLGraphCanvas;
// ComfyUI seemed to break us again, but couldn't repro. No reason to not check, I guess.
// https://github.com/rgthree/rgthree-comfy/issues/71
if (canvas?.ds?.offset) {
canvas.ds.offset[0] = -this.pos[0] + 16;
canvas.ds.offset[1] = -this.pos[1] + 40;
}
if (canvas?.ds?.scale != null) {
canvas.ds.scale = Number(this.widgets[1]!.value || 1);
}
canvas.setDirty(true, true);
}
}
app.registerExtension({
name: "rgthree.Bookmark",
registerCustomNodes() {
Bookmark.setUp(Bookmark);
},
});
@@ -1,50 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
import { NodeTypesString } from "./constants.js";
import type {LGraphNode} from 'typings/litegraph.js';
const MODE_BYPASS = 4;
const MODE_ALWAYS = 0;
class BypasserNode extends BaseNodeModeChanger {
static override exposedActions = ["Bypass all", "Enable all", "Toggle all"];
static override type = NodeTypesString.FAST_BYPASSER;
static override title = NodeTypesString.FAST_BYPASSER;
override readonly modeOn = MODE_ALWAYS;
override readonly modeOff = MODE_BYPASS;
constructor(title = BypasserNode.title) {
super(title);
}
override async handleAction(action: string) {
if (action === "Bypass all") {
for (const widget of this.widgets) {
this.forceWidgetOff(widget, true);
}
} else if (action === "Enable all") {
for (const widget of this.widgets) {
this.forceWidgetOn(widget, true);
}
} else if (action === "Toggle all") {
for (const widget of this.widgets) {
this.forceWidgetToggle(widget, true);
}
}
}
}
app.registerExtension({
name: "rgthree.Bypasser",
registerCustomNodes() {
BypasserNode.setUp(BypasserNode);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == BypasserNode.title) {
(node as any)._tempWidth = node.size[0];
}
}
});
@@ -1,268 +0,0 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import { RgthreeDialog, RgthreeDialogOptions } from "rgthree/common/dialog.js";
import { createElement as $el, querySelectorAll as $$ } from "rgthree/common/utils_dom.js";
import { checkmark, logoRgthree } from "rgthree/common/media/svgs.js";
import { rgthree } from "./rgthree.js";
import { SERVICE as CONFIG_SERVICE } from "./config_service.js";
/** Types of config used as a hint for the form handling. */
enum ConfigType {
UNKNOWN,
BOOLEAN,
STRING,
NUMBER,
}
const TYPE_TO_STRING = {
[ConfigType.UNKNOWN]: "unknown",
[ConfigType.BOOLEAN]: "boolean",
[ConfigType.STRING]: "string",
[ConfigType.NUMBER]: "number",
};
type ConfigurationSchema = {
key: string;
type: ConfigType;
label: string;
options?: string[] | number[];
description?: string;
subconfig?: ConfigurationSchema[];
};
/**
* A static schema of sorts to layout options found in the config.
*/
const CONFIGURABLE: { features: ConfigurationSchema[] } = {
features: [
{
key: "features.patch_recursive_execution",
type: ConfigType.BOOLEAN,
label: "Optimize ComfyUI's Execution",
description:
"Patches ComfyUI's backend execution making complex workflows 1000's of times faster." +
"<br>⚠️ Disable if execution seems broken due to forward ComfyUI changes.",
},
{
key: "features.progress_bar.enabled",
type: ConfigType.BOOLEAN,
label: "Prompt Progress Bar",
description: `Shows a minimal progress bar for nodes and steps at the top of the app.`,
subconfig: [
{
key: "features.progress_bar.height",
type: ConfigType.NUMBER,
label: "Height of the bar",
},
{
key: "features.progress_bar.position",
type: ConfigType.STRING,
label: "Position at top or bottom of window",
options: ["top", "bottom"],
},
],
},
{
key: "features.menu_queue_selected_nodes",
type: ConfigType.BOOLEAN,
label: "(Menu) Show 'Queue Selected Output Nodes'",
description:
"Will show a menu item in the right-click context menus to queue (only) the selected " +
"output nodes.",
},
{
key: "features.menu_auto_nest.subdirs",
type: ConfigType.BOOLEAN,
label: "(Menu) Auto Nest Subdirectories",
description:
"When a large, flat list of values contain sub-directories, auto nest them. (Like, for " +
"a large list of checkpoints).",
subconfig: [
{
key: "features.menu_auto_nest.threshold",
type: ConfigType.NUMBER,
label: "Number of items needed to trigger nesting.",
},
],
},
{
key: "features.show_alerts_for_corrupt_workflows",
type: ConfigType.BOOLEAN,
label: "Detect Corrupt Workflows",
description:
"Will show a message at the top of the screen when loading a workflow that has " +
"corrupt linking data.",
},
],
};
/**
* Creates a new fieldrow for main or sub configuration items.
*/
function fieldrow(item: ConfigurationSchema) {
const initialValue = CONFIG_SERVICE.getConfigValue(item.key);
const container = $el(`div.fieldrow.-type-${TYPE_TO_STRING[item.type]}`, {
dataset: {
name: item.key,
initial: initialValue,
type: item.type,
},
});
$el(`label[for="${item.key}"]`, {
children: [
$el(`span[text="${item.label}"]`),
item.description ? $el("small", { html: item.description }) : null,
],
parent: container,
});
let input;
if (item.options?.length) {
input = $el<HTMLSelectElement>(`select[id="${item.key}"]`, {
parent: container,
children: item.options.map((o) => {
return $el<HTMLOptionElement>(`option[value="${String(o)}"]`, {
text: String(o),
selected: o === initialValue,
});
}),
});
} else if (item.type === ConfigType.BOOLEAN) {
container.classList.toggle("-checked", initialValue);
input = $el<HTMLInputElement>(`input[type="checkbox"][id="${item.key}"]`, {
parent: container,
checked: initialValue,
});
} else {
input = $el(`input[id="${item.key}"]`, {
parent: container,
value: initialValue,
});
}
$el("div.fieldrow-value", { children: [input], parent: container });
return container;
}
/**
* A dialog to edit rgthree-comfy settings and config.
*/
export class RgthreeConfigDialog extends RgthreeDialog {
constructor() {
const content = $el("div");
const features = $el(`fieldset`, { children: [$el(`legend[text="Features"]`)] });
for (const feature of CONFIGURABLE.features) {
const container = $el("div.formrow");
container.appendChild(fieldrow(feature));
if (feature.subconfig) {
for (const subfeature of feature.subconfig) {
container.appendChild(fieldrow(subfeature));
}
}
features.appendChild(container);
}
content.appendChild(features);
content.addEventListener("input", (e) => {
const changed = this.getChangedFormData();
($$(".save-button", this.element)[0] as HTMLButtonElement).disabled =
!Object.keys(changed).length;
});
content.addEventListener("change", (e) => {
const changed = this.getChangedFormData();
($$(".save-button", this.element)[0] as HTMLButtonElement).disabled =
!Object.keys(changed).length;
});
const options: RgthreeDialogOptions = {
class: "-iconed -settings",
title: logoRgthree + `<h2>Settings - rgthree-comfy</h2>`,
content,
onBeforeClose: () => {
const changed = this.getChangedFormData();
if (Object.keys(changed).length) {
return confirm("Looks like there are unsaved changes. Are you sure you want close?");
}
return true;
},
buttons: [
{
label: "Save",
disabled: true,
className: "rgthree-button save-button -blue",
callback: async (e) => {
const changed = this.getChangedFormData();
if (!Object.keys(changed).length) {
this.close();
return;
}
const success = await CONFIG_SERVICE.setConfigValues(changed);
if (success) {
this.close();
rgthree.showMessage({
id: "config-success",
message: `${checkmark} Successfully saved rgthree-comfy settings!`,
timeout: 4000,
});
($$(".save-button", this.element)[0] as HTMLButtonElement).disabled = true;
} else {
alert("There was an error saving rgthree-comfy configuration.");
}
},
},
],
};
super(options);
}
getChangedFormData() {
return $$("[data-name]", this.contentElement).reduce((acc: { [key: string]: any }, el) => {
const name = el.dataset["name"]!;
const type = el.dataset["type"]!;
const initialValue = CONFIG_SERVICE.getConfigValue(name);
let currentValueEl = $$("input, textarea, select", el)[0] as HTMLInputElement;
let currentValue: any = null;
if (type === String(ConfigType.BOOLEAN)) {
currentValue = currentValueEl.checked;
// Not sure I like this side effect in here, but it's easy to just do it now.
el.classList.toggle("-checked", currentValue);
} else {
currentValue = currentValueEl?.value;
if (type === String(ConfigType.NUMBER)) {
currentValue = Number(currentValue) || initialValue;
}
}
if (currentValue !== initialValue) {
acc[name] = currentValue;
}
return acc;
}, {});
}
}
app.ui.settings.addSetting({
id: "rgthree.config",
name: "Open rgthree-comfy config",
type: () => {
// Adds a row to open the dialog from the ComfyUI settings.
return $el("tr.rgthree-comfyui-settings-row", {
children: [
$el("td", {
child: `<div>${logoRgthree} [rgthree-comfy] configuration / settings</div>`,
}),
$el("td", {
child: $el('button.rgthree-button.-blue[text="rgthree-comfy settings"]', {
events: {
click: (e: PointerEvent) => {
new RgthreeConfigDialog().show();
},
},
}),
}),
],
});
},
});
@@ -1,36 +0,0 @@
// @ts-ignore
import { rgthreeConfig } from "rgthree/config.js";
import { getObjectValue, setObjectValue } from "rgthree/common/shared_utils.js";
import { rgthreeApi } from "rgthree/common/rgthree_api.js";
/**
* A singleton service exported as `SERVICE` to handle configuration routines.
*/
class ConfigService extends EventTarget {
getConfigValue(key: string, def?: any) {
return getObjectValue(rgthreeConfig, key, def);
}
/**
* Given an object of key:value changes it will send to the server and wait for a successful
* response before setting the values on the local rgthreeConfig.
*/
async setConfigValues(changed: { [key: string]: any }) {
const body = new FormData();
body.append("json", JSON.stringify(changed));
const response = await rgthreeApi.fetchJson("/config", { method: "POST", body });
if (response.status === "ok") {
for (const [key, value] of Object.entries(changed)) {
setObjectValue(rgthreeConfig, key, value);
this.dispatchEvent(new CustomEvent("config-change", { detail: { key, value } }));
}
} else {
return false;
}
return true;
}
}
/** The ConfigService singleton. */
export const SERVICE = new ConfigService();
@@ -1,23 +0,0 @@
export function addRgthree(str: string) {
return str + ' (rgthree)';
}
export function stripRgthree(str: string) {
return str.replace(/\s*\(rgthree\)$/, '');
}
export const NodeTypesString = {
NODE_MODE_RELAY: addRgthree('Mute / Bypass Relay'),
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
FAST_MUTER: addRgthree('Fast Muter'),
FAST_BYPASSER: addRgthree('Fast Bypasser'),
FAST_GROUPS_MUTER: addRgthree('Fast Groups Muter'),
FAST_GROUPS_BYPASSER: addRgthree('Fast Groups Bypasser'),
FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'),
NODE_COLLECTOR: addRgthree('Node Collector'),
REROUTE: addRgthree('Reroute'),
RANDOM_UNMUTER: addRgthree('Random Unmuter'),
BOOKMARK: addRgthree('Bookmark'),
IMAGE_COMPARER: addRgthree('Image Comparer'),
}
@@ -1,418 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
import type {
INodeInputSlot,
INodeOutputSlot,
LGraph,
LLink,
LGraphCanvas as TLGraphCanvas,
LiteGraph as TLiteGraph,
LGraphNode as TLGraphNode,
} from "typings/litegraph.js";
import type { ComfyApp, ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
// @ts-ignore
import { app } from "../../scripts/app.js";
import {
IoDirection,
addConnectionLayoutSupport,
addMenuItem,
applyMixins,
matchLocalSlotsToServer,
replaceNode,
} from "./utils.js";
import { RgthreeBaseNode, RgthreeBaseServerNode } from "./base_node.js";
import { rgthree } from "./rgthree.js";
declare const LGraphNode: typeof TLGraphNode;
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphCanvas: typeof TLGraphCanvas;
/**
* Takes a non-context node and determins for its input or output slot, if there is a valid
* connection for an opposite context output or input slot.
*/
function findMatchingIndexByTypeOrName(otherNode: TLGraphNode, otherSlot: INodeInputSlot|INodeOutputSlot, ctxSlots: INodeInputSlot[]|INodeOutputSlot[]) {
const otherNodeType = (otherNode.type || '').toUpperCase();
const otherNodeName = (otherNode.title || '').toUpperCase();
let otherSlotType = otherSlot.type as string;
if (Array.isArray(otherSlotType) || otherSlotType.includes(',')) {
otherSlotType = 'COMBO';
}
const otherSlotName = otherSlot.name.toUpperCase().replace('OPT_', '').replace('_NAME', '');
let ctxSlotIndex = -1;
if (["CONDITIONING", "INT", "STRING", "FLOAT", "COMBO"].includes(otherSlotType)) {
ctxSlotIndex = ctxSlots.findIndex((ctxSlot) => {
const ctxSlotName = ctxSlot.name.toUpperCase().replace('OPT_', '').replace('_NAME', '');
let ctxSlotType = ctxSlot.type as string;
if (Array.isArray(ctxSlotType) || ctxSlotType.includes(',')) {
ctxSlotType = 'COMBO';
}
if (ctxSlotType !== otherSlotType) {
return false;
}
// Straightforward matches.
if(ctxSlotName === otherSlotName
|| (ctxSlotName === "SEED" && otherSlotName.includes("SEED"))
|| (ctxSlotName === "STEP_REFINER" && otherSlotName.includes("AT_STEP"))
|| (ctxSlotName === "STEP_REFINER" && otherSlotName.includes("REFINER_STEP"))) {
return true;
}
// If postive other node, try to match conditining and text.
if ((otherNodeType.includes('POSITIVE') || otherNodeName.includes('POSITIVE')) &&
(
(ctxSlotName === 'POSITIVE' && otherSlotType === 'CONDITIONING')
|| (ctxSlotName === 'TEXT_POS_G' && otherSlotName.includes("TEXT_G"))
|| (ctxSlotName === 'TEXT_POS_L' && otherSlotName.includes("TEXT_L"))
)
) {
return true;
}
if ((otherNodeType.includes('NEGATIVE') || otherNodeName.includes('NEGATIVE')) &&
(
(ctxSlotName === 'NEGATIVE' && otherSlotType === 'CONDITIONING')
|| (ctxSlotName === 'TEXT_NEG_G' && otherSlotName.includes("TEXT_G"))
|| (ctxSlotName === 'TEXT_NEG_L' && otherSlotName.includes("TEXT_L"))
)
) {
return true;
}
return false;
});
} else {
ctxSlotIndex = ctxSlots.map((s) => s.type).indexOf(otherSlotType);
}
return ctxSlotIndex;
}
/**
* A Base Context node for other context based nodes to extend.
*/
class BaseContextNode extends RgthreeBaseServerNode {
constructor(title: string) {
super(title);
}
// LiteGraph adds more spacing than we want when calculating a nodes' `_collapsed_width`, so we'll
// override it with a setter and re-set it measured exactly as we want.
___collapsed_width: number = 0;
//@ts-ignore - TS Doesn't like us overriding a property with accessors but, too bad.
override get _collapsed_width() {
return this.___collapsed_width;
}
override set _collapsed_width(width: number) {
const canvas = app.canvas as TLGraphCanvas;
const ctx = canvas.canvas.getContext('2d')!;
const oldFont = ctx.font;
ctx.font = canvas.title_text_font;
let title = this.title.trim();
this.___collapsed_width = 30 + (title ? 10 + ctx.measureText(title).width : 0);
ctx.font = oldFont;
}
override connectByType<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string,
): T | null {
let canConnect =
super.connectByType &&
super.connectByType.call(this, slot, sourceNode, sourceSlotType, optsIn);
if (!super.connectByType) {
canConnect = LGraphNode.prototype.connectByType.call(
this,
slot,
sourceNode,
sourceSlotType,
optsIn,
);
}
if (!canConnect && slot === 0) {
const ctrlKey = rgthree.ctrlKey;
// Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
// Unfortunately, we don't know which are null now, so we'll just connect any that are
// not already connected.
for (const [index, input] of (sourceNode.inputs || []).entries()) {
if (input.link && !ctrlKey) {
continue;
}
const thisOutputSlot = findMatchingIndexByTypeOrName(sourceNode, input, this.outputs);
if (thisOutputSlot > -1) {
this.connect(thisOutputSlot, sourceNode, index);
}
}
}
return null;
}
override connectByTypeOutput<T = any>(slot: string | number, sourceNode: TLGraphNode, sourceSlotType: string, optsIn: string): T | null {
let canConnect =
super.connectByTypeOutput &&
super.connectByTypeOutput.call(this, slot, sourceNode, sourceSlotType, optsIn);
if (!super.connectByType) {
canConnect = LGraphNode.prototype.connectByTypeOutput.call(
this,
slot,
sourceNode,
sourceSlotType,
optsIn,
);
}
if (!canConnect && slot === 0) {
const ctrlKey = rgthree.ctrlKey;
// Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
// Unfortunately, we don't know which are null now, so we'll just connect any that are
// not already connected.
for (const [index, output] of (sourceNode.outputs || []).entries()) {
if (output.links?.length && !ctrlKey) {
continue;
}
const thisInputSlot = findMatchingIndexByTypeOrName(sourceNode, output, this.inputs);
if (thisInputSlot > -1) {
sourceNode.connect(index, this, thisInputSlot);
}
}
}
return null;
}
static override setUp(comfyClass: any, ctxClass: any) {
RgthreeBaseServerNode.registerForOverride(comfyClass, ctxClass);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
addConnectionLayoutSupport(ctxClass, app, [
["Left", "Right"],
["Right", "Left"],
]);
setTimeout(() => {
ctxClass.category = comfyClass.category;
});
}
}
/**
* The original Context node.
*/
class ContextNode extends BaseContextNode {
static override title = "Context (rgthree)";
static override type = "Context (rgthree)";
static comfyClass = "Context (rgthree)";
constructor(title = ContextNode.title) {
super(title);
}
static override setUp(comfyClass: any) {
BaseContextNode.setUp(comfyClass, ContextNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextNode, app, {
name: "Convert To Context Big",
callback: (node) => {
replaceNode(node, ContextBigNode.type);
},
});
}
}
/**
* The Context Big node.
*/
class ContextBigNode extends BaseContextNode {
static override title = "Context Big (rgthree)";
static override type = "Context Big (rgthree)";
static comfyClass = "Context Big (rgthree)";
constructor(title = ContextBigNode.title) {
super(title);
}
static override setUp(comfyClass: any) {
BaseContextNode.setUp(comfyClass, ContextBigNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextBigNode, app, {
name: "Convert To Context (Original)",
callback: (node) => {
replaceNode(node, ContextNode.type);
},
});
}
}
/**
* The Context Switch (original) node.
*/
class ContextSwitchNode extends BaseContextNode {
static override title = "Context Switch (rgthree)";
static override type = "Context Switch (rgthree)";
static comfyClass = "Context Switch (rgthree)";
constructor(title = ContextSwitchNode.title) {
super(title);
}
static override setUp(comfyClass: any) {
BaseContextNode.setUp(comfyClass, ContextSwitchNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextSwitchNode, app, {
name: "Convert To Context Switch Big",
callback: (node) => {
replaceNode(node, ContextSwitchBigNode.type);
},
});
}
}
/**
* The Context Switch Big node.
*/
class ContextSwitchBigNode extends BaseContextNode {
static override title = "Context Switch Big (rgthree)";
static override type = "Context Switch Big (rgthree)";
static comfyClass = "Context Switch Big (rgthree)";
constructor(title = ContextSwitchBigNode.title) {
super(title);
}
static override setUp(comfyClass: any) {
BaseContextNode.setUp(comfyClass, ContextSwitchBigNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextSwitchBigNode, app, {
name: "Convert To Context Switch",
callback: (node) => {
replaceNode(node, ContextSwitchNode.type);
},
});
}
}
/**
* The Context Merge (original) node.
*/
class ContextMergeNode extends BaseContextNode {
static override title = "Context Merge (rgthree)";
static override type = "Context Merge (rgthree)";
static comfyClass = "Context Merge (rgthree)";
constructor(title = ContextMergeNode.title) {
super(title);
}
static override setUp(comfyClass: any) {
BaseContextNode.setUp(comfyClass, ContextMergeNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextMergeNode, app, {
name: "Convert To Context Merge Big",
callback: (node) => {
replaceNode(node, ContextMergeBigNode.type);
},
});
}
}
/**
* The Context Switch Big node.
*/
class ContextMergeBigNode extends BaseContextNode {
static override title = "Context Merge Big (rgthree)";
static override type = "Context Merge Big (rgthree)";
static comfyClass = "Context Merge Big (rgthree)";
constructor(title = ContextMergeBigNode.title) {
super(title);
}
static override setUp(comfyClass: any) {
BaseContextNode.setUp(comfyClass, ContextMergeBigNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextMergeBigNode, app, {
name: "Convert To Context Switch",
callback: (node) => {
replaceNode(node, ContextMergeNode.type);
},
});
}
}
const contextNodes = [ContextNode, ContextBigNode, ContextSwitchNode, ContextSwitchBigNode, ContextMergeNode, ContextMergeBigNode];
const contextTypeToServerDef: { [type: string]: ComfyObjectInfo } = {};
function fixBadConfigs(node: ContextNode) {
// Dumb mistake, but let's fix our mispelling. This will probably need to stay in perpetuity to
// keep any old workflows operating.
const wrongName = node.outputs.find((o, i) => o.name === 'CLIP_HEIGTH');
if (wrongName) {
wrongName.name = 'CLIP_HEIGHT';
}
}
app.registerExtension({
name: "rgthree.Context",
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
// Loop over out context nodes and see if any match the server data.
if (nodeData.name === ContextNode.type) {
}
for (const ctxClass of contextNodes) {
if (nodeData.name === ctxClass.type) {
ctxClass.nodeData = nodeData;
ctxClass.nodeType = nodeType;
contextTypeToServerDef[ctxClass.type] = nodeData;
ctxClass.setUp(nodeType as any);
break;
}
}
},
async nodeCreated(node: TLGraphNode) {
const type = node.type || (node.constructor as any).type;
const serverDef = type && contextTypeToServerDef[type];
if (serverDef) {
fixBadConfigs(node as ContextNode);
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
// Switches don't need to change inputs, only context outputs
if (!type!.includes("Switch") && !type!.includes("Merge")) {
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
}
// }, 100);
}
},
/**
* When we're loaded from the server, check if we're using an out of date version and update our
* inputs / outputs to match.
*/
async loadedGraphNode(node: TLGraphNode) {
const type = node.type || (node.constructor as any).type;
const serverDef = type && contextTypeToServerDef[type];
if (serverDef) {
fixBadConfigs(node as ContextNode);
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
// Switches don't need to change inputs, only context outputs
if (!type!.includes("Switch") && !type!.includes("Merge")) {
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
}
}
},
});
@@ -1,85 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {
SerializedLGraphNode,
LGraphNode as TLGraphNode,
LiteGraph as TLiteGraph,
} from "typings/litegraph.js";
import type { ComfyApp, ComfyObjectInfo } from "typings/comfy.js";
import { addConnectionLayoutSupport, replaceNode } from "./utils.js";
import { rgthree } from "./rgthree.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
let hasShownAlertForUpdatingInt = false;
app.registerExtension({
name: "rgthree.DisplayAny",
async beforeRegisterNodeDef(
nodeType: typeof LGraphNode,
nodeData: ComfyObjectInfo,
app: ComfyApp,
) {
if (nodeData.name === "Display Any (rgthree)" || nodeData.name === "Display Int (rgthree)") {
(nodeType as any).title_mode = LiteGraph.NO_TITLE;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
(this as any).showValueWidget = ComfyWidgets["STRING"](
this,
"output",
["STRING", { multiline: true }],
app,
).widget;
(this as any).showValueWidget.inputEl!.readOnly = true;
(this as any).showValueWidget.serializeValue = async (
node: TLGraphNode,
index: number,
) => {
const n = rgthree.getNodeFromInitialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff(node);
if (n) {
// Since we need a round trip to get the value, the serizalized value means nothing, and
// saving it to the metadata would just be confusing. So, we clear it here.
n.widgets_values![index] = "";
} else {
console.warn('No serialized node found in workflow. May be attributed to '
+ 'https://github.com/comfyanonymous/ComfyUI/issues/2193');
}
return "";
};
};
addConnectionLayoutSupport(nodeType, app, [["Left"], ["Right"]]);
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
(this as any).showValueWidget.value = message.text[0];
};
}
},
// This ports Display Int to DisplayAny, but ComfyUI still shows an error.
// If https://github.com/comfyanonymous/ComfyUI/issues/1527 is fixed, this could work.
// async loadedGraphNode(node: TLGraphNode) {
// if (node.type === "Display Int (rgthree)") {
// replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]]));
// if (!hasShownAlertForUpdatingInt) {
// hasShownAlertForUpdatingInt = true;
// setTimeout(() => {
// alert(
// "Don't worry, your 'Display Int' nodes have been updated to the new " +
// "'Display Any' nodes! You can ignore the error message underneath (for that node)." +
// "\n\nThanks.\n- rgthree",
// );
// }, 128);
// }
// }
// },
});
@@ -1,366 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { RgthreeBaseNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import { ComfyApp, ComfyWidget } from "typings/comfy.js";
import type {
IWidget,
LGraph,
LGraphNode,
SerializedLGraphNode,
} from "typings/litegraph.js";
import type { Constructor } from "typings/index.js";
import { addMenuItem } from "./utils.js";
import { rgthree } from "./rgthree.js";
const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
/**
* The Fast Actions Button.
*
* This adds a button that the user can connect any node to and then choose an action to take on
* that node when the button is pressed. Default actions are "Mute," "Bypass," and "Enable," but
* Nodes can expose actions additional actions that can then be called back.
*/
class FastActionsButton extends BaseAnyInputConnectedNode {
static override type = NodeTypesString.FAST_ACTIONS_BUTTON;
static override title = NodeTypesString.FAST_ACTIONS_BUTTON;
static "@buttonText" = { type: "string" };
static "@shortcutModifier" = {
type: "combo",
values: ["ctrl", "alt", "shift"],
};
static "@shortcutKey" = { type: "string" };
static collapsible = false;
override readonly isVirtualNode = true;
override serialize_widgets = true;
readonly buttonWidget: IWidget;
readonly widgetToData = new Map<
IWidget,
{ comfy?: ComfyApp; node?: LGraphNode }
>();
readonly nodeIdtoFunctionCache = new Map<number, string>();
readonly keypressBound;
readonly keyupBound;
private executingFromShortcut = false;
constructor(title?: string) {
super(title);
this.properties["buttonText"] = "🎬 Action!";
this.properties["shortcutModifier"] = "alt";
this.properties["shortcutKey"] = "";
this.buttonWidget = this.addWidget(
"button",
this.properties["buttonText"],
null,
() => {
this.executeConnectedNodes();
},
{ serialize: false },
);
this.keypressBound = this.onKeypress.bind(this);
this.keyupBound = this.onKeyup.bind(this);
}
/** When we're given data to configure, like from a PNG or JSON. */
override configure(info: SerializedLGraphNode<LGraphNode>): void {
super.configure(info);
// Since we add the widgets dynamically, we need to wait to set their values
// with a short timeout.
setTimeout(() => {
if (info.widgets_values) {
for (let [index, value] of info.widgets_values.entries()) {
if (index > 0) {
if (value.startsWith("comfy_action:")) {
value = value.replace("comfy_action:", "")
this.addComfyActionWidget(index, value);
}
if (this.widgets[index]) {
this.widgets[index]!.value = value;
}
}
}
}
}, 100);
}
override clone() {
const cloned = super.clone();
cloned.properties["buttonText"] = "🎬 Action!";
cloned.properties["shortcutKey"] = "";
return cloned;
}
override onAdded(graph: LGraph): void {
window.addEventListener("keydown", this.keypressBound);
window.addEventListener("keyup", this.keyupBound);
}
override onRemoved(): void {
window.removeEventListener("keydown", this.keypressBound);
window.removeEventListener("keyup", this.keyupBound);
}
async onKeypress(event: KeyboardEvent) {
const target = (event.target as HTMLElement)!;
if (
this.executingFromShortcut ||
target.localName == "input" ||
target.localName == "textarea"
) {
return;
}
if (
this.properties["shortcutKey"].trim() &&
this.properties["shortcutKey"].toLowerCase() === event.key.toLowerCase()
) {
const shortcutModifier = this.properties["shortcutModifier"];
let good = shortcutModifier === "ctrl" && event.ctrlKey;
good = good || (shortcutModifier === "alt" && event.altKey);
good = good || (shortcutModifier === "shift" && event.shiftKey);
good = good || (shortcutModifier === "meta" && event.metaKey);
if (good) {
setTimeout(() => {
this.executeConnectedNodes();
}, 20);
this.executingFromShortcut = true;
event.preventDefault();
event.stopImmediatePropagation();
app.canvas.dirty_canvas = true;
return false;
}
}
return;
}
onKeyup(event: KeyboardEvent) {
const target = (event.target as HTMLElement)!;
if (target.localName == "input" || target.localName == "textarea") {
return;
}
this.executingFromShortcut = false;
}
override onPropertyChanged(
property: string,
value: any,
_prevValue: any,
): boolean | void {
if (property == "buttonText") {
this.buttonWidget.name = value;
}
if (property == "shortcutKey") {
value = value.trim();
this.properties["shortcutKey"] = (value && value[0].toLowerCase()) || "";
}
}
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]) {
// Remove any widgets and data for widgets that are no longer linked.
for (const [widget, data] of this.widgetToData.entries()) {
if (!data.node) {
continue;
}
if (!linkedNodes.includes(data.node)) {
const index = this.widgets.indexOf(widget);
if (index > -1) {
this.widgetToData.delete(widget);
this.removeWidget(widget);
} else {
rgthree.logger.debug('Fast Action Button - Connected widget is not in widgets... weird.');
}
}
}
const badNodes: LGraphNode[] = []; // Nodes that are deleted elsewhere may not exist in linkedNodes.
let indexOffset = 1; // Start with button, increment when we hit a non-node widget (like comfy)
for (const [index, node] of linkedNodes.entries()) {
// Sometimes linkedNodes is stale.
if (!node) {
rgthree.logger.debug('Fast Action Button - linkedNode provided that does not exist. ');
badNodes.push(node);
continue;
}
let widgetAtSlot = this.widgets[index + indexOffset];
if (widgetAtSlot && this.widgetToData.get(widgetAtSlot)?.comfy) {
indexOffset++;
widgetAtSlot = this.widgets[index + indexOffset];
}
if (!widgetAtSlot || this.widgetToData.get(widgetAtSlot)?.node?.id !== node.id) {
// Find the next widget that matches the node.
let widget: IWidget | null = null;
for (let i = index + indexOffset; i < this.widgets.length; i++) {
if (this.widgetToData.get(this.widgets[i]!)?.node?.id === node.id) {
widget = this.widgets.splice(i, 1)[0]!;
this.widgets.splice(index + indexOffset, 0, widget);
break;
}
}
if (!widget) {
// Add a widget at this spot.
const exposedActions: string[] =
(node.constructor as any).exposedActions || [];
widget = this.addWidget("combo", node.title, "None", "", {
values: ["None", "Mute", "Bypass", "Enable", ...exposedActions],
});
(widget as ComfyWidget).serializeValue = async (
_node: SerializedLGraphNode,
_index: number,
) => {
return widget?.value;
};
this.widgetToData.set(widget, { node });
}
}
}
// Go backwards through widgets, and remove any that are not in out widgetToData
for (
let i = this.widgets.length - 1;
i > linkedNodes.length + indexOffset - 1;
i--
) {
const widgetAtSlot = this.widgets[i];
if (widgetAtSlot && this.widgetToData.get(widgetAtSlot)?.comfy) {
continue;
}
this.removeWidget(widgetAtSlot);
}
}
override removeWidget(widgetOrSlot?: number | IWidget): void {
const widget =
typeof widgetOrSlot === "number"
? this.widgets[widgetOrSlot]
: widgetOrSlot;
if (widget && this.widgetToData.has(widget)) {
this.widgetToData.delete(widget);
}
super.removeWidget(widgetOrSlot);
}
/**
* Runs through the widgets, and executes the actions.
*/
async executeConnectedNodes() {
for (const widget of this.widgets) {
if (widget == this.buttonWidget) {
continue;
}
const action = widget.value;
const { comfy, node } = this.widgetToData.get(widget) ?? {};
if (comfy) {
if (action === "Queue Prompt") {
await comfy.queuePrompt();
}
continue;
}
if (node) {
if (action === "Mute") {
node.mode = MODE_MUTE;
} else if (action === "Bypass") {
node.mode = MODE_BYPASS;
} else if (action === "Enable") {
node.mode = MODE_ALWAYS;
}
// If there's a handleAction, always call it.
if ((node as RgthreeBaseNode).handleAction) {
await (node as RgthreeBaseNode).handleAction(action);
}
app.graph.change();
continue;
}
console.warn("Fast Actions Button has a widget without correct data.");
}
}
/**
* Adds a ComfyActionWidget at the provided slot (or end).
*/
addComfyActionWidget(slot?: number, value?: string) {
let widget = this.addWidget(
"combo",
"Comfy Action",
"None",
() => {
if (widget.value.startsWith("MOVE ")) {
this.widgets.push(
this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!,
);
widget.value = (widget as any)["lastValue_"];
} else if (widget.value.startsWith("REMOVE ")) {
this.removeWidget(widget);
}
(widget as any)["lastValue_"] = widget.value;
},
{
values: ["None", "Queue Prompt", "REMOVE Comfy Action", "MOVE to end"],
},
);
(widget as any)["lastValue_"] = value;
(widget as ComfyWidget).serializeValue = async (
_node: SerializedLGraphNode,
_index: number,
) => {
return `comfy_app:${widget?.value}`;
};
this.widgetToData.set(widget, { comfy: app });
if (slot != null) {
this.widgets.splice(
slot,
0,
this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!,
);
}
return widget;
}
override onSerialize(o: SerializedLGraphNode) {
super.onSerialize && super.onSerialize(o);
for (let [index, value] of (o.widgets_values || []).entries()) {
if (this.widgets[index]?.name === "Comfy Action") {
o.widgets_values![index] = `comfy_action:${value}`;
}
}
}
static override setUp<T extends RgthreeBaseNode>(clazz: Constructor<T>) {
BaseAnyInputConnectedNode.setUp(clazz);
addMenuItem(clazz, app, {
name: "➕ Append a Comfy Action",
callback: (nodeArg: LGraphNode) => {
(nodeArg as FastActionsButton).addComfyActionWidget();
},
});
}
}
app.registerExtension({
name: "rgthree.FastActionsButton",
registerCustomNodes() {
FastActionsButton.setUp(FastActionsButton);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == FastActionsButton.title) {
(node as FastActionsButton)._tempWidth = node.size[0];
}
},
});
@@ -1,48 +0,0 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import { FastGroupsMuter } from "./fast_groups_muter.js";
import {
type LGraphNode,
type LiteGraph as TLiteGraph,
} from "typings/litegraph.js";
declare const LiteGraph: typeof TLiteGraph;
/**
* Fast Bypasser implementation that looks for groups in the workflow and adds toggles to mute them.
*/
export class FastGroupsBypasser extends FastGroupsMuter {
static override type = NodeTypesString.FAST_GROUPS_BYPASSER;
static override title = NodeTypesString.FAST_GROUPS_BYPASSER;
static override exposedActions = ["Bypass all", "Enable all", "Toggle all"];
protected override helpActions = 'bypass and enable';
override readonly modeOn = LiteGraph.ALWAYS;
override readonly modeOff = 4; // Used by Comfy for "bypass"
constructor(title = FastGroupsBypasser.title) {
super(title);
}
static override setUp<T extends RgthreeBaseNode>(clazz: new (title?: string) => T) {
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
}
}
app.registerExtension({
name: "rgthree.FastGroupsBypasser",
registerCustomNodes() {
FastGroupsBypasser.setUp(FastGroupsBypasser);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == FastGroupsMuter.title) {
(node as FastGroupsBypasser).tempSize = [...node.size];
}
},
});
@@ -1,666 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import {
type LGraphNode,
type LGraph as TLGraph,
type LiteGraph as TLiteGraph,
LGraphCanvas as TLGraphCanvas,
Vector2,
SerializedLGraphNode,
IWidget,
LGraphGroup,
Vector4,
} from "typings/litegraph.js";
import { fitString } from "./utils_canvas.js";
declare const LGraphCanvas: typeof TLGraphCanvas;
declare const LiteGraph: typeof TLiteGraph;
const PROPERTY_SORT = "sort";
const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet";
const PROPERTY_MATCH_COLORS = "matchColors";
const PROPERTY_MATCH_TITLE = "matchTitle";
const PROPERTY_SHOW_NAV = "showNav";
const PROPERTY_RESTRICTION = "toggleRestriction";
/**
* A service that keeps global state that can be shared by multiple FastGroupsMuter or
* FastGroupsBypasser nodes rather than calculate it on it's own.
*/
class FastGroupsService {
private msThreshold = 400;
private msLastUnsorted = 0;
private msLastAlpha = 0;
private msLastPosition = 0;
private groupsUnsorted: LGraphGroup[] = [];
private groupsSortedAlpha: LGraphGroup[] = [];
private groupsSortedPosition: LGraphGroup[] = [];
private readonly fastGroupNodes: FastGroupsMuter[] = [];
private runScheduledForMs: number|null = null;
private runScheduleTimeout: number|null = null;
private runScheduleAnimation: number|null = null;
private cachedNodeBoundings: {[key: number]: Vector4}|null = null;
constructor() {
// Don't need to do anything, wait until a signal.
}
addFastGroupNode(node: FastGroupsMuter) {
this.fastGroupNodes.push(node);
// Schedule it because the node may not be ready to refreshWidgets (like, when added it may
// not have cloned properties to filter against, etc.).
this.scheduleRun(8);
}
removeFastGroupNode(node: FastGroupsMuter) {
const index = this.fastGroupNodes.indexOf(node);
if (index > -1) {
this.fastGroupNodes.splice(index, 1);
}
// If we have no more group nodes, then clear out data; it could be because of a canvas clear.
if (!this.fastGroupNodes?.length) {
this.clearScheduledRun();
this.groupsUnsorted = [];
this.groupsSortedAlpha = [];
this.groupsSortedPosition = [];
}
}
private run() {
// We only run if we're scheduled, so if we're not, then bail.
if (!this.runScheduledForMs) {
return;
}
for (const node of this.fastGroupNodes) {
node.refreshWidgets();
}
this.clearScheduledRun();
this.scheduleRun();
}
private scheduleRun(ms = 500) {
// If we got a request for an immediate schedule and already have on scheduled for longer, then
// cancel the long one to expediate a fast one.
if (this.runScheduledForMs && ms < this.runScheduledForMs) {
this.clearScheduledRun();
}
if (!this.runScheduledForMs && this.fastGroupNodes.length) {
this.runScheduledForMs = ms;
this.runScheduleTimeout = setTimeout(() => {
this.runScheduleAnimation = requestAnimationFrame(() => this.run());
}, ms);
}
}
private clearScheduledRun() {
this.runScheduleTimeout && clearTimeout(this.runScheduleTimeout);
this.runScheduleAnimation && cancelAnimationFrame(this.runScheduleAnimation);
this.runScheduleTimeout = null;
this.runScheduleAnimation = null;
this.runScheduledForMs = null;
}
/**
* Returns the boundings for all nodes on the graph, then clears it after a short delay. This is
* to increase efficiency by caching the nodes' boundings when multiple groups are on the page.
*/
getBoundingsForAllNodes() {
if (!this.cachedNodeBoundings) {
this.cachedNodeBoundings = {};
for (const node of app.graph._nodes) {
this.cachedNodeBoundings[node.id] = node.getBounding();
}
setTimeout(() => {
this.cachedNodeBoundings = null;
}, 50);
}
return this.cachedNodeBoundings;
}
/**
* This overrides `LGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when
* calculating for many groups at once (only compute all nodes once in `getBoundingsForAllNodes`).
*/
recomputeInsideNodesForGroup(group: LGraphGroup) {
const cachedBoundings = this.getBoundingsForAllNodes();
const nodes = group.graph._nodes;
group._nodes.length = 0;
for (const node of nodes) {
const node_bounding = cachedBoundings[node.id];
if (!node_bounding || !LiteGraph.overlapBounding(group._bounding, node_bounding)) {
continue;
}
group._nodes.push(node);
}
}
/**
* Everything goes through getGroupsUnsorted, so we only get groups once. However, LiteGraph's
* `recomputeInsideNodes` is inefficient when calling multiple groups (it iterates over all nodes
* each time). So, we'll do our own dang thing, once.
*/
private getGroupsUnsorted(now: number) {
const graph = app.graph as TLGraph;
if (!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold) {
this.groupsUnsorted = [...graph._groups];
for (const group of this.groupsUnsorted) {
this.recomputeInsideNodesForGroup(group);
(group as any)._rgthreeHasAnyActiveNode = group._nodes.some(
(n) => n.mode === LiteGraph.ALWAYS,
);
}
this.msLastUnsorted = now;
}
return this.groupsUnsorted;
}
private getGroupsAlpha(now: number) {
const graph = app.graph as TLGraph;
if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) {
this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => {
return a.title.localeCompare(b.title);
});
this.msLastAlpha = now;
}
return this.groupsSortedAlpha;
}
private getGroupsPosition(now: number) {
const graph = app.graph as TLGraph;
if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) {
this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => {
// Sort by y, then x, clamped to 30.
const aY = Math.floor(a._pos[1] / 30);
const bY = Math.floor(b._pos[1] / 30);
if (aY == bY) {
const aX = Math.floor(a._pos[0] / 30);
const bX = Math.floor(b._pos[0] / 30);
return aX - bX;
}
return aY - bY;
});
this.msLastPosition = now;
}
return this.groupsSortedPosition;
}
getGroups(sort?: string) {
const now = +new Date();
if (sort === "alphanumeric") {
return this.getGroupsAlpha(now);
}
if (sort === "position") {
return this.getGroupsPosition(now);
}
return this.getGroupsUnsorted(now);
}
}
const SERVICE = new FastGroupsService();
/**
* Fast Muter implementation that looks for groups in the workflow and adds toggles to mute them.
*/
export class FastGroupsMuter extends RgthreeBaseNode {
static override type = NodeTypesString.FAST_GROUPS_MUTER;
static override title = NodeTypesString.FAST_GROUPS_MUTER;
override isVirtualNode = true;
static override exposedActions = ["Mute all", "Enable all", "Toggle all"];
readonly modeOn: number = LiteGraph.ALWAYS;
readonly modeOff: number = LiteGraph.NEVER;
private debouncerTempWidth: number = 0;
tempSize: Vector2 | null = null;
// We don't need to serizalize since we'll just be checking group data on startup anyway
override serialize_widgets = false;
protected helpActions = "must and unmute";
static "@matchColors" = { type: "string" };
static "@matchTitle" = { type: "string" };
static "@showNav" = { type: "boolean" };
static "@sort" = {
type: "combo",
values: ["position", "alphanumeric", "custom alphabet"],
};
static "@customSortAlphabet" = { type: "string" };
static "@toggleRestriction" = {
type: "combo",
values: ["default", "max one", "always one"],
};
constructor(title = FastGroupsMuter.title) {
super(title);
this.properties[PROPERTY_MATCH_COLORS] = "";
this.properties[PROPERTY_MATCH_TITLE] = "";
this.properties[PROPERTY_SHOW_NAV] = true;
this.properties[PROPERTY_SORT] = "position";
this.properties[PROPERTY_SORT_CUSTOM_ALPHA] = "";
this.properties[PROPERTY_RESTRICTION] = "default";
this.addOutput("OPT_CONNECTION", "*");
}
override onAdded(graph: TLGraph): void {
SERVICE.addFastGroupNode(this);
}
override onRemoved(): void {
SERVICE.removeFastGroupNode(this);
}
refreshWidgets() {
const canvas = app.canvas as TLGraphCanvas;
let sort = this.properties?.[PROPERTY_SORT] || "position";
let customAlphabet: string[] | null = null;
if (sort === "custom alphabet") {
const customAlphaStr = this.properties?.[PROPERTY_SORT_CUSTOM_ALPHA]?.replace(/\n/g, "");
if (customAlphaStr && customAlphaStr.trim()) {
customAlphabet = customAlphaStr.includes(",")
? customAlphaStr.toLocaleLowerCase().split(",")
: customAlphaStr.toLocaleLowerCase().trim().split("");
}
if (!customAlphabet?.length) {
sort = "alphanumeric";
customAlphabet = null;
}
}
const groups = [...SERVICE.getGroups(sort)];
// The service will return pre-sorted groups for alphanumeric and position. If this node has a
// custom sort, then we need to sort it manually.
if (customAlphabet?.length) {
groups.sort((a, b) => {
let aIndex = -1;
let bIndex = -1;
// Loop and find indexes. As we're finding multiple, a single for loop is more efficient.
for (const [index, alpha] of customAlphabet!.entries()) {
aIndex =
aIndex < 0 ? (a.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : aIndex;
bIndex =
bIndex < 0 ? (b.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : bIndex;
if (aIndex > -1 && bIndex > -1) {
break;
}
}
// Now compare.
if (aIndex > -1 && bIndex > -1) {
const ret = aIndex - bIndex;
if (ret === 0) {
return a.title.localeCompare(b.title);
}
return ret;
} else if (aIndex > -1) {
return -1;
} else if (bIndex > -1) {
return 1;
}
return a.title.localeCompare(b.title);
});
}
// See if we're filtering by colors, and match against the built-in keywords and actuial hex
// values.
let filterColors = (
(this.properties?.[PROPERTY_MATCH_COLORS] as string)?.split(",") || []
).filter((c) => c.trim());
if (filterColors.length) {
filterColors = filterColors.map((color) => {
color = color.trim().toLocaleLowerCase();
if (LGraphCanvas.node_colors[color]) {
color = LGraphCanvas.node_colors[color]!.groupcolor;
}
color = color.replace("#", "").toLocaleLowerCase();
if (color.length === 3) {
color = color.replace(/(.)(.)(.)/, "$1$1$2$2$3$3");
}
return `#${color}`;
});
}
// Go over the groups
let index = 0;
for (const group of groups) {
if (filterColors.length) {
let groupColor = group.color.replace("#", "").trim().toLocaleLowerCase();
if (groupColor.length === 3) {
groupColor = groupColor.replace(/(.)(.)(.)/, "$1$1$2$2$3$3");
}
groupColor = `#${groupColor}`;
if (!filterColors.includes(groupColor)) {
continue;
}
}
if (this.properties?.[PROPERTY_MATCH_TITLE]?.trim()) {
try {
if (!new RegExp(this.properties[PROPERTY_MATCH_TITLE], "i").exec(group.title)) {
continue;
}
} catch (e) {
console.error(e);
continue;
}
}
this.widgets = this.widgets || [];
const widgetName = `Enable ${group.title}`;
let widget = this.widgets.find((w) => w.name === widgetName);
if (!widget) {
// When we add a widget, litegraph is going to mess up the size, so we
// store it so we can retrieve it in computeSize. Hacky..
this.tempSize = [...this.size];
widget = this.addCustomWidget<IWidget<boolean>>({
name: "RGTHREE_TOGGLE_AND_NAV",
label: "",
value: false,
disabled: false,
options: { on: "yes", off: "no" },
draw: function (
ctx: CanvasRenderingContext2D,
node: LGraphNode,
width: number,
posY: number,
height: number,
) {
const lowQuality = (canvas.ds?.scale || 1) <= 0.5;
let margin = 15;
let outline_color = LiteGraph.WIDGET_OUTLINE_COLOR;
let background_color = LiteGraph.WIDGET_BGCOLOR;
let text_color = LiteGraph.WIDGET_TEXT_COLOR;
let secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
const showNav = node.properties?.[PROPERTY_SHOW_NAV] !== false;
// Draw background.
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
ctx.roundRect(margin, posY, width - margin * 2, height, lowQuality ? [0] : [height * 0.5]);
ctx.fill();
if (!lowQuality) {
ctx.stroke();
}
// Render from right to left, since the text on left will take available space.
// `currentX` markes the current x position moving backwards.
let currentX = width - margin;
// The nav arrow
if (!lowQuality && showNav) {
currentX -= 7; // Arrow space margin
const midY = posY + height * 0.5;
ctx.fillStyle = ctx.strokeStyle = "#89A";
ctx.lineJoin = "round";
ctx.lineCap = "round";
const arrow = new Path2D(`M${currentX} ${midY} l -7 6 v -3 h -7 v -6 h 7 v -3 z`);
ctx.fill(arrow);
ctx.stroke(arrow);
currentX -= 14;
currentX -= 7;
ctx.strokeStyle = outline_color;
ctx.stroke(new Path2D(`M ${currentX} ${posY} v ${height}`));
} else if (lowQuality && showNav) {
currentX -= 28;
}
// The toggle itself.
currentX -= 7;
ctx.fillStyle = this.value ? "#89A" : "#333";
ctx.beginPath();
const toggleRadius = height * 0.36;
ctx.arc(currentX - toggleRadius, posY + height * 0.5, toggleRadius, 0, Math.PI * 2);
ctx.fill();
currentX -= toggleRadius * 2;
if (!lowQuality) {
currentX -= 4;
ctx.textAlign = "right";
ctx.fillStyle = this.value ? text_color : secondary_text_color;
const label = this.label || this.name;
const toggleLabelOn = this.options.on || "true";
const toggleLabelOff = this.options.off || "false";
ctx.fillText(
this.value ? toggleLabelOn : toggleLabelOff,
currentX,
posY + height * 0.7,
);
currentX -= Math.max(
ctx.measureText(toggleLabelOn).width,
ctx.measureText(toggleLabelOff).width,
);
currentX -= 7;
ctx.textAlign = "left";
let maxLabelWidth = width - margin - 10 - (width - currentX);
if (label != null) {
ctx.fillText(fitString(ctx, label, maxLabelWidth), margin + 10, posY + height * 0.7);
}
}
},
serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number) {
return this.value;
},
mouse(event: PointerEvent, pos: Vector2, node: LGraphNode) {
if (event.type == "pointerdown") {
if (
node.properties?.[PROPERTY_SHOW_NAV] !== false &&
pos[0] >= node.size[0] - 15 - 28 - 1
) {
const canvas = app.canvas as TLGraphCanvas;
const lowQuality = (canvas.ds?.scale || 1) <= 0.5;
if (!lowQuality) {
// Clicked on right half with nav arrow, go to the group, center on group and set
// zoom to see it all.
canvas.centerOnNode(group);
const zoomCurrent = canvas.ds?.scale || 1;
const zoomX = canvas.canvas.width / group._size[0] - 0.02;
const zoomY = canvas.canvas.height / group._size[1] - 0.02;
canvas.setZoom(Math.min(zoomCurrent, zoomX, zoomY), [
canvas.canvas.width / 2,
canvas.canvas.height / 2,
]);
canvas.setDirty(true, true);
}
} else {
this.value = !this.value;
setTimeout(() => {
this.callback?.(this.value, app.canvas, node, pos, event);
}, 20);
}
}
return true;
},
});
(widget as any).doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => {
group.recomputeInsideNodes();
const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS);
let newValue = force != null ? force : !hasAnyActiveNodes;
if (skipOtherNodeCheck !== true) {
if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(' one')) {
for (const widget of this.widgets) {
(widget as any).doModeChange(false, true);
}
} else if (!newValue && this.properties?.[PROPERTY_RESTRICTION] === 'always one') {
newValue = this.widgets.every(w => !w.value || w === widget);
}
}
for (const node of group._nodes) {
node.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
}
(group as any)._rgthreeHasAnyActiveNode = newValue;
widget!.value = newValue;
app.graph.setDirtyCanvas(true, false);
};
widget.callback = () => {
(widget as any).doModeChange();
};
this.setSize(this.computeSize());
}
if (widget.name != widgetName) {
widget.name = widgetName;
this.setDirtyCanvas(true, false);
}
if (widget.value != (group as any)._rgthreeHasAnyActiveNode) {
widget.value = (group as any)._rgthreeHasAnyActiveNode;
this.setDirtyCanvas(true, false);
}
if (this.widgets[index] !== widget) {
const oldIndex = this.widgets.findIndex((w) => w === widget);
this.widgets.splice(index, 0, this.widgets.splice(oldIndex, 1)[0]!);
this.setDirtyCanvas(true, false);
}
index++;
}
// Everything should now be in order, so let's remove all remaining widgets.
while ((this.widgets || [])[index]) {
this.removeWidget(index++);
}
}
override computeSize(out?: Vector2) {
let size = super.computeSize(out);
if (this.tempSize) {
size[0] = Math.max(this.tempSize[0], size[0]);
size[1] = Math.max(this.tempSize[1], size[1]);
// We sometimes get repeated calls to compute size, so debounce before clearing.
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
this.debouncerTempWidth = setTimeout(() => {
this.tempSize = null;
}, 32);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
override async handleAction(action: string) {
if (action === "Mute all" || action === "Bypass all") {
const alwaysOne = this.properties?.[PROPERTY_RESTRICTION] === "always one";
for (const [index, widget] of this.widgets.entries()) {
(widget as any)?.doModeChange(alwaysOne && !index ? true : false, true);
}
} else if (action === "Enable all") {
const onlyOne = this.properties?.[PROPERTY_RESTRICTION].includes(" one");
for (const [index, widget] of this.widgets.entries()) {
(widget as any)?.doModeChange(onlyOne && index > 0 ? false : true, true);
}
} else if (action === "Toggle all") {
const onlyOne = this.properties?.[PROPERTY_RESTRICTION].includes(" one");
let foundOne = false;
for (const [index, widget] of this.widgets.entries()) {
// If you have only one, then we'll stop at the first.
let newValue: boolean = onlyOne && foundOne ? false : !widget.value;
foundOne = foundOne || newValue;
(widget as any)?.doModeChange(newValue, true);
}
// And if you have always one, then we'll flip the last
if (!foundOne && this.properties?.[PROPERTY_RESTRICTION] === "always one") {
(this.widgets[this.widgets.length - 1] as any)?.doModeChange(true, true);
}
}
}
override getHelp() {
return `
<p>The ${this.type!.replace(
"(rgthree)",
"",
)} is an input-less node that automatically collects all groups in your current
workflow and allows you to quickly ${
(this as FastGroupsMuter).helpActions
} all nodes within the group.</p>
<ul>
<li>
<p>
<strong>Properties.</strong> You can change the following properties (by right-clicking
on the node, and select "Properties" or "Properties Panel" from the menu):
</p>
<ul>
<li><p>
<code>${PROPERTY_MATCH_COLORS}</code> - Only add groups that match the provided
colors. Can be ComfyUI colors (red, pale_blue) or hex codes (#a4d399). Multiple can be
added, comma delimited.
</p></li>
<li><p>
<code>${PROPERTY_MATCH_TITLE}</code> - Filter the list of toggles by title match
(string match, or regular expression).
</p></li>
<li><p>
<code>${PROPERTY_SHOW_NAV}</code> - Add / remove a quick navigation arrow to take you
to the group. <i>(default: true)</i>
</p></li>
<li><p>
<code>${PROPERTY_SORT}</code> - Sort the toggles' order by "alphanumeric", graph
"position", or "custom alphabet". <i>(default: "position")</i>
</p></li>
<li>
<p>
<code>${PROPERTY_SORT_CUSTOM_ALPHA}</code> - When the
<code>${PROPERTY_SORT}</code> property is "custom alphabet" you can define the
alphabet to use here, which will match the <i>beginning</i> of each group name and
sort against it. If group titles do not match any custom alphabet entry, then they
will be put after groups that do, ordered alphanumerically.
</p>
<p>
This can be a list of single characters, like "zyxw..." or comma delimited strings
for more control, like "sdxl,pro,sd,n,p".
</p>
<p>
Note, when two group title match the same custom alphabet entry, the <i>normal
alphanumeric alphabet</i> breaks the tie. For instance, a custom alphabet of
"e,s,d" will order groups names like "SDXL, SEGS, Detailer" eventhough the custom
alphabet has an "e" before "d" (where one may expect "SE" to be before "SD").
</p>
<p>
To have "SEGS" appear before "SDXL" you can use longer strings. For instance, the
custom alphabet value of "se,s,f" would work here.
</p>
</li>
<li><p>
<code>${PROPERTY_RESTRICTION}</code> - Optionally, attempt to restrict the number of
widgets that can be enabled to a maximum of one, or always one.
</p>
<p><em><strong>Note:</strong> If using "max one" or "always one" then this is only
enforced when clicking a toggle on this node; if nodes within groups are changed
outside of the initial toggle click, then these restriction will not be enforced, and
could result in a state where more than one toggle is enabled. This could also happen
if nodes are overlapped with multiple groups.
</p></li>
</ul>
</li>
</ul>`;
}
static override setUp<T extends RgthreeBaseNode>(clazz: new (title?: string) => T) {
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
}
}
app.registerExtension({
name: "rgthree.FastGroupsMuter",
registerCustomNodes() {
FastGroupsMuter.setUp(FastGroupsMuter);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == FastGroupsMuter.title) {
(node as FastGroupsMuter).tempSize = [...node.size];
}
},
});
@@ -1,266 +0,0 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
// @ts-ignore
import { api } from "../../scripts/api.js";
import { RgthreeBaseServerNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import { ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
import { ContextMenuItem, IWidget, LGraphCanvas, LGraphNode, SerializedLGraphNode, Vector2 } from "typings/litegraph.js";
import { addConnectionLayoutSupport } from "./utils.js";
/**
* Compares two images in one canvas node.
*/
export class RgthreeImageComparer extends RgthreeBaseServerNode {
static override title = NodeTypesString.IMAGE_COMPARER;
static override type = NodeTypesString.IMAGE_COMPARER;
static comfyClass = NodeTypesString.IMAGE_COMPARER;
// These is what the core preview image node uses to show the context menu. May not be that helpful
// since it likely will always be "0" when a context menu is invoked without manually changing
// something.
imageIndex: number = 0;
imgs: InstanceType<typeof Image>[] = [];
override serialize_widgets = true;
protected isPointerDown = false;
protected isPointerOver = false;
protected pointerOverPos: Vector2 = [0,0];
private canvasWidget: IWidget|null = null;
static "@comparer_mode" = {
type: "combo",
values: ["Slide", "Click"],
};
constructor(title = RgthreeImageComparer.title) {
super(title);
this.properties['comparer_mode'] = 'Slide';
}
override onExecuted(output: any) {
super.onExecuted?.(output);
// Set the widget, it will then set the `node.imgs` property for us.
this.canvasWidget!.value = (output.images || []).map((d: any) => api.apiURL(
`/view?filename=${encodeURIComponent(d.filename)}&type=${d.type}&subfolder=${
d.subfolder
}${app.getPreviewFormatParam()}${app.getRandParam()}`,
));
}
private drawWidgetImage(ctx: CanvasRenderingContext2D, image: InstanceType<typeof Image>|undefined, y: number, cropX?: number) {
if (!image?.naturalWidth || !image?.naturalHeight) {
return;
}
let [nodeWidth, nodeHeight] = this.size;
const imageAspect = image.naturalWidth / image.naturalHeight;
let height = nodeHeight - y;
const widgetAspect = nodeWidth / height;
let targetWidth, targetHeight;
let offsetX = 0;
if (imageAspect > widgetAspect) {
targetWidth = nodeWidth;
targetHeight = nodeWidth / imageAspect;
} else {
targetHeight = height;
targetWidth = height * imageAspect;
offsetX = (nodeWidth - targetWidth) / 2;
}
const widthMultiplier = image.naturalWidth / targetWidth;
const sourceX = 0;
const sourceY = 0;
const sourceWidth = cropX != null ? (cropX - offsetX) * widthMultiplier : image.naturalWidth;
const sourceHeight = image.naturalHeight;
const destX = (nodeWidth - targetWidth) / 2;
const destY = y + (height - targetHeight) / 2;
const destWidth = cropX != null ? (cropX - offsetX) : targetWidth;
const destHeight = targetHeight;
ctx.drawImage(
image,
sourceX,
sourceY,
sourceWidth,
sourceHeight,
destX,
destY,
destWidth,
destHeight);
if (cropX != null
&& cropX >= (nodeWidth - targetWidth) / 2
&& cropX <= targetWidth + offsetX) {
let globalCompositeOperation = ctx.globalCompositeOperation;
ctx.globalCompositeOperation = "difference";
ctx.moveTo(cropX, destY);
ctx.lineTo(cropX, destY + destHeight)
ctx.strokeStyle = 'rgba(255,255,255, 1)';
ctx.stroke();
ctx.globalCompositeOperation = globalCompositeOperation;
}
}
override onNodeCreated() {
const node = this;
const widget: any = {
type: "RGTHREE_CANVAS_COMPARER" as any,
name: "CANVAS_COMPARER",
options: { serialize: false },
_value: [],
set value(v) {
this._value = v;
node.imgs = [];
if (v && v.length) {
for (let i = 0; i < 2; i++) {
let img = new Image();
img.src = v[i];
node.imgs.push(img);
}
}
},
get value() {
return this._value;
},
draw(ctx: CanvasRenderingContext2D, node: RgthreeImageComparer, width: number, y: number) {
let [nodeWidth, nodeHeight] = node.size;
if (node.properties?.['comparer_mode'] === 'Click') {
const image = node.isPointerDown ? node.imgs[1] : node.imgs[0];
node.drawWidgetImage(ctx, image, y);
} else {
node.drawWidgetImage(ctx, node.imgs[0], y);
if (node.isPointerOver) {
node.drawWidgetImage(ctx, node.imgs[1], y, node.pointerOverPos[0]);
}
}
},
computeSize(...args: any[]) {
return [64, 64];
},
// serializeValue(serializedNode: any, widgetIndex: any) {
// const value = node.imgs.map(i => i.src)
// this.value = value;
// return value;
// }
};
this.canvasWidget = this.addCustomWidget(widget as any);
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
}
/**
* Sets mouse as down or up based on param. If it's down, we also loop to check pointer is still
* down. This is because LiteGraph doesn't fire `onMouseUp` every time there's a mouse up, so we
* need to manually monitor `pointer_is_down` and, when it's no longer true, set mouse as up here.
*/
private setIsPointerDown(down: boolean = this.isPointerDown) {
const newIsDown = down && !!app.canvas.pointer_is_down;
if (this.isPointerDown !== newIsDown) {
this.isPointerDown = newIsDown;
this.setDirtyCanvas(true, false);
}
this.imageIndex = this.isPointerDown ? 1 : 0;
if (this.isPointerDown) {
requestAnimationFrame(() => {
this.setIsPointerDown();
});
}
}
override onMouseDown(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
super.onMouseDown?.(event, pos, graphCanvas);
this.setIsPointerDown(true);
}
override onMouseEnter(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
super.onMouseEnter?.(event, pos, graphCanvas);
this.setIsPointerDown(!!app.canvas.pointer_is_down);
this.isPointerOver = true;
}
override onMouseLeave(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
super.onMouseLeave?.(event, pos, graphCanvas);
this.setIsPointerDown(false);
this.isPointerOver = false;
}
override onMouseMove(event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas): void {
super.onMouseMove?.(event, pos, graphCanvas);
this.pointerOverPos = [...pos];
this.imageIndex = this.pointerOverPos[0] > (this.size[0] / 2) ? 1 : 0;
}
override getHelp(): string {
return `
<p>
The ${this.type!.replace("(rgthree)", "",)} node compares two images on top of each other.
</p>
<ul>
<li>
<p>
<strong>Notes</strong>
</p>
<ul>
<li><p>
The right-click menu may show image options (Open Image, Save Image, etc.) which will
correspond to the first image (image_a) if clicked on the left-half of the node, or
the second image if on the right half of the node.
</p></li>
</ul>
</li>
<li>
<p>
<strong>Inputs</strong>
</p>
<ul>
<li><p>
<code>image_a</code> <i>Required.</i> The first image to use to compare. If image_b is
not supplied and image_a is a batch, the comparer will use the first two images of
image_a.
</p></li>
<li><p>
<code>image_b</code> <i>Optional.</i> The second image to use to compare. Optional
only if image_a is a batch with two images.
</p></li>
</ul>
</li>
<li>
<p>
<strong>Properties.</strong> You can change the following properties (by right-clicking
on the node, and select "Properties" or "Properties Panel" from the menu):
</p>
<ul>
<li><p>
<code>comparer_mode</code> - Choose between "Slide" and "Click". Defaults to "Slide".
</p></li>
</ul>
</li>
</ul>`;
}
static override setUp(comfyClass: any) {
RgthreeBaseServerNode.registerForOverride(comfyClass, RgthreeImageComparer);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
addConnectionLayoutSupport(RgthreeImageComparer, app, [
["Left", "Right"],
["Right", "Left"],
]);
setTimeout(() => {
RgthreeImageComparer.category = comfyClass.category;
});
}
}
app.registerExtension({
name: "rgthree.ImageComparer",
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
if (nodeData.name === RgthreeImageComparer.type) {
RgthreeImageComparer.nodeType = nodeType;
RgthreeImageComparer.nodeData = nodeData;
RgthreeImageComparer.setUp(nodeType as any);
}
},
});
@@ -1,76 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
// @ts-ignore
import type {ComfyApp, ComfyObjectInfo,} from 'typings/comfy.js';
import type {Constructor} from 'typings/index.js'
import { RgthreeBaseNode } from "./base_node.js";
import { applyMixins } from "./utils.js";
import { IComboWidget, IWidget, LGraph, LGraphCanvas, LGraphNode, SerializedLGraphNode, Vector2 } from "typings/litegraph.js";
class ImageInsetCrop extends RgthreeBaseNode {
static override type = '__OVERRIDE_ME__';
static comfyClass = '__OVERRIDE_ME__';
static override exposedActions = ['Reset Crop'];
static maxResolution = 8192;
override onAdded(graph: LGraph): void {
const measurementWidget = this.widgets[0]!;
let callback = measurementWidget.callback;
measurementWidget.callback = (...args) => {
this.setWidgetStep()
callback && callback.apply(measurementWidget, [...args]);
}
this.setWidgetStep();
}
override configure(info: SerializedLGraphNode<LGraphNode>): void {
super.configure(info);
this.setWidgetStep();
}
private setWidgetStep() {
const measurementWidget = this.widgets[0]!;
for (let i = 1; i <= 4; i++) {
if (measurementWidget.value === 'Pixels') {
this.widgets[i]!.options.step = 80;
this.widgets[i]!.options.max = ImageInsetCrop.maxResolution;
} else {
this.widgets[i]!.options.step = 10;
this.widgets[i]!.options.max = 99;
}
}
}
override async handleAction(action: string): Promise<void> {
if (action === 'Reset Crop') {
for (const widget of this.widgets) {
if (['left', 'right', 'top', 'bottom'].includes(widget.name!)) {
widget.value = 0;
}
}
}
}
static override setUp<T extends RgthreeBaseNode>(clazz: any) {
ImageInsetCrop.title = clazz.title;
ImageInsetCrop.comfyClass = clazz.comfyClass;
setTimeout(() => {
ImageInsetCrop.category = clazz.category;
});
applyMixins(clazz, [RgthreeBaseNode, ImageInsetCrop]);
}
}
app.registerExtension({
name: "rgthree.ImageInsetCrop",
async beforeRegisterNodeDef(nodeType: Constructor<LGraphNode>, nodeData: ComfyObjectInfo, _app: ComfyApp) {
if (nodeData.name === "Image Inset Crop (rgthree)") {
ImageInsetCrop.setUp(nodeType);
}
},
});
@@ -1,100 +0,0 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import type { ContextMenuItem, LiteGraph as TLiteGraph, ContextMenu, } from "typings/litegraph.js";
import {rgthree} from "./rgthree.js"
import { SERVICE as CONFIG_SERVICE } from "./config_service.js";
declare const LiteGraph: typeof TLiteGraph;
/**
* Handles a large, flat list of string values given ContextMenu and breaks it up into subfolder, if
* they exist. This is experimental and initially built to work for CheckpointLoaderSimple.
*/
app.registerExtension({
name: "rgthree.ContextMenuAutoNest",
async setup() {
const logger = rgthree.newLogSession('[ContextMenuAutoNest]');
const existingContextMenu = LiteGraph.ContextMenu;
// @ts-ignore: TypeScript doesn't like this override.
LiteGraph.ContextMenu = function (values: ContextMenuItem[], options: any) {
const threshold = CONFIG_SERVICE.getConfigValue("features.menu_auto_nest.threshold", 20);
const enabled = CONFIG_SERVICE.getConfigValue("features.menu_auto_nest.subdirs", false);
// If we're not enabled, or are incompatible, then just call out safely.
if (!enabled
|| (values?.length || 0) <= threshold
|| !options?.callback
|| values.some(i => typeof i !== 'string')) {
if (enabled) {
const [n, v] = logger.infoParts('Skipping context menu auto nesting for incompatible menu.');
console[n]?.(...v);
}
console.log('just pass through.')
return existingContextMenu.apply(this as any, [...arguments] as any);
}
// For now, only allow string values.
const compatValues = values as unknown as string[];
const originalValues = [...compatValues];
const folders: {[key:string]: string[]} = {};
const specialOps: string[] = [];
const folderless: string[] = [];
for (const value of compatValues) {
const splitBy = value.indexOf('/') > -1 ? '/' : '\\';
const valueSplit = value.split(splitBy);
if (valueSplit.length > 1) {
const key = valueSplit.shift()!;
folders[key] = folders[key] || [];
folders[key]!.push(valueSplit.join(splitBy));
} else if (value === 'CHOOSE' || value.startsWith('DISABLE ')) {
specialOps.push(value);
} else {
folderless.push(value);
}
}
const foldersCount = Object.values(folders).length;
if (foldersCount > 0) {
const oldcallback = options.callback;
options.callback = null;
const newCallback = (item: ContextMenuItem, options: any) => {
oldcallback(originalValues.find(i => i.endsWith(item!.content), options));
};
const [n, v] = logger.infoParts(`Nested folders found (${foldersCount}).`);
console[n]?.(...v);
const newValues: ContextMenuItem[] = [];
for (const [folderName, folder] of Object.entries(folders)) {
newValues.push({
content: folderName,
has_submenu: true,
callback: () => { /* no-op */},
submenu: {
options: folder.map(f => ({
content: f,
callback: newCallback
})),
}
});
}
values = ([] as ContextMenuItem[]).concat(specialOps.map(f => ({
content: f,
callback: newCallback
})), newValues, folderless.map(f => ({
content: f,
callback: newCallback
})));
}
return existingContextMenu.call(this as any, values, options);
};
LiteGraph.ContextMenu.prototype = existingContextMenu.prototype;
},
});
@@ -1,74 +0,0 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import type { LGraphCanvas, ContextMenuItem } from "typings/litegraph.js";
import type { ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
const clipboardSupportedPromise = new Promise<boolean>(async (resolve) => {
try {
// MDN says to check this, but it doesn't work in Mozilla... however, in secure contexts
// (localhost included), it's given by default if the user has it flagged.. so we should be
// able to check in the latter ClipboardItem too.
const result = await navigator.permissions.query({ name: "clipboard-write" } as any);
resolve(result.state === "granted");
return;
} catch (e) {
try {
if (!navigator.clipboard.write) {
throw new Error();
}
new ClipboardItem({ "image/png": new Blob([], { type: "image/png" }) });
resolve(true);
return;
} catch (e) {
resolve(false);
}
}
});
/**
* Adds a "Copy Image" to images in similar fashion to the "native" Open Image and Save Image
* options.
*/
app.registerExtension({
name: "rgthree.CopyImageToClipboard",
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
if (nodeData.name.toLowerCase().includes("image")) {
if (await clipboardSupportedPromise) {
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (
canvas: LGraphCanvas,
options: ContextMenuItem[],
) {
getExtraMenuOptions ? getExtraMenuOptions.apply(this, arguments) : undefined;
// If we already have a copy image somehow, then let's skip ours.
if (this.imgs?.length) {
let img =
this.imgs[this.imageIndex || 0] || this.imgs[this.overIndex || 0] || this.imgs[0];
const foundIdx = options.findIndex((option) => option?.content.includes("Copy Image"));
if (img && foundIdx === -1) {
const menuItem: ContextMenuItem = {
content: "Copy Image (rgthree)",
callback: () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
canvas.toBlob((blob) => {
navigator.clipboard.write([new ClipboardItem({ "image/png": blob! })]);
});
},
};
let idx = options.findIndex((option) => option?.content.includes("Open Image")) + 1;
if (idx != null) {
options.splice(idx, 0, menuItem);
} else {
options.unshift(menuItem);
}
}
}
};
}
}
},
});
@@ -1,80 +0,0 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {
LGraphCanvas as TLGraphCanvas,
ContextMenuItem,
LGraphNode,
LiteGraph as TLiteGraph,
} from "typings/litegraph.js";
import type { ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
import { rgthree } from "./rgthree.js";
import { SERVICE as CONFIG_SERVICE } from "./config_service.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphCanvas: typeof TLGraphCanvas;
function getOutputNodesFromSelected(canvas: TLGraphCanvas) {
return (
(canvas.selected_nodes &&
Object.values(canvas.selected_nodes).filter((n) => {
return (
n.mode != LiteGraph.NEVER &&
((n.constructor as any).nodeData as ComfyObjectInfo)?.output_node
);
})) ||
[]
);
}
function showQueueNodesMenuIfOutputNodesAreSelected(
existingOptions: ContextMenuItem[],
node?: LGraphNode,
) {
if (CONFIG_SERVICE.getConfigValue("features.menu_queue_selected_nodes") != false) {
const canvas = app.canvas as TLGraphCanvas;
const outputNodes = getOutputNodesFromSelected(canvas);
const menuItem = {
content: `Queue Selected Output Nodes (rgthree) &nbsp;`,
className: "rgthree-contextmenu-item",
callback: () => {
rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
},
disabled: !outputNodes.length,
};
let idx = existingOptions.findIndex((o) => o?.content === "Outputs") + 1;
idx = idx || existingOptions.findIndex((o) => o?.content === "Align") + 1;
idx = idx || 3;
existingOptions.splice(idx, 0, menuItem);
}
return existingOptions;
}
/**
* Adds a "Queue Node" menu item to all output nodes, working with `rgthree.queueOutputNode` to
* execute only a single node's path.
*/
app.registerExtension({
name: "rgthree.QueueNode",
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (
canvas: TLGraphCanvas,
options: ContextMenuItem[],
) {
getExtraMenuOptions ? getExtraMenuOptions.apply(this, arguments) : undefined;
showQueueNodesMenuIfOutputNodesAreSelected(options, this);
};
},
async setup() {
console.log("rgthree-menu-setup");
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function (...args: any[]) {
const options = getCanvasMenuOptions.apply(this, [...args] as any);
showQueueNodesMenuIfOutputNodesAreSelected(options);
return options;
};
},
});
@@ -1,50 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
import { NodeTypesString } from "./constants.js";
import type {LGraphNode} from 'typings/litegraph.js';
const MODE_MUTE = 2;
const MODE_ALWAYS = 0;
class MuterNode extends BaseNodeModeChanger {
static override exposedActions = ["Mute all", "Enable all", "Toggle all"];
static override type = NodeTypesString.FAST_MUTER;
static override title = NodeTypesString.FAST_MUTER;
override readonly modeOn = MODE_ALWAYS;
override readonly modeOff = MODE_MUTE;
constructor(title = MuterNode.title) {
super(title);
}
override async handleAction(action: string) {
if (action === "Mute all") {
for (const widget of this.widgets) {
this.forceWidgetOff(widget, true);
}
} else if (action === "Enable all") {
for (const widget of this.widgets) {
this.forceWidgetOn(widget, true);
}
} else if (action === "Toggle all") {
for (const widget of this.widgets) {
this.forceWidgetToggle(widget, true);
}
}
}
}
app.registerExtension({
name: "rgthree.Muter",
registerCustomNodes() {
MuterNode.setUp(MuterNode);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == MuterNode.title) {
(node as any)._tempWidth = node.size[0];
}
}
});
@@ -1,138 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {LLink, LGraph, ContextMenuItem, LGraphCanvas, SerializedLGraphNode, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IContextMenuOptions, ContextMenu} from 'typings/litegraph.js';
import { addConnectionLayoutSupport } from "./utils.js";
import { wait } from "rgthree/common/shared_utils.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
// @ts-ignore
import { BaseCollectorNode } from './base_node_collector.js';
import { NodeTypesString } from "./constants.js";
declare const LiteGraph: typeof TLiteGraph;
/**
* The Collector Node. Takes any number of inputs as connections for nodes and collects them into
* one outputs. The next node will decide what to do with them.
*
* Currently only works with the Fast Muter, Fast Bypasser, and Fast Actions Button.
*/
class CollectorNode extends BaseCollectorNode {
static override type = NodeTypesString.NODE_COLLECTOR;
static override title = NodeTypesString.NODE_COLLECTOR;
constructor(title = CollectorNode.title) {
super(title);
}
}
/** Legacy "Combiner" */
class CombinerNode extends CollectorNode {
static legacyType = "Node Combiner (rgthree)";
static override title = "‼️ Node Combiner [DEPRECATED]";
constructor(title = CombinerNode.title) {
super(title);
const note = ComfyWidgets["STRING"](this, "last_seed", ["STRING", { multiline: true }], app).widget;
note.inputEl.value = 'The Node Combiner has been renamed to Node Collector. You can right-click and select "Update to Node Collector" to attempt to automatically update.';
note.inputEl.readOnly = true;
note.inputEl.style.backgroundColor = '#332222';
note.inputEl.style.fontWeight = 'bold';
note.inputEl.style.fontStyle = 'italic';
note.inputEl.style.opacity = '0.8';
this.getExtraMenuOptions = (_: LGraphCanvas, options: ContextMenuItem[]) => {
options.splice(options.length - 1, 0,
{
content: "‼️ Update to Node Collector",
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
updateCombinerToCollector(this);
}
}
);
}
}
override configure(info: SerializedLGraphNode) {
super.configure(info);
if (this.title != CombinerNode.title && !this.title.startsWith('‼️')) {
this.title = '‼️ ' + this.title;
}
}
}
/**
* Updates a Node Combiner to a Node Collector.
*/
async function updateCombinerToCollector(node: TLGraphNode) {
if (node.type === CombinerNode.legacyType) {
// Create a new CollectorNode.
const newNode = new CollectorNode();
if (node.title != CombinerNode.title) {
newNode.title = node.title.replace('‼️ ', '');
}
// Port the position, size, and properties from the old node.
newNode.pos = [...node.pos];
newNode.size = [...node.size];
newNode.properties = {...node.properties};
// We now collect the links data, inputs and outputs, of the old node since these will be
// lost when we remove it.
const links: any[] = [];
for (const [index, output] of node.outputs.entries()) {
for (const linkId of (output.links || [])) {
const link: LLink = (app.graph as LGraph).links[linkId]!;
if (!link) continue;
const targetNode = app.graph.getNodeById(link.target_id);
links.push({node: newNode, slot: index, targetNode, targetSlot: link.target_slot});
}
}
for (const [index, input] of node.inputs.entries()) {
const linkId = input.link;
if (linkId) {
const link: LLink = (app.graph as LGraph).links[linkId]!;
const originNode = app.graph.getNodeById(link.origin_id);
links.push({node: originNode, slot: link.origin_slot, targetNode: newNode, targetSlot: index});
}
}
// Add the new node, remove the old node.
app.graph.add(newNode);
await wait();
// Now go through and connect the other nodes up as they were.
for (const link of links) {
link.node.connect(link.slot, link.targetNode, link.targetSlot);
}
await wait();
app.graph.remove(node);
}
}
app.registerExtension({
name: "rgthree.NodeCollector",
registerCustomNodes() {
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(CollectorNode, app, [['Left','Right'],['Right','Left']]);
LiteGraph.registerNodeType(CollectorNode.title, CollectorNode);
CollectorNode.category = CollectorNode._category;
},
});
app.registerExtension({
name: "rgthree.NodeCombiner",
registerCustomNodes() {
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(CombinerNode, app, [['Left','Right'],['Right','Left']]);
LiteGraph.registerNodeType(CombinerNode.legacyType, CombinerNode);
CombinerNode.category = CombinerNode._category;
},
});
@@ -1,251 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {
INodeInputSlot,
INodeOutputSlot,
IWidget,
LGraphCanvas,
LGraphNode,
LLink,
LiteGraph as TLiteGraph,
Vector2,
} from "typings/litegraph.js";
import type { NodeMode } from "typings/comfy.js";
import {
PassThroughFollowing,
addConnectionLayoutSupport,
getConnectedInputNodesAndFilterPassThroughs,
getConnectedOutputNodesAndFilterPassThroughs,
} from "./utils.js";
import { wait } from "rgthree/common/shared_utils.js";
import { BaseCollectorNode } from "./base_node_collector.js";
import { NodeTypesString, stripRgthree } from "./constants.js";
import { fitString } from "./utils_canvas.js";
declare const LiteGraph: typeof TLiteGraph;
const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
const MODE_IGNORE = -99; // MADE THIS UP.
const MODE_TO_OPTION = new Map([
[MODE_ALWAYS, 'ACTIVE'],
[MODE_MUTE, 'MUTE'],
[MODE_BYPASS, 'BYPASS'],
[MODE_IGNORE, 'IGNORE'],
]);
const OPTION_TO_MODE = new Map([
['ACTIVE', MODE_ALWAYS],
['MUTE', MODE_MUTE],
['BYPASS', MODE_BYPASS],
['IGNORE',MODE_IGNORE],
]);
const MODE_TO_PROPERTY = new Map([
[MODE_MUTE, 'on_muted_inputs'],
[MODE_BYPASS, 'on_bypassed_inputs'],
[MODE_ALWAYS, 'on_any_active_inputs'],
]);
/**
* Like a BaseCollectorNode, this relay node connects to a Repeater and changes it mode (so it can go
* on to mute it's connections).
*/
class NodeModeRelay extends BaseCollectorNode {
override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL;
static override type = NodeTypesString.NODE_MODE_RELAY;
static override title = NodeTypesString.NODE_MODE_RELAY;
static "@on_muted_inputs" = {
type: "combo",
values: ["MUTE", "ACTIVE", "BYPASS", "NOTHING"],
};
static "@on_bypassed_inputs" = {
type: "combo",
values: ["BYPASS", "ACTIVE", "MUTE", "NOTHING"],
};
static "@on_any_active_inputs" = {
type: "combo",
values: ["BYPASS", "ACTIVE", "MUTE", "NOTHING"],
};
constructor(title?: string) {
super(title);
setTimeout(() => {
this.stabilize();
}, 500);
// We want to customize the output, so remove the one BaseCollectorNode adds, and add out own.
this.removeOutput(0);
this.addOutput("REPEATER", "_NODE_REPEATER_", {
color_on: "#Fc0",
color_off: "#a80",
shape: LiteGraph.ARROW_SHAPE,
});
this.properties['on_muted_inputs'] = 'MUTE';
this.properties['on_bypassed_inputs'] = 'BYPASS';
this.properties['on_any_active_inputs'] = 'ACTIVE';
}
override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: LGraphCanvas): void {
if (this.flags?.collapsed) {
return;
}
if (
this.properties['on_muted_inputs'] !== 'MUTE' ||
this.properties['on_bypassed_inputs'] !== 'BYPASS' ||
this.properties['on_any_active_inputs'] != 'ACTIVE') {
let margin = 15;
ctx.textAlign = "left";
let label = `*(MUTE > ${this.properties['on_muted_inputs']}, `;
label += `BYPASS > ${this.properties['on_bypassed_inputs']}, `;
label += `ACTIVE > ${this.properties['on_any_active_inputs']})`;
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
const oldFont = ctx.font;
ctx.font = "italic " + (LiteGraph.NODE_SUBTEXT_SIZE - 2) + "px Arial";
ctx.fillText(fitString(ctx, label, this.size[0] - 20), 15, this.size[1] - 6);
ctx.font = oldFont;
}
}
override computeSize(out: Vector2) {
let size = super.computeSize(out);
if (
this.properties['on_muted_inputs'] !== 'MUTE' ||
this.properties['on_bypassed_inputs'] !== 'BYPASS' ||
this.properties['on_any_active_inputs'] != 'ACTIVE') {
size[1] += 17;
}
return size;
}
override onConnectOutput(
outputIndex: number,
inputType: string | -1,
inputSlot: INodeInputSlot,
inputNode: LGraphNode,
inputIndex: number,
): boolean {
let canConnect = super.onConnectOutput?.(
outputIndex,
inputType,
inputSlot,
inputNode,
inputIndex,
);
let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0] ?? inputNode;
return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER;
}
override onConnectionsChange(
type: number,
slotIndex: number,
isConnected: boolean,
link_info: LLink,
ioSlot: INodeOutputSlot | INodeInputSlot,
): void {
super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot);
setTimeout(() => {
this.stabilize();
}, 500);
}
stabilize() {
// If we aren't connected to a repeater, then theres no sense in checking. And if we are, but
// have no inputs, then we're also not ready.
if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) {
return;
}
const inputNodes = getConnectedInputNodesAndFilterPassThroughs(
this,
this,
-1,
this.inputsPassThroughFollowing,
);
let mode: NodeMode | -99 | null = undefined;
for (const inputNode of inputNodes) {
// If we haven't set our mode to be, then let's set it. Otherwise, mode will stick if it
// remains constant, otherwise, if we hit an ALWAYS, then we'll unmute all repeaters and
// if not then we won't do anything.
if (mode === undefined) {
mode = inputNode.mode;
} else if (mode === inputNode.mode && MODE_REPEATS.includes(mode)) {
continue;
} else if (inputNode.mode === MODE_ALWAYS || mode === MODE_ALWAYS) {
mode = MODE_ALWAYS;
} else {
mode = null;
}
}
if (mode != null) {
const propertyVal = this.properties?.[MODE_TO_PROPERTY.get(mode)||''];
const newMode = OPTION_TO_MODE.get(propertyVal)
mode = (newMode !== null ? newMode : mode) as NodeMode | -99;
if (mode !== null && mode !== MODE_IGNORE) {
if (this.outputs?.length) {
const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
for (const outputNode of outputNodes) {
outputNode.mode = mode;
wait(16).then(() => {
outputNode.setDirtyCanvas(true, true);
});
}
}
}
}
setTimeout(() => {
this.stabilize();
}, 500);
}
override getHelp() {
return `
<p>
This node will relay its input nodes' modes (Mute, Bypass, or Active) to a connected
${stripRgthree(NodeTypesString.NODE_MODE_REPEATER)} (which would then repeat that mode
change to all of its inputs).
</p>
<ul>
<li><p>
When all connected input nodes are muted, the relay will set a connected repeater to
mute (by default).
</p></li>
<li><p>
When all connected input nodes are bypassed, the relay will set a connected repeater to
bypass (by default).
</p></li>
<li><p>
When any connected input nodes are active, the relay will set a connected repeater to
active (by default).
</p></li>
</ul>
<p>
Note, you can change which signals get sent on the above in the <code>Properties</code>.
For instance, you could configure an inverse relay which will send a MUTE when any of its
inputs are active (instead of sending an ACTIVE signal), and send an ACTIVE signal when all
of its inputs are muted (instead of sending a MUTE signal), etc.
</p>
`;
}
}
app.registerExtension({
name: "rgthree.NodeModeRepeaterHelper",
registerCustomNodes() {
addConnectionLayoutSupport(NodeModeRelay, app, [
["Left", "Right"],
["Right", "Left"],
]);
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
NodeModeRelay.category = NodeModeRelay._category;
},
});
@@ -1,214 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
// @ts-ignore
import { BaseCollectorNode } from "./base_node_collector.js";
import { NodeTypesString, stripRgthree } from "./constants.js";
import type {
INodeInputSlot,
INodeOutputSlot,
LGraphGroup,
LGraphNode,
LLink,
LiteGraph as TLiteGraph,
} from "typings/litegraph.js";
import {
PassThroughFollowing,
addConnectionLayoutSupport,
getConnectedInputNodesAndFilterPassThroughs,
getConnectedOutputNodesAndFilterPassThroughs,
} from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
class NodeModeRepeater extends BaseCollectorNode {
override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL;
static override type = NodeTypesString.NODE_MODE_REPEATER;
static override title = NodeTypesString.NODE_MODE_REPEATER;
private hasRelayInput = false;
private hasTogglerOutput = false;
constructor(title?: string) {
super(title);
this.removeOutput(0);
this.addOutput("OPT_CONNECTION", "*", {
color_on: "#Fc0",
color_off: "#a80",
});
}
override onConnectOutput(
outputIndex: number,
inputType: string | -1,
inputSlot: INodeInputSlot,
inputNode: LGraphNode,
inputIndex: number,
): boolean {
// We can only connect to a a FAST_MUTER or FAST_BYPASSER if we aren't connectged to a relay, since the relay wins.
let canConnect = !this.hasRelayInput;
canConnect = canConnect && super.onConnectOutput(
outputIndex,
inputType,
inputSlot,
inputNode,
inputIndex,
);
// Output can only connect to a FAST MUTER, FAST BYPASSER, NODE_COLLECTOR OR ACTION BUTTON
let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0] || inputNode;
return (
canConnect &&
[
NodeTypesString.FAST_MUTER,
NodeTypesString.FAST_BYPASSER,
NodeTypesString.NODE_COLLECTOR,
NodeTypesString.FAST_ACTIONS_BUTTON,
NodeTypesString.REROUTE,
NodeTypesString.RANDOM_UNMUTER,
].includes(nextNode.type || "")
);
}
override onConnectInput(
inputIndex: number,
outputType: string | -1,
outputSlot: INodeOutputSlot,
outputNode: LGraphNode,
outputIndex: number,
): boolean {
// We can only connect to a a FAST_MUTER or FAST_BYPASSER if we aren't connectged to a relay, since the relay wins.
let canConnect = super.onConnectInput?.(
inputIndex,
outputType,
outputSlot,
outputNode,
outputIndex,
);
// Output can only connect to a FAST MUTER or FAST BYPASSER
let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, outputNode)[0] || outputNode;
const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY;
return canConnect && (!isNextNodeRelay || !this.hasTogglerOutput);
}
override onConnectionsChange(
type: number,
slotIndex: number,
isConnected: boolean,
linkInfo: LLink,
ioSlot: INodeOutputSlot | INodeInputSlot,
): void {
super.onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot);
let hasTogglerOutput = false;
let hasRelayInput = false;
const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
for (const outputNode of outputNodes) {
if (
outputNode?.type === NodeTypesString.FAST_MUTER ||
outputNode?.type === NodeTypesString.FAST_BYPASSER
) {
hasTogglerOutput = true;
break;
}
}
const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this);
for (const [index, inputNode] of inputNodes.entries()) {
if (inputNode?.type === NodeTypesString.NODE_MODE_RELAY) {
// We can't be connected to a relay if we're connected to a toggler. Something has gone wrong.
if (hasTogglerOutput) {
console.log(
`Can't be connected to a Relay if also output to a toggler.`,
);
this.disconnectInput(index);
} else {
hasRelayInput = true;
if (this.inputs[index]) {
this.inputs[index]!.color_on = "#FC0";
this.inputs[index]!.color_off = "#a80";
}
}
} else {
inputNode.mode = this.mode;
}
}
this.hasTogglerOutput = hasTogglerOutput;
this.hasRelayInput = hasRelayInput;
// If we have a relay input, then we should remove the toggler output, or add it if not.
if (this.hasRelayInput) {
if (this.outputs[0]) {
this.disconnectOutput(0);
this.removeOutput(0);
}
} else if (!this.outputs[0]) {
this.addOutput("OPT_CONNECTION", "*", {
color_on: "#Fc0",
color_off: "#a80",
});
}
}
/** When a mode change, we want all connected nodes to match except for connected relays. */
override onModeChange() {
super.onModeChange();
const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this).filter(node => node.type !== NodeTypesString.NODE_MODE_RELAY);
if (linkedNodes.length) {
for (const node of linkedNodes) {
if (node.type !== NodeTypesString.NODE_MODE_RELAY) {
node.mode = this.mode;
}
}
} else if (app.graph._groups?.length) {
// No linked nodes.. check if we're in a group.
for (const group of app.graph._groups as LGraphGroup[]) {
group.recomputeInsideNodes();
if (group._nodes?.includes(this)) {
for (const node of group._nodes) {
node.mode = this.mode;
}
}
}
}
}
override getHelp(): string {
return `
<p>
When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all
connected input nodes, or, if there are no connected nodes AND it is overlapping a group,
"repeat" it's mode to all nodes in that group.
</p>
<ul>
<li><p>
Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}
or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly
mute/bypass all its connected nodes.
</p></li>
<li><p>
Optionally, connect a ${stripRgthree(NodeTypesString.NODE_MODE_RELAY)} to this nodes
inputs to have it automatically toggle its mode. If connected, this will always take
precedence (and disconnect any connected fast togglers).
</p></li>
</ul>
`;
}
}
app.registerExtension({
name: "rgthree.NodeModeRepeater",
registerCustomNodes() {
addConnectionLayoutSupport(NodeModeRepeater, app, [
["Left", "Right"],
["Right", "Left"],
]);
LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater);
NodeModeRepeater.category = NodeModeRepeater._category;
},
});
@@ -1,48 +0,0 @@
// / <reference path='../node_modules/litegraph.js/src/litegraph.d.ts' />
// @ts-ignore
import {app} from '../../scripts/app.js';
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from 'typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo, ComfyGraphNode} from 'typings/comfy.js'
import {addConnectionLayoutSupport} from './utils.js';
import { PowerPrompt } from './base_power_prompt.js';
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
let nodeData: ComfyObjectInfo | null = null;
app.registerExtension({
name: 'rgthree.PowerPrompt',
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, passedNodeData: ComfyObjectInfo, _app: ComfyApp) {
if (passedNodeData.name.includes('Power Prompt') && passedNodeData.name.includes('rgthree')) {
nodeData = passedNodeData;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
(this as any).powerPrompt = new PowerPrompt(this as ComfyGraphNode, passedNodeData);
}
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
}
},
async loadedGraphNode(node: TLGraphNode) {
if (node.type === 'Power Prompt (rgthree)') {
setTimeout(() => {
// If the first output is STRING, then it's the text output from the initial launch.
// Let's port it to the new
if (node.outputs[0]!.type === 'STRING') {
if (node.outputs[0]!.links) {
node.outputs[3]!.links = node.outputs[3]!.links || [];
for (const link of node.outputs[0]!.links) {
node.outputs[3]!.links.push(link);
app.graph.links[link].origin_slot = 3;
}
node.outputs[0]!.links = null;
}
node.outputs[0]!.type = nodeData!.output![0] as string;
node.outputs[0]!.name = nodeData!.output_name![0] || node.outputs[0]!.type as string;
node.outputs[0]!.color_on = undefined;
node.outputs[0]!.color_off = undefined;
}
}, 50)
}
}
});
@@ -1,124 +0,0 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { RgthreeBaseNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import { rgthree } from "./rgthree.js";
import type {LGraphNode} from 'typings/litegraph.js';
import { getConnectedInputNodesAndFilterPassThroughs } from "./utils.js";
const MODE_MUTE = 2;
const MODE_ALWAYS = 0;
class RandomUnmuterNode extends BaseAnyInputConnectedNode {
static override exposedActions = ['Mute all', 'Enable all'];
static override type = NodeTypesString.RANDOM_UNMUTER;
static override title = RandomUnmuterNode.type;
readonly modeOn = MODE_ALWAYS;
readonly modeOff = MODE_MUTE;
tempEnabledNode: LGraphNode | null = null;
processingQueue: boolean = false;
onQueueBound = this.onQueue.bind(this);
onQueueEndBound = this.onQueueEnd.bind(this);
onGraphtoPromptBound = this.onGraphtoPrompt.bind(this);
onGraphtoPromptEndBound = this.onGraphtoPromptEnd.bind(this);
constructor(title = RandomUnmuterNode.title) {
super(title);
rgthree.addEventListener('queue', this.onQueueBound);
rgthree.addEventListener('queue-end', this.onQueueEndBound);
rgthree.addEventListener('graph-to-prompt', this.onGraphtoPromptBound);
rgthree.addEventListener('graph-to-prompt-end', this.onGraphtoPromptEndBound);
}
override onRemoved() {
rgthree.removeEventListener('queue', this.onQueueBound);
rgthree.removeEventListener('queue-end', this.onQueueEndBound);
rgthree.removeEventListener('graph-to-prompt', this.onGraphtoPromptBound);
rgthree.removeEventListener('graph-to-prompt-end', this.onGraphtoPromptEndBound);
}
onQueue(event: Event) {
this.processingQueue = true;
}
onQueueEnd(event: Event) {
this.processingQueue = false;
}
onGraphtoPrompt(event: Event) {
if (!this.processingQueue) {
return;
}
this.tempEnabledNode = null;
// Check that all are muted and, if so, choose one to unmute.
const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this);
let allMuted = true;
if (linkedNodes.length) {
for (const node of linkedNodes) {
if (node.mode !== this.modeOff) {
allMuted = false;
break;
}
}
if (allMuted) {
this.tempEnabledNode = linkedNodes[Math.floor(Math.random() * linkedNodes.length)] || null;
if (this.tempEnabledNode) {
this.tempEnabledNode.mode = this.modeOn;
}
}
}
}
onGraphtoPromptEnd(event: Event) {
if (this.tempEnabledNode) {
this.tempEnabledNode.mode = this.modeOff;
this.tempEnabledNode = null;
}
}
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]): void {
// No-op, no widgets.
}
override getHelp(): string {
return `
<p>
Use this node to unmute on of its inputs randomly when the graph is queued (and, immediately
mute it back).
</p>
<ul>
<li><p>
NOTE: All input nodes MUST be muted to start; if not this node will not randomly unmute
another. (This is powerful, as the generated image can be dragged in and the chosen input
will already by unmuted and work w/o any further action.)
</p></li>
<li><p>
TIP: Connect a Repeater's output to this nodes input and place that Repeater on a group
without any other inputs, and it will mute/unmute the entire group.
</p></li>
</ul>
`;
}
static override setUp<T extends RgthreeBaseNode>(clazz: new(title?: string) => T) {
BaseAnyInputConnectedNode.setUp(clazz);
}
}
app.registerExtension({
name: "rgthree.RandomUnmuter",
registerCustomNodes() {
RandomUnmuterNode.setUp(RandomUnmuterNode);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == RandomUnmuterNode.title) {
(node as any)._tempWidth = node.size[0];
}
}
});

Some files were not shown because too many files have changed in this diff Show More