Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f940ddfbb | ||
|
|
47d0836ca7 | ||
|
|
69254f964e | ||
|
|
4f6cece74b | ||
|
|
507571302d | ||
|
|
00f35ebd71 | ||
|
|
fcea21270b | ||
|
|
bbb5c5ed4d | ||
|
|
9e83c296c5 | ||
|
|
3a01046617 | ||
|
|
633d6ba8fb | ||
|
|
a197b650c3 | ||
|
|
a6f6df5e3a | ||
|
|
9efc63e729 | ||
|
|
f7ab5bc179 | ||
|
|
aea87a98b0 | ||
|
|
f0f820f28d | ||
|
|
a3925b8024 | ||
|
|
fc24ef272a | ||
|
|
efebc58aab | ||
|
|
c38183adf6 | ||
|
|
4ca1d7be96 | ||
|
|
05a6ff1db5 | ||
|
|
cc23fe1f1e | ||
|
|
9972360927 | ||
|
|
7d808129d4 | ||
|
|
c93998c0d4 | ||
|
|
58eef6f518 | ||
|
|
f48b991ba3 | ||
|
|
50dee1bfe6 | ||
|
|
0aa1c281d0 | ||
|
|
5e3d02ec6f | ||
|
|
72830f1c62 | ||
|
|
7b0062996d | ||
|
|
c13810262a | ||
|
|
43d11c7a25 | ||
|
|
f418be396b | ||
|
|
678e13a44c | ||
|
|
3cbcce5277 | ||
|
|
3d95fd942e | ||
|
|
882f407446 | ||
|
|
0034199e76 | ||
|
|
c01735c1b1 | ||
|
|
d7e62f754c | ||
|
|
1f6e63d665 | ||
|
|
8bbeb2b670 | ||
|
|
c4c2b8e9f1 | ||
|
|
7a11b8cf28 | ||
|
|
5f041918c3 | ||
|
|
3f63d2a4fb | ||
|
|
2faace6edd | ||
|
|
2f80a16124 | ||
|
|
006885dc51 | ||
|
|
9d768d24f6 | ||
|
|
616b0242a6 | ||
|
|
332c5e795f | ||
|
|
5bba13e43c | ||
|
|
fe0dabddab | ||
|
|
a017fcccb9 | ||
|
|
bfcfca57ea | ||
|
|
f578e89a81 | ||
|
|
2ad18864ce | ||
|
|
9e4754089d | ||
|
|
ab4783c277 | ||
|
|
8d139bccad | ||
|
|
9e917321f0 | ||
|
|
7258657b1e | ||
|
|
ec3b3b9ce7 | ||
|
|
133a2756d2 | ||
|
|
4d37d67373 | ||
|
|
06581df8b6 | ||
|
|
47be4d4236 | ||
|
|
51f26839f6 | ||
|
|
79b924c8c2 | ||
|
|
92160cf524 | ||
|
|
1d0f553315 | ||
|
|
9f1eaf46ed | ||
|
|
5d8ab65260 | ||
|
|
e95353130c | ||
|
|
12066db612 | ||
|
|
ef2054f262 | ||
|
|
3cb4df567d | ||
|
|
b8ec1d8556 | ||
|
|
6ba369c4ce | ||
|
|
a85e77e3f1 | ||
|
|
aef29a64a1 | ||
|
|
2a77abd2db | ||
|
|
ceb78339f6 | ||
|
|
ace25204cc | ||
|
|
a698a335aa | ||
|
|
48a556c4db | ||
|
|
b6f56fbd82 |
+2
-1
@@ -3,4 +3,5 @@ __pycache__
|
||||
wildcards/**
|
||||
.vscode/
|
||||
.idea/
|
||||
node_modules/
|
||||
node_modules/
|
||||
rgthree_config.json
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": false,
|
||||
"bracketSameLine": true
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
[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
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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
|
||||
@@ -6,7 +6,7 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
|
||||
# Install
|
||||
|
||||
1. Install [ComfyUi](https://github.com/comfyanonymous/ComfyUI).
|
||||
1. Install the great [ComfyUi](https://github.com/comfyanonymous/ComfyUI).
|
||||
2. Clone this repo into `custom_modules`:
|
||||
```
|
||||
cd ComfyUI/custom_nodes
|
||||
@@ -14,7 +14,9 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
```
|
||||
3. Start up ComfyUI.
|
||||
|
||||
## 🐛 Graph Linking Issues
|
||||
|
||||
If your workflows sometimes have missing connections, or even errors on load, start up ComfyUI and go to http://127.0.0.1:8188/extensions/rgthree-comfy/html/links.html which will allow you to drop in an image or workflow json file and check for and fix any bad links.
|
||||
|
||||
# The Nodes
|
||||
|
||||
@@ -39,22 +41,28 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
> <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.
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
|
||||
## Context
|
||||
## 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.
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
|
||||
## Display Int
|
||||
> Nothing special, an 'output node' that displays an int _after execution_.
|
||||
|
||||
## Display Any
|
||||
> Displays most any piece of text data from the backend _after execution_.
|
||||
|
||||
|
||||
## Lora Loader Stack
|
||||
@@ -73,11 +81,20 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
> </details>
|
||||
|
||||
|
||||
## Power Prompt (Simple)
|
||||
## 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)_.
|
||||
|
||||
|
||||
## Context Switch
|
||||
## 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>
|
||||
@@ -92,6 +109,7 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
> A powerful 'control panel' node to quickly toggle connected node allowing it 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>
|
||||
|
||||
@@ -99,6 +117,16 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
## 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/ror 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.
|
||||
@@ -107,7 +135,7 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
|
||||
|
||||
## Mute / Bypass Repeater
|
||||
> A powerful node that will dispatch it's Mute/Bypass/Active mode to all connected input nodes.
|
||||
> 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>
|
||||
>
|
||||
@@ -133,11 +161,11 @@ A collection of nodes I've created while messing around with Stable Diffusion. I
|
||||
|
||||
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 extra work than is needed. While these switches may have a selector to choose which input to pass along, they don't stop execution of the other inputs, which will be wasted. Instead, Context Switch works by choosing the first non-empty context to pass along. Muting is one way to make a previous node empty, and causes no extra work to be done when set up correctly.
|
||||
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_ 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.
|
||||
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.
|
||||
|
||||
|
||||
+16
-6
@@ -1,15 +1,25 @@
|
||||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
import glob
|
||||
import json
|
||||
|
||||
from config import RGTHREE_CONFIG
|
||||
|
||||
THIS_DIR=os.path.dirname(os.path.abspath(__file__))
|
||||
DIR_DEV_JS=os.path.abspath(f'{THIS_DIR}/js')
|
||||
DIR_WEB_JS=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree')
|
||||
DIR_DEV=os.path.abspath(f'{THIS_DIR}/web')
|
||||
DIR_WEB=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy')
|
||||
|
||||
shutil.rmtree(DIR_DEV_JS)
|
||||
js_files = glob.glob(os.path.join(THIS_DIR, 'web/**/*.js'))
|
||||
for file in js_files:
|
||||
os.remove(file)
|
||||
|
||||
subprocess.run(["tsc"])
|
||||
subprocess.run(["./node_modules/typescript/bin/tsc"])
|
||||
|
||||
|
||||
shutil.rmtree(DIR_WEB_JS)
|
||||
shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True)
|
||||
if os.path.exists(DIR_WEB):
|
||||
shutil.rmtree(DIR_WEB)
|
||||
shutil.copytree(DIR_DEV, DIR_WEB, dirs_exist_ok=True)
|
||||
|
||||
with open(os.path.join(DIR_WEB, 'rgthree_config.js'), 'w', encoding = 'UTF-8') as file:
|
||||
file.write('export const rgthreeConfig = ' + json.dumps(RGTHREE_CONFIG))
|
||||
|
||||
+247
-24
@@ -6,50 +6,273 @@
|
||||
"""
|
||||
|
||||
import glob
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
|
||||
from server import PromptServer
|
||||
from .config import RGTHREE_CONFIG
|
||||
|
||||
# from .server import server
|
||||
|
||||
from .py.log import log_welcome
|
||||
from .py.context import RgthreeContext
|
||||
from .py.context_switch import RgthreeContextSwitch
|
||||
from .py.display_int import RgthreeDisplayInt
|
||||
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.context_dynamic import RgthreeDynamicContext
|
||||
from .py.context_dynamic_switch import RgthreeContextDynamicSwitch
|
||||
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
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
RgthreeContext.NAME: RgthreeContext,
|
||||
RgthreeContextSwitch.NAME: RgthreeContextSwitch,
|
||||
RgthreeDisplayInt.NAME: RgthreeDisplayInt,
|
||||
RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack,
|
||||
RgthreeSeed.NAME: RgthreeSeed,
|
||||
RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage,
|
||||
RgthreePowerPrompt.NAME: RgthreePowerPrompt,
|
||||
RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple,
|
||||
RgthreeDynamicContext.NAME: RgthreeDynamicContext,
|
||||
RgthreeContextDynamicSwitch.NAME: RgthreeContextDynamicSwitch,
|
||||
RgthreeBigContext.NAME: RgthreeBigContext,
|
||||
RgthreeContext.NAME: RgthreeContext,
|
||||
RgthreeContextSwitch.NAME: RgthreeContextSwitch,
|
||||
RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig,
|
||||
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,
|
||||
}
|
||||
|
||||
THIS_DIR=os.path.dirname(os.path.abspath(__file__))
|
||||
DIR_DEV_JS=os.path.abspath(f'{THIS_DIR}/js')
|
||||
DIR_PY=os.path.abspath(f'{THIS_DIR}/py')
|
||||
DIR_WEB_JS=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree')
|
||||
if not os.path.exists(DIR_WEB_JS):
|
||||
os.makedirs(DIR_WEB_JS)
|
||||
|
||||
shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True)
|
||||
# This doesn't import correctly..
|
||||
# WEB_DIRECTORY = "./web"
|
||||
|
||||
nodes=[]
|
||||
NOT_NODES=['constants','log','utils']
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DIR_DEV_WEB = os.path.abspath(f'{THIS_DIR}/web/')
|
||||
DIR_PY = os.path.abspath(f'{THIS_DIR}/py')
|
||||
|
||||
# remove old directory.
|
||||
OLD_DIR_WEB = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree')
|
||||
if os.path.exists(OLD_DIR_WEB):
|
||||
shutil.rmtree(OLD_DIR_WEB)
|
||||
|
||||
DIR_WEB = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy')
|
||||
if os.path.exists(DIR_WEB):
|
||||
shutil.rmtree(DIR_WEB)
|
||||
os.makedirs(DIR_WEB)
|
||||
|
||||
shutil.copytree(DIR_DEV_WEB, DIR_WEB, dirs_exist_ok=True)
|
||||
|
||||
|
||||
def extend_config(default_config, user_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
|
||||
|
||||
|
||||
with open(os.path.join(DIR_WEB, 'rgthree_config.js'), 'w', encoding = 'UTF-8') as file:
|
||||
file.write('export const rgthreeConfig = ' + json.dumps(RGTHREE_CONFIG))
|
||||
|
||||
# shutil.copy(os.path.join(THIS_DIR, 'rgthree_config.json'), os.path.join(DIR_WEB, 'rgthree_config.js'))
|
||||
|
||||
|
||||
NOT_NODES = ['constants', 'log', 'utils', 'rgthree']
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
||||
|
||||
for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=DIR_DEV_JS):
|
||||
name = os.path.splitext(file)[0]
|
||||
if name not in nodes and name not in NOT_NODES and not name.startswith('_') and not name.startswith('base'):
|
||||
nodes.append(name)
|
||||
nodes = []
|
||||
for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=os.path.join(DIR_DEV_WEB, 'js')):
|
||||
name = os.path.splitext(file)[0]
|
||||
if name not in nodes and name not in NOT_NODES and not name.startswith(
|
||||
'_') and not name.startswith('base') and not 'utils' in name:
|
||||
nodes.append(name)
|
||||
|
||||
log_welcome(num_nodes=len(nodes))
|
||||
|
||||
|
||||
# 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.
|
||||
if 'patch_recursive_execution' in RGTHREE_CONFIG and RGTHREE_CONFIG['patch_recursive_execution']:
|
||||
import execution
|
||||
|
||||
msg = "\33[32m[rgthree] Optimizing ComfyUI recursive execution. \33[33mIf queueing and/or re-queueing seems "
|
||||
msg += "broken, change \"patch_recursive_execution\" to false in rgthree_config.json \33[0m"
|
||||
print(msg)
|
||||
|
||||
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,))
|
||||
|
||||
|
||||
execution.rgthree_cache_recursive_output_delete_if_changed_output = {}
|
||||
execution.rgthree_cache_recursive_will_execute = {}
|
||||
|
||||
def rgthree_execute(self, *args, **kwargs):
|
||||
# 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 = {}
|
||||
return self.old_execute(*args, **kwargs)
|
||||
|
||||
|
||||
def rgthree_recursive_will_execute(prompt, outputs, current_item, *args, **kwargs):
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
execution.PromptExecutor.old_execute = execution.PromptExecutor.execute
|
||||
execution.PromptExecutor.execute = rgthree_execute
|
||||
|
||||
execution.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.old_recursive_will_execute = execution.recursive_will_execute
|
||||
execution.recursive_will_execute = rgthree_recursive_will_execute
|
||||
|
||||
|
||||
print()
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
def extend_config(default_config, user_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
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_CONFIG_FILE = os.path.join(THIS_DIR, 'rgthree_config.json.default')
|
||||
with open(DEFAULT_CONFIG_FILE, 'r', encoding = 'UTF-8') as file:
|
||||
config = re.sub(r"(?:^|\s)//.*", "", file.read(), flags=re.MULTILINE)
|
||||
rgthree_config_default = json.loads(config)
|
||||
|
||||
CONFIG_FILE = os.path.join(THIS_DIR, 'rgthree_config.json')
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r', encoding = 'UTF-8') as file:
|
||||
config = re.sub(r"(?:^|\s)//.*", "", file.read(), flags=re.MULTILINE)
|
||||
rgthree_config_user = json.loads(config)
|
||||
else:
|
||||
rgthree_config_user = {}
|
||||
|
||||
RGTHREE_CONFIG = extend_config(rgthree_config_default, rgthree_config_user)
|
||||
@@ -1,24 +0,0 @@
|
||||
export class RgthreeBaseNode extends LGraphNode {
|
||||
constructor(title = RgthreeBaseNode.title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
if (title == '__NEED_NAME__') {
|
||||
throw new Error('RgthreeBaseNode needs overrides.');
|
||||
}
|
||||
this.properties = this.properties || {};
|
||||
}
|
||||
onModeChange() {
|
||||
}
|
||||
set mode(mode) {
|
||||
if (this.mode_ != mode) {
|
||||
this.mode_ = mode;
|
||||
this.onModeChange();
|
||||
}
|
||||
}
|
||||
get mode() {
|
||||
return this.mode_;
|
||||
}
|
||||
}
|
||||
RgthreeBaseNode.title = "__NEED_NAME__";
|
||||
RgthreeBaseNode.category = 'rgthree';
|
||||
RgthreeBaseNode._category = 'rgthree';
|
||||
@@ -1,53 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
export class BaseCollectorNode extends RgthreeBaseNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
this.addInput("", "*");
|
||||
this.addOutput("Output", "*");
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
return cloned;
|
||||
}
|
||||
updateOutputLinks(startNode = this) {
|
||||
const type = startNode.constructor.type;
|
||||
if (startNode.onConnectionsChainChange) {
|
||||
startNode.onConnectionsChainChange();
|
||||
}
|
||||
if (startNode === this || (type === null || type === void 0 ? void 0 : type.includes('Reroute')) || (type === null || type === void 0 ? void 0 : type.includes('Combiner'))) {
|
||||
for (const output of startNode.outputs) {
|
||||
if (!output.links || !output.links.length)
|
||||
continue;
|
||||
for (const linkId of output.links) {
|
||||
const link = app.graph.links[linkId];
|
||||
if (!link)
|
||||
continue;
|
||||
const targetNode = app.graph.getNodeById(link.target_id);
|
||||
targetNode && this.updateOutputLinks(targetNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onConnectionsChange(_type, _slotIndex, _isConnected, link_info, _ioSlot) {
|
||||
if (!link_info)
|
||||
return;
|
||||
this.stabilizeInputsOutputs();
|
||||
this.updateOutputLinks();
|
||||
}
|
||||
stabilizeInputsOutputs() {
|
||||
var _a, _b;
|
||||
for (let index = this.inputs.length - 1; index >= 0; index--) {
|
||||
const input = this.inputs[index];
|
||||
if (!input.link) {
|
||||
this.removeInput(index);
|
||||
}
|
||||
}
|
||||
this.addInput('', '*');
|
||||
const outputLength = ((_b = (_a = this.outputs[0]) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.length) || 0;
|
||||
if (outputLength > 1) {
|
||||
this.outputs[0].links.length = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, wait } from "./utils.js";
|
||||
export class BaseNodeModeChanger extends RgthreeBaseNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
this.debouncer = 0;
|
||||
this.schedulePromise = null;
|
||||
this.modeOn = -1;
|
||||
this.modeOff = -1;
|
||||
wait(10).then(() => {
|
||||
if (this.modeOn < 0 || this.modeOff < 0) {
|
||||
throw new Error('modeOn and modeOff must be overridden.');
|
||||
}
|
||||
});
|
||||
this.addInput("", "*");
|
||||
}
|
||||
scheduleStabilizeWidgets() {
|
||||
if (!this.schedulePromise) {
|
||||
this.schedulePromise = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(this.stabilizeWidgets());
|
||||
this.schedulePromise = null;
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
return this.schedulePromise;
|
||||
}
|
||||
stabilizeWidgets() {
|
||||
if (!this.graph) {
|
||||
return;
|
||||
}
|
||||
const linkedNodes = getConnectedInputNodes(app, this);
|
||||
this.stabilizeInputsOutputs();
|
||||
for (const [index, node] of linkedNodes.entries()) {
|
||||
let widget = this.widgets && this.widgets[index];
|
||||
if (!widget) {
|
||||
this._tempWidth = this.size[0];
|
||||
widget = this.addWidget('toggle', '', false, '', { "on": 'yes', "off": 'no' });
|
||||
}
|
||||
this.setWidget(widget, node);
|
||||
}
|
||||
if (this.widgets && this.widgets.length > linkedNodes.length) {
|
||||
this._tempWidth = this.size[0];
|
||||
this.widgets.length = linkedNodes.length;
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
setTimeout(() => { this.stabilizeWidgets(); }, 500);
|
||||
}
|
||||
setWidget(widget, linkedNode) {
|
||||
const off = linkedNode.mode === this.modeOff;
|
||||
widget.name = `Enable ${linkedNode.title}`;
|
||||
widget.options = { 'on': 'yes', 'off': 'no' };
|
||||
widget.value = !off;
|
||||
widget.callback = () => {
|
||||
const off = linkedNode.mode === this.modeOff;
|
||||
linkedNode.mode = (off ? this.modeOn : this.modeOff);
|
||||
widget.value = off;
|
||||
};
|
||||
}
|
||||
onConnectionsChainChange() {
|
||||
this.scheduleStabilizeWidgets();
|
||||
}
|
||||
onConnectionsChange(_type, _index, _connected, _linkInfo, _ioSlot) {
|
||||
this.scheduleStabilizeWidgets();
|
||||
}
|
||||
removeInput(slot) {
|
||||
this._tempWidth = this.size[0];
|
||||
return super.removeInput(slot);
|
||||
}
|
||||
addInput(name, type, extra_info) {
|
||||
this._tempWidth = this.size[0];
|
||||
return super.addInput(name, type, extra_info);
|
||||
}
|
||||
stabilizeInputsOutputs() {
|
||||
let hasEmptyInput = false;
|
||||
for (let index = this.inputs.length - 1; index >= 0; index--) {
|
||||
const input = this.inputs[index];
|
||||
if (!input.link) {
|
||||
if (index < this.inputs.length - 1) {
|
||||
this.removeInput(index);
|
||||
}
|
||||
else {
|
||||
hasEmptyInput = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
!hasEmptyInput && this.addInput('', '*');
|
||||
}
|
||||
computeSize(out) {
|
||||
var _a, _b;
|
||||
let size = super.computeSize(out);
|
||||
if (this._tempWidth) {
|
||||
size[0] = this._tempWidth;
|
||||
this._tempWidth = null;
|
||||
}
|
||||
if (this.properties['collapse_connections']) {
|
||||
const rows = Math.max(((_a = this.inputs) === null || _a === void 0 ? void 0 : _a.length) || 0, ((_b = this.outputs) === null || _b === void 0 ? void 0 : _b.length) || 0, 1) - 1;
|
||||
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
|
||||
}
|
||||
setTimeout(() => {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}, 16);
|
||||
return size;
|
||||
}
|
||||
static setUp(clazz) {
|
||||
addMenuItem(clazz, app, {
|
||||
name: 'Refresh',
|
||||
callback: (node) => { node.scheduleStabilizeWidgets(); }
|
||||
});
|
||||
addMenuItem(clazz, app, {
|
||||
name: (node) => { var _a; return (`${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']) ? 'Show' : 'Collapse'} Connections`); },
|
||||
property: 'collapse_connections',
|
||||
prepareValue: (_value, node) => { var _a; return !((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']); },
|
||||
callback: (_node) => { app.graph.setDirtyCanvas(true, true); }
|
||||
});
|
||||
addConnectionLayoutSupport(clazz, app, [['Left'], ['Right']]);
|
||||
LiteGraph.registerNodeType(clazz.type, clazz);
|
||||
clazz.category = clazz._category;
|
||||
}
|
||||
}
|
||||
BaseNodeModeChanger.collapsible = false;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { addConnectionLayoutSupport } from "./utils.js";
|
||||
app.registerExtension({
|
||||
name: "rgthree.Context",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "Context (rgthree)") {
|
||||
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
}
|
||||
},
|
||||
});
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { addConnectionLayoutSupport, addMenuSubMenu } from "./utils.js";
|
||||
app.registerExtension({
|
||||
name: "rgthree.Reroute",
|
||||
registerCustomNodes() {
|
||||
class RerouteNode extends LGraphNode {
|
||||
constructor(title = RerouteNode.title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
this.resizable = false;
|
||||
this.size = RerouteNode.size;
|
||||
this.addInput("", "*");
|
||||
this.addOutput("", "*");
|
||||
setTimeout(() => this.applyNodeSize(), 20);
|
||||
}
|
||||
configure(info) {
|
||||
super.configure(info);
|
||||
this.applyNodeSize();
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
cloned.inputs[0].type = '*';
|
||||
cloned.outputs[0].type = '*';
|
||||
return cloned;
|
||||
}
|
||||
onConnectionsChange(type, _slotIndex, connected, _link_info, _ioSlot) {
|
||||
var _a, _b, _c;
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = [];
|
||||
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
||||
const linkId = this.outputs[0].links[i];
|
||||
const link = app.graph.links[linkId];
|
||||
linksToDisconnect.push(link);
|
||||
}
|
||||
for (const link of linksToDisconnect) {
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
node.disconnectInput(link.target_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
let currentNode = this;
|
||||
let updateNodes = [];
|
||||
let inputType = null;
|
||||
let inputNode = null;
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode);
|
||||
const linkId = currentNode.inputs[0].link;
|
||||
if (linkId !== null) {
|
||||
const link = app.graph.links[linkId];
|
||||
const node = app.graph.getNodeById(link.origin_id);
|
||||
const type = node.constructor.type;
|
||||
if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) {
|
||||
if (node === this) {
|
||||
currentNode.disconnectInput(link.target_slot);
|
||||
currentNode = null;
|
||||
}
|
||||
else {
|
||||
currentNode = node;
|
||||
}
|
||||
}
|
||||
else {
|
||||
inputNode = currentNode;
|
||||
inputType = (_b = (_a = node.outputs[link.origin_slot]) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
currentNode = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const nodes = [this];
|
||||
let outputType = null;
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop();
|
||||
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
||||
if (outputs.length) {
|
||||
for (const linkId of outputs) {
|
||||
const link = app.graph.links[linkId];
|
||||
if (!link)
|
||||
continue;
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
if (!node)
|
||||
continue;
|
||||
const type = node.constructor.type;
|
||||
if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) {
|
||||
nodes.push(node);
|
||||
updateNodes.push(node);
|
||||
}
|
||||
else {
|
||||
const nodeOutType = node.inputs && node.inputs[link === null || link === void 0 ? void 0 : link.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
|
||||
if (inputType && nodeOutType !== inputType && nodeOutType !== '*') {
|
||||
node.disconnectInput(link.target_slot);
|
||||
}
|
||||
else {
|
||||
outputType = nodeOutType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
const displayType = inputType || outputType || "*";
|
||||
const color = LGraphCanvas.link_type_colors[displayType];
|
||||
for (const node of updateNodes) {
|
||||
node.outputs[0].type = inputType || "*";
|
||||
node.__outputType = displayType;
|
||||
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
|
||||
node.size = node.computeSize();
|
||||
(_c = node.applyNodeSize) === null || _c === void 0 ? void 0 : _c.call(node);
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputNode) {
|
||||
const link = app.graph.links[inputNode.inputs[0].link];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
}
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
applyNodeSize() {
|
||||
this.properties['size'] = this.properties['size'] || RerouteNode.size;
|
||||
this.properties['size'] = [Number(this.properties['size'][0]), Number(this.properties['size'][1])];
|
||||
this.size = this.properties['size'];
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
RerouteNode.title = "Reroute (rgthree)";
|
||||
RerouteNode.category = 'rgthree';
|
||||
RerouteNode._category = 'rgthree';
|
||||
RerouteNode.title_mode = LiteGraph.NO_TITLE;
|
||||
RerouteNode.collapsable = false;
|
||||
RerouteNode.layout_slot_offset = 5;
|
||||
RerouteNode.size = [40, 30];
|
||||
addConnectionLayoutSupport(RerouteNode, app, [
|
||||
["Left", "Right"],
|
||||
["Left", "Top"],
|
||||
["Left", "Bottom"],
|
||||
["Right", "Left"],
|
||||
["Right", "Top"],
|
||||
["Right", "Bottom"],
|
||||
["Top", "Left"],
|
||||
["Top", "Right"],
|
||||
["Top", "Bottom"],
|
||||
["Bottom", "Left"],
|
||||
["Bottom", "Right"],
|
||||
["Bottom", "Top"],
|
||||
], (node) => { node.applyNodeSize(); });
|
||||
addMenuSubMenu(RerouteNode, app, {
|
||||
name: 'Height',
|
||||
property: 'size',
|
||||
options: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
}
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [node.size[0], Number(value)],
|
||||
callback: (node) => node.applyNodeSize()
|
||||
});
|
||||
addMenuSubMenu(RerouteNode, app, {
|
||||
name: 'Width',
|
||||
property: 'size',
|
||||
options: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
}
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [Number(value), node.size[1]],
|
||||
callback: (node) => node.applyNodeSize()
|
||||
});
|
||||
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
|
||||
RerouteNode.category = RerouteNode._category;
|
||||
},
|
||||
});
|
||||
-304
@@ -1,304 +0,0 @@
|
||||
import { api } from '../../scripts/api.js';
|
||||
const oldApiGetNodeDefs = api.getNodeDefs;
|
||||
api.getNodeDefs = async function () {
|
||||
const defs = await oldApiGetNodeDefs.call(api);
|
||||
this.dispatchEvent(new CustomEvent('fresh-node-defs', { detail: defs }));
|
||||
return defs;
|
||||
};
|
||||
var IoDirection;
|
||||
(function (IoDirection) {
|
||||
IoDirection[IoDirection["INPUT"] = 0] = "INPUT";
|
||||
IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT";
|
||||
})(IoDirection || (IoDirection = {}));
|
||||
const PADDING = 0;
|
||||
export const LAYOUT_LABEL_TO_DATA = {
|
||||
'Left': [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]],
|
||||
'Right': [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]],
|
||||
'Top': [LiteGraph.UP, [0.5, 0], [0, PADDING]],
|
||||
'Bottom': [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]],
|
||||
};
|
||||
const OPPOSITE_LABEL = {
|
||||
'Left': 'Right',
|
||||
'Right': 'Left',
|
||||
'Top': 'Bottom',
|
||||
'Bottom': 'Top',
|
||||
};
|
||||
export function addMenuItem(node, _app, config) {
|
||||
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
|
||||
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
|
||||
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
|
||||
const idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
|
||||
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
|
||||
content: typeof config.name == 'function' ? config.name(this) : config.name,
|
||||
callback: (_value, _options, _event, _parentMenu, _node) => {
|
||||
if (config.property) {
|
||||
this.properties = this.properties || {};
|
||||
this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property];
|
||||
}
|
||||
config.callback && config.callback(this);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
export function addMenuSubMenu(node, _app, config) {
|
||||
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
|
||||
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
|
||||
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
|
||||
const idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
|
||||
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
|
||||
content: typeof config.name == 'function' ? config.name(this) : config.name,
|
||||
has_submenu: true,
|
||||
callback: (_value, _options, event, parentMenu, _node) => {
|
||||
new LiteGraph.ContextMenu(config.options.map(option => ({ content: option })), {
|
||||
event,
|
||||
parentMenu,
|
||||
callback: (value, _options, _event, _parentMenu, _node) => {
|
||||
if (config.property) {
|
||||
this.properties = this.properties || {};
|
||||
this.properties[config.property] = config.prepareValue ? config.prepareValue(value.content, this) : value.content;
|
||||
}
|
||||
config.callback && config.callback(this);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
export function addConnectionLayoutSupport(node, app, options = [['Left', 'Right'], ['Right', 'Left']], callback) {
|
||||
addMenuSubMenu(node, app, {
|
||||
name: 'Connections Layout',
|
||||
property: 'connections_layout',
|
||||
options: options.map(option => option[0] + (option[1] ? ' -> ' + option[1] : '')),
|
||||
prepareValue: (value, node) => {
|
||||
var _a;
|
||||
const values = value.split(' -> ');
|
||||
if (!values[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) {
|
||||
values[1] = OPPOSITE_LABEL[values[0]];
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[values[0]] || !LAYOUT_LABEL_TO_DATA[values[1]]) {
|
||||
throw new Error(`New Layout invalid: [${values[0]}, ${values[1]}]`);
|
||||
}
|
||||
return values;
|
||||
},
|
||||
callback: (node) => {
|
||||
callback && callback(node);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
},
|
||||
});
|
||||
node.prototype.getConnectionPos = function (isInput, slotNumber, out) {
|
||||
return getConnectionPosForLayout(this, isInput, slotNumber, out);
|
||||
};
|
||||
}
|
||||
export function setConnectionsLayout(node, newLayout = ['Left', 'Right']) {
|
||||
var _a;
|
||||
if (!newLayout[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) {
|
||||
newLayout[1] = OPPOSITE_LABEL[newLayout[0]];
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[newLayout[0]] || !LAYOUT_LABEL_TO_DATA[newLayout[1]]) {
|
||||
throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`);
|
||||
}
|
||||
node.properties = node.properties || {};
|
||||
node.properties['connections_layout'] = newLayout;
|
||||
}
|
||||
export function setConnectionsCollapse(node, collapseConnections = null) {
|
||||
node.properties = node.properties || {};
|
||||
collapseConnections = collapseConnections !== null ? collapseConnections : !node.properties['collapse_connections'];
|
||||
node.properties['collapse_connections'] = collapseConnections;
|
||||
}
|
||||
export function getConnectionPosForLayout(node, isInput, slotNumber, out) {
|
||||
var _a, _b, _c;
|
||||
out = out || new Float32Array(2);
|
||||
node.properties = node.properties || {};
|
||||
const layout = node.properties['connections_layout'] || ['Left', 'Right'];
|
||||
const collapseConnections = node.properties['collapse_connections'] || false;
|
||||
const offset = (_a = node.constructor.layout_slot_offset) !== null && _a !== void 0 ? _a : (LiteGraph.NODE_SLOT_HEIGHT * 0.5);
|
||||
let side = isInput ? layout[0] : layout[1];
|
||||
const otherSide = isInput ? layout[1] : layout[0];
|
||||
const data = LAYOUT_LABEL_TO_DATA[side];
|
||||
const slotList = node[isInput ? 'inputs' : 'outputs'];
|
||||
const cxn = slotList[slotNumber];
|
||||
if (!cxn) {
|
||||
console.log('No connection found.. weird', isInput, slotNumber);
|
||||
return out;
|
||||
}
|
||||
if (cxn.disabled) {
|
||||
if (cxn.color_on !== '#666665') {
|
||||
cxn._color_on_org = cxn._color_on_org || cxn.color_on;
|
||||
cxn._color_off_org = cxn._color_off_org || cxn.color_off;
|
||||
}
|
||||
cxn.color_on = '#666665';
|
||||
cxn.color_off = '#666665';
|
||||
}
|
||||
else if (cxn.color_on === '#666665') {
|
||||
cxn.color_on = cxn._color_on_org || undefined;
|
||||
cxn.color_off = cxn._color_off_org || undefined;
|
||||
}
|
||||
const displaySlot = collapseConnections ? 0 : (slotNumber - slotList.reduce((count, ioput, index) => {
|
||||
count += index < slotNumber && ioput.hidden ? 1 : 0;
|
||||
return count;
|
||||
}, 0));
|
||||
cxn.dir = data[0];
|
||||
if (node.size[0] == 10 && ['Left', 'Right'].includes(side) && ['Top', 'Bottom'].includes(otherSide)) {
|
||||
side = otherSide === 'Top' ? 'Bottom' : 'Top';
|
||||
}
|
||||
else if (node.size[1] == 10 && ['Top', 'Bottom'].includes(side) && ['Left', 'Right'].includes(otherSide)) {
|
||||
side = otherSide === 'Left' ? 'Right' : 'Left';
|
||||
}
|
||||
if (side === 'Left') {
|
||||
if (node.flags.collapsed) {
|
||||
var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
|
||||
out[0] = node.pos[0];
|
||||
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
|
||||
}
|
||||
else {
|
||||
if (!isInput && !cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
cxn.label = ' ';
|
||||
}
|
||||
else if (isInput && cxn.has_old_label) {
|
||||
cxn.has_old_label = false;
|
||||
cxn.label = cxn.old_label;
|
||||
cxn.old_label = undefined;
|
||||
}
|
||||
out[0] = node.pos[0] + offset;
|
||||
if ((_b = node.constructor) === null || _b === void 0 ? void 0 : _b.type.includes('Reroute')) {
|
||||
out[1] = node.pos[1] + (node.size[1] * .5);
|
||||
}
|
||||
else {
|
||||
out[1] =
|
||||
node.pos[1] +
|
||||
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
|
||||
(node.constructor.slot_start_y || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (side === 'Right') {
|
||||
if (node.flags.collapsed) {
|
||||
var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
|
||||
out[0] = node.pos[0] + w;
|
||||
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
|
||||
}
|
||||
else {
|
||||
if (isInput && !cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
cxn.label = ' ';
|
||||
}
|
||||
else if (!isInput && cxn.has_old_label) {
|
||||
cxn.has_old_label = false;
|
||||
cxn.label = cxn.old_label;
|
||||
cxn.old_label = undefined;
|
||||
}
|
||||
out[0] = node.pos[0] + node.size[0] + 1 - offset;
|
||||
if ((_c = node.constructor) === null || _c === void 0 ? void 0 : _c.type.includes('Reroute')) {
|
||||
out[1] = node.pos[1] + (node.size[1] * .5);
|
||||
}
|
||||
else {
|
||||
out[1] =
|
||||
node.pos[1] +
|
||||
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
|
||||
(node.constructor.slot_start_y || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (side === 'Top') {
|
||||
if (!cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
cxn.label = ' ';
|
||||
}
|
||||
out[0] = node.pos[0] + (node.size[0] * .5);
|
||||
out[1] = node.pos[1] + offset;
|
||||
}
|
||||
else if (side === 'Bottom') {
|
||||
if (!cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
cxn.label = ' ';
|
||||
}
|
||||
out[0] = node.pos[0] + (node.size[0] * .5);
|
||||
out[1] = node.pos[1] + node.size[1] - offset;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
export function wait(ms = 16, value) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => { resolve(value); }, ms);
|
||||
});
|
||||
}
|
||||
export function addHelp(node, app) {
|
||||
const help = node.help;
|
||||
if (help) {
|
||||
addMenuItem(node, app, {
|
||||
name: '🛟 Node Help',
|
||||
property: 'help',
|
||||
callback: (_node) => { alert(help); }
|
||||
});
|
||||
}
|
||||
}
|
||||
export function isPassThroughType(node) {
|
||||
var _a;
|
||||
const type = (_a = node === null || node === void 0 ? void 0 : node.constructor) === null || _a === void 0 ? void 0 : _a.type;
|
||||
return (type === null || type === void 0 ? void 0 : type.includes('Reroute'))
|
||||
|| (type === null || type === void 0 ? void 0 : type.includes('Node Combiner'))
|
||||
|| (type === null || type === void 0 ? void 0 : type.includes('Node Collector'));
|
||||
}
|
||||
export function getConnectedInputNodes(app, startNode, currentNode) {
|
||||
return getConnectedNodes(app, startNode, IoDirection.INPUT, currentNode);
|
||||
}
|
||||
export function getConnectedOutputNodes(app, startNode, currentNode) {
|
||||
return getConnectedNodes(app, startNode, IoDirection.OUTPUT, currentNode);
|
||||
}
|
||||
function getConnectedNodes(app, startNode, dir = IoDirection.INPUT, currentNode) {
|
||||
var _a, _b;
|
||||
currentNode = currentNode || startNode;
|
||||
let rootNodes = [];
|
||||
const slotsToRemove = [];
|
||||
if (startNode === currentNode || isPassThroughType(currentNode)) {
|
||||
const removeDups = startNode === currentNode;
|
||||
let linkIds;
|
||||
if (dir == IoDirection.OUTPUT) {
|
||||
linkIds = (_a = currentNode.outputs) === null || _a === void 0 ? void 0 : _a.flatMap(i => i.links);
|
||||
}
|
||||
else {
|
||||
linkIds = (_b = currentNode.inputs) === null || _b === void 0 ? void 0 : _b.map(i => i.link);
|
||||
}
|
||||
let graph = app.graph;
|
||||
for (const linkId of linkIds) {
|
||||
const link = (linkId != null && graph.links[linkId]);
|
||||
if (!link) {
|
||||
continue;
|
||||
}
|
||||
const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id;
|
||||
const originNode = graph.getNodeById(connectedId);
|
||||
if (!link) {
|
||||
console.error('No connected node found... weird');
|
||||
continue;
|
||||
}
|
||||
if (isPassThroughType(originNode)) {
|
||||
for (const foundNode of getConnectedNodes(app, startNode, dir, originNode)) {
|
||||
if (!rootNodes.includes(foundNode)) {
|
||||
rootNodes.push(foundNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (rootNodes.includes(originNode)) {
|
||||
const connectedSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot;
|
||||
removeDups && (slotsToRemove.push(connectedSlot));
|
||||
}
|
||||
else {
|
||||
rootNodes.push(originNode);
|
||||
}
|
||||
}
|
||||
for (const slot of slotsToRemove) {
|
||||
if (dir == IoDirection.OUTPUT) {
|
||||
startNode.disconnectOutput(slot);
|
||||
}
|
||||
else {
|
||||
startNode.disconnectInput(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rootNodes;
|
||||
}
|
||||
Generated
+20
-4
@@ -5,13 +5,29 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6"
|
||||
"prettier": "3.0.3",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
},
|
||||
"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/typescript": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
||||
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
||||
"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",
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6"
|
||||
"prettier": "3.0.3",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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,)
|
||||
+24
-49
@@ -1,58 +1,33 @@
|
||||
"""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
|
||||
|
||||
ctx_keys = ["model", "clip", "vae", "positive", "negative", "latent", "images", "seed"]
|
||||
def new_context(context=None, model=None, clip=None, vae=None, positive=None, negative=None, latent=None, images=None, seed=None):
|
||||
ctx = {}
|
||||
for key in ctx_keys:
|
||||
v = None
|
||||
v = v if v != None else model if key == 'model' else None
|
||||
v = v if v != None else clip if key == 'clip' else None
|
||||
v = v if v != None else vae if key == 'vae' else None
|
||||
v = v if v != None else positive if key == 'positive' else None
|
||||
v = v if v != None else negative if key == 'negative' else None
|
||||
v = v if v != None else latent if key == 'latent' else None
|
||||
v = v if v != None else images if key == 'images' else None
|
||||
v = v if v != None else seed if key == 'seed' else None
|
||||
ctx[key] = a_b(v, d_k(context, key))
|
||||
return ctx
|
||||
|
||||
def d_k(dct, key, default=None):
|
||||
return dct[key] if dct != None and key in dct else default
|
||||
|
||||
def a_b(a, b):
|
||||
return a if a != None else b
|
||||
|
||||
|
||||
class RgthreeContext:
|
||||
"""The initial Context node.
|
||||
|
||||
NAME = get_name('Context')
|
||||
CATEGORY = get_category()
|
||||
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.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"base_ctx": ("RGTHREE_CONTEXT",),
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
"vae": ("VAE",),
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
"latent": ("LATENT",),
|
||||
"images": ("IMAGE", ),
|
||||
"seed": ("INT", {"forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
},
|
||||
}
|
||||
RETURN_TYPES = ("RGTHREE_CONTEXT", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING", "LATENT", "IMAGE", "INT",)
|
||||
RETURN_NAMES = ("CONTEXT", "MODEL", "CLIP", "VAE", "POSITIVE", "NEGATIVE", "LATENT", "IMAGE", "SEED",)
|
||||
FUNCTION = "convert"
|
||||
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"
|
||||
},
|
||||
}
|
||||
|
||||
def convert(self, base_ctx=None, model=None, clip=None, vae=None, positive=None, negative=None, latent=None, images=None, seed=None, prompt=None):
|
||||
ctx = new_context(context=base_ctx, model=model, clip=clip, vae=vae, positive=positive, negative=negative, latent=latent, images=images, seed=seed)
|
||||
return (ctx, ctx['model'], ctx['clip'], ctx['vae'], ctx['positive'], ctx['negative'], ctx['latent'], ctx['images'], ctx['seed'],)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""The Dynamic Context node."""
|
||||
from .constants import get_category, get_name
|
||||
from .utils import ByPassTypeTuple
|
||||
|
||||
|
||||
class RgthreeDynamicContext:
|
||||
"""The Dynamic Context node.
|
||||
|
||||
Similar to the static Context and Context Big nodes, this allows users to add any number and
|
||||
variety of inputs to a Dynamic Context node, and return the outputs by key name.
|
||||
"""
|
||||
|
||||
NAME = get_name("Dynamic Context")
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls): # pylint: disable = invalid-name,missing-function-docstring
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"base_ctx": ("DYNAMIC_CONTEXT",),
|
||||
"output_keys": ("DYNAMIC_CONTEXT_OUTPUTS",), # This is a hidden widget of the output keys
|
||||
},
|
||||
"hidden": {},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ByPassTypeTuple(("DYNAMIC_CONTEXT",))
|
||||
RETURN_NAMES = ByPassTypeTuple(("CONTEXT",))
|
||||
FUNCTION = "main"
|
||||
|
||||
def main(self, base_ctx=None, output_keys=None, **kwargs):
|
||||
"""Creates a new context from the provided data, with an optional base ctx to start."""
|
||||
new_ctx = base_ctx.copy() if base_ctx is not None else {}
|
||||
|
||||
for key_raw, value in kwargs.items():
|
||||
key = key_raw.upper()
|
||||
if key.startswith('+ '):
|
||||
key = key[2:]
|
||||
if key == "base_ctx":
|
||||
continue
|
||||
new_ctx[key] = value
|
||||
|
||||
res = [new_ctx]
|
||||
output_keys = output_keys.split(',') if output_keys is not None else []
|
||||
for key in output_keys:
|
||||
res.append(new_ctx[key] if key in new_ctx else None)
|
||||
return tuple(res)
|
||||
@@ -0,0 +1,59 @@
|
||||
"""The original Context Switch."""
|
||||
from .constants import get_category, get_name
|
||||
from .context_utils import is_context_empty
|
||||
from .utils import ByPassTypeTuple
|
||||
|
||||
|
||||
class RgthreeContextDynamicSwitch:
|
||||
"""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("Dynamic Context Switch")
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"output_keys": ("DYNAMIC_CONTEXT_OUTPUTS",), # This is a hidden widget of the output keys
|
||||
"ctx_01": ("DYNAMIC_CONTEXT",),
|
||||
"ctx_02": ("DYNAMIC_CONTEXT",),
|
||||
"ctx_03": ("DYNAMIC_CONTEXT",),
|
||||
"ctx_04": ("DYNAMIC_CONTEXT",),
|
||||
"ctx_05": ("DYNAMIC_CONTEXT",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ByPassTypeTuple(("DYNAMIC_CONTEXT", ))
|
||||
RETURN_NAMES = ByPassTypeTuple(("CONTEXT", ))
|
||||
FUNCTION = "switch"
|
||||
|
||||
def switch(self, output_keys=None, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None):
|
||||
"""Chooses the first non-empty Context to output.
|
||||
"""
|
||||
base_ctx = None
|
||||
if not is_context_empty(ctx_01):
|
||||
base_ctx = ctx_01
|
||||
elif not is_context_empty(ctx_02):
|
||||
base_ctx = ctx_02
|
||||
elif not is_context_empty(ctx_03):
|
||||
base_ctx = ctx_03
|
||||
elif not is_context_empty(ctx_04):
|
||||
base_ctx = ctx_04
|
||||
elif not is_context_empty(ctx_05):
|
||||
base_ctx = ctx_05
|
||||
|
||||
new_ctx = base_ctx.copy() if not is_context_empty(base_ctx) else None
|
||||
res = [new_ctx]
|
||||
output_keys = output_keys.split(',') if output_keys is not None else []
|
||||
for key in output_keys:
|
||||
if new_ctx is None:
|
||||
res.append(None)
|
||||
else:
|
||||
res.append(new_ctx[key] if key in new_ctx else None)
|
||||
|
||||
return tuple(res)
|
||||
+42
-36
@@ -1,45 +1,51 @@
|
||||
"""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)
|
||||
|
||||
def is_context_empty(ctx):
|
||||
if ctx == None:
|
||||
return True
|
||||
return all(v == None for v in ctx.values())
|
||||
|
||||
class RgthreeContextSwitch:
|
||||
"""The initial Context Switch node.
|
||||
|
||||
NAME = get_name('Context Switch')
|
||||
CATEGORY = get_category()
|
||||
For now, this will remain as-is but is otherwise backwards compatible with other Context nodes
|
||||
outputs.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"ctx_01": ("RGTHREE_CONTEXT",),
|
||||
"ctx_02": ("RGTHREE_CONTEXT",),
|
||||
"ctx_03": ("RGTHREE_CONTEXT",),
|
||||
"ctx_04": ("RGTHREE_CONTEXT",),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
},
|
||||
}
|
||||
NAME = get_name("Context Switch")
|
||||
CATEGORY = get_category()
|
||||
|
||||
RETURN_TYPES = ("RGTHREE_CONTEXT", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING", "LATENT", "IMAGE", "INT",)
|
||||
RETURN_NAMES = ("CONTEXT", "MODEL", "CLIP", "VAE", "POSITIVE", "NEGATIVE", "LATENT", "IMAGE", "SEED",)
|
||||
FUNCTION = "switch"
|
||||
@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",),
|
||||
},
|
||||
}
|
||||
|
||||
def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, prompt=None):
|
||||
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
|
||||
if ctx != None:
|
||||
return (ctx, ctx['model'], ctx['clip'], ctx['vae'], ctx['positive'], ctx['negative'], ctx['latent'], ctx['images'], ctx['seed'],)
|
||||
return (None,None,None,None,None,None,None,None,None,)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,102 @@
|
||||
"""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 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())
|
||||
@@ -0,0 +1,71 @@
|
||||
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,23 +0,0 @@
|
||||
from .constants import get_category, get_name
|
||||
|
||||
class RgthreeDisplayInt:
|
||||
|
||||
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,)}}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""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,)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""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,70 +1,88 @@
|
||||
# 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_FG = {
|
||||
'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',
|
||||
'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',
|
||||
}
|
||||
COLORS_STYLE = {
|
||||
'RESET': '\33[0m',
|
||||
'BOLD': '\33[1m',
|
||||
'NORMAL': '\33[22m',
|
||||
'ITALIC': '\33[3m',
|
||||
'UNDERLINE': '\33[4m',
|
||||
'BLINK': '\33[5m',
|
||||
'BLINK2': '\33[6m',
|
||||
'SELECTED': '\33[7m',
|
||||
'RESET': '\33[0m',
|
||||
'BOLD': '\33[1m',
|
||||
'NORMAL': '\33[22m',
|
||||
'ITALIC': '\33[3m',
|
||||
'UNDERLINE': '\33[4m',
|
||||
'BLINK': '\33[5m',
|
||||
'BLINK2': '\33[6m',
|
||||
'SELECTED': '\33[7m',
|
||||
}
|
||||
COLORS_BG = {
|
||||
'BLACK': '\33[40m',
|
||||
'RED': '\33[41m',
|
||||
'GREEN': '\33[42m',
|
||||
'YELLOW': '\33[43m',
|
||||
'BLUE': '\33[44m',
|
||||
'MAGENTA': '\33[45m',
|
||||
'CYAN': '\33[46m',
|
||||
'WHITE': '\33[47m',
|
||||
'GREY': '\33[100m',
|
||||
'BRIGHT_RED': '\33[101m',
|
||||
'BRIGHT_GREEN': '\33[102m',
|
||||
'BRIGHT_YELLOW': '\33[103m',
|
||||
'BRIGHT_BLUE': '\33[104m',
|
||||
'BRIGHT_MAGENTA': '\33[105m',
|
||||
'BRIGHT_CYAN': '\33[106m',
|
||||
'BRIGHT_WHITE': '\33[107m',
|
||||
'BLACK': '\33[40m',
|
||||
'RED': '\33[41m',
|
||||
'GREEN': '\33[42m',
|
||||
'YELLOW': '\33[43m',
|
||||
'BLUE': '\33[44m',
|
||||
'MAGENTA': '\33[45m',
|
||||
'CYAN': '\33[46m',
|
||||
'WHITE': '\33[47m',
|
||||
'GREY': '\33[100m',
|
||||
'BRIGHT_RED': '\33[101m',
|
||||
'BRIGHT_GREEN': '\33[102m',
|
||||
'BRIGHT_YELLOW': '\33[103m',
|
||||
'BRIGHT_BLUE': '\33[104m',
|
||||
'BRIGHT_MAGENTA': '\33[105m',
|
||||
'BRIGHT_CYAN': '\33[106m',
|
||||
'BRIGHT_WHITE': '\33[107m',
|
||||
}
|
||||
|
||||
|
||||
def log_welcome(num_nodes=None):
|
||||
msg='{}{}rgthree\'s comfy nodes:{}{} Loaded'.format(COLORS_FG['GREEN'], COLORS_STYLE['BOLD'], COLORS_STYLE['RESET'], COLORS_STYLE['BOLD'])
|
||||
if num_nodes:
|
||||
print('{} {} exciting nodes.{}'.format(msg, num_nodes, COLORS_STYLE['RESET']))
|
||||
else:
|
||||
print('{}.{}'.format(msg, COLORS_STYLE['RESET']))
|
||||
"""Logs the welcome message."""
|
||||
msg = f"{COLORS_FG['GREEN']}{COLORS_STYLE['BOLD']}[rgthree] Loaded"
|
||||
print()
|
||||
if num_nodes:
|
||||
print(f"{msg} {num_nodes} exciting nodes.{COLORS_STYLE['RESET']}")
|
||||
else:
|
||||
print(f"{msg}.{COLORS_STYLE['RESET']}")
|
||||
|
||||
|
||||
def log_node_success(node_name, message):
|
||||
_log_node(COLORS_FG["GREEN"], node_name, message, prefix='✓ ')
|
||||
"""Logs a success message."""
|
||||
_log_node(COLORS_FG["GREEN"], node_name, message)
|
||||
|
||||
|
||||
def log_node_info(node_name, message):
|
||||
_log_node(COLORS_FG["CYAN"], node_name, message, prefix='🛈 ')
|
||||
"""Logs an info message."""
|
||||
_log_node(COLORS_FG["CYAN"], node_name, message)
|
||||
|
||||
def log_node(node_name, message):
|
||||
_log_node(COLORS_FG["CYAN"], node_name, message, prefix=' ')
|
||||
|
||||
def log_node_warn(node_name, message):
|
||||
_log_node(COLORS_FG["YELLOW"], node_name, message, prefix='⚠ ')
|
||||
"""Logs an warn message."""
|
||||
_log_node(COLORS_FG["YELLOW"], node_name, message)
|
||||
|
||||
|
||||
def log_node(node_name, message):
|
||||
"""Logs a message."""
|
||||
_log_node(COLORS_FG["CYAN"], node_name, message)
|
||||
|
||||
|
||||
def _log_node(color, node_name, message, prefix=''):
|
||||
print(f'{COLORS_STYLE["BOLD"]}{color}{prefix}rgthree {node_name.replace(" (rgthree)", "")}:{COLORS_STYLE["RESET"]} {message}')
|
||||
print(_get_log_msg(color, node_name, message, prefix=prefix))
|
||||
|
||||
|
||||
def _get_log_msg(color, node_name, message, prefix=''):
|
||||
msg = f'{COLORS_STYLE["BOLD"]}{color}{prefix}[rgthree] {node_name.replace(" (rgthree)", "")}'
|
||||
msg += f':{COLORS_STYLE["RESET"]} {message}'
|
||||
return msg
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ class RgthreeLoraLoaderStack:
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
|
||||
+78
-87
@@ -1,104 +1,95 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
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')
|
||||
|
||||
def get_and_strip_loras(prompt, silent=False):
|
||||
pattern='<lora:([^:>]*?)(?::(-?\d*(?:\.\d*)?))?>'
|
||||
lora_paths=folder_paths.get_filename_list('loras')
|
||||
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_filename=match[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(NODE_NAME, f'Skipping "{tag_filename}" with strength of zero')
|
||||
|
||||
# Let's be flexible. If the lora filename in the tag doesn't have the extension or
|
||||
# path prefix, let's still find and load it.
|
||||
if tag_filename not in lora_paths:
|
||||
found_tag_filename=None
|
||||
for index, value in enumerate(lora_filenames_no_ext):
|
||||
if value in tag_filename:
|
||||
found_tag_filename=lora_paths[index]
|
||||
break
|
||||
if found_tag_filename:
|
||||
# if not silent:
|
||||
# log_node_info(NODE_NAME, f'Found "{found_tag_filename}" for "{tag_filename}" in prompt')
|
||||
tag_filename=found_tag_filename
|
||||
else:
|
||||
if not silent:
|
||||
log_node_warn(NODE_NAME, f'Lora "{tag_filename}" not found, skipping.')
|
||||
continue
|
||||
|
||||
loras.append({'lora':tag_filename, 'strength':strength})
|
||||
|
||||
return (re.sub(pattern, '', prompt), loras)
|
||||
|
||||
NODE_NAME = get_name('Power Prompt')
|
||||
|
||||
|
||||
class RgthreePowerPrompt:
|
||||
|
||||
NAME = NODE_NAME
|
||||
CATEGORY = get_category()
|
||||
NAME = NODE_NAME
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
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,),
|
||||
}
|
||||
}
|
||||
@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'
|
||||
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, 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)
|
||||
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, 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, f'Loras not processed, keeping for TEXT output.')
|
||||
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, skipped, unfound = get_and_strip_loras(prompt, log_node=NODE_NAME, silent=True)
|
||||
total_loras = len(loras) + len(skipped) + len(unfound)
|
||||
if total_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)
|
||||
conditioning = None
|
||||
if opt_clip != None:
|
||||
conditioning = CLIPTextEncode().encode(opt_clip, prompt)[0]
|
||||
|
||||
return (conditioning, opt_model, opt_clip, prompt)
|
||||
|
||||
@@ -10,7 +10,7 @@ class RgthreePowerPromptSimple(RgthreePowerPrompt):
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""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 = []
|
||||
unfound_loras = []
|
||||
skipped_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:
|
||||
if not silent:
|
||||
log_node_info(log_node, f'Skipping "{tag_path}" with strength of zero')
|
||||
skipped_loras.append({'lora': tag_path, 'strength': strength})
|
||||
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.')
|
||||
unfound_loras.append({'lora': tag_path, 'strength': strength})
|
||||
continue
|
||||
|
||||
loras.append({'lora': tag_path, 'strength': strength})
|
||||
|
||||
return (re.sub(pattern, '', prompt), loras, skipped_loras, unfound_loras)
|
||||
@@ -1,49 +1,63 @@
|
||||
from nodes import EmptyLatentImage
|
||||
from .constants import get_category, get_name
|
||||
|
||||
|
||||
class RgthreeSDXLEmptyLatentImage:
|
||||
|
||||
NAME = get_name('SDXL Empty Latent Image')
|
||||
CATEGORY = get_category()
|
||||
NAME = get_name('SDXL Empty Latent Image')
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
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}),
|
||||
}
|
||||
}
|
||||
@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"
|
||||
RETURN_TYPES = ("LATENT", "INT", "INT")
|
||||
RETURN_NAMES = ("LATENT", "CLIP_WIDTH", "CLIP_HEIGHT")
|
||||
FUNCTION = "generate"
|
||||
|
||||
# def generate(self, dimensions, clip_scale, batch_size, custom_width=1024, custom_height=1024):
|
||||
def generate(self, dimensions, clip_scale, batch_size):
|
||||
# if dimensions == 'Custom':
|
||||
# width = custom_width
|
||||
# height = custom_height
|
||||
# else:
|
||||
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),)
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
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, skipped_g, unfound_g = get_and_strip_loras(prompt_g, True)
|
||||
prompt_l, loras_l, skipped_l, unfound_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, skipped_g, unfound_g = get_and_strip_loras(prompt_g)
|
||||
prompt_l, loras_l, skipped_l, unfound_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, skipped_g, unfound_g = get_and_strip_loras(prompt_g, True)
|
||||
_prompt_stripped_l, loras_l, skipped_l, unfound_l = get_and_strip_loras(prompt_l, True)
|
||||
loras = len(loras_g) + len(loras_l)
|
||||
unfound_loras = len(unfound_g) + len(unfound_l)
|
||||
total_loras = loras + unfound_loras + len(skipped_g) + len(skipped_l)
|
||||
if total_loras:
|
||||
log_node_warn(NODE_NAME, f'Found {total_loras} lora tags in prompt but model & clip were not supplied!')
|
||||
if unfound_loras:
|
||||
log_node_warn(NODE_NAME, f'Note: {unfound_loras} lora tags were not found!')
|
||||
log_node_info(NODE_NAME, 'Loras not processed but NOT stripped. Keeping in case used 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
|
||||
@@ -0,0 +1,106 @@
|
||||
"""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)
|
||||
+22
-15
@@ -1,22 +1,29 @@
|
||||
"""See node."""
|
||||
from .constants import get_category, get_name
|
||||
|
||||
|
||||
class RgthreeSeed:
|
||||
"""See node."""
|
||||
|
||||
NAME = get_name('Seed')
|
||||
CATEGORY = get_category()
|
||||
NAME = get_name('Seed')
|
||||
CATEGORY = get_category()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"seed": ("INT", {"default": 0, "min": -1125899906842624, "max": 1125899906842624}),
|
||||
},
|
||||
}
|
||||
@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):
|
||||
return (seed,)
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("SEED",)
|
||||
FUNCTION = "main"
|
||||
|
||||
def main(self, seed=0):
|
||||
"""Returns the passed seed on execution."""
|
||||
return (seed,)
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
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("*")
|
||||
|
||||
|
||||
class ByPassTypeTuple(tuple):
|
||||
"""A special class that will return additional "AnyType" strings beyond defined values.
|
||||
Credit to Trung0246
|
||||
"""
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index > len(self) - 1:
|
||||
return AnyType("*")
|
||||
return super().__getitem__(index)
|
||||
@@ -0,0 +1,13 @@
|
||||
// COPY THIS FILE BEFORE MAKING CHANGES TO: rgthree_config.json
|
||||
{
|
||||
"patch_recursive_execution": true,
|
||||
"monitor_bad_links": false,
|
||||
"nodes": {
|
||||
"reroute": {
|
||||
"default_width": 40,
|
||||
"default_height": 30,
|
||||
"default_resizable": false,
|
||||
"default_layout": ["Left", "Right"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// / <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 || connectedType?.name || 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"]]);
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.AnySwitch",
|
||||
async beforeRegisterNodeDef(
|
||||
nodeType: LGraphNodeConstructor,
|
||||
nodeData: ComfyObjectInfo,
|
||||
app: ComfyApp,
|
||||
) {
|
||||
if (nodeData.name === "Any Switch (rgthree)") {
|
||||
AnySwitchforMixin.setUp(nodeType as any);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,308 @@
|
||||
// / <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 type {Vector2, LLink, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} 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();
|
||||
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;
|
||||
}
|
||||
+202
-13
@@ -1,22 +1,42 @@
|
||||
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
|
||||
// @ts-ignore
|
||||
import {app} from "../../scripts/app.js";
|
||||
import { NodeMode } from "./typings/comfy.js";
|
||||
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
|
||||
import type {IWidget, SerializedLGraphNode, LiteGraph as TLiteGraph, LGraphNode as TLGraphNode, LGraphCanvas} 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";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
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';
|
||||
|
||||
isVirtualNode = true;
|
||||
/** 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;
|
||||
|
||||
configuring = false;
|
||||
|
||||
isVirtualNode = false;
|
||||
|
||||
constructor(title = RgthreeBaseNode.title) {
|
||||
super(title);
|
||||
@@ -26,23 +46,192 @@ export class RgthreeBaseNode extends LGraphNode {
|
||||
this.properties = this.properties || {};
|
||||
}
|
||||
|
||||
mode_: NodeMode;
|
||||
|
||||
/** When a mode change, we want all connected nodes to match. */
|
||||
onModeChange() {
|
||||
// Override
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static setUp<T extends RgthreeBaseNode>(...args: any[]) {
|
||||
// No-op.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
rgthree.logger.debug(`For "${nodeId}", replacing default ComfyNode implementation with custom ${
|
||||
clazz.type || clazz.name || clazz.title} class.`);
|
||||
}
|
||||
|
||||
return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
|
||||
}
|
||||
|
||||
+46
-48
@@ -1,17 +1,19 @@
|
||||
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
|
||||
import type { LLink, INodeOutputSlot, LGraphNode } from "litegraph.js";
|
||||
// @ts-ignore
|
||||
import { app } from "../../scripts/app.js";
|
||||
// @ts-ignore
|
||||
import { ComfyWidgets } from "../../scripts/widgets.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";
|
||||
|
||||
import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode} from './typings/litegraph.js';
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
/**
|
||||
* Base collector node that monitors changing inputs and outputs.
|
||||
*/
|
||||
export class BaseCollectorNode extends BaseAnyInputConnectedNode {
|
||||
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
|
||||
export class BaseCollectorNode extends RgthreeBaseNode {
|
||||
|
||||
override isVirtualNode = true;
|
||||
/**
|
||||
* 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);
|
||||
@@ -24,45 +26,41 @@ export class BaseCollectorNode extends RgthreeBaseNode {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
private updateOutputLinks(startNode: TLGraphNode = this) {
|
||||
const type = (startNode.constructor as typeof TLGraphNode).type;
|
||||
// @ts-ignore
|
||||
if (startNode.onConnectionsChainChange) {
|
||||
// @ts-ignore
|
||||
startNode.onConnectionsChainChange();
|
||||
}
|
||||
if (startNode === this || type?.includes('Reroute') || type?.includes('Combiner')) {
|
||||
for (const output of startNode.outputs) {
|
||||
if (!output.links || !output.links.length) continue;
|
||||
for (const linkId of output.links) {
|
||||
const link: LLink = (app.graph as LGraph).links[linkId]!;
|
||||
if (!link) continue;
|
||||
const targetNode: TLGraphNode = (app.graph as LGraph).getNodeById(link.target_id)!;
|
||||
targetNode && this.updateOutputLinks(targetNode)
|
||||
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;
|
||||
}
|
||||
|
||||
override onConnectionsChange(_type: number, _slotIndex: number, _isConnected: boolean, link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
|
||||
if (!link_info) return;
|
||||
this.stabilizeInputsOutputs();
|
||||
// Follow outputs to see if we need to trigger an onConnectionChange.
|
||||
this.updateOutputLinks();
|
||||
}
|
||||
|
||||
private stabilizeInputsOutputs() {
|
||||
for (let index = this.inputs.length - 1; index >= 0; index--) {
|
||||
const input = this.inputs[index]!;
|
||||
if (!input.link) {
|
||||
this.removeInput(index);
|
||||
}
|
||||
}
|
||||
this.addInput('', '*');
|
||||
|
||||
const outputLength = this.outputs[0]?.links?.length || 0;
|
||||
if (outputLength > 1) {
|
||||
this.outputs[0]!.links!.length = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
// / <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 {Vector2, LLink, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
|
||||
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, wait } from "./utils.js";
|
||||
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
|
||||
import { PassThroughFollowing } from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
|
||||
export class BaseNodeModeChanger extends RgthreeBaseNode {
|
||||
export class BaseNodeModeChanger extends BaseAnyInputConnectedNode {
|
||||
|
||||
override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL;
|
||||
|
||||
static collapsible = false;
|
||||
|
||||
override isVirtualNode = true;
|
||||
debouncer: number = 0;
|
||||
schedulePromise: Promise<void> | null = null;
|
||||
|
||||
// These Must be overriden
|
||||
readonly modeOn: number = -1;
|
||||
@@ -28,27 +29,10 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
|
||||
throw new Error('modeOn and modeOff must be overridden.');
|
||||
}
|
||||
});
|
||||
this.addInput("", "*");
|
||||
this.addOutput("OPT_CONNECTION", "*");
|
||||
}
|
||||
|
||||
scheduleStabilizeWidgets() {
|
||||
if (!this.schedulePromise) {
|
||||
this.schedulePromise = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(this.stabilizeWidgets());
|
||||
this.schedulePromise = null;
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
return this.schedulePromise;
|
||||
}
|
||||
|
||||
stabilizeWidgets() {
|
||||
if (!this.graph) {
|
||||
return;
|
||||
}
|
||||
const linkedNodes = getConnectedInputNodes(app, this);
|
||||
this.stabilizeInputsOutputs();
|
||||
override handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
|
||||
for (const [index, node] of linkedNodes.entries()) {
|
||||
let widget = this.widgets && this.widgets[index];
|
||||
if (!widget) {
|
||||
@@ -57,16 +41,11 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
|
||||
(this as any)._tempWidth = this.size[0];
|
||||
widget = this.addWidget('toggle', '', false, '', {"on": 'yes', "off": 'no'});
|
||||
}
|
||||
this.setWidget(widget, node);
|
||||
node && this.setWidget(widget, node);
|
||||
}
|
||||
if (this.widgets && this.widgets.length > linkedNodes.length) {
|
||||
// When we 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];
|
||||
this.widgets.length = linkedNodes.length
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
setTimeout(() => { this.stabilizeWidgets(); }, 500);
|
||||
}
|
||||
|
||||
setWidget(widget: IWidget, linkedNode: TLGraphNode) {
|
||||
@@ -74,83 +53,26 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
|
||||
widget.name = `Enable ${linkedNode.title}`;
|
||||
widget.options = {'on': 'yes', 'off': 'no'}
|
||||
widget.value = !off;
|
||||
widget.callback = () => {
|
||||
const off = linkedNode.mode === this.modeOff;
|
||||
(widget as any).doModeChange = (force?: boolean) => {
|
||||
let off = force == null ? linkedNode.mode === this.modeOff : force;
|
||||
linkedNode.mode = (off ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
|
||||
widget!.value = off;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onConnectionsChainChange() {
|
||||
this.scheduleStabilizeWidgets();
|
||||
}
|
||||
|
||||
override onConnectionsChange(_type: number, _index: number, _connected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
|
||||
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);
|
||||
}
|
||||
|
||||
private stabilizeInputsOutputs() {
|
||||
let hasEmptyInput = false;
|
||||
for (let index = this.inputs.length - 1; index >= 0; index--) {
|
||||
const input = this.inputs[index]!;
|
||||
if (!input.link) {
|
||||
if (index < this.inputs.length - 1) {
|
||||
this.removeInput(index);
|
||||
} else {
|
||||
hasEmptyInput = true;
|
||||
}
|
||||
}
|
||||
widget.callback = () => {
|
||||
(widget as any).doModeChange();
|
||||
}
|
||||
!hasEmptyInput && this.addInput('', '*');
|
||||
}
|
||||
|
||||
override computeSize(out: Vector2) {
|
||||
let size = super.computeSize(out);
|
||||
if ((this as any)._tempWidth) {
|
||||
size[0] = (this as any)._tempWidth;
|
||||
(this as any)._tempWidth = null;
|
||||
}
|
||||
// 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;
|
||||
forceWidgetOff(widget: IWidget) {
|
||||
(widget as any).doModeChange(false);
|
||||
}
|
||||
forceWidgetOn(widget: IWidget) {
|
||||
(widget as any).doModeChange(true);
|
||||
}
|
||||
|
||||
static setUp<T extends BaseNodeModeChanger>(clazz: new(...args: any[]) => T) {
|
||||
// @ts-ignore: Fix incorrect litegraph typings.
|
||||
addMenuItem(clazz, app, {
|
||||
name: 'Refresh',
|
||||
callback: (node) => {(node as T).scheduleStabilizeWidgets()}
|
||||
});
|
||||
|
||||
// @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)}
|
||||
});
|
||||
|
||||
// @ts-ignore: Fix incorrect litegraph typings.
|
||||
addConnectionLayoutSupport(clazz, app, [['Left'],['Right']]);
|
||||
|
||||
LiteGraph.registerNodeType((clazz as any).type, clazz);
|
||||
(clazz as any).category = (clazz as any)._category;
|
||||
static override setUp<T extends RgthreeBaseNode>(clazz: new(title?: string) => T) {
|
||||
BaseAnyInputConnectedNode.setUp(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
// / <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} from './typings/litegraph.js';
|
||||
import type {ComfyObjectInfo, ComfyGraphNode} from './typings/comfy.js';
|
||||
import { wait } from "./shared_utils.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;
|
||||
|
||||
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 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 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 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ const MODE_ALWAYS = 0;
|
||||
|
||||
class BypasserNode extends BaseNodeModeChanger {
|
||||
|
||||
static override exposedActions = ['Bypass all', 'Enable all'];
|
||||
|
||||
static override type = NodeTypesString.FAST_BYPASSER;
|
||||
static override title = NodeTypesString.FAST_BYPASSER;
|
||||
override readonly modeOn = MODE_ALWAYS;
|
||||
@@ -18,6 +20,19 @@ class BypasserNode extends BaseNodeModeChanger {
|
||||
constructor(title = BypasserNode.title) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
|
||||
override async handleAction(action: string) {
|
||||
if (action === 'Bypass all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOff(widget);
|
||||
}
|
||||
} else if (action === 'Enable all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOn(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
@@ -12,4 +12,8 @@ export const NodeTypesString = {
|
||||
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
|
||||
FAST_MUTER: addRgthree('Fast Muter'),
|
||||
FAST_BYPASSER: addRgthree('Fast Bypasser'),
|
||||
FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'),
|
||||
NODE_COLLECTOR: addRgthree('Node Collector'),
|
||||
REROUTE: addRgthree('Reroute'),
|
||||
RANDOM_UNMUTER: addRgthree('Random Unmuter'),
|
||||
}
|
||||
+337
-14
@@ -1,23 +1,346 @@
|
||||
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
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";
|
||||
// @ts-ignore
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
|
||||
import type {ComfyApp, ComfyObjectInfo} from './typings/comfy.js'
|
||||
import { addConnectionLayoutSupport } from "./utils.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
import {
|
||||
IoDirection,
|
||||
addConnectionLayoutSupport,
|
||||
addMenuItem,
|
||||
matchLocalSlotsToServer,
|
||||
replaceNode,
|
||||
} from "./utils.js";
|
||||
import { RgthreeBaseNode, RgthreeBaseServerNode } from "./base_node.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
|
||||
/**
|
||||
* 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', '');
|
||||
const otherSlotLabel = (otherSlot.label || otherSlotName).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', '');
|
||||
const ctxSlotLabel = (ctxSlot.label || ctxSlotName).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
|
||||
|| (ctxSlotLabel && otherSlotLabel && ctxSlotLabel == otherSlotLabel)
|
||||
|| (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.
|
||||
*/
|
||||
export class BaseContextNode extends RgthreeBaseServerNode {
|
||||
constructor(title: string) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
addMenuItem(ContextSwitchBigNode, app, {
|
||||
name: "Convert To Context Switch",
|
||||
callback: (node) => {
|
||||
replaceNode(node, ContextSwitchNode.type);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addWidgetForDynamicContextOutputs(node: TLGraphNode, inputName: string) {
|
||||
node.addCustomWidget({
|
||||
name: inputName,
|
||||
value: '',
|
||||
draw(ctx, node, width, posY, height) {
|
||||
return;
|
||||
},
|
||||
computeSize(width) {
|
||||
return [0,0];
|
||||
},
|
||||
serializeValue() {
|
||||
const value = (node.outputs || []).map((o, i) => i > 0 && o.name).filter(n => n !== false).join(',');
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const contextNodes = [ContextNode, ContextBigNode, ContextSwitchNode, ContextSwitchBigNode];
|
||||
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: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) {
|
||||
if (nodeData.name === "Context (rgthree)") {
|
||||
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) {
|
||||
}
|
||||
|
||||
// This isn't super useful, because R->L removes the names in order to work with
|
||||
// litegraph's hardcoded L->R math.. but, ¯\_(ツ)_/¯
|
||||
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
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);
|
||||
if (!type!.includes('Dynamic')) {
|
||||
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
|
||||
// Switches don't need to change inputs, only context outputs
|
||||
if (!type!.includes("Switch")) {
|
||||
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);
|
||||
if (!type!.includes('Dynamic')) {
|
||||
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
|
||||
// Switches don't need to change inputs, only context outputs
|
||||
if (!type!.includes("Switch")) {
|
||||
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
// / <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";
|
||||
|
||||
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: SerializedLGraphNode,
|
||||
index: number,
|
||||
) => {
|
||||
// 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.
|
||||
node.widgets_values![index] = "";
|
||||
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,43 +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, ComfyWidget} from './typings/comfy.js'
|
||||
import { addConnectionLayoutSupport } from "./utils.js";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.DisplayInt",
|
||||
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) {
|
||||
if (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: SerializedLGraphNode, index: number) => {
|
||||
// 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.
|
||||
node.widgets_values![index] = '';
|
||||
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];
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,366 @@
|
||||
// / <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];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
|
||||
import type { SerializedGraph, BadLinksData } from "index";
|
||||
import { fixBadLinks } from "../link_fixer.js";
|
||||
// @ts-ignore
|
||||
import { getPngMetadata } from "/scripts/pnginfo.js";
|
||||
|
||||
|
||||
function wait(ms = 16, value?: any) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(value);
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
const logger = {
|
||||
logTo: console as Console | HTMLElement,
|
||||
log: (...args: any[]) => {
|
||||
logger.logTo === console
|
||||
? console.log(...args)
|
||||
: ((logger.logTo as HTMLElement).innerText += args.join(",") + "\n");
|
||||
},
|
||||
};
|
||||
|
||||
const findBadLinksLogger = {
|
||||
log: async (...args: any[]) => {
|
||||
logger.log(...args);
|
||||
// await wait(48);
|
||||
},
|
||||
};
|
||||
|
||||
export class LinkPage {
|
||||
private containerEl: HTMLDivElement;
|
||||
private figcaptionEl: HTMLElement;
|
||||
private btnFix: HTMLButtonElement;
|
||||
private outputeMessageEl: HTMLDivElement;
|
||||
private outputImageEl: HTMLImageElement;
|
||||
|
||||
private file?: File | Blob;
|
||||
private graph?: SerializedGraph;
|
||||
private graphResults?: BadLinksData;
|
||||
private graphFinalResults?: BadLinksData;
|
||||
|
||||
constructor() {
|
||||
// const consoleEl = document.getElementById("console")!;
|
||||
this.containerEl = document.querySelector(".box")!;
|
||||
this.figcaptionEl = document.querySelector("figcaption")!;
|
||||
this.outputeMessageEl = document.querySelector(".output")!;
|
||||
this.outputImageEl = document.querySelector(".output-image")!;
|
||||
this.btnFix = document.querySelector(".btn-fix")!;
|
||||
|
||||
// Need to prevent on dragover to allow drop...
|
||||
document.addEventListener(
|
||||
"dragover",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
false,
|
||||
);
|
||||
document.addEventListener("drop", (e) => {
|
||||
this.onDrop(e);
|
||||
});
|
||||
this.btnFix.addEventListener("click", (e) => {
|
||||
this.onFixClick(e);
|
||||
});
|
||||
}
|
||||
|
||||
private async onFixClick(e: MouseEvent) {
|
||||
if (!this.graphResults || !this.graph) {
|
||||
this.updateUi("⛔ Fix button click without results.");
|
||||
return;
|
||||
}
|
||||
// Fix
|
||||
let graphFinalResults = fixBadLinks(this.graph, true);
|
||||
// Confirm
|
||||
graphFinalResults = fixBadLinks(graphFinalResults.graph, true);
|
||||
// This should have happened, but try to run it through again if there's till an issue.
|
||||
if (graphFinalResults.patched || graphFinalResults.deleted) {
|
||||
graphFinalResults = fixBadLinks(graphFinalResults.graph, true);
|
||||
}
|
||||
this.graphFinalResults = graphFinalResults;
|
||||
|
||||
await this.saveFixedWorkflow();
|
||||
|
||||
if (graphFinalResults.hasBadLinks) {
|
||||
this.updateUi("⛔ Hmm... Still detecting bad links. Can you file an issue at https://github.com/rgthree/rgthree-comfy/issues with your image/workflow.");
|
||||
} else {
|
||||
this.updateUi("✅ Workflow fixed.<br><br><small>Please load new saved workflow json and double check linking and execution.</small>");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async onDrop(event: DragEvent) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
|
||||
if (event.dataTransfer.files.length && event.dataTransfer.files?.[0]?.type !== "image/bmp") {
|
||||
await this.handleFile(event.dataTransfer.files[0]!);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try loading the first URI in the transfer list
|
||||
const validTypes = ["text/uri-list", "text/x-moz-url"];
|
||||
const match = [...event.dataTransfer.types].find((t) => validTypes.find((v) => t === v));
|
||||
if (match) {
|
||||
const uri = event.dataTransfer.getData(match)?.split("\n")?.[0];
|
||||
if (uri) {
|
||||
await this.handleFile(await (await fetch(uri)).blob());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.file = undefined;
|
||||
this.graph = undefined;
|
||||
this.graphResults = undefined;
|
||||
this.graphFinalResults = undefined;
|
||||
this.updateUi();
|
||||
}
|
||||
|
||||
private updateUi(msg?: string) {
|
||||
this.outputeMessageEl.innerHTML = "";
|
||||
if (this.file && !this.containerEl.classList.contains("-has-file")) {
|
||||
this.containerEl.classList.add("-has-file");
|
||||
this.figcaptionEl.innerHTML = (this.file as File).name || this.file.type;
|
||||
if (this.file.type === "application/json") {
|
||||
this.outputImageEl.src = "icon_file_json.png";
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => (this.outputImageEl.src = reader.result as string);
|
||||
reader.readAsDataURL(this.file);
|
||||
}
|
||||
} else if (!this.file && this.containerEl.classList.contains("-has-file")) {
|
||||
this.containerEl.classList.remove("-has-file");
|
||||
this.outputImageEl.src = "";
|
||||
this.outputImageEl.removeAttribute("src");
|
||||
}
|
||||
|
||||
if (this.graphResults) {
|
||||
this.containerEl.classList.add("-has-results");
|
||||
if (!this.graphResults.patched && !this.graphResults.deleted) {
|
||||
this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow.";
|
||||
} else {
|
||||
this.containerEl.classList.add("-has-fixable-results");
|
||||
this.outputeMessageEl.innerHTML = `⚠️ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`;
|
||||
}
|
||||
} else {
|
||||
this.containerEl.classList.remove("-has-results");
|
||||
this.containerEl.classList.remove("-has-fixable-results");
|
||||
}
|
||||
|
||||
if (msg) {
|
||||
this.outputeMessageEl.innerHTML = msg;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFile(file: File | Blob) {
|
||||
this.file = file;
|
||||
this.updateUi();
|
||||
|
||||
let workflow: string | null = null;
|
||||
if (file.type.startsWith("image/")) {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
workflow = pngInfo?.workflow;
|
||||
} else if (
|
||||
file.type === "application/json" ||
|
||||
(file instanceof File && file.name.endsWith(".json"))
|
||||
) {
|
||||
workflow = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
if (!workflow) {
|
||||
this.updateUi("⛔ No workflow found in dropped item.");
|
||||
} else {
|
||||
try {
|
||||
this.graph = JSON.parse(workflow);
|
||||
} catch (e) {
|
||||
this.graph = undefined;
|
||||
}
|
||||
if (!this.graph) {
|
||||
this.updateUi("⛔ Invalid workflow found in dropped item.");
|
||||
} else {
|
||||
this.loadGraphData(this.graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadGraphData(graphData: SerializedGraph) {
|
||||
this.graphResults = await fixBadLinks(graphData);
|
||||
this.updateUi();
|
||||
}
|
||||
|
||||
private async saveFixedWorkflow() {
|
||||
if (!this.graphFinalResults) {
|
||||
this.updateUi("⛔ Save w/o final graph patched.");
|
||||
return false;
|
||||
}
|
||||
|
||||
let filename: string|null = (this.file as File).name || 'workflow.json';
|
||||
let filenames = filename.split('.');
|
||||
filenames.pop();
|
||||
filename = filenames.join('.');
|
||||
filename += '_fixed.json';
|
||||
filename = prompt("Save workflow as:", filename);
|
||||
if (!filename) return false;
|
||||
if (!filename.toLowerCase().endsWith(".json")) {
|
||||
filename += ".json";
|
||||
}
|
||||
const json = JSON.stringify(this.graphFinalResults.graph, null, 2);
|
||||
const blob = new Blob([json], {type: "application/json"});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.download = filename;
|
||||
anchor.href = url;
|
||||
anchor.style.display = 'none';
|
||||
document.body.appendChild(anchor);
|
||||
await wait();
|
||||
anchor.click();
|
||||
await wait();
|
||||
anchor.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// / <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 "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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,394 @@
|
||||
import type { BadLinksData, SerializedGraph, SerializedLink, SerializedNode } from "index";
|
||||
import type { LGraph, LGraphNode, LLink } from "./typings/litegraph";
|
||||
|
||||
enum IoDirection {
|
||||
INPUT,
|
||||
OUTPUT,
|
||||
}
|
||||
|
||||
function getNodeById(graph: SerializedGraph | LGraph, id: number) {
|
||||
if ((graph as LGraph).getNodeById) {
|
||||
return (graph as LGraph).getNodeById(id);
|
||||
}
|
||||
graph = graph as SerializedGraph;
|
||||
return graph.nodes.find((n) => n.id === id)!;
|
||||
}
|
||||
|
||||
function extendLink(link: SerializedLink) {
|
||||
return {
|
||||
link: link,
|
||||
id: link[0],
|
||||
origin_id: link[1],
|
||||
origin_slot: link[2],
|
||||
target_id: link[3],
|
||||
target_slot: link[4],
|
||||
type: link[5],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a SerializedGraph or live LGraph and inspects the links and nodes to ensure the linking
|
||||
* makes logical sense. Can apply fixes when passed the `fix` argument as true.
|
||||
*
|
||||
* Note that fixes are a best-effort attempt. Seems to get it correct in most cases, but there is a
|
||||
* chance it correct an anomoly that results in placing an incorrect link (say, if there were two
|
||||
* links in the data). Users should take care to not overwrite work until manually checking the
|
||||
* result.
|
||||
*/
|
||||
export function fixBadLinks(
|
||||
graph: SerializedGraph | LGraph,
|
||||
fix = false,
|
||||
silent = false,
|
||||
logger: { log: (...args: any[]) => void } = console,
|
||||
): BadLinksData {
|
||||
const patchedNodeSlots: {
|
||||
[nodeId: string]: {
|
||||
inputs?: { [slot: number]: number | null };
|
||||
outputs?: {
|
||||
[slots: number]: {
|
||||
links: number[];
|
||||
changes: { [linkId: number]: "ADD" | "REMOVE" };
|
||||
};
|
||||
};
|
||||
};
|
||||
} = {};
|
||||
// const logger = this.newLogSession("[findBadLinks]");
|
||||
const data: { patchedNodes: Array<SerializedNode | LGraphNode>; deletedLinks: number[] } = {
|
||||
patchedNodes: [],
|
||||
deletedLinks: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal patch node. We keep track of changes in patchedNodeSlots in case we're in a dry run.
|
||||
*/
|
||||
async function patchNodeSlot(
|
||||
node: SerializedNode | LGraphNode,
|
||||
ioDir: IoDirection,
|
||||
slot: number,
|
||||
linkId: number,
|
||||
op: "ADD" | "REMOVE",
|
||||
) {
|
||||
patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {};
|
||||
const patchedNode = patchedNodeSlots[node.id]!;
|
||||
if (ioDir == IoDirection.INPUT) {
|
||||
patchedNode["inputs"] = patchedNode["inputs"] || {};
|
||||
// We can set to null (delete), so undefined means we haven't set it at all.
|
||||
if (patchedNode["inputs"]![slot] !== undefined) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"]![
|
||||
slot
|
||||
]!} Skipping.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
let linkIdToSet = op === "REMOVE" ? null : linkId;
|
||||
patchedNode["inputs"]![slot] = linkIdToSet;
|
||||
if (fix) {
|
||||
// node.inputs[slot]!.link = linkIdToSet;
|
||||
}
|
||||
} else {
|
||||
patchedNode["outputs"] = patchedNode["outputs"] || {};
|
||||
patchedNode["outputs"]![slot] = patchedNode["outputs"]![slot] || {
|
||||
links: [...(node.outputs?.[slot]?.links || [])],
|
||||
changes: {},
|
||||
};
|
||||
if (patchedNode["outputs"]![slot]!["changes"]![linkId] !== undefined) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
` > Already set ${node.id}.outputs[${slot}] to ${
|
||||
patchedNode["inputs"]![slot]
|
||||
}! Skipping.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
patchedNode["outputs"]![slot]!["changes"]![linkId] = op;
|
||||
if (op === "ADD") {
|
||||
let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId);
|
||||
if (linkIdIndex !== -1) {
|
||||
!silent && logger.log(` > Hmmm.. asked to add ${linkId} but it is already in list...`);
|
||||
return false;
|
||||
}
|
||||
patchedNode["outputs"]![slot]!["links"].push(linkId);
|
||||
if (fix) {
|
||||
node.outputs = node.outputs || [];
|
||||
node.outputs[slot] = node.outputs[slot] || ({} as any);
|
||||
node.outputs[slot]!.links = node.outputs[slot]!.links || [];
|
||||
node.outputs[slot]!.links!.push(linkId);
|
||||
}
|
||||
} else {
|
||||
let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId);
|
||||
if (linkIdIndex === -1) {
|
||||
!silent && logger.log(` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`);
|
||||
return false;
|
||||
}
|
||||
patchedNode["outputs"]![slot]!["links"].splice(linkIdIndex, 1);
|
||||
if (fix) {
|
||||
node.outputs?.[slot]!.links!.splice(linkIdIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.patchedNodes.push(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal to check if a node (or patched data) has a linkId.
|
||||
*/
|
||||
function nodeHasLinkId(
|
||||
node: SerializedNode | LGraphNode,
|
||||
ioDir: IoDirection,
|
||||
slot: number,
|
||||
linkId: number,
|
||||
) {
|
||||
// Patched data should be canonical. We can double check if fixing too.
|
||||
let has = false;
|
||||
if (ioDir === IoDirection.INPUT) {
|
||||
let nodeHasIt = node.inputs?.[slot]?.link === linkId;
|
||||
if (patchedNodeSlots[node.id]?.["inputs"]) {
|
||||
let patchedHasIt = patchedNodeSlots[node.id]!["inputs"]![slot] === linkId;
|
||||
// If we're fixing, double check that node matches.
|
||||
if (fix && nodeHasIt !== patchedHasIt) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
has = patchedHasIt;
|
||||
} else {
|
||||
has = !!nodeHasIt;
|
||||
}
|
||||
} else {
|
||||
let nodeHasIt = node.outputs?.[slot]?.links?.includes(linkId);
|
||||
if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"][linkId]) {
|
||||
let patchedHasIt = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.includes(linkId);
|
||||
// If we're fixing, double check that node matches.
|
||||
if (fix && nodeHasIt !== patchedHasIt) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
has = !!patchedHasIt;
|
||||
} else {
|
||||
has = !!nodeHasIt;
|
||||
}
|
||||
}
|
||||
return has;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal to check if a node (or patched data) has a linkId.
|
||||
*/
|
||||
function nodeHasAnyLink(node: SerializedNode | LGraphNode, ioDir: IoDirection, slot: number) {
|
||||
// Patched data should be canonical. We can double check if fixing too.
|
||||
let hasAny = false;
|
||||
if (ioDir === IoDirection.INPUT) {
|
||||
let nodeHasAny = node.inputs?.[slot]?.link != null;
|
||||
if (patchedNodeSlots[node.id]?.["inputs"]) {
|
||||
let patchedHasAny = patchedNodeSlots[node.id]!["inputs"]![slot] != null;
|
||||
// If we're fixing, double check that node matches.
|
||||
if (fix && nodeHasAny !== patchedHasAny) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
hasAny = patchedHasAny;
|
||||
} else {
|
||||
hasAny = !!nodeHasAny;
|
||||
}
|
||||
} else {
|
||||
let nodeHasAny = node.outputs?.[slot]?.links?.length;
|
||||
if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"]) {
|
||||
let patchedHasAny = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.length;
|
||||
// If we're fixing, double check that node matches.
|
||||
if (fix && nodeHasAny !== patchedHasAny) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
hasAny = !!patchedHasAny;
|
||||
} else {
|
||||
hasAny = !!nodeHasAny;
|
||||
}
|
||||
}
|
||||
return hasAny;
|
||||
}
|
||||
|
||||
let links: Array<SerializedLink | LLink> = [];
|
||||
if (!Array.isArray(graph.links)) {
|
||||
Object.values(graph.links).reduce((acc, v) => {
|
||||
acc[v.id] = v;
|
||||
return acc;
|
||||
}, links);
|
||||
} else {
|
||||
links = graph.links;
|
||||
}
|
||||
|
||||
const linksReverse = [...links];
|
||||
linksReverse.reverse();
|
||||
for (let l of linksReverse) {
|
||||
if (!l) continue;
|
||||
const link = (l as LLink).origin_slot != null ? (l as LLink) : extendLink(l as SerializedLink);
|
||||
|
||||
const originNode = getNodeById(graph, link.origin_id);
|
||||
const originHasLink = () =>
|
||||
nodeHasLinkId(originNode!, IoDirection.OUTPUT, link.origin_slot, link.id);
|
||||
const patchOrigin = (op: "ADD" | "REMOVE", id = link.id) =>
|
||||
patchNodeSlot(originNode!, IoDirection.OUTPUT, link.origin_slot, id, op);
|
||||
|
||||
const targetNode = getNodeById(graph, link.target_id);
|
||||
const targetHasLink = () =>
|
||||
nodeHasLinkId(targetNode!, IoDirection.INPUT, link.target_slot, link.id);
|
||||
const targetHasAnyLink = () => nodeHasAnyLink(targetNode!, IoDirection.INPUT, link.target_slot);
|
||||
const patchTarget = (op: "ADD" | "REMOVE", id = link.id) =>
|
||||
patchNodeSlot(targetNode!, IoDirection.INPUT, link.target_slot, id, op);
|
||||
|
||||
const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`;
|
||||
const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`;
|
||||
|
||||
if (!originNode || !targetNode) {
|
||||
if (!originNode && !targetNode) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
`Link ${link.id} is invalid, ` +
|
||||
`both origin ${link.origin_id} and target ${link.target_id} do not exist`,
|
||||
);
|
||||
} else if (!originNode) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
`Link ${link.id} is funky... ` +
|
||||
`origin ${link.origin_id} does not exist, but target ${link.target_id} does.`,
|
||||
);
|
||||
if (targetHasLink()) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`,
|
||||
);
|
||||
patchTarget("REMOVE", -1);
|
||||
}
|
||||
} else if (!targetNode) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
`Link ${link.id} is funky... ` +
|
||||
`target ${link.target_id} does not exist, but origin ${link.origin_id} does.`,
|
||||
);
|
||||
if (originHasLink()) {
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`);
|
||||
patchOrigin("REMOVE");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetHasLink() || originHasLink()) {
|
||||
if (!originHasLink()) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`,
|
||||
);
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`);
|
||||
patchOrigin("ADD");
|
||||
} else if (!targetHasLink()) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
`${link.id} is funky... ${targetLog} is NOT correct (is ${targetNode.inputs?.[
|
||||
link.target_slot
|
||||
]?.link}), but ${originLog} contains it`,
|
||||
);
|
||||
if (!targetHasAnyLink()) {
|
||||
!silent && logger.log(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`);
|
||||
let patched = patchTarget("ADD");
|
||||
if (!patched) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`,
|
||||
);
|
||||
patched = patchOrigin("REMOVE");
|
||||
}
|
||||
} else {
|
||||
!silent &&
|
||||
logger.log(
|
||||
` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`,
|
||||
);
|
||||
patchOrigin("REMOVE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we've cleaned up the inputs, outputs, run through it looking for dangling links.,
|
||||
for (let l of linksReverse) {
|
||||
if (!l) continue;
|
||||
const link = (l as LLink).origin_slot != null ? (l as LLink) : extendLink(l as SerializedLink);
|
||||
const originNode = getNodeById(graph, link.origin_id);
|
||||
const targetNode = getNodeById(graph, link.target_id);
|
||||
// Now that we've manipulated the linking, check again if they both exist.
|
||||
if (
|
||||
(!originNode || !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) &&
|
||||
(!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id))
|
||||
) {
|
||||
!silent &&
|
||||
logger.log(
|
||||
`${link.id} is def invalid; BOTH origin node ${link.origin_id} ${
|
||||
originNode ? "is removed" : `doesn\'t have ${link.id}`
|
||||
} and ${link.origin_id} target node ${
|
||||
link.target_id ? "is removed" : `doesn\'t have ${link.id}`
|
||||
}.`,
|
||||
);
|
||||
data.deletedLinks.push(link.id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're fixing, then we've been patching along the way. Now go through and actually delete
|
||||
// the zombie links from `app.graph.links`
|
||||
if (fix) {
|
||||
for (let i = data.deletedLinks.length - 1; i >= 0; i--) {
|
||||
!silent && logger.log(`Deleting link #${data.deletedLinks[i]}.`);
|
||||
if ((graph as LGraph).getNodeById) {
|
||||
delete graph.links[data.deletedLinks[i]!];
|
||||
} else {
|
||||
graph = graph as SerializedGraph;
|
||||
// Sometimes we got objects for links if passed after ComfyUI's loadGraphData modifies the
|
||||
// data. We make a copy now, but can handle the bastardized objects just in case.
|
||||
const idx = graph.links.findIndex(
|
||||
(l) => l && (l[0] === data.deletedLinks[i] || (l as any).id === data.deletedLinks[i]),
|
||||
);
|
||||
if (idx === -1) {
|
||||
logger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`);
|
||||
}
|
||||
logger.log(`splicing ${idx} from links`);
|
||||
graph.links.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
// If we're a serialized graph, we can filter out the links because it's just an array.
|
||||
if (!(graph as LGraph).getNodeById) {
|
||||
graph.links = (graph as SerializedGraph).links.filter((l) => !!l);
|
||||
}
|
||||
}
|
||||
if (!data.patchedNodes.length && !data.deletedLinks.length) {
|
||||
!silent && logger.log(`No bad links detected.`);
|
||||
return {
|
||||
hasBadLinks: false,
|
||||
fixed: false,
|
||||
graph,
|
||||
patched: data.patchedNodes.length,
|
||||
deleted: data.deletedLinks.length,
|
||||
};
|
||||
}
|
||||
!silent &&
|
||||
logger.log(
|
||||
`${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${
|
||||
data.deletedLinks.length || "no"
|
||||
} stale link removals.`,
|
||||
);
|
||||
|
||||
|
||||
let hasBadLinks: boolean = !!(data.patchedNodes.length || data.deletedLinks.length);
|
||||
// If we're fixing, then let's run it again to see if there are no more bad links.
|
||||
if (fix && !silent) {
|
||||
const rerun = fixBadLinks(graph, false, true);
|
||||
hasBadLinks = rerun.hasBadLinks;
|
||||
}
|
||||
|
||||
return {
|
||||
hasBadLinks,
|
||||
fixed: !!hasBadLinks && fix,
|
||||
graph,
|
||||
patched: data.patchedNodes.length,
|
||||
deleted: data.deletedLinks.length,
|
||||
};
|
||||
}
|
||||
+14
@@ -10,6 +10,8 @@ const MODE_ALWAYS = 0;
|
||||
|
||||
class MuterNode extends BaseNodeModeChanger {
|
||||
|
||||
static override exposedActions = ['Mute all', 'Enable all'];
|
||||
|
||||
static override type = NodeTypesString.FAST_MUTER;
|
||||
static override title = NodeTypesString.FAST_MUTER;
|
||||
override readonly modeOn = MODE_ALWAYS;
|
||||
@@ -18,6 +20,18 @@ class MuterNode extends BaseNodeModeChanger {
|
||||
constructor(title = MuterNode.title) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
override async handleAction(action: string) {
|
||||
if (action === 'Mute all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOff(widget);
|
||||
}
|
||||
} else if (action === 'Enable all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOn(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
+16
-8
@@ -2,29 +2,37 @@
|
||||
// @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, wait } from "./utils.js";
|
||||
import { addConnectionLayoutSupport } from "./utils.js";
|
||||
import { wait } from "./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;
|
||||
|
||||
|
||||
/** Legacy "Combiner" */
|
||||
/**
|
||||
* 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 = "Node Collector (rgthree)";
|
||||
static override title = "Node Collector (rgthree)";
|
||||
|
||||
static legacyType = "Node Combiner (rgthree)";
|
||||
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 override legacyType = "Node Combiner (rgthree)";
|
||||
static legacyType = "Node Combiner (rgthree)";
|
||||
static override title = "‼️ Node Combiner [DEPRECATED]";
|
||||
|
||||
constructor(title = CombinerNode.title) {
|
||||
@@ -63,7 +71,7 @@ class CombinerNode extends CollectorNode {
|
||||
* Updates a Node Combiner to a Node Collector.
|
||||
*/
|
||||
async function updateCombinerToCollector(node: TLGraphNode) {
|
||||
if (node.type === CollectorNode.legacyType) {
|
||||
if (node.type === CombinerNode.legacyType) {
|
||||
// Create a new CollectorNode.
|
||||
const newNode = new CollectorNode();
|
||||
if (node.title != CombinerNode.title) {
|
||||
|
||||
+71
-36
@@ -1,13 +1,26 @@
|
||||
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
|
||||
// @ts-ignore
|
||||
import { app } from "../../scripts/app.js";
|
||||
import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.js';
|
||||
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes, wait} from "./utils.js";
|
||||
// @ts-ignore
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
// @ts-ignore
|
||||
import { BaseCollectorNode } from './base_node_collector.js';
|
||||
import { NodeMode } from "./typings/comfy.js";
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph as TLiteGraph,
|
||||
} from "./typings/litegraph.js";
|
||||
import type { NodeMode } from "./typings/comfy.js";
|
||||
import {
|
||||
PassThroughFollowing,
|
||||
addConnectionLayoutSupport,
|
||||
addHelp,
|
||||
filterOutPassthroughNodes,
|
||||
getConnectedInputNodes,
|
||||
getConnectedInputNodesAndFilterPassThroughs,
|
||||
getConnectedOutputNodes,
|
||||
getConnectedOutputNodesAndFilterPassThroughs,
|
||||
} from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
import { BaseCollectorNode } from "./base_node_collector.js";
|
||||
import { NodeTypesString, stripRgthree } from "./constants.js";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
@@ -17,45 +30,65 @@ const MODE_MUTE = 2;
|
||||
const MODE_BYPASS = 4;
|
||||
const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
|
||||
|
||||
|
||||
/**
|
||||
* 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 help = [
|
||||
`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).`,
|
||||
`${stripRgthree(
|
||||
NodeTypesString.NODE_MODE_REPEATER,
|
||||
)} (which would then repeat that mode change to all of its inputs).`,
|
||||
`\n`,
|
||||
`\n- When all connected input nodes are muted, the relay will set a connected repeater to mute.`,
|
||||
`\n- When all connected input nodes are bypassed, the relay will set a connected repeater to bypass.`,
|
||||
`\n- When any connected input nodes are active, the relay will set a connected repeater to active.`,
|
||||
].join(' ');
|
||||
].join(" ");
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
|
||||
setTimeout(() => { this.stabilize(); }, 500);
|
||||
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',
|
||||
this.addOutput("REPEATER", "_NODE_REPEATER_", {
|
||||
color_on: "#Fc0",
|
||||
color_off: "#a80",
|
||||
shape: LiteGraph.ARROW_SHAPE,
|
||||
});
|
||||
}
|
||||
|
||||
override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number): boolean {
|
||||
let canConnect = true;
|
||||
if (super.onConnectOutput) {
|
||||
canConnect = super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
||||
}
|
||||
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] ?? inputNode;
|
||||
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 {
|
||||
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);
|
||||
setTimeout(() => {
|
||||
this.stabilize();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
stabilize() {
|
||||
@@ -64,8 +97,8 @@ class NodeModeRelay extends BaseCollectorNode {
|
||||
if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) {
|
||||
return;
|
||||
}
|
||||
const inputNodes = getConnectedInputNodes(app, this);
|
||||
let mode: NodeMode|null = undefined;
|
||||
const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this, this, -1, this.inputsPassThroughFollowing);
|
||||
let mode: NodeMode | 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
|
||||
@@ -83,29 +116,31 @@ class NodeModeRelay extends BaseCollectorNode {
|
||||
|
||||
if (mode != null) {
|
||||
if (this.outputs?.length) {
|
||||
const outputNodes = getConnectedOutputNodes(app, this);
|
||||
const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
|
||||
for (const outputNode of outputNodes) {
|
||||
outputNode.mode = mode
|
||||
outputNode.mode = mode;
|
||||
wait(16).then(() => {
|
||||
outputNode.setDirtyCanvas(true, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(() => { this.stabilize(); }, 500);
|
||||
setTimeout(() => {
|
||||
this.stabilize();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.NodeModeRepeaterHelper",
|
||||
registerCustomNodes() {
|
||||
|
||||
name: "rgthree.NodeModeRepeaterHelper",
|
||||
registerCustomNodes() {
|
||||
addConnectionLayoutSupport(NodeModeRelay, app, [
|
||||
["Left", "Right"],
|
||||
["Right", "Left"],
|
||||
]);
|
||||
addHelp(NodeModeRelay, app);
|
||||
addConnectionLayoutSupport(NodeModeRelay, app, [['Left','Right'],['Right','Left']]);
|
||||
|
||||
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
|
||||
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
|
||||
NodeModeRelay.category = NodeModeRelay._category;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+122
-52
@@ -2,33 +2,52 @@
|
||||
// @ts-ignore
|
||||
import { app } from "../../scripts/app.js";
|
||||
// @ts-ignore
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
// @ts-ignore
|
||||
import { BaseCollectorNode } from './base_node_collector.js';
|
||||
import { BaseCollectorNode } from "./base_node_collector.js";
|
||||
import { NodeTypesString, stripRgthree } from "./constants.js";
|
||||
|
||||
import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.js';
|
||||
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes} from "./utils.js";
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph as TLiteGraph,
|
||||
} from "./typings/litegraph.js";
|
||||
import {
|
||||
PassThroughFollowing,
|
||||
addConnectionLayoutSupport,
|
||||
addHelp,
|
||||
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;
|
||||
|
||||
static help = [
|
||||
`When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`,
|
||||
`connected input nodes.`,
|
||||
`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.`,
|
||||
`\n`,
|
||||
`\n- Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}`,
|
||||
`or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly`,
|
||||
`\n- 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.`,
|
||||
`\n- Optionally, connect a ${stripRgthree(NodeTypesString.NODE_MODE_RELAY)} to this nodes'`,
|
||||
`\n- 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)`,
|
||||
].join(' ');
|
||||
].join(" ");
|
||||
|
||||
private hasRelayInput = false;
|
||||
private hasTogglerOutput = false;
|
||||
@@ -36,63 +55,101 @@ class NodeModeRepeater extends BaseCollectorNode {
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
this.removeOutput(0);
|
||||
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
|
||||
color_on: '#Fc0',
|
||||
color_off: '#a80',
|
||||
this.addOutput("OPT_CONNECTION", "*", {
|
||||
color_on: "#Fc0",
|
||||
color_off: "#a80",
|
||||
});
|
||||
}
|
||||
|
||||
override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number): boolean {
|
||||
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;
|
||||
if (super.onConnectOutput) {
|
||||
canConnect = canConnect && super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
||||
}
|
||||
// Output can only connect to a FAST MUTER or FAST BYPASSER
|
||||
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] || inputNode;
|
||||
return canConnect && (nextNode.type === NodeTypesString.FAST_MUTER || nextNode.type === NodeTypesString.FAST_BYPASSER);
|
||||
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 {
|
||||
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 = true;
|
||||
if (super.onConnectInput) {
|
||||
canConnect = canConnect && super.onConnectInput?.(inputIndex, outputType, outputSlot, outputNode, outputIndex);
|
||||
}
|
||||
let canConnect = super.onConnectInput?.(
|
||||
inputIndex,
|
||||
outputType,
|
||||
outputSlot,
|
||||
outputNode,
|
||||
outputIndex,
|
||||
);
|
||||
// Output can only connect to a FAST MUTER or FAST BYPASSER
|
||||
let nextNode = getConnectedOutputNodes(app, this, outputNode)[0] || outputNode;
|
||||
const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY
|
||||
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 {
|
||||
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 = getConnectedOutputNodes(app, this);
|
||||
const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
|
||||
for (const outputNode of outputNodes) {
|
||||
if (outputNode?.type === NodeTypesString.FAST_MUTER || outputNode?.type === NodeTypesString.FAST_BYPASSER) {
|
||||
if (
|
||||
outputNode?.type === NodeTypesString.FAST_MUTER ||
|
||||
outputNode?.type === NodeTypesString.FAST_BYPASSER
|
||||
) {
|
||||
hasTogglerOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const inputNodes = getConnectedInputNodes(app, this);
|
||||
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.`);
|
||||
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';
|
||||
this.inputs[index]!.color_on = "#FC0";
|
||||
this.inputs[index]!.color_off = "#a80";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -110,9 +167,9 @@ class NodeModeRepeater extends BaseCollectorNode {
|
||||
this.removeOutput(0);
|
||||
}
|
||||
} else if (!this.outputs[0]) {
|
||||
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
|
||||
color_on: '#Fc0',
|
||||
color_off: '#a80',
|
||||
this.addOutput("OPT_CONNECTION", "*", {
|
||||
color_on: "#Fc0",
|
||||
color_off: "#a80",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -120,24 +177,37 @@ class NodeModeRepeater extends BaseCollectorNode {
|
||||
/** When a mode change, we want all connected nodes to match except for connected relays. */
|
||||
override onModeChange() {
|
||||
super.onModeChange();
|
||||
const linkedNodes = getConnectedInputNodes(app, this);
|
||||
for (const node of linkedNodes) {
|
||||
if (node.type !== NodeTypesString.NODE_MODE_RELAY) {
|
||||
node.mode = this.mode;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.NodeModeRepeater",
|
||||
registerCustomNodes() {
|
||||
|
||||
name: "rgthree.NodeModeRepeater",
|
||||
registerCustomNodes() {
|
||||
addConnectionLayoutSupport(NodeModeRepeater, app, [
|
||||
["Left", "Right"],
|
||||
["Right", "Left"],
|
||||
]);
|
||||
addHelp(NodeModeRepeater, app);
|
||||
addConnectionLayoutSupport(NodeModeRepeater, app, [['Left','Right'],['Right','Left']]);
|
||||
|
||||
LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater);
|
||||
LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater);
|
||||
NodeModeRepeater.category = NodeModeRepeater._category;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+5
-280
@@ -1,300 +1,25 @@
|
||||
// / <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} from './typings/litegraph.js';
|
||||
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
|
||||
import type {ComfyApp, ComfyObjectInfo, ComfyGraphNode} from './typings/comfy.js'
|
||||
import {addConnectionLayoutSupport, wait} from './utils.js';
|
||||
import {addConnectionLayoutSupport} from './utils.js';
|
||||
import { PowerPrompt } from './base_power_prompt.js';
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
|
||||
/** Wraps a node instance keeping closure without mucking the finicky types. */
|
||||
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;
|
||||
|
||||
constructor(node: ComfyGraphNode, nodeData: ComfyObjectInfo) {
|
||||
this.node = node;
|
||||
this.node.properties = this.node.properties || {};
|
||||
|
||||
this.nodeData = nodeData;
|
||||
this.isSimple = this.nodeData.name.includes('Simple');
|
||||
|
||||
this.promptEl = (node.widgets[0]! as any).inputEl;
|
||||
this.addAndHandleKeyboardLoraEditWeight();
|
||||
|
||||
this.patchNodeRefresh();
|
||||
|
||||
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 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 canConnect && !this.node.outputs[outputIndex]!.disabled;
|
||||
}
|
||||
|
||||
// Strip all widgets but prompt (we'll re-add them in refreshCombos)
|
||||
this.node.widgets.splice(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 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]);
|
||||
}
|
||||
|
||||
findAndPatchCombos() {
|
||||
// for (const widget of this.node.widgets) {
|
||||
// if (widget.type === 'combo' && widget.name!.startsWith('insert_')) {
|
||||
// widget.callback = (selected) => this.onPromptComboCallback(widget as IComboWidget, selected);
|
||||
// if (widget.options.values.length === 1) {
|
||||
// widget.disabled = true;
|
||||
// }
|
||||
// // Override comput size so we can add some padding after the last widget. Not sure why it's
|
||||
// // funky, perhaps the multiline text area.
|
||||
// (widget as any).oldComputeSize = widget.computeSize;
|
||||
// let node = this.node;
|
||||
// widget.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;
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
refreshCombos(nodeData: ComfyObjectInfo) {
|
||||
|
||||
this.nodeData = nodeData;
|
||||
// Add the combo for hidden inputs of nodeData
|
||||
let data = this.nodeData.input?.optional || {};
|
||||
data = Object.assign(data, this.nodeData.input?.hidden || {});
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {//Object.entries(this.nodeData.input?.hidden || {})) {
|
||||
if (Array.isArray(value[0])) {
|
||||
const values = value[0] as string[];
|
||||
if (key.startsWith('insert')) {
|
||||
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";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodeData: ComfyObjectInfo | null = null;
|
||||
app.registerExtension({
|
||||
name: 'rgthree.PowerPrompt',
|
||||
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, passedNodeData: ComfyObjectInfo, _app: ComfyApp) {
|
||||
if (passedNodeData.name.startsWith('Power Prompt') && passedNodeData.name.includes('rgthree')) {
|
||||
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);
|
||||
}
|
||||
|
||||
// This won't actually work until such a thing exists in app.js#refreshComboInNodes
|
||||
// @ts-ignore
|
||||
// nodeType.prototype.onRefreshCombos = function (newNodeData: any) {
|
||||
// (this as any).powerPrompt.refreshCombos(newNodeData);
|
||||
// }
|
||||
|
||||
// This isn't super useful, because R->L removes the names in order to work with
|
||||
// litegraph's hardcoded L->R math.. but, ¯\_(ツ)_/¯
|
||||
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
}
|
||||
},
|
||||
@@ -320,4 +45,4 @@ app.registerExtension({
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// / <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 { addHelp, 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;
|
||||
|
||||
static help = [
|
||||
`Use this node to unmute on of its inputs randomly when the graph is queued (and, immediately`,
|
||||
`mute it back).`,
|
||||
`\n`,
|
||||
`\n- NOTE: All input nodes MUST be muted to start; if not this node will not randomly unmute another.`,
|
||||
`\n(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.)`,
|
||||
`\n- 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.`,
|
||||
].join(" ");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
static override setUp<T extends RgthreeBaseNode>(clazz: new(title?: string) => T) {
|
||||
BaseAnyInputConnectedNode.setUp(clazz);
|
||||
addHelp(clazz)
|
||||
}
|
||||
|
||||
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]): void {
|
||||
// No-op, no widgets.
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.RandomUnmuter",
|
||||
registerCustomNodes() {
|
||||
RandomUnmuterNode.setUp(RandomUnmuterNode);
|
||||
},
|
||||
loadedGraphNode(node: LGraphNode) {
|
||||
if (node.type == RandomUnmuterNode.title) {
|
||||
(node as any)._tempWidth = node.size[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
+406
-89
@@ -1,61 +1,129 @@
|
||||
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
|
||||
// @ts-ignore
|
||||
import { app } from "../../scripts/app.js";
|
||||
import type {Vector2, LLink, LGraphCanvas as TLGraphCanvas, LGraph, SerializedLGraphNode, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
|
||||
import { addConnectionLayoutSupport, addMenuSubMenu } from "./utils.js";
|
||||
// @ts-ignore
|
||||
import {rgthreeConfig} from "./rgthree_config.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
import type {
|
||||
Vector2,
|
||||
LLink,
|
||||
LGraphCanvas as TLGraphCanvas,
|
||||
LGraph,
|
||||
SerializedLGraphNode,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
LGraphNode as TLGraphNode,
|
||||
LiteGraph as TLiteGraph,
|
||||
} from "./typings/litegraph.js";
|
||||
import {
|
||||
LAYOUT_CLOCKWISE,
|
||||
LAYOUT_LABEL_OPPOSITES,
|
||||
LAYOUT_LABEL_TO_DATA,
|
||||
addConnectionLayoutSupport,
|
||||
addMenuItem,
|
||||
getConnectedOutputNodesAndFilterPassThroughs,
|
||||
getSlotLinks,
|
||||
isValidConnection,
|
||||
} from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
declare const LGraphCanvas: typeof TLGraphCanvas;
|
||||
|
||||
const rerouteConfig = rgthreeConfig?.['nodes']?.['reroute'] || {};
|
||||
let configWidth = Math.max(Math.round((Number(rerouteConfig['default_width']) || 40) / 10) * 10, 10);
|
||||
let configHeight = Math.max(Math.round((Number(rerouteConfig['default_height']) || 30) / 10) * 10, 10);
|
||||
// Don't allow too small sizes. Granted, 400 is too small, but at least you can right click and
|
||||
// resize... 10x10 you cannot.
|
||||
while(configWidth * configHeight < 400) {
|
||||
configWidth += 10;
|
||||
configHeight += 10;
|
||||
}
|
||||
const configDefaultSize = [configWidth, configHeight] as Vector2;
|
||||
const configResizable = !!rerouteConfig['default_resizable'];
|
||||
let configLayout: [string, string] = rerouteConfig['default_layout'];
|
||||
if (!Array.isArray(configLayout)) {
|
||||
configLayout = ['Left', 'Right'];
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[configLayout[0]]) {
|
||||
configLayout[0] = 'Left';
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[configLayout[1]] || configLayout[0] == configLayout[1]) {
|
||||
configLayout[1] = LAYOUT_LABEL_OPPOSITES[configLayout[0]]!;
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.Reroute",
|
||||
registerCustomNodes() {
|
||||
class RerouteNode extends LGraphNode {
|
||||
name: "rgthree.Reroute",
|
||||
registerCustomNodes() {
|
||||
|
||||
static override title = "Reroute (rgthree)";
|
||||
// `category` seems to get reset at register, so we'll
|
||||
// re-reset it after the register call. ¯\_(ツ)_/¯
|
||||
static category = 'rgthree';
|
||||
static _category = 'rgthree';
|
||||
static readonly title_mode = LiteGraph.NO_TITLE;
|
||||
static collapsable = false;
|
||||
class RerouteNode extends LGraphNode {
|
||||
static override title = "Reroute (rgthree)";
|
||||
// `category` seems to get reset at register, so we'll
|
||||
// re-reset it after the register call. ¯\_(ツ)_/¯
|
||||
static category = "rgthree";
|
||||
static _category = "rgthree";
|
||||
static readonly title_mode = LiteGraph.NO_TITLE;
|
||||
static collapsable = false;
|
||||
static layout_slot_offset = 5;
|
||||
static size: Vector2 = [40, 30]; // Starting size, read from within litegraph.core
|
||||
static size: Vector2 = configDefaultSize; // Starting size, read from within litegraph.core
|
||||
|
||||
readonly isVirtualNode?: boolean;
|
||||
readonly hideSlotLabels: boolean;
|
||||
|
||||
constructor(title = RerouteNode.title) {
|
||||
private configuring = true;
|
||||
|
||||
defaultConnectionsLayout = configLayout;
|
||||
|
||||
constructor(title = RerouteNode.title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
this.resizable = false;
|
||||
this.isVirtualNode = true;
|
||||
this.hideSlotLabels = true;
|
||||
this.setResizable(this.properties['resizable'] ?? configResizable);
|
||||
this.size = RerouteNode.size; // Starting size.
|
||||
this.addInput("", "*");
|
||||
this.addOutput("", "*");
|
||||
this.addInput("", "*");
|
||||
this.addOutput("", "*");
|
||||
setTimeout(() => this.applyNodeSize(), 20);
|
||||
}
|
||||
|
||||
override configure(info: SerializedLGraphNode) {
|
||||
this.configuring = true;
|
||||
super.configure(info);
|
||||
this.setResizable(this.properties['resizable'] ?? configResizable);
|
||||
this.applyNodeSize();
|
||||
this.configuring = false;
|
||||
}
|
||||
|
||||
setResizable(resizable: boolean) {
|
||||
this.properties['resizable'] = !!resizable;
|
||||
this.resizable = this.properties['resizable'];
|
||||
}
|
||||
|
||||
override clone() {
|
||||
const cloned = super.clone();
|
||||
cloned.inputs[0]!.type = '*';
|
||||
cloned.outputs[0]!.type = '*';
|
||||
cloned.inputs[0]!.type = "*";
|
||||
cloned.outputs[0]!.type = "*";
|
||||
return cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied a good bunch of this from the original reroute included with comfy.
|
||||
*/
|
||||
override onConnectionsChange(type: number, _slotIndex: number, connected: boolean, _link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
|
||||
override onConnectionsChange(
|
||||
type: number,
|
||||
_slotIndex: number,
|
||||
connected: boolean,
|
||||
_link_info: LLink,
|
||||
_ioSlot: INodeOutputSlot | INodeInputSlot,
|
||||
) {
|
||||
// Prevent multiple connections to different types when we have no input
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
// Ignore wildcard nodes as these will be updated to real types
|
||||
const types = new Set(this.outputs[0]!.links!.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
|
||||
const types = new Set(
|
||||
this.outputs[0]!.links!.map((l) => app.graph.links[l].type).filter(
|
||||
(t) => t !== "*",
|
||||
),
|
||||
);
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = [];
|
||||
for (let i = 0; i < this.outputs[0]!.links!.length - 1; i++) {
|
||||
@@ -69,33 +137,92 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.configuring) {
|
||||
return;
|
||||
}
|
||||
this.stabilize();
|
||||
if (type === LiteGraph.INPUT) {
|
||||
this.updateDownstream(connected ? 'connect' : 'disconnect', {index: 0, name: this.inputs[0]!.name});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates connected nodes of a change (checking and calling updateFromUpstream). Used for Dynamic Context.
|
||||
*/
|
||||
private updateDownstream(
|
||||
update: "connect" | "disconnect" | "move" | "update",
|
||||
updatedIndexes: {index: number; name: string; from?: number},
|
||||
) {
|
||||
const nodes = getConnectedOutputNodesAndFilterPassThroughs(this, this, 0);
|
||||
for (const node of nodes) {
|
||||
(node as any)?.updateFromUpstream?.(update, this, updatedIndexes);
|
||||
}
|
||||
}
|
||||
|
||||
override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: TLGraphCanvas): void {
|
||||
if (this.properties?.['showLabel']) {
|
||||
const low_quality = canvas.ds.scale < 0.6;
|
||||
if (low_quality || this.size[0] <= 10) {
|
||||
return;
|
||||
}
|
||||
const fontSize = Math.min(14, ((this.size[1] * 0.65)|0));
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.font = `${fontSize}px Arial`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(String(this.title && this.title !== RerouteNode.title ? this.title : this.outputs?.[0]?.type || ''), this.size[0] / 2, (this.size[1] / 2), this.size[0] - 30);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectOutput(slot: string | number, targetNode?: TLGraphNode | undefined): boolean {
|
||||
return super.disconnectOutput(slot, targetNode);
|
||||
}
|
||||
|
||||
stabilize() {
|
||||
// If we are currently "configuring" then skip this stabilization. The connected nodes may
|
||||
// not yet be configured.
|
||||
if (this.configuring) {
|
||||
return;
|
||||
}
|
||||
// Find root input
|
||||
let currentNode: TLGraphNode|null = this;
|
||||
let currentNode: TLGraphNode | null = this;
|
||||
let updateNodes = [];
|
||||
let input = null;
|
||||
let inputType = null;
|
||||
let inputNode = null;
|
||||
let inputNodeOutputSlot = null;
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode);
|
||||
const linkId: number | null = currentNode.inputs[0]!.link;
|
||||
if (linkId !== null) {
|
||||
const link: LLink = (app.graph as LGraph).links[linkId]!;
|
||||
const node: TLGraphNode = (app.graph as LGraph).getNodeById(link.origin_id)!;
|
||||
const node: TLGraphNode = (app.graph as LGraph).getNodeById(
|
||||
link.origin_id,
|
||||
)!;
|
||||
if (!node) {
|
||||
// Bummer, somthing happened.. should we cleanup?
|
||||
app.graph.removeLink(linkId)
|
||||
currentNode = null;
|
||||
break;
|
||||
}
|
||||
const type = (node.constructor as typeof TLGraphNode).type;
|
||||
if (type?.includes("Reroute")) {
|
||||
if (node === this) {
|
||||
// We've found a circle
|
||||
currentNode.disconnectInput(link.target_slot);
|
||||
currentNode = null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Move the previous node
|
||||
currentNode = node;
|
||||
}
|
||||
} else {
|
||||
// We've found the end
|
||||
inputNode = currentNode;
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null;
|
||||
inputNode = node;
|
||||
inputNodeOutputSlot = link.origin_slot;
|
||||
input = node.outputs[inputNodeOutputSlot] ?? null;
|
||||
inputType = input?.type ?? null;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -107,10 +234,12 @@ app.registerExtension({
|
||||
|
||||
// Find all outputs
|
||||
const nodes: TLGraphNode[] = [this];
|
||||
let outputNodes: TLGraphNode[] = [];
|
||||
let outputType = null;
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop()!;
|
||||
const outputs = (currentNode.outputs ? currentNode.outputs[0]!.links : []) || [];
|
||||
const outputs =
|
||||
(currentNode.outputs ? currentNode.outputs[0]!.links : []) || [];
|
||||
if (outputs.length) {
|
||||
for (const linkId of outputs) {
|
||||
const link = app.graph.links[linkId];
|
||||
@@ -118,22 +247,35 @@ app.registerExtension({
|
||||
// When disconnecting sometimes the link is still registered
|
||||
if (!link) continue;
|
||||
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
const node = app.graph.getNodeById(link.target_id) as TLGraphNode;
|
||||
// Don't know why this ever happens.. but it did around the repeater..
|
||||
if (!node) continue;
|
||||
const type = node.constructor.type;
|
||||
const type = (node.constructor as any).type;
|
||||
if (type?.includes("Reroute")) {
|
||||
// Follow reroute nodes
|
||||
nodes.push(node);
|
||||
updateNodes.push(node);
|
||||
} else {
|
||||
// We've found an output
|
||||
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
|
||||
if (inputType && nodeOutType !== inputType && nodeOutType !== '*') {
|
||||
const output = node.inputs?.[link.target_slot] ?? null;
|
||||
const nodeOutType = output?.type;
|
||||
if (nodeOutType == null) {
|
||||
console.warn(`[rgthree] Reroute - Connected node ${node.id} does not have type information for slot ${link.target_slot}. Skipping connection enforcement, but something is odd with that node.`);
|
||||
} else if (
|
||||
inputType &&
|
||||
inputType !== "*" &&
|
||||
nodeOutType !== "*" &&
|
||||
!isValidConnection(input, output)
|
||||
) {
|
||||
// The output doesnt match our input so disconnect it
|
||||
console.warn(`[rgthree] Reroute - Disconnecting connected node's input (${node.id}.${link.target_slot}) (${node.type}) because its type (${String(nodeOutType)}) does not match the reroute type (${String(inputType)})`);
|
||||
node.disconnectInput(link.target_slot);
|
||||
} else {
|
||||
if (outputType != null && outputType !== nodeOutType) {
|
||||
console.warn(`[rgthree] Reroute - Mismatching output types..`);
|
||||
}
|
||||
outputType = nodeOutType;
|
||||
outputNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,13 +291,13 @@ app.registerExtension({
|
||||
for (const node of updateNodes) {
|
||||
// If we dont have an input type we are always wildcard but we'll show the output type
|
||||
// This lets you change the output link to a different type and all nodes will update
|
||||
node.outputs[0].type = inputType || "*";
|
||||
node.__outputType = displayType;
|
||||
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
|
||||
node.outputs[0]!.type = inputType || "*";
|
||||
(node as any).__outputType = displayType;
|
||||
node.outputs[0]!.name = input?.name || "";
|
||||
node.size = node.computeSize();
|
||||
node.applyNodeSize?.();
|
||||
(node as any).applyNodeSize?.();
|
||||
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
for (const l of node.outputs[0]!.links || []) {
|
||||
const link = app.graph.links[l];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
@@ -163,60 +305,82 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode) {
|
||||
const link = app.graph.links[inputNode.inputs[0]!.link];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
if (inputNode && inputNodeOutputSlot != null) {
|
||||
const links = inputNode.outputs[inputNodeOutputSlot]!.links;
|
||||
for (const l of links || []) {
|
||||
const link = app.graph.links[l];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
// (inputNode as any)?.onConnectionsChainChange?.(this);
|
||||
// for (const outputNode of outputNodes) {
|
||||
// (outputNode as any)?.onConnectionsChainChange?.(this);
|
||||
// }
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
applyNodeSize() {
|
||||
this.properties['size'] = this.properties['size'] || RerouteNode.size;
|
||||
this.properties['size'] = [Number(this.properties['size'][0]), Number(this.properties['size'][1])];
|
||||
this.size = this.properties['size'];
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore: Fix incorrect litegraph typings.
|
||||
addConnectionLayoutSupport(RerouteNode, app, [
|
||||
["Left","Right"],
|
||||
["Left","Top"],
|
||||
["Left","Bottom"],
|
||||
["Right","Left"],
|
||||
["Right","Top"],
|
||||
["Right","Bottom"],
|
||||
["Top","Left"],
|
||||
["Top","Right"],
|
||||
["Top","Bottom"],
|
||||
["Bottom","Left"],
|
||||
["Bottom","Right"],
|
||||
["Bottom","Top"],
|
||||
], (node) => {(node as RerouteNode).applyNodeSize();});
|
||||
|
||||
|
||||
// @ts-ignore: Fix incorrect litegraph typings.
|
||||
addMenuSubMenu(RerouteNode, app, {
|
||||
name: 'Height',
|
||||
property: 'size',
|
||||
options: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
override computeSize(out?: Vector2 | undefined): Vector2 {
|
||||
// Secret funcionality for me that I don't want to explain. Hold down ctrl while dragging
|
||||
// to allow 10,10 dragging size.
|
||||
if (app.canvas.resizing_node?.id === this.id && rgthree.ctrlKey) {
|
||||
return [10, 10];
|
||||
}
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [node.size[0], Number(value)],
|
||||
callback: (node) => (node as RerouteNode).applyNodeSize()
|
||||
return super.computeSize(out);
|
||||
}
|
||||
|
||||
override onResize(size: Vector2) {
|
||||
// If the canvas is currently resizing our node, then we want to save it to our properties.
|
||||
if (app.canvas.resizing_node?.id === this.id) {
|
||||
this.properties["size"] = [
|
||||
size[0],
|
||||
size[1],
|
||||
];
|
||||
// If we end up resizing under the minimum size (like, we're holding down the secret crtl)
|
||||
// then let's no longer make us resizable. When we let go.
|
||||
if (size[0] < 40 || size[0] < 30) {
|
||||
this.setResizable(false);
|
||||
}
|
||||
}
|
||||
if (super.onResize) {
|
||||
super.onResize(size);
|
||||
}
|
||||
}
|
||||
|
||||
applyNodeSize() {
|
||||
this.properties["size"] = this.properties["size"] || RerouteNode.size;
|
||||
this.properties["size"] = [
|
||||
Number(this.properties["size"][0]),
|
||||
Number(this.properties["size"][1]),
|
||||
];
|
||||
this.size = this.properties["size"];
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: (node) => `${node.properties?.['showLabel'] ? "Hide" : "Show"} Label/Title`,
|
||||
property: 'showLabel',
|
||||
callback: async (node, value) => {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore: Fix incorrect litegraph typings.
|
||||
addMenuSubMenu(RerouteNode, app, {
|
||||
name: 'Width',
|
||||
property: 'size',
|
||||
options: (() => {
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: (node) => `${node.resizable ? 'No' : 'Allow'} Resizing`,
|
||||
callback: (node) => {
|
||||
(node as RerouteNode).setResizable(!node.resizable);
|
||||
node.size[0] = Math.max(40, node.size[0]);
|
||||
node.size[1] = Math.max(30, node.size[1]);
|
||||
(node as RerouteNode).applyNodeSize();
|
||||
},
|
||||
});
|
||||
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Static Width",
|
||||
property: "size",
|
||||
subMenuOptions: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
@@ -224,12 +388,165 @@ app.registerExtension({
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [Number(value), node.size[1]],
|
||||
callback: (node) => (node as RerouteNode).applyNodeSize()
|
||||
callback: (node) => {
|
||||
(node as RerouteNode).setResizable(false);
|
||||
(node as RerouteNode).applyNodeSize();
|
||||
},
|
||||
});
|
||||
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Static Height",
|
||||
property: "size",
|
||||
subMenuOptions: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
}
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [node.size[0], Number(value)],
|
||||
callback: (node) => {
|
||||
(node as RerouteNode).setResizable(false);
|
||||
(node as RerouteNode).applyNodeSize();
|
||||
},
|
||||
});
|
||||
|
||||
addConnectionLayoutSupport(
|
||||
RerouteNode,
|
||||
app,
|
||||
[
|
||||
["Left", "Right"],
|
||||
["Left", "Top"],
|
||||
["Left", "Bottom"],
|
||||
["Right", "Left"],
|
||||
["Right", "Top"],
|
||||
["Right", "Bottom"],
|
||||
["Top", "Left"],
|
||||
["Top", "Right"],
|
||||
["Top", "Bottom"],
|
||||
["Bottom", "Left"],
|
||||
["Bottom", "Right"],
|
||||
["Bottom", "Top"],
|
||||
],
|
||||
(node) => {
|
||||
(node as RerouteNode).applyNodeSize();
|
||||
},
|
||||
);
|
||||
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Rotate",
|
||||
subMenuOptions: [
|
||||
"Rotate 90° Clockwise",
|
||||
"Rotate 90° Counter-Clockwise",
|
||||
"Rotate 180°",
|
||||
null,
|
||||
"Flip Horizontally",
|
||||
"Flip Vertically",
|
||||
],
|
||||
callback: (node, value) => {
|
||||
const w = node.size[0];
|
||||
const h = node.size[1];
|
||||
node.properties["connections_layout"] = node.properties[
|
||||
"connections_layout"
|
||||
] || (node as RerouteNode).defaultConnectionsLayout;
|
||||
const inputDirIndex = LAYOUT_CLOCKWISE.indexOf(
|
||||
node.properties["connections_layout"][0],
|
||||
);
|
||||
const outputDirIndex = LAYOUT_CLOCKWISE.indexOf(
|
||||
node.properties["connections_layout"][1],
|
||||
);
|
||||
if (value?.startsWith("Rotate 90°")) {
|
||||
node.size[0] = h;
|
||||
node.size[1] = w;
|
||||
if (value.includes("Counter")) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex - 1) % 4) + 4) % 4];
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex - 1) % 4) + 4) % 4];
|
||||
} else {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 1) % 4) + 4) % 4];
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 1) % 4) + 4) % 4];
|
||||
}
|
||||
} else if (value?.startsWith("Rotate 180°")) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4];
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4];
|
||||
} else if (value?.startsWith("Flip Horizontally")) {
|
||||
if (
|
||||
["Left", "Right"].includes(node.properties["connections_layout"][0])
|
||||
) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
if (
|
||||
["Left", "Right"].includes(node.properties["connections_layout"][1])
|
||||
) {
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
} else if (value?.startsWith("Flip Vertically")) {
|
||||
if (
|
||||
["Top", "Bottom"].includes(node.properties["connections_layout"][0])
|
||||
) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
if (
|
||||
["Top", "Bottom"].includes(
|
||||
node.properties["connections_layout"][1],
|
||||
)
|
||||
) {
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
|
||||
RerouteNode.category = RerouteNode._category;
|
||||
},
|
||||
});
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Clone New Reroute...",
|
||||
subMenuOptions: [
|
||||
"Before",
|
||||
"After",
|
||||
],
|
||||
callback: async (node, value) => {
|
||||
const clone = node.clone();
|
||||
const pos = [...node.pos];
|
||||
if (value === 'Before') {
|
||||
clone.pos = [pos[0]! - 20, pos[1]! - 20];
|
||||
app.graph.add(clone);
|
||||
await wait();
|
||||
const inputLinks = getSlotLinks(node.inputs[0]);
|
||||
for (const inputLink of inputLinks) {
|
||||
const link = inputLink.link;
|
||||
const linkedNode = app.graph.getNodeById(link.origin_id) as TLGraphNode;
|
||||
if (linkedNode) {
|
||||
linkedNode.connect(0, clone, 0);
|
||||
}
|
||||
}
|
||||
clone.connect(0, node, 0);
|
||||
} else {
|
||||
clone.pos = [pos[0]! + 20, pos[1]! + 20];
|
||||
app.graph.add(clone);
|
||||
await wait();
|
||||
const outputLinks = getSlotLinks(node.outputs[0]);
|
||||
node.connect(0, clone, 0);
|
||||
for (const outputLink of outputLinks) {
|
||||
const link = outputLink.link;
|
||||
const linkedNode = app.graph.getNodeById(link.target_id) as TLGraphNode;
|
||||
if (linkedNode) {
|
||||
clone.connect(0, linkedNode, link.target_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
|
||||
RerouteNode.category = RerouteNode._category;
|
||||
},
|
||||
});
|
||||
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
import type { LGraphNode, SerializedLGraphNode, serializedLGraph } from "litegraph.js";
|
||||
// @ts-ignore
|
||||
import { app } from "../../scripts/app.js";
|
||||
// @ts-ignore
|
||||
import {rgthreeConfig} from "./rgthree_config.js";
|
||||
import { fixBadLinks } from "./link_fixer.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
|
||||
export enum LogLevel {
|
||||
IMPORTANT = 1,
|
||||
ERROR,
|
||||
WARN,
|
||||
INFO,
|
||||
DEBUG,
|
||||
}
|
||||
|
||||
type ConsoleLogFns = "log" | "error" | "warn" | "debug" | "info";
|
||||
const LogLevelToMethod: { [key in LogLevel]: ConsoleLogFns } = {
|
||||
[LogLevel.IMPORTANT]: "log",
|
||||
[LogLevel.ERROR]: "error",
|
||||
[LogLevel.WARN]: "warn",
|
||||
[LogLevel.INFO]: "info",
|
||||
[LogLevel.DEBUG]: "debug",
|
||||
};
|
||||
const LogLevelToCSS: { [key in LogLevel]: string } = {
|
||||
[LogLevel.IMPORTANT]: "font-weight:bold; color:blue;",
|
||||
[LogLevel.ERROR]: "",
|
||||
[LogLevel.WARN]: "",
|
||||
[LogLevel.INFO]: "",
|
||||
[LogLevel.DEBUG]: "font-style: italic;",
|
||||
};
|
||||
|
||||
let GLOBAL_LOG_LEVEL = LogLevel.DEBUG;
|
||||
|
||||
/** A basic wrapper around logger. */
|
||||
class Logger {
|
||||
log(level: LogLevel, message: string, ...args: any[]) {
|
||||
if (level <= GLOBAL_LOG_LEVEL) {
|
||||
const css = LogLevelToCSS[level] || "";
|
||||
console[LogLevelToMethod[level]](`%c${message}`, css, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A log session, with the name as the prefix. A new session will stack prefixes.
|
||||
*/
|
||||
class LogSession {
|
||||
logger = new Logger();
|
||||
constructor(readonly name?: string) {}
|
||||
|
||||
log(levelOrMessage: LogLevel | string, message?: string, ...args: any[]) {
|
||||
let level = typeof levelOrMessage === "string" ? LogLevel.INFO : levelOrMessage;
|
||||
if (typeof levelOrMessage === "string") {
|
||||
message = levelOrMessage;
|
||||
}
|
||||
this.logger.log(level, `${this.name || ""}${message ? " " + message : ""}`, ...args);
|
||||
}
|
||||
|
||||
debug(message?: string, ...args: any[]) {
|
||||
this.log(LogLevel.DEBUG, message, ...args);
|
||||
}
|
||||
|
||||
info(message?: string, ...args: any[]) {
|
||||
this.log(LogLevel.INFO, message, ...args);
|
||||
}
|
||||
|
||||
error(message?: string, ...args: any[]) {
|
||||
this.log(LogLevel.ERROR, message, ...args);
|
||||
}
|
||||
|
||||
newSession(name?: string) {
|
||||
return new LogSession(`${this.name}${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export type RgthreeUiMessage = {
|
||||
id: string;
|
||||
message: string;
|
||||
type?: 'warn'|'info'|null;
|
||||
timeout?: number;
|
||||
// closeable?: boolean; // TODO
|
||||
actions?: Array<{
|
||||
label: string;
|
||||
href?: string;
|
||||
callback?: (event: MouseEvent) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A global class as 'rgthree'; exposed on wiindow. Lots can go in here.
|
||||
*/
|
||||
class Rgthree {
|
||||
/** Are any functional keys pressed in this given moment? */
|
||||
ctrlKey = false;
|
||||
altKey = false;
|
||||
metaKey = false;
|
||||
shiftKey = false;
|
||||
|
||||
logger = new LogSession("[rgthree]");
|
||||
|
||||
monitorBadLinksAlerted = false;
|
||||
monitorLinkTimeout: number|null = null;
|
||||
|
||||
async clearAllMessages() {
|
||||
let container = document.querySelector('.rgthree-top-messages-container');
|
||||
container && (container.innerHTML = '');
|
||||
}
|
||||
|
||||
async showMessage(data: RgthreeUiMessage) {
|
||||
let container = document.querySelector('.rgthree-top-messages-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.classList.add('rgthree-top-messages-container');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
// Hide if we exist.
|
||||
await this.hideMessage(data.id);
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.setAttribute('type', data.type || 'info');
|
||||
|
||||
const message = document.createElement('span');
|
||||
message.innerText = data.message;
|
||||
messageContainer.appendChild(message);
|
||||
|
||||
for (let a = 0; a < (data.actions || []).length; a++) {
|
||||
const action = data.actions![a]!;
|
||||
if (a > 0) {
|
||||
const sep = document.createElement('span');
|
||||
sep.innerHTML = ' | ';
|
||||
messageContainer.appendChild(sep);
|
||||
}
|
||||
|
||||
const actionEl = document.createElement('a');
|
||||
actionEl.innerText = action.label;
|
||||
if (action.href) {
|
||||
actionEl.target = '_blank';
|
||||
actionEl.href = action.href;
|
||||
}
|
||||
if (action.callback) {
|
||||
actionEl.onclick = (e) => {
|
||||
return action.callback!(e);
|
||||
}
|
||||
}
|
||||
messageContainer.appendChild(actionEl);
|
||||
}
|
||||
|
||||
const messageAnimContainer = document.createElement('div');
|
||||
messageAnimContainer.setAttribute('msg-id', data.id);
|
||||
messageAnimContainer.appendChild(messageContainer);
|
||||
container.appendChild(messageAnimContainer);
|
||||
|
||||
// Add. Wait. Measure. Wait. Anim.
|
||||
await wait(64);
|
||||
messageAnimContainer.style.marginTop = `-${messageAnimContainer.offsetHeight}px`;
|
||||
await wait(64);
|
||||
messageAnimContainer.classList.add('-show');
|
||||
|
||||
if (data.timeout) {
|
||||
await wait(data.timeout);
|
||||
this.hideMessage(data.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async hideMessage(id: string) {
|
||||
const msg = document.querySelector(`.rgthree-top-messages-container > [msg-id="${id}"]`);
|
||||
if (msg?.classList.contains('-show')) {
|
||||
msg.classList.remove('-show');
|
||||
await wait(750);
|
||||
}
|
||||
msg && msg.remove();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
this.ctrlKey = !!e.ctrlKey;
|
||||
this.altKey = !!e.altKey;
|
||||
this.metaKey = !!e.metaKey;
|
||||
this.shiftKey = !!e.shiftKey;
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", (e) => {
|
||||
this.ctrlKey = !!e.ctrlKey;
|
||||
this.altKey = !!e.altKey;
|
||||
this.metaKey = !!e.metaKey;
|
||||
this.shiftKey = !!e.shiftKey;
|
||||
});
|
||||
|
||||
// Override the loadGraphData so we can check for bad links and ask the user to fix them.
|
||||
const that = this;
|
||||
|
||||
const queuePrompt = app.queuePrompt as Function;
|
||||
app.queuePrompt = async function() {
|
||||
that.fireEvent('queue', {});
|
||||
let promise = queuePrompt.apply(app, [...arguments]);
|
||||
that.fireEvent('queue-end', {});
|
||||
return promise;
|
||||
}
|
||||
|
||||
const graphToPrompt = app.graphToPrompt as Function;
|
||||
app.graphToPrompt = async function() {
|
||||
that.fireEvent('graph-to-prompt', {});
|
||||
let promise = graphToPrompt.apply(app, [...arguments]);
|
||||
await promise;
|
||||
that.fireEvent('graph-to-prompt-end', {});
|
||||
return promise;
|
||||
}
|
||||
|
||||
const clean = app.clean;
|
||||
app.clean = function() {
|
||||
that.clearAllMessages();
|
||||
clean && clean.call(app, ...arguments);
|
||||
};
|
||||
|
||||
const loadGraphData = app.loadGraphData;
|
||||
app.loadGraphData = function(graph: serializedLGraph) {
|
||||
if (that.monitorLinkTimeout) {
|
||||
clearTimeout(that.monitorLinkTimeout);
|
||||
that.monitorLinkTimeout = null;
|
||||
}
|
||||
that.clearAllMessages();
|
||||
// Try to make a copy to use, because ComfyUI's loadGraphData will modify it.
|
||||
let graphCopy: serializedLGraph|null;
|
||||
try {
|
||||
graphCopy = JSON.parse(JSON.stringify(graph));
|
||||
} catch(e) {
|
||||
graphCopy = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const wasLoadingAborted = document.querySelector('.comfy-modal-content')?.textContent?.includes('Loading aborted due');
|
||||
const graphToUse = wasLoadingAborted ? (graphCopy || graph) : app.graph
|
||||
const fixBadLinksResult = fixBadLinks(graphToUse);
|
||||
if (fixBadLinksResult.hasBadLinks) {
|
||||
that.showMessage({
|
||||
id: 'bad-links',
|
||||
type: 'warn',
|
||||
message: 'The workflow you\'ve loaded may have connection/linking data that could be fixed.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open fixer',
|
||||
href: '/extensions/rgthree-comfy/html/links.html',
|
||||
},
|
||||
{
|
||||
label: 'Fix in place',
|
||||
href: '/extensions/rgthree-comfy/html/links.html',
|
||||
callback: (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (confirm('This will attempt to fix in place. Please make sure to have a saved copy of your workflow.')) {
|
||||
const fixBadLinksResult = fixBadLinks(graphToUse, true);
|
||||
if (!fixBadLinksResult.hasBadLinks) {
|
||||
that.hideMessage('bad-links');
|
||||
alert('Success! It\'s possible some valid links may have been affected. Please check and verify your workflow.');
|
||||
wasLoadingAborted && app.loadGraphData(fixBadLinksResult.graph);
|
||||
if (rgthreeConfig['monitor_bad_links']) {
|
||||
that.monitorLinkTimeout = setTimeout(() => {
|
||||
that.monitorBadLinks();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
} else if (rgthreeConfig['monitor_bad_links']) {
|
||||
that.monitorLinkTimeout = setTimeout(() => {
|
||||
that.monitorBadLinks();
|
||||
}, 5000);
|
||||
}
|
||||
}, 100);
|
||||
loadGraphData && loadGraphData.call(app, ...arguments);
|
||||
}
|
||||
|
||||
wait(100).then(() => {
|
||||
this.injectRgthreeCss();
|
||||
});
|
||||
}
|
||||
|
||||
private injectRgthreeCss() {
|
||||
let link = document.createElement("link");
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = 'extensions/rgthree-comfy/rgthree.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
private readonly eventsToFns = new Map<string, Set<(ev: Event) => void>>();
|
||||
|
||||
addEventListener(event: string, fn: (ev: Event) => void) {
|
||||
if (!this.eventsToFns.has(event)) {
|
||||
this.eventsToFns.set(event, new Set());
|
||||
}
|
||||
this.eventsToFns.get(event)!.add(fn);
|
||||
}
|
||||
|
||||
removeEventListener(event: string, fn: (ev: Event) => void) {
|
||||
if (this.eventsToFns.has(event)) {
|
||||
this.eventsToFns.get(event)!.delete(fn);
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(event: string, data: any) {
|
||||
if (this.eventsToFns.has(event)) {
|
||||
for (let fn of this.eventsToFns.get(event)!) {
|
||||
const e = new CustomEvent(event, {detail:data});
|
||||
fn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLogLevel(level: LogLevel) {
|
||||
GLOBAL_LOG_LEVEL = level;
|
||||
}
|
||||
|
||||
log(levelOrMessage: LogLevel | string, message?: string, ...args: any[]) {
|
||||
this.logger.log(levelOrMessage, message, ...args);
|
||||
}
|
||||
|
||||
newLogSession(name?: string) {
|
||||
return this.logger.newSession(name);
|
||||
}
|
||||
|
||||
monitorBadLinks() {
|
||||
const badLinksFound = fixBadLinks(app.graph);
|
||||
if (badLinksFound.hasBadLinks && !this.monitorBadLinksAlerted) {
|
||||
this.monitorBadLinksAlerted = true;
|
||||
alert(`Problematic links just found in live data. Can you save your workflow and file a bug with the last few steps you took to trigger this at https://github.com/rgthree/rgthree-comfy/issues. Thank you!`)
|
||||
} else if (!badLinksFound.hasBadLinks) {
|
||||
// Clear the alert once fixed so we can alert again.
|
||||
this.monitorBadLinksAlerted = false;
|
||||
}
|
||||
this.monitorLinkTimeout = setTimeout(() => {
|
||||
this.monitorBadLinks();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
export const rgthree = new Rgthree();
|
||||
// @ts-ignore. Expose it on window because, why not.
|
||||
window.rgthree = rgthree;
|
||||
|
||||
+117
-64
@@ -1,15 +1,24 @@
|
||||
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
|
||||
// @ts-ignore
|
||||
import {app} from "../../scripts/app.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
// @ts-ignore
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import type {SerializedLGraphNode, ContextMenuItem, IContextMenuOptions, ContextMenu, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
|
||||
import type {ComfyApp, ComfyObjectInfo, ComfyWidget, ComfyGraphNode} from './typings/comfy.js'
|
||||
import type {
|
||||
SerializedLGraphNode,
|
||||
ContextMenuItem,
|
||||
IContextMenuOptions,
|
||||
ContextMenu,
|
||||
LGraphNode as TLGraphNode,
|
||||
LiteGraph as TLiteGraph,
|
||||
IWidget,
|
||||
} from "./typings/litegraph.js";
|
||||
import type { ComfyApp, ComfyObjectInfo, ComfyWidget, ComfyGraphNode } from "./typings/comfy.js";
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
|
||||
declare const LiteGraph: typeof TLiteGraph;
|
||||
declare const LGraphNode: typeof TLGraphNode;
|
||||
|
||||
const LAST_SEED_BUTTON_LABEL = '♻️ (Use Last Queued Seed)';
|
||||
const LAST_SEED_BUTTON_LABEL = "♻️ (Use Last Queued Seed)";
|
||||
|
||||
const SPECIAL_SEED_RANDOM = -1;
|
||||
const SPECIAL_SEED_INCREMENT = -2;
|
||||
@@ -23,31 +32,44 @@ interface SeedSerializedCtx {
|
||||
|
||||
/** Wraps a node instance keeping closure without mucking the finicky types. */
|
||||
class SeedControl {
|
||||
|
||||
readonly node: ComfyGraphNode;
|
||||
|
||||
lastSeed?:number = undefined;
|
||||
lastSeed?: number = undefined;
|
||||
serializedCtx: SeedSerializedCtx = {};
|
||||
seedWidget: ComfyWidget;
|
||||
lastSeedButton: ComfyWidget;
|
||||
lastSeedValue: ComfyWidget|null = null;
|
||||
lastSeedValue: ComfyWidget | null = null;
|
||||
|
||||
constructor(node: ComfyGraphNode) {
|
||||
this.node = node;
|
||||
|
||||
(this.node.constructor as any).exposedActions = ["Randomize Each Time", "Use Last Queued Seed"];
|
||||
const handleAction = (this.node as RgthreeBaseNode).handleAction;
|
||||
(this.node as RgthreeBaseNode).handleAction = async (action: string) => {
|
||||
handleAction && handleAction.call(this.node, action);
|
||||
if (action === "Randomize Each Time") {
|
||||
this.seedWidget.value = SPECIAL_SEED_RANDOM;
|
||||
} else if (action === "Use Last Queued Seed") {
|
||||
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
|
||||
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
|
||||
this.lastSeedButton.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.node.properties = this.node.properties || {};
|
||||
|
||||
// Grab the already available widgets, and remove the built-in control_after_generate
|
||||
for (const [i, w] of this.node.widgets.entries()) {
|
||||
if (w.name === 'seed') {
|
||||
if (w.name === "seed") {
|
||||
this.seedWidget = w as ComfyWidget;
|
||||
} else if (w.name === 'control_after_generate') {
|
||||
} else if (w.name === "control_after_generate") {
|
||||
this.node.widgets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!this.seedWidget) {
|
||||
throw new Error('Something\'s wrong; expected seed widget');
|
||||
throw new Error("Something's wrong; expected seed widget");
|
||||
}
|
||||
|
||||
const randMax = Math.min(1125899906842624, this.seedWidget.options.max);
|
||||
@@ -57,22 +79,40 @@ class SeedControl {
|
||||
const randMin = Math.max(0, this.seedWidget.options.min);
|
||||
const randomRange = (randMax - Math.max(0, randMin)) / (this.seedWidget.options.step / 10);
|
||||
|
||||
this.node.addWidget('button', '🎲 Randomize Each Time', null, () => {
|
||||
this.seedWidget.value = SPECIAL_SEED_RANDOM;
|
||||
}, {serialize: false}) as ComfyWidget;
|
||||
this.node.addWidget(
|
||||
"button",
|
||||
"🎲 Randomize Each Time",
|
||||
null,
|
||||
() => {
|
||||
this.seedWidget.value = SPECIAL_SEED_RANDOM;
|
||||
},
|
||||
{ serialize: false },
|
||||
) as ComfyWidget;
|
||||
|
||||
this.node.addWidget('button', '🎲 New Fixed Random', null, () => {
|
||||
this.seedWidget.value = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
}, {serialize: false});
|
||||
this.node.addWidget(
|
||||
"button",
|
||||
"🎲 New Fixed Random",
|
||||
null,
|
||||
() => {
|
||||
this.seedWidget.value =
|
||||
Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
},
|
||||
{ serialize: false },
|
||||
);
|
||||
|
||||
this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
|
||||
this.seedWidget.value = this.lastSeed;
|
||||
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
|
||||
this.lastSeedButton.disabled = true;
|
||||
}, {width: 50, serialize: false});
|
||||
this.lastSeedButton = this.node.addWidget(
|
||||
"button",
|
||||
LAST_SEED_BUTTON_LABEL,
|
||||
null,
|
||||
() => {
|
||||
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
|
||||
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
|
||||
this.lastSeedButton.disabled = true;
|
||||
},
|
||||
{ width: 50, serialize: false },
|
||||
);
|
||||
this.lastSeedButton.disabled = true;
|
||||
|
||||
|
||||
/**
|
||||
* When we serialize the value, check if our seed widget is -1 and, if so, generate
|
||||
* a random number and set that to the input value. Also, set it in the passed graph node
|
||||
@@ -83,21 +123,24 @@ class SeedControl {
|
||||
const inputSeed = this.seedWidget.value;
|
||||
this.serializedCtx = {
|
||||
inputSeed: this.seedWidget.value,
|
||||
}
|
||||
};
|
||||
|
||||
// If our input seed was a special seed, then handle it.
|
||||
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed!)) {
|
||||
// If the last seed was not a special seed and we have increment/decrement, then do that on the last seed.
|
||||
if (typeof this.lastSeed === 'number' && !SPECIAL_SEEDS.includes(this.lastSeed)) {
|
||||
// If the last seed was not a special seed and we have increment/decrement, then do that on
|
||||
// the last seed.
|
||||
if (typeof this.lastSeed === "number" && !SPECIAL_SEEDS.includes(this.lastSeed)) {
|
||||
if (inputSeed === SPECIAL_SEED_INCREMENT) {
|
||||
this.serializedCtx.seedUsed = this.lastSeed + 1;
|
||||
} else if (inputSeed === SPECIAL_SEED_INCREMENT) {
|
||||
} else if (inputSeed === SPECIAL_SEED_DECREMENT) {
|
||||
this.serializedCtx.seedUsed = this.lastSeed - 1;
|
||||
}
|
||||
}
|
||||
// If we don't have a seed to use, or it's special seed (like we incremented into one), then we randomize.
|
||||
// If we don't have a seed to use, or it's special seed (like we incremented into one), then
|
||||
// we randomize.
|
||||
if (!this.serializedCtx.seedUsed || SPECIAL_SEEDS.includes(this.serializedCtx.seedUsed)) {
|
||||
this.serializedCtx.seedUsed = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
this.serializedCtx.seedUsed =
|
||||
Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
}
|
||||
} else {
|
||||
this.serializedCtx.seedUsed = this.seedWidget.value;
|
||||
@@ -108,7 +151,7 @@ class SeedControl {
|
||||
this.lastSeed = this.serializedCtx.seedUsed!;
|
||||
// Enabled the 'Last seed' Button
|
||||
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed!)) {
|
||||
this.lastSeedButton.name = `♻️ ${this.serializedCtx.seedUsed}`
|
||||
this.lastSeedButton.name = `♻️ ${this.serializedCtx.seedUsed}`;
|
||||
this.lastSeedButton.disabled = false;
|
||||
if (this.lastSeedValue) {
|
||||
this.lastSeedValue.value = `Last Seed: ${this.serializedCtx.seedUsed}`;
|
||||
@@ -119,7 +162,7 @@ class SeedControl {
|
||||
}
|
||||
|
||||
return this.serializedCtx.seedUsed;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* After the widget has been queued, change back to "-1" if we started as "-1".
|
||||
@@ -129,37 +172,44 @@ class SeedControl {
|
||||
this.seedWidget.value = this.serializedCtx.inputSeed;
|
||||
}
|
||||
this.serializedCtx = {};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.node.getExtraMenuOptions = (_: TLGraphNode, options: ContextMenuItem[]) => {
|
||||
options.splice(options.length - 1, 0,
|
||||
{
|
||||
content: "Show/Hide Last Seed Value",
|
||||
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
|
||||
this.node.properties['showLastSeed'] = !this.node.properties['showLastSeed'];
|
||||
if (this.node.properties['showLastSeed']) {
|
||||
this.addLastSeedValue();
|
||||
} else {
|
||||
this.removeLastSeedValue();
|
||||
}
|
||||
this.node.getExtraMenuOptions = (_: TLGraphNode, options: ContextMenuItem[]) => {
|
||||
options.splice(options.length - 1, 0, {
|
||||
content: "Show/Hide Last Seed Value",
|
||||
callback: (
|
||||
_value: ContextMenuItem,
|
||||
_options: IContextMenuOptions,
|
||||
_event: MouseEvent,
|
||||
_parentMenu: ContextMenu | undefined,
|
||||
_node: TLGraphNode,
|
||||
) => {
|
||||
this.node.properties["showLastSeed"] = !this.node.properties["showLastSeed"];
|
||||
if (this.node.properties["showLastSeed"]) {
|
||||
this.addLastSeedValue();
|
||||
} else {
|
||||
this.removeLastSeedValue();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
addLastSeedValue() {
|
||||
if (this.lastSeedValue) return;
|
||||
this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", { multiline: true }], app).widget;
|
||||
this.lastSeedValue = ComfyWidgets["STRING"](
|
||||
this.node,
|
||||
"last_seed",
|
||||
["STRING", { multiline: true }],
|
||||
app,
|
||||
).widget;
|
||||
this.lastSeedValue!.inputEl!.readOnly = true;
|
||||
this.lastSeedValue!.inputEl!.style.fontSize = '0.75rem';
|
||||
this.lastSeedValue!.inputEl!.style.textAlign = 'center';
|
||||
this.lastSeedValue!.inputEl!.style.fontSize = "0.75rem";
|
||||
this.lastSeedValue!.inputEl!.style.textAlign = "center";
|
||||
this.lastSeedValue!.serializeValue = async (node: SerializedLGraphNode, index: number) => {
|
||||
node.widgets_values![index] = '';
|
||||
return '';
|
||||
}
|
||||
node.widgets_values![index] = "";
|
||||
return "";
|
||||
};
|
||||
this.node.computeSize();
|
||||
}
|
||||
|
||||
@@ -173,15 +223,18 @@ class SeedControl {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "rgthree.Seed",
|
||||
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, _app: ComfyApp) {
|
||||
if (nodeData.name === "Seed (rgthree)") {
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||
name: "rgthree.Seed",
|
||||
async beforeRegisterNodeDef(
|
||||
nodeType: typeof LGraphNode,
|
||||
nodeData: ComfyObjectInfo,
|
||||
_app: ComfyApp,
|
||||
) {
|
||||
if (nodeData.name === "Seed (rgthree)") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||
(this as any).seedControl = new SeedControl(this as ComfyGraphNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* A bunch of shared utils that can be used in ComfyUI, as well as in any single-HTML pages.
|
||||
*/
|
||||
|
||||
|
||||
export type Resolver<T> = {
|
||||
id: string;
|
||||
completed: boolean;
|
||||
resolved: boolean;
|
||||
rejected: boolean;
|
||||
promise: Promise<T>;
|
||||
resolve: (data: T) => void;
|
||||
reject: () => void;
|
||||
timeout: number|null;
|
||||
// A caller property to store a defer timeout on.
|
||||
deferredTimeout?: number|null;
|
||||
deferredData?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new `Resolver` type that allows creating a "disconnected" `Promise` that can be
|
||||
* returned and resolved separately.
|
||||
*/
|
||||
export function getResolver<T>(timeout: number = 5000) : Resolver<T> {
|
||||
const resolver: Partial<Resolver<T>> = {};
|
||||
resolver.id = generateId(8);
|
||||
resolver.completed = false;
|
||||
resolver.resolved = false;
|
||||
resolver.rejected = false;
|
||||
resolver.promise = new Promise((resolve, reject) => {
|
||||
resolver.reject = () => {
|
||||
resolver.completed = true;
|
||||
resolver.rejected = true;
|
||||
reject();
|
||||
}
|
||||
resolver.resolve = (data: T) => {
|
||||
resolver.completed = true;
|
||||
resolver.resolved = true;
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
resolver.timeout = setTimeout(() => {
|
||||
if (!resolver.completed) {
|
||||
resolver.reject!();
|
||||
}
|
||||
}, timeout);
|
||||
return resolver as Resolver<T>;
|
||||
}
|
||||
|
||||
|
||||
/** Waits a certain number of ms, as a `Promise.` */
|
||||
export function wait(ms = 16, value?: any) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(value);
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function dec2hex(dec: number) {
|
||||
return dec.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
/** Generates an unique id of a specific length. */
|
||||
export function generateId(length: number) {
|
||||
const arr = new Uint8Array(length / 2)
|
||||
crypto.getRandomValues(arr);
|
||||
return Array.from(arr, dec2hex).join('');
|
||||
}
|
||||
Vendored
+10
-1
@@ -1,4 +1,5 @@
|
||||
import { LGraphNode, IWidget, SerializedLGraphNode } from "./litegraph";
|
||||
import type { LGraphNode, IWidget, SerializedLGraphNode } from "./litegraph";
|
||||
import type {Constructor} from './index';
|
||||
import { ComfyApp } from "../../../../web/scripts/app";
|
||||
export { ComfyApp } from "../../../../web/scripts/app";
|
||||
|
||||
@@ -14,6 +15,14 @@ export interface ComfyGraphNode extends LGraphNode {
|
||||
onExecuted(message: any): void;
|
||||
}
|
||||
|
||||
export interface ComfyNode extends LGraphNode {
|
||||
comfyClass: string;
|
||||
}
|
||||
|
||||
export interface ComfyNodeConstructor extends Constructor<ComfyNode> {
|
||||
static title: string;
|
||||
static comfyClass: string;
|
||||
}
|
||||
|
||||
export type NodeMode = 0|1|2|3|4|undefined;
|
||||
|
||||
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
import { LGraph } from "litegraph";
|
||||
|
||||
export type Constructor<T> = new(...args: any[]) => T;
|
||||
|
||||
export type SerializedLink = [
|
||||
number, // this.id,
|
||||
number, // this.origin_id,
|
||||
number, // this.origin_slot,
|
||||
number, // this.target_id,
|
||||
number, // this.target_slot,
|
||||
number, // this.type
|
||||
];
|
||||
|
||||
export interface SerializedNodeInput {
|
||||
name: string;
|
||||
type: string;
|
||||
link: number;
|
||||
}
|
||||
export interface SerializedNodeOutput {
|
||||
name: string;
|
||||
type: string;
|
||||
link: number;
|
||||
slot_index: number;
|
||||
links: number[];
|
||||
}
|
||||
export interface SerializedNode {
|
||||
id: number;
|
||||
inputs: SerializedNodeInput[];
|
||||
outputs: SerializedNodeOutput[];
|
||||
mode: number;
|
||||
order: number;
|
||||
pos: [number, number];
|
||||
properties: any;
|
||||
size: [number, number];
|
||||
type: string;
|
||||
widgets_values: Array<number | string>;
|
||||
}
|
||||
|
||||
export interface SerializedGraph {
|
||||
config: any;
|
||||
extra: any;
|
||||
groups: any;
|
||||
last_link_id: number;
|
||||
last_node_id: number;
|
||||
links: SerializedLink[];
|
||||
nodes: SerializedNode[];
|
||||
}
|
||||
|
||||
export interface BadLinksData<T = SerializedGraph|LGraph> {
|
||||
hasBadLinks: boolean;
|
||||
fixed: boolean;
|
||||
graph: T;
|
||||
patched: number;
|
||||
deleted: number;
|
||||
}
|
||||
Vendored
+58
-6
@@ -38,6 +38,8 @@ export interface INodeSlot {
|
||||
hidden?: boolean;
|
||||
// @rgthree
|
||||
disabled?: boolean;
|
||||
// @rgthree
|
||||
removable?: boolean;
|
||||
}
|
||||
|
||||
export interface INodeInputSlot extends INodeSlot {
|
||||
@@ -56,7 +58,7 @@ export type WidgetCallback<T extends IWidget = IWidget> = (
|
||||
event?: MouseEvent
|
||||
) => void;
|
||||
|
||||
// #rgthree
|
||||
// @rgthree
|
||||
export type WidgetComboCallback<T extends IWidget = IWidget> = (
|
||||
this: T,
|
||||
value: T["value"][0],
|
||||
@@ -97,6 +99,7 @@ export interface IWidget<TValue = any, TOptions = any> {
|
||||
): boolean;
|
||||
/** Called by `LGraphNode.computeSize` */
|
||||
computeSize?(width: number): [number, number];
|
||||
serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number): TValue;
|
||||
}
|
||||
export interface IButtonWidget extends IWidget<null, {}> {
|
||||
type: "button";
|
||||
@@ -200,7 +203,7 @@ export const LiteGraph: {
|
||||
CIRCLE_SHAPE: 3;
|
||||
CARD_SHAPE: 4;
|
||||
ARROW_SHAPE: 5;
|
||||
SQUARE_SHAPE: 6;
|
||||
GRID_SHAPE: 6;
|
||||
|
||||
//enums
|
||||
INPUT: 1;
|
||||
@@ -253,6 +256,9 @@ export const LiteGraph: {
|
||||
}
|
||||
>;
|
||||
|
||||
//@rgthree
|
||||
isValidConnection(type: string|string[], type: string|string[]):boolean;
|
||||
|
||||
createNode<T extends LGraphNode = LGraphNode>(type: string): T;
|
||||
/** Register a node class so it can be listed when the user wants to create a new one */
|
||||
registerNodeType<T extends LGraphNode = LGraphNode>(type: string, base: { new (title?: string): T }): void;
|
||||
@@ -612,7 +618,14 @@ export type SerializedLGraphNode<T extends LGraphNode = LGraphNode> = {
|
||||
|
||||
/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */
|
||||
export declare class LGraphNode {
|
||||
static title_color: string;
|
||||
|
||||
// @rgthree added
|
||||
findInputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number
|
||||
findOutputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number
|
||||
|
||||
// end @rgthree added
|
||||
|
||||
static title_color?: string;
|
||||
static title: string;
|
||||
static type: null | string;
|
||||
static widgets_up: boolean;
|
||||
@@ -677,6 +690,13 @@ export declare class LGraphNode {
|
||||
/** if true, the node will show the bgcolor as 'red' */
|
||||
has_errors?: boolean;
|
||||
|
||||
// @rgthree
|
||||
setSize(size: Vector2): void;
|
||||
onResize?(size: Vector2): void;
|
||||
onInputClick(slot: number, event: MouseEvent): void;
|
||||
onOutputClick(slot: number, event: MouseEvent): void;
|
||||
getConnectionPos(isInput: boolean, slotNumber: number, out: Vector2): Vector2;
|
||||
|
||||
/** configure a node from an object containing the serialized info */
|
||||
configure(info: SerializedLGraphNode): void;
|
||||
/** serialize the content */
|
||||
@@ -886,6 +906,22 @@ export declare class LGraphNode {
|
||||
targetNode: LGraphNode,
|
||||
targetSlot: number | string
|
||||
): T | null;
|
||||
|
||||
connectByTypeOutput<T = any>(
|
||||
slot: number | string,
|
||||
sourceNode: LGraphNode,
|
||||
sourceSlotType: string,
|
||||
optsIn: string
|
||||
): T | null;
|
||||
|
||||
connectByType<T = any>(
|
||||
slot: number | string,
|
||||
sourceNode: LGraphNode,
|
||||
sourceSlotType: string,
|
||||
optsIn: string
|
||||
): T | null;
|
||||
|
||||
|
||||
/**
|
||||
* disconnect one output to an specific node
|
||||
* @param slot (could be the number of the slot or the string with the name of the slot)
|
||||
@@ -929,11 +965,13 @@ export declare class LGraphNode {
|
||||
// https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance
|
||||
onDrawBackground?(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvas: HTMLCanvasElement
|
||||
// @rgthree fixed
|
||||
canvas: LGraphCanvas
|
||||
): void;
|
||||
onDrawForeground?(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvas: HTMLCanvasElement
|
||||
// @rgthree fixed
|
||||
canvas: LGraphCanvas
|
||||
): void;
|
||||
|
||||
// https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour
|
||||
@@ -1051,13 +1089,25 @@ export declare class LGraphNode {
|
||||
|
||||
/** Called by `LGraphCanvas.processContextMenu` */
|
||||
getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[];
|
||||
getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[];
|
||||
// rgthree
|
||||
getSlotMenuOptions?(slotItem: {slot: number, input?: INodeInputSlot, output?: INodeOutputSlot, link_pos: Vector2}): ContextMenuItem[];
|
||||
|
||||
getExtraMenuOptions?(canvas: LGraphCanvas, options: ContextMenuItem[]): void;
|
||||
}
|
||||
|
||||
export type LGraphNodeConstructor<T extends LGraphNode = LGraphNode> = {
|
||||
new (): T;
|
||||
|
||||
// @rgthree
|
||||
title_mode?:
|
||||
typeof LiteGraph.NORMAL_TITLE |
|
||||
typeof LiteGraph.TRANSPARENT_TITLE |
|
||||
typeof LiteGraph.AUTOHIDE_TITLE |
|
||||
typeof LiteGraph.NO_TITLE;
|
||||
title: string;
|
||||
category: string;
|
||||
type: string;
|
||||
comfyClass?: string;
|
||||
};
|
||||
|
||||
export type SerializedLGraphGroup = {
|
||||
@@ -1071,6 +1121,8 @@ export declare class LGraphGroup {
|
||||
private _bounding: Vector4;
|
||||
color: string;
|
||||
font: string;
|
||||
// @rgthree
|
||||
_nodes: LGraphNode[];
|
||||
|
||||
configure(o: SerializedLGraphGroup): void;
|
||||
serialize(): SerializedLGraphGroup;
|
||||
|
||||
+663
-189
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -2,10 +2,14 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "ESNext",
|
||||
"typeRoots": [
|
||||
"./ts/typings",
|
||||
],
|
||||
"outDir": "./js/",
|
||||
// "typeRoots": [
|
||||
// "./ts/typings",
|
||||
// ],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"*": ["ts/typings/*"],
|
||||
},
|
||||
"outDir": "web/",
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
@@ -16,8 +20,8 @@
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
@@ -28,7 +32,7 @@
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"include": [
|
||||
"ts/*.ts",
|
||||
"ts/*.ts", "ts/**/*.ts", "ts/typings/index.d.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { IoDirection, addConnectionLayoutSupport, applyMixins, followConnectionUntilType, } from "./utils.js";
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
let hasShownAlertForUpdatingInt = false;
|
||||
class AnySwitchforMixin extends RgthreeBaseNode {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.scheduleStabilizePromise = null;
|
||||
this.nodeType = null;
|
||||
}
|
||||
onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot) {
|
||||
var _a;
|
||||
(_a = super.onConnectionsChange) === null || _a === void 0 ? void 0 : _a.call(this, 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() {
|
||||
let connectedType = followConnectionUntilType(this, IoDirection.INPUT, undefined, true);
|
||||
if (!connectedType) {
|
||||
connectedType = followConnectionUntilType(this, IoDirection.OUTPUT, undefined, true);
|
||||
}
|
||||
this.nodeType = (connectedType === null || connectedType === void 0 ? void 0 : connectedType.type) || "*";
|
||||
for (const input of this.inputs) {
|
||||
input.type = this.nodeType;
|
||||
}
|
||||
for (const output of this.outputs) {
|
||||
output.type = this.nodeType;
|
||||
output.label =
|
||||
output.type === 'RGTHREE_CONTEXT' ? 'CONTEXT' :
|
||||
Array.isArray(this.nodeType) || this.nodeType.includes(",")
|
||||
? (connectedType === null || connectedType === void 0 ? void 0 : connectedType.label) || (connectedType === null || connectedType === void 0 ? void 0 : connectedType.name) || String(this.nodeType)
|
||||
: String(this.nodeType);
|
||||
}
|
||||
}
|
||||
static setUp(nodeType) {
|
||||
AnySwitchforMixin.title = nodeType.title;
|
||||
AnySwitchforMixin.type = nodeType.type || nodeType.title;
|
||||
AnySwitchforMixin.comfyClass = nodeType.comfyClass;
|
||||
setTimeout(() => {
|
||||
AnySwitchforMixin.category = nodeType.category;
|
||||
});
|
||||
applyMixins(nodeType, [RgthreeBaseNode, AnySwitchforMixin]);
|
||||
addConnectionLayoutSupport(nodeType, app, [["Left"], ["Right"]]);
|
||||
}
|
||||
}
|
||||
AnySwitchforMixin.comfyClass = "";
|
||||
app.registerExtension({
|
||||
name: "rgthree.AnySwitch",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "Any Switch (rgthree)") {
|
||||
AnySwitchforMixin.setUp(nodeType);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
import { PassThroughFollowing, addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodes, getConnectedOutputNodesAndFilterPassThroughs } from "./utils.js";
|
||||
export class BaseAnyInputConnectedNode extends RgthreeBaseNode {
|
||||
constructor(title = BaseAnyInputConnectedNode.title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
this.inputsPassThroughFollowing = PassThroughFollowing.NONE;
|
||||
this.debouncerTempWidth = 0;
|
||||
this.schedulePromise = null;
|
||||
this.addInput("", "*");
|
||||
}
|
||||
scheduleStabilizeWidgets(ms = 100) {
|
||||
if (!this.schedulePromise) {
|
||||
this.schedulePromise = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.schedulePromise = null;
|
||||
this.doStablization();
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
return this.schedulePromise;
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
while (cloned.inputs.length > 1) {
|
||||
cloned.removeInput(cloned.inputs.length - 1);
|
||||
}
|
||||
if (cloned.inputs[0]) {
|
||||
cloned.inputs[0].label = '';
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
stabilizeInputsOutputs() {
|
||||
var _a;
|
||||
const hasEmptyInput = !((_a = this.inputs[this.inputs.length - 1]) === null || _a === void 0 ? void 0 : _a.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 === null || node === void 0 ? void 0 : node.title) || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
doStablization() {
|
||||
if (!this.graph) {
|
||||
return;
|
||||
}
|
||||
this._tempWidth = this.size[0];
|
||||
const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this);
|
||||
this.stabilizeInputsOutputs();
|
||||
this.handleLinkedNodesStabilization(linkedNodes);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
this.scheduleStabilizeWidgets(500);
|
||||
}
|
||||
handleLinkedNodesStabilization(linkedNodes) {
|
||||
linkedNodes;
|
||||
throw new Error('handleLinkedNodesStabilization should be overridden.');
|
||||
}
|
||||
onConnectionsChainChange() {
|
||||
this.scheduleStabilizeWidgets();
|
||||
}
|
||||
onConnectionsChange(type, index, connected, linkInfo, ioSlot) {
|
||||
super.onConnectionsChange && super.onConnectionsChange(type, index, connected, linkInfo, ioSlot);
|
||||
if (!linkInfo)
|
||||
return;
|
||||
const connectedNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
|
||||
for (const node of connectedNodes) {
|
||||
if (node.onConnectionsChainChange) {
|
||||
node.onConnectionsChainChange();
|
||||
}
|
||||
}
|
||||
this.scheduleStabilizeWidgets();
|
||||
}
|
||||
removeInput(slot) {
|
||||
this._tempWidth = this.size[0];
|
||||
return super.removeInput(slot);
|
||||
}
|
||||
addInput(name, type, extra_info) {
|
||||
this._tempWidth = this.size[0];
|
||||
return super.addInput(name, type, extra_info);
|
||||
}
|
||||
addWidget(type, name, value, callback, options) {
|
||||
this._tempWidth = this.size[0];
|
||||
return super.addWidget(type, name, value, callback, options);
|
||||
}
|
||||
removeWidget(widgetOrSlot) {
|
||||
this._tempWidth = this.size[0];
|
||||
super.removeWidget(widgetOrSlot);
|
||||
}
|
||||
computeSize(out) {
|
||||
var _a, _b;
|
||||
let size = super.computeSize(out);
|
||||
if (this._tempWidth) {
|
||||
size[0] = this._tempWidth;
|
||||
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
|
||||
this.debouncerTempWidth = setTimeout(() => {
|
||||
this._tempWidth = null;
|
||||
}, 32);
|
||||
}
|
||||
if (this.properties['collapse_connections']) {
|
||||
const rows = Math.max(((_a = this.inputs) === null || _a === void 0 ? void 0 : _a.length) || 0, ((_b = this.outputs) === null || _b === void 0 ? void 0 : _b.length) || 0, 1) - 1;
|
||||
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
|
||||
}
|
||||
setTimeout(() => {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}, 16);
|
||||
return size;
|
||||
}
|
||||
onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) {
|
||||
let canConnect = true;
|
||||
if (super.onConnectOutput) {
|
||||
canConnect = super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
||||
}
|
||||
if (canConnect) {
|
||||
const nodes = getConnectedInputNodes(this);
|
||||
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;
|
||||
}
|
||||
onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
|
||||
let canConnect = true;
|
||||
if (super.onConnectInput) {
|
||||
canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex);
|
||||
}
|
||||
if (canConnect) {
|
||||
const nodes = getConnectedOutputNodes(this);
|
||||
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;
|
||||
}
|
||||
connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn) {
|
||||
const lastInput = this.inputs[this.inputs.length - 1];
|
||||
if (!(lastInput === null || lastInput === void 0 ? void 0 : lastInput.link) && (lastInput === null || lastInput === void 0 ? void 0 : lastInput.type) === '*') {
|
||||
var sourceSlot = sourceNode.findOutputSlotByType(sourceSlotType, false, true);
|
||||
return sourceNode.connect(sourceSlot, this, slot);
|
||||
}
|
||||
return super.connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn);
|
||||
}
|
||||
static setUp(clazz) {
|
||||
addConnectionLayoutSupport(clazz, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
addMenuItem(clazz, app, {
|
||||
name: (node) => { var _a; return (`${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']) ? 'Show' : 'Collapse'} Connections`); },
|
||||
property: 'collapse_connections',
|
||||
prepareValue: (_value, node) => { var _a; return !((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']); },
|
||||
callback: (_node) => { app.graph.setDirtyCanvas(true, true); }
|
||||
});
|
||||
LiteGraph.registerNodeType(clazz.type, clazz);
|
||||
clazz.category = clazz._category;
|
||||
}
|
||||
}
|
||||
const oldLGraphNodeConnectByType = LGraphNode.prototype.connectByType;
|
||||
LGraphNode.prototype.connectByType = function connectByType(slot, sourceNode, sourceSlotType, optsIn) {
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
export class RgthreeBaseNode extends LGraphNode {
|
||||
constructor(title = RgthreeBaseNode.title) {
|
||||
super(title);
|
||||
this._tempWidth = 0;
|
||||
this.configuring = false;
|
||||
this.isVirtualNode = false;
|
||||
if (title == '__NEED_NAME__') {
|
||||
throw new Error('RgthreeBaseNode needs overrides.');
|
||||
}
|
||||
this.properties = this.properties || {};
|
||||
}
|
||||
configure(info) {
|
||||
this.configuring = true;
|
||||
super.configure(info);
|
||||
for (const w of (this.widgets || [])) {
|
||||
w.last_y = w.last_y || 0;
|
||||
}
|
||||
this.configuring = false;
|
||||
}
|
||||
set mode(mode) {
|
||||
if (this.mode_ != mode) {
|
||||
this.mode_ = mode;
|
||||
this.onModeChange();
|
||||
}
|
||||
}
|
||||
get mode() {
|
||||
return this.mode_;
|
||||
}
|
||||
onModeChange() {
|
||||
}
|
||||
async handleAction(action) {
|
||||
action;
|
||||
}
|
||||
removeWidget(widgetOrSlot) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
static setUp(...args) {
|
||||
}
|
||||
}
|
||||
RgthreeBaseNode.exposedActions = [];
|
||||
RgthreeBaseNode.title = "__NEED_NAME__";
|
||||
RgthreeBaseNode.category = 'rgthree';
|
||||
RgthreeBaseNode._category = 'rgthree';
|
||||
const overriddenServerNodes = new Map();
|
||||
export class RgthreeBaseServerNode extends RgthreeBaseNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.serialize_widgets = true;
|
||||
this.setupFromServerNodeData();
|
||||
}
|
||||
getWidgets() {
|
||||
return ComfyWidgets;
|
||||
}
|
||||
onDrawForeground(ctx, canvas) {
|
||||
var _a, _b;
|
||||
const nodeType = this.constructor.nodeType;
|
||||
(_b = (_a = nodeType === null || nodeType === void 0 ? void 0 : nodeType.prototype) === null || _a === void 0 ? void 0 : _a.onDrawForeground) === null || _b === void 0 ? void 0 : _b.apply(this, [ctx, canvas]);
|
||||
super.onDrawForeground && super.onDrawForeground(ctx, canvas);
|
||||
}
|
||||
async setupFromServerNodeData() {
|
||||
var _a, _b, _c;
|
||||
const nodeData = this.constructor.nodeData;
|
||||
if (!nodeData) {
|
||||
throw Error('No node data');
|
||||
}
|
||||
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: 1, minHeight: 1, widget: null };
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
if ((_a = inputData[1]) === null || _a === void 0 ? void 0 : _a.forceInput) {
|
||||
this.addInput(inputName, type);
|
||||
}
|
||||
else {
|
||||
let widgetCreated = true;
|
||||
if (Array.isArray(type)) {
|
||||
Object.assign(config, WIDGETS.COMBO(this, inputName, inputData, app) || {});
|
||||
}
|
||||
else if (`${type}:${inputName}` in WIDGETS) {
|
||||
Object.assign(config, WIDGETS[`${type}:${inputName}`](this, inputName, inputData, app) || {});
|
||||
}
|
||||
else if (type in WIDGETS) {
|
||||
Object.assign(config, WIDGETS[type](this, inputName, inputData, app) || {});
|
||||
}
|
||||
else {
|
||||
this.addInput(inputName, type);
|
||||
widgetCreated = false;
|
||||
}
|
||||
if (widgetCreated && ((_b = inputData[1]) === null || _b === void 0 ? void 0 : _b.forceInput) && (config === null || config === void 0 ? void 0 : config.widget)) {
|
||||
if (!config.widget.options)
|
||||
config.widget.options = {};
|
||||
config.widget.options.forceInput = inputData[1].forceInput;
|
||||
}
|
||||
if (widgetCreated && ((_c = inputData[1]) === null || _c === void 0 ? void 0 : _c.defaultInput) && (config === null || config === void 0 ? void 0 : 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 registerForOverride(comfyClass, rgthreeClass) {
|
||||
if (overriddenServerNodes.has(comfyClass)) {
|
||||
throw Error(`Already have a class to overridde ${comfyClass.type || comfyClass.name || comfyClass.title}`);
|
||||
}
|
||||
overriddenServerNodes.set(comfyClass, rgthreeClass);
|
||||
}
|
||||
}
|
||||
RgthreeBaseServerNode.nodeData = null;
|
||||
RgthreeBaseServerNode.nodeType = null;
|
||||
const oldregisterNodeType = LiteGraph.registerNodeType;
|
||||
LiteGraph.registerNodeType = function (nodeId, baseClass) {
|
||||
const clazz = overriddenServerNodes.get(baseClass) || baseClass;
|
||||
if (clazz !== baseClass) {
|
||||
rgthree.logger.debug(`For "${nodeId}", replacing default ComfyNode implementation with custom ${clazz.type || clazz.name || clazz.title} class.`);
|
||||
}
|
||||
return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { rgthree } from "./rgthree.js";
|
||||
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
||||
import { PassThroughFollowing, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, shouldPassThrough } from "./utils.js";
|
||||
export class BaseCollectorNode extends BaseAnyInputConnectedNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.inputsPassThroughFollowing = PassThroughFollowing.REROUTE_ONLY;
|
||||
this.addInput("", "*");
|
||||
this.addOutput("Output", "*");
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
return cloned;
|
||||
}
|
||||
handleLinkedNodesStabilization(linkedNodes) {
|
||||
}
|
||||
onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
|
||||
let canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex);
|
||||
if (canConnect) {
|
||||
const allConnectedNodes = getConnectedInputNodes(this);
|
||||
const nodesAlreadyInSlot = getConnectedInputNodes(this, undefined, inputIndex);
|
||||
if (allConnectedNodes.includes(outputNode)) {
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
||||
import { PassThroughFollowing } from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
export class BaseNodeModeChanger extends BaseAnyInputConnectedNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.inputsPassThroughFollowing = PassThroughFollowing.ALL;
|
||||
this.isVirtualNode = true;
|
||||
this.modeOn = -1;
|
||||
this.modeOff = -1;
|
||||
wait(10).then(() => {
|
||||
if (this.modeOn < 0 || this.modeOff < 0) {
|
||||
throw new Error('modeOn and modeOff must be overridden.');
|
||||
}
|
||||
});
|
||||
this.addOutput("OPT_CONNECTION", "*");
|
||||
}
|
||||
handleLinkedNodesStabilization(linkedNodes) {
|
||||
for (const [index, node] of linkedNodes.entries()) {
|
||||
let widget = this.widgets && this.widgets[index];
|
||||
if (!widget) {
|
||||
this._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;
|
||||
}
|
||||
}
|
||||
setWidget(widget, linkedNode) {
|
||||
const off = linkedNode.mode === this.modeOff;
|
||||
widget.name = `Enable ${linkedNode.title}`;
|
||||
widget.options = { 'on': 'yes', 'off': 'no' };
|
||||
widget.value = !off;
|
||||
widget.doModeChange = (force) => {
|
||||
let off = force == null ? linkedNode.mode === this.modeOff : force;
|
||||
linkedNode.mode = (off ? this.modeOn : this.modeOff);
|
||||
widget.value = off;
|
||||
};
|
||||
widget.callback = () => {
|
||||
widget.doModeChange();
|
||||
};
|
||||
}
|
||||
forceWidgetOff(widget) {
|
||||
widget.doModeChange(false);
|
||||
}
|
||||
forceWidgetOn(widget) {
|
||||
widget.doModeChange(true);
|
||||
}
|
||||
static setUp(clazz) {
|
||||
BaseAnyInputConnectedNode.setUp(clazz);
|
||||
}
|
||||
}
|
||||
BaseNodeModeChanger.collapsible = false;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { app } from '../../scripts/app.js';
|
||||
import { api } from '../../scripts/api.js';
|
||||
import { addConnectionLayoutSupport, wait } from './utils.js';
|
||||
class PowerPrompt {
|
||||
import { wait } from "./shared_utils.js";
|
||||
export class PowerPrompt {
|
||||
constructor(node, nodeData) {
|
||||
this.combos = {};
|
||||
this.combosValues = {};
|
||||
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].inputEl;
|
||||
@@ -33,7 +33,18 @@ class PowerPrompt {
|
||||
}
|
||||
return canConnect && !this.node.outputs[outputIndex].disabled;
|
||||
};
|
||||
this.node.widgets.splice(1);
|
||||
const onPropertyChanged = this.node.onPropertyChanged;
|
||||
this.node.onPropertyChanged = (property, value, prevValue) => {
|
||||
onPropertyChanged && onPropertyChanged.call(this, property, value, prevValue);
|
||||
if (property === 'combos_filter') {
|
||||
this.refreshCombos(this.nodeData);
|
||||
}
|
||||
};
|
||||
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();
|
||||
@@ -67,17 +78,29 @@ class PowerPrompt {
|
||||
onFreshNodeDefs(event) {
|
||||
this.refreshCombos(event.detail[this.nodeData.name]);
|
||||
}
|
||||
findAndPatchCombos() {
|
||||
shouldRemoveServerWidget(widget) {
|
||||
var _a, _b, _c, _d;
|
||||
return ((_a = widget.name) === null || _a === void 0 ? void 0 : _a.startsWith('insert_')) || ((_b = widget.name) === null || _b === void 0 ? void 0 : _b.startsWith('target_')) || ((_c = widget.name) === null || _c === void 0 ? void 0 : _c.startsWith('crop_')) || ((_d = widget.name) === null || _d === void 0 ? void 0 : _d.startsWith('values_'));
|
||||
}
|
||||
refreshCombos(nodeData) {
|
||||
var _a, _b;
|
||||
var _a, _b, _c;
|
||||
this.nodeData = nodeData;
|
||||
let data = ((_a = this.nodeData.input) === null || _a === void 0 ? void 0 : _a.optional) || {};
|
||||
data = Object.assign(data, ((_b = this.nodeData.input) === null || _b === void 0 ? void 0 : _b.hidden) || {});
|
||||
let filter = null;
|
||||
if ((_a = this.node.properties['combos_filter']) === null || _a === void 0 ? void 0 : _a.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;
|
||||
}
|
||||
}
|
||||
let data = Object.assign({}, ((_b = this.nodeData.input) === null || _b === void 0 ? void 0 : _b.optional) || {}, ((_c = this.nodeData.input) === null || _c === void 0 ? void 0 : _c.hidden) || {});
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (Array.isArray(value[0])) {
|
||||
const values = value[0];
|
||||
let values = value[0];
|
||||
if (key.startsWith('insert')) {
|
||||
values = filter ? values.filter((v, i) => i < 1 || (i == 1 && v.match(/^disable\s[a-z]/i)) || (filter === null || filter === void 0 ? void 0 : 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]) {
|
||||
@@ -153,7 +176,7 @@ class PowerPrompt {
|
||||
let fullText = this.promptEl.value;
|
||||
let selectedText = fullText.substring(start, end);
|
||||
if (!selectedText) {
|
||||
const stopOn = "<>() \r\n\t";
|
||||
const stopOn = "<>()\r\n\t";
|
||||
if (fullText[start] == '>') {
|
||||
start -= 2;
|
||||
end -= 2;
|
||||
@@ -191,38 +214,3 @@ class PowerPrompt {
|
||||
};
|
||||
}
|
||||
}
|
||||
let nodeData = null;
|
||||
app.registerExtension({
|
||||
name: 'rgthree.PowerPrompt',
|
||||
async beforeRegisterNodeDef(nodeType, passedNodeData, _app) {
|
||||
if (passedNodeData.name.startsWith('Power Prompt') && passedNodeData.name.includes('rgthree')) {
|
||||
nodeData = passedNodeData;
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||
this.powerPrompt = new PowerPrompt(this, passedNodeData);
|
||||
};
|
||||
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
}
|
||||
},
|
||||
async loadedGraphNode(node) {
|
||||
if (node.type === 'Power Prompt (rgthree)') {
|
||||
setTimeout(() => {
|
||||
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];
|
||||
node.outputs[0].name = nodeData.output_name[0] || node.outputs[0].type;
|
||||
node.outputs[0].color_on = undefined;
|
||||
node.outputs[0].color_off = undefined;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -9,7 +9,20 @@ class BypasserNode extends BaseNodeModeChanger {
|
||||
this.modeOn = MODE_ALWAYS;
|
||||
this.modeOff = MODE_BYPASS;
|
||||
}
|
||||
async handleAction(action) {
|
||||
if (action === 'Bypass all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOff(widget);
|
||||
}
|
||||
}
|
||||
else if (action === 'Enable all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOn(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BypasserNode.exposedActions = ['Bypass all', 'Enable all'];
|
||||
BypasserNode.type = NodeTypesString.FAST_BYPASSER;
|
||||
BypasserNode.title = NodeTypesString.FAST_BYPASSER;
|
||||
app.registerExtension({
|
||||
@@ -9,4 +9,8 @@ export const NodeTypesString = {
|
||||
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
|
||||
FAST_MUTER: addRgthree('Fast Muter'),
|
||||
FAST_BYPASSER: addRgthree('Fast Bypasser'),
|
||||
FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'),
|
||||
NODE_COLLECTOR: addRgthree('Node Collector'),
|
||||
REROUTE: addRgthree('Reroute'),
|
||||
RANDOM_UNMUTER: addRgthree('Random Unmuter'),
|
||||
};
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { IoDirection, addConnectionLayoutSupport, addMenuItem, matchLocalSlotsToServer, replaceNode, } from "./utils.js";
|
||||
import { RgthreeBaseServerNode } from "./base_node.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
function findMatchingIndexByTypeOrName(otherNode, otherSlot, ctxSlots) {
|
||||
const otherNodeType = (otherNode.type || '').toUpperCase();
|
||||
const otherNodeName = (otherNode.title || '').toUpperCase();
|
||||
let otherSlotType = otherSlot.type;
|
||||
if (Array.isArray(otherSlotType) || otherSlotType.includes(',')) {
|
||||
otherSlotType = 'COMBO';
|
||||
}
|
||||
const otherSlotName = otherSlot.name.toUpperCase().replace('OPT_', '').replace('_NAME', '');
|
||||
const otherSlotLabel = (otherSlot.label || otherSlotName).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', '');
|
||||
const ctxSlotLabel = (ctxSlot.label || ctxSlotName).toUpperCase().replace('OPT_', '').replace('_NAME', '');
|
||||
let ctxSlotType = ctxSlot.type;
|
||||
if (Array.isArray(ctxSlotType) || ctxSlotType.includes(',')) {
|
||||
ctxSlotType = 'COMBO';
|
||||
}
|
||||
if (ctxSlotType !== otherSlotType) {
|
||||
return false;
|
||||
}
|
||||
if (ctxSlotName === otherSlotName
|
||||
|| (ctxSlotLabel && otherSlotLabel && ctxSlotLabel == otherSlotLabel)
|
||||
|| (ctxSlotName === "SEED" && otherSlotName.includes("SEED"))
|
||||
|| (ctxSlotName === "STEP_REFINER" && otherSlotName.includes("AT_STEP"))
|
||||
|| (ctxSlotName === "STEP_REFINER" && otherSlotName.includes("REFINER_STEP"))) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
export class BaseContextNode extends RgthreeBaseServerNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
}
|
||||
connectByType(slot, sourceNode, sourceSlotType, optsIn) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn) {
|
||||
var _a;
|
||||
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;
|
||||
for (const [index, output] of (sourceNode.outputs || []).entries()) {
|
||||
if (((_a = output.links) === null || _a === void 0 ? void 0 : _a.length) && !ctrlKey) {
|
||||
continue;
|
||||
}
|
||||
const thisInputSlot = findMatchingIndexByTypeOrName(sourceNode, output, this.inputs);
|
||||
if (thisInputSlot > -1) {
|
||||
sourceNode.connect(index, this, thisInputSlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static setUp(comfyClass, ctxClass) {
|
||||
RgthreeBaseServerNode.registerForOverride(comfyClass, ctxClass);
|
||||
addConnectionLayoutSupport(ctxClass, app, [
|
||||
["Left", "Right"],
|
||||
["Right", "Left"],
|
||||
]);
|
||||
setTimeout(() => {
|
||||
ctxClass.category = comfyClass.category;
|
||||
});
|
||||
}
|
||||
}
|
||||
class ContextNode extends BaseContextNode {
|
||||
constructor(title = ContextNode.title) {
|
||||
super(title);
|
||||
}
|
||||
static setUp(comfyClass) {
|
||||
BaseContextNode.setUp(comfyClass, ContextNode);
|
||||
addMenuItem(ContextNode, app, {
|
||||
name: "Convert To Context Big",
|
||||
callback: (node) => {
|
||||
replaceNode(node, ContextBigNode.type);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ContextNode.title = "Context (rgthree)";
|
||||
ContextNode.type = "Context (rgthree)";
|
||||
ContextNode.comfyClass = "Context (rgthree)";
|
||||
class ContextBigNode extends BaseContextNode {
|
||||
constructor(title = ContextBigNode.title) {
|
||||
super(title);
|
||||
}
|
||||
static setUp(comfyClass) {
|
||||
BaseContextNode.setUp(comfyClass, ContextBigNode);
|
||||
addMenuItem(ContextBigNode, app, {
|
||||
name: "Convert To Context (Original)",
|
||||
callback: (node) => {
|
||||
replaceNode(node, ContextNode.type);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ContextBigNode.title = "Context Big (rgthree)";
|
||||
ContextBigNode.type = "Context Big (rgthree)";
|
||||
ContextBigNode.comfyClass = "Context Big (rgthree)";
|
||||
class ContextSwitchNode extends BaseContextNode {
|
||||
constructor(title = ContextSwitchNode.title) {
|
||||
super(title);
|
||||
}
|
||||
static setUp(comfyClass) {
|
||||
BaseContextNode.setUp(comfyClass, ContextSwitchNode);
|
||||
addMenuItem(ContextSwitchNode, app, {
|
||||
name: "Convert To Context Switch Big",
|
||||
callback: (node) => {
|
||||
replaceNode(node, ContextSwitchBigNode.type);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ContextSwitchNode.title = "Context Switch (rgthree)";
|
||||
ContextSwitchNode.type = "Context Switch (rgthree)";
|
||||
ContextSwitchNode.comfyClass = "Context Switch (rgthree)";
|
||||
class ContextSwitchBigNode extends BaseContextNode {
|
||||
constructor(title = ContextSwitchBigNode.title) {
|
||||
super(title);
|
||||
}
|
||||
static setUp(comfyClass) {
|
||||
BaseContextNode.setUp(comfyClass, ContextSwitchBigNode);
|
||||
addMenuItem(ContextSwitchBigNode, app, {
|
||||
name: "Convert To Context Switch",
|
||||
callback: (node) => {
|
||||
replaceNode(node, ContextSwitchNode.type);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ContextSwitchBigNode.title = "Context Switch Big (rgthree)";
|
||||
ContextSwitchBigNode.type = "Context Switch Big (rgthree)";
|
||||
ContextSwitchBigNode.comfyClass = "Context Switch Big (rgthree)";
|
||||
function addWidgetForDynamicContextOutputs(node, inputName) {
|
||||
node.addCustomWidget({
|
||||
name: inputName,
|
||||
value: '',
|
||||
draw(ctx, node, width, posY, height) {
|
||||
return;
|
||||
},
|
||||
computeSize(width) {
|
||||
return [0, 0];
|
||||
},
|
||||
serializeValue() {
|
||||
const value = (node.outputs || []).map((o, i) => i > 0 && o.name).filter(n => n !== false).join(',');
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
const contextNodes = [ContextNode, ContextBigNode, ContextSwitchNode, ContextSwitchBigNode];
|
||||
const contextTypeToServerDef = {};
|
||||
function fixBadConfigs(node) {
|
||||
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, nodeData) {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
async nodeCreated(node) {
|
||||
const type = node.type || node.constructor.type;
|
||||
const serverDef = type && contextTypeToServerDef[type];
|
||||
if (serverDef) {
|
||||
fixBadConfigs(node);
|
||||
if (!type.includes('Dynamic')) {
|
||||
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
|
||||
if (!type.includes("Switch")) {
|
||||
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadedGraphNode(node) {
|
||||
const type = node.type || node.constructor.type;
|
||||
const serverDef = type && contextTypeToServerDef[type];
|
||||
if (serverDef) {
|
||||
fixBadConfigs(node);
|
||||
if (!type.includes('Dynamic')) {
|
||||
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
|
||||
if (!type.includes("Switch")) {
|
||||
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,745 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { IoDirection, PassThroughFollowing, followConnectionUntilType, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedNodesInfo, getConnectedOutputNodesAndFilterPassThroughs, shouldPassThrough, } from "./utils.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
import { BaseContextNode } from "./context.js";
|
||||
class ContextDynamicNodeBase extends BaseContextNode {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.hasShadowInputs = false;
|
||||
}
|
||||
getContextInputsList() {
|
||||
return this.inputs;
|
||||
}
|
||||
onConnectionsChainChange(arg) {
|
||||
console.log("ContextDynamicNodeBase: onConnectionsChainChange", this.id, arg);
|
||||
}
|
||||
onNodeCreated() {
|
||||
const inputs = this.getContextInputsList();
|
||||
if (inputs[inputs.length - 1].type === "*") {
|
||||
this.removeOutput(inputs.length - 1);
|
||||
}
|
||||
else {
|
||||
this.addInput("+", "*");
|
||||
}
|
||||
}
|
||||
getWidgets() {
|
||||
return Object.assign({}, super.getWidgets(), {
|
||||
DYNAMIC_CONTEXT_OUTPUTS: (node, inputName, inputData, app) => {
|
||||
node.addCustomWidget({
|
||||
name: inputName,
|
||||
value: "",
|
||||
draw(ctx, node, width, posY, height) {
|
||||
return;
|
||||
},
|
||||
computeSize(width) {
|
||||
return [0, 0];
|
||||
},
|
||||
serializeValue() {
|
||||
const value = (node.outputs || [])
|
||||
.map((o, i) => i > 0 && o.name)
|
||||
.filter((n) => n !== false)
|
||||
.join(",");
|
||||
return value;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
stripOwnedPrefix(name) {
|
||||
return name.replace(/^\+\s*/, "");
|
||||
}
|
||||
addOwnedPrefix(name) {
|
||||
return `+ ${this.stripOwnedPrefix(name)}`;
|
||||
}
|
||||
isOwnedInput(inputOrName) {
|
||||
const name = typeof inputOrName == "string" ? inputOrName : (inputOrName === null || inputOrName === void 0 ? void 0 : inputOrName.name) || "";
|
||||
return name.startsWith("+ ") || name === "+";
|
||||
}
|
||||
getNextUniqueNameForThisNode(desiredName) {
|
||||
const inputs = this.getContextInputsList();
|
||||
const allExistingKeys = inputs.map((i) => this.stripOwnedPrefix(i.name).toLocaleUpperCase());
|
||||
desiredName = this.stripOwnedPrefix(desiredName);
|
||||
let newName = desiredName;
|
||||
let n = 0;
|
||||
while (allExistingKeys.includes(newName.toLocaleUpperCase())) {
|
||||
newName = `${desiredName}.${++n}`;
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot) {
|
||||
var _a;
|
||||
(_a = super.onConnectionsChange) === null || _a === void 0 ? void 0 : _a.call(this, type, slotIndex, isConnected, linkInfo, ioSlot);
|
||||
if (this.configuring) {
|
||||
return;
|
||||
}
|
||||
if (type === LiteGraph.INPUT) {
|
||||
if (isConnected) {
|
||||
this.handleInputConnected(slotIndex);
|
||||
}
|
||||
else {
|
||||
this.handleInputDisconnected(slotIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
handleInputConnected(slotIndex) {
|
||||
}
|
||||
handleInputDisconnected(slotIndex) {
|
||||
}
|
||||
updateFromUpstream(update, node, updatedSlotData) {
|
||||
}
|
||||
provideInputsData() {
|
||||
const inputs = this.getContextInputsList();
|
||||
return inputs
|
||||
.map((input, index) => ({
|
||||
name: this.stripOwnedPrefix(input.name),
|
||||
type: String(input.type),
|
||||
index,
|
||||
}))
|
||||
.filter((i) => i.type !== "*");
|
||||
}
|
||||
updateDownstream(update, updatedSlotData) {
|
||||
var _a;
|
||||
const nodes = getConnectedOutputNodesAndFilterPassThroughs(this, this, 0);
|
||||
for (const node of nodes) {
|
||||
(_a = node === null || node === void 0 ? void 0 : node.updateFromUpstream) === null || _a === void 0 ? void 0 : _a.call(node, update, this, updatedSlotData);
|
||||
}
|
||||
}
|
||||
addContextInput(name, type, slot = -1) {
|
||||
const inputs = this.getContextInputsList();
|
||||
if (this.hasShadowInputs) {
|
||||
inputs.push({ name, type });
|
||||
}
|
||||
else {
|
||||
this.addInput(name, type);
|
||||
}
|
||||
if (slot > -1) {
|
||||
inputs.splice(slot, 0, inputs.splice(inputs.length - 1, 1)[0]);
|
||||
}
|
||||
else {
|
||||
slot = inputs.length - 1;
|
||||
}
|
||||
if (type !== "*") {
|
||||
const output = this.addOutput(name.toUpperCase(), type);
|
||||
if (type === "COMBO" || String(type).includes(",") || Array.isArray(type)) {
|
||||
output.widget = true;
|
||||
}
|
||||
if (slot > -1) {
|
||||
this.outputs.splice(slot, 0, this.outputs.splice(this.outputs.length - 1, 1)[0]);
|
||||
}
|
||||
this.fixInputsOutputsLinkSlots();
|
||||
this.updateDownstream("connect", { index: slot, name });
|
||||
}
|
||||
}
|
||||
removeContextInput(slot) {
|
||||
if (this.hasShadowInputs) {
|
||||
const inputs = this.getContextInputsList();
|
||||
const input = inputs.splice(slot, 1)[0];
|
||||
if (this.outputs[slot]) {
|
||||
this.removeOutput(slot);
|
||||
}
|
||||
this.updateDownstream("disconnect", { index: slot, name: input.name });
|
||||
this.fixInputsOutputsLinkSlots();
|
||||
}
|
||||
else {
|
||||
this.removeInput(slot);
|
||||
}
|
||||
}
|
||||
moveContextInput(slotFrom, slotTo) {
|
||||
const inputs = this.getContextInputsList();
|
||||
if (slotTo === 'bottom') {
|
||||
slotTo = inputs.length - 1;
|
||||
}
|
||||
if (slotFrom === slotTo) {
|
||||
return;
|
||||
}
|
||||
let newIndex = slotTo + (slotFrom < slotTo ? -1 : 0);
|
||||
const input = inputs.splice(slotFrom, 1)[0];
|
||||
inputs.splice(newIndex, 0, input);
|
||||
this.outputs.splice(newIndex, 0, ...this.outputs.splice(slotFrom, 1));
|
||||
this.fixInputsOutputsLinkSlots();
|
||||
this.updateDownstream("move", { index: slotTo, from: slotFrom, name: input.name });
|
||||
}
|
||||
renameContextInput(index, newName, forceOwnBool = null) {
|
||||
const inputs = this.getContextInputsList();
|
||||
const input = inputs[index];
|
||||
const oldName = input.name;
|
||||
newName = this.stripOwnedPrefix(newName.trim() || this.getSlotDefaultInputLabel(index));
|
||||
if (forceOwnBool === true || (this.isOwnedInput(oldName) && forceOwnBool !== false)) {
|
||||
newName = this.addOwnedPrefix(newName);
|
||||
}
|
||||
input.name = newName;
|
||||
this.outputs[index].name = this.stripOwnedPrefix(inputs[index].name).toUpperCase();
|
||||
this.updateDownstream("update", { index, name: newName });
|
||||
}
|
||||
fixInputsOutputsLinkSlots() {
|
||||
if (!this.hasShadowInputs) {
|
||||
const inputs = this.inputs;
|
||||
for (let index = inputs.length - 1; index > 0; index--) {
|
||||
const input = inputs[index];
|
||||
if ((input === null || input === void 0 ? void 0 : input.link) != null) {
|
||||
app.graph.links[input.link].target_slot = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
const outputs = this.outputs;
|
||||
for (let index = outputs.length - 1; index > 0; index--) {
|
||||
const output = outputs[index];
|
||||
for (const link of (output === null || output === void 0 ? void 0 : output.links) || []) {
|
||||
app.graph.links[link].origin_slot = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
getSlotDefaultInputLabel(slot) {
|
||||
const inputs = this.getContextInputsList();
|
||||
const input = inputs[slot];
|
||||
let defaultLabel = this.stripOwnedPrefix(input.name).toLowerCase();
|
||||
return defaultLabel.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
ContextDynamicNodeBase.logger = rgthree.newLogSession("[Dynamic Context]");
|
||||
class ContextDynamicNode extends ContextDynamicNodeBase {
|
||||
static setUp(comfyClass) {
|
||||
BaseContextNode.setUp(comfyClass, ContextDynamicNode);
|
||||
}
|
||||
constructor(title = ContextDynamicNode.title) {
|
||||
super(title);
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
while (cloned.inputs.length > 1) {
|
||||
cloned.removeInput(cloned.inputs.length - 1);
|
||||
}
|
||||
cloned.addInput("+", "*");
|
||||
return cloned;
|
||||
}
|
||||
removeInput(slot) {
|
||||
const input = this.inputs[slot];
|
||||
super.removeInput(slot);
|
||||
if (this.outputs[slot]) {
|
||||
this.removeOutput(slot);
|
||||
}
|
||||
this.fixInputsOutputsLinkSlots();
|
||||
this.updateDownstream("disconnect", { index: slot, name: input.name });
|
||||
this.stabilizeNames();
|
||||
}
|
||||
handleInputConnected(slotIndex) {
|
||||
const inputs = this.getContextInputsList();
|
||||
const ioSlot = inputs[slotIndex];
|
||||
if (slotIndex === 0) {
|
||||
const baseNodes = getConnectedInputNodesAndFilterPassThroughs(this, this, 0);
|
||||
const baseNodesDynamicCtx = baseNodes[0];
|
||||
if (baseNodesDynamicCtx === null || baseNodesDynamicCtx === void 0 ? void 0 : baseNodesDynamicCtx.provideInputsData) {
|
||||
for (const input of baseNodesDynamicCtx.provideInputsData()) {
|
||||
const inputs = this.getContextInputsList();
|
||||
if (input.name === "base_ctx" || input.name === "+") {
|
||||
continue;
|
||||
}
|
||||
const foundIndex = inputs.findIndex(i => this.stripOwnedPrefix(i.name) === input.name);
|
||||
if (foundIndex > -1) {
|
||||
this.moveContextInput(foundIndex, input.index);
|
||||
this.renameContextInput(input.index, input.name, false);
|
||||
}
|
||||
else {
|
||||
this.addContextInput(input.name, input.type, input.index);
|
||||
this.stabilizeNames();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ioSlot.type === "*") {
|
||||
let cxn = null;
|
||||
if (ioSlot.link) {
|
||||
cxn = followConnectionUntilType(this, IoDirection.INPUT, slotIndex, true);
|
||||
}
|
||||
if (cxn === null || cxn === void 0 ? void 0 : cxn.type) {
|
||||
let name = cxn.name;
|
||||
if (name.match(/^(\+\s*)?[A-Z_]+$/)) {
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
name = this.getNextUniqueNameForThisNode(name);
|
||||
if (!this.outputs[slotIndex]) {
|
||||
this.addOutput("*", "*");
|
||||
}
|
||||
inputs[slotIndex].type = cxn.type;
|
||||
inputs[slotIndex].removable = true;
|
||||
this.outputs[slotIndex].type = cxn.type;
|
||||
this.updateDownstream("connect", { index: slotIndex, name: this.stripOwnedPrefix(name) });
|
||||
this.renameContextInput(slotIndex, name, true);
|
||||
if (cxn.type === "COMBO" || cxn.type.includes(",") || Array.isArray(cxn.type)) {
|
||||
this.outputs[slotIndex].widget = true;
|
||||
}
|
||||
this.addInput("+", "*");
|
||||
}
|
||||
}
|
||||
}
|
||||
handleInputDisconnected(slotIndex) {
|
||||
var _a, _b;
|
||||
const inputs = this.getContextInputsList();
|
||||
if (slotIndex === 0) {
|
||||
for (let index = inputs.length - 1; index > 0; index--) {
|
||||
if (index === 0 || index === inputs.length - 1) {
|
||||
continue;
|
||||
}
|
||||
const input = inputs[index];
|
||||
if (!this.isOwnedInput(input.name)) {
|
||||
if (input.link || ((_b = (_a = this.outputs[index]) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.length)) {
|
||||
this.renameContextInput(index, input.name, true);
|
||||
}
|
||||
else {
|
||||
this.removeContextInput(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setSize(this.computeSize());
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
updateFromUpstream(update, node, updatedSlotData) {
|
||||
var _a, _b;
|
||||
console.log("----- ContextDynamicNode :: updateFromUpstream", arguments);
|
||||
const inputs = this.getContextInputsList();
|
||||
if (update == "connect") {
|
||||
const baseInputsData = node.provideInputsData();
|
||||
const baseIndex = updatedSlotData.index;
|
||||
const baseInputData = baseInputsData[baseIndex];
|
||||
const name = this.getNextUniqueNameForThisNode(baseInputData.name);
|
||||
const foundIndex = inputs.findIndex(i => this.stripOwnedPrefix(i.name) === baseInputData.name);
|
||||
if (foundIndex > -1) {
|
||||
this.moveContextInput(foundIndex, baseIndex);
|
||||
this.renameContextInput(baseIndex, baseInputData.name, false);
|
||||
}
|
||||
else {
|
||||
this.addContextInput(baseInputData.name, baseInputData.type, baseInputData.index);
|
||||
this.stabilizeNames();
|
||||
}
|
||||
}
|
||||
else if (update == "disconnect") {
|
||||
if ((_b = (_a = this.outputs[updatedSlotData.index]) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.length) {
|
||||
this.renameContextInput(updatedSlotData.index, updatedSlotData.name, true);
|
||||
this.moveContextInput(updatedSlotData.index, 'bottom');
|
||||
}
|
||||
else {
|
||||
this.removeContextInput(updatedSlotData.index);
|
||||
}
|
||||
}
|
||||
else if (update === "move") {
|
||||
this.moveContextInput(updatedSlotData.from, updatedSlotData.index);
|
||||
}
|
||||
else if (update == "update") {
|
||||
const baseInputsData = node.provideInputsData();
|
||||
const baseIndex = updatedSlotData.index;
|
||||
const baseInput = baseInputsData[baseIndex];
|
||||
this.renameContextInput(baseIndex, baseInput.name);
|
||||
this.stabilizeNames();
|
||||
}
|
||||
this.setSize(this.computeSize());
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
stabilizeNames() {
|
||||
const inputs = this.getContextInputsList();
|
||||
const names = [];
|
||||
for (const [index, input] of inputs.entries()) {
|
||||
if (index === 0 || index === inputs.length - 1) {
|
||||
continue;
|
||||
}
|
||||
input.label = undefined;
|
||||
this.outputs[index].label = undefined;
|
||||
let origName = this.stripOwnedPrefix(input.name).replace(/\.\d+$/, "");
|
||||
let name = input.name;
|
||||
if (!this.isOwnedInput(name)) {
|
||||
names.push(name.toLocaleUpperCase());
|
||||
}
|
||||
else {
|
||||
let n = 0;
|
||||
name = this.addOwnedPrefix(origName);
|
||||
while (names.includes(this.stripOwnedPrefix(name).toLocaleUpperCase())) {
|
||||
name = `${this.addOwnedPrefix(origName)}.${++n}`;
|
||||
}
|
||||
names.push(this.stripOwnedPrefix(name).toLocaleUpperCase());
|
||||
if (input.name !== name) {
|
||||
this.renameContextInput(index, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onConnectInput(inputIdx, outputType, outputSlot, outputNode, outputIndex) {
|
||||
let canConnect = true;
|
||||
if (super.onConnectInput) {
|
||||
canConnect = super.onConnectInput(inputIdx, outputType, outputSlot, outputNode, outputIndex);
|
||||
}
|
||||
if (canConnect && outputNode instanceof ContextDynamicNode && outputIndex === 0 && inputIdx !== 0) {
|
||||
ContextDynamicNodeBase.logger.error("Currently, you can only connect a context node in the first slot.");
|
||||
canConnect = false;
|
||||
}
|
||||
return canConnect;
|
||||
}
|
||||
getSlotMenuOptions(info) {
|
||||
const opts = [];
|
||||
if (info.input) {
|
||||
if (this.isOwnedInput(info.input.name) && info.input.type !== '*') {
|
||||
opts.push({
|
||||
content: "✏️ Rename Input",
|
||||
callback: () => {
|
||||
var dialog = app.canvas.createDialog("<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>", {});
|
||||
var dialogInput = dialog.querySelector("input");
|
||||
if (dialogInput) {
|
||||
dialogInput.value = this.stripOwnedPrefix(info.input.name || "");
|
||||
}
|
||||
var inner = () => {
|
||||
app.graph.beforeChange();
|
||||
this.renameContextInput(info.slot, dialogInput.value);
|
||||
this.stabilizeNames();
|
||||
this.setDirtyCanvas(true, true);
|
||||
dialog.close();
|
||||
app.graph.afterChange();
|
||||
};
|
||||
dialog.querySelector("button").addEventListener("click", inner);
|
||||
dialogInput.addEventListener("keydown", (e) => {
|
||||
var _a;
|
||||
dialog.is_modified = true;
|
||||
if (e.keyCode == 27) {
|
||||
dialog.close();
|
||||
}
|
||||
else if (e.keyCode == 13) {
|
||||
inner();
|
||||
}
|
||||
else if (e.keyCode != 13 && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.localName) != "textarea") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
dialogInput.focus();
|
||||
},
|
||||
});
|
||||
opts.push({
|
||||
content: "🗑️ Delete Input",
|
||||
callback: () => {
|
||||
this.removeInput(info.slot);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
ContextDynamicNode.title = "Dynamic Context (rgthree)";
|
||||
ContextDynamicNode.type = "Dynamic Context (rgthree)";
|
||||
ContextDynamicNode.comfyClass = "Dynamic Context (rgthree)";
|
||||
class ContextDynamicSwitchNode extends ContextDynamicNodeBase {
|
||||
constructor(title = ContextDynamicSwitchNode.title) {
|
||||
super(title);
|
||||
this.hasShadowInputs = true;
|
||||
this.lastInputsList = [];
|
||||
this.shadowInputs = [
|
||||
{ name: "base_ctx", type: "DYNAMIC_CONTEXT" },
|
||||
{ name: "+", type: "*" },
|
||||
];
|
||||
let alerted = false;
|
||||
setInterval(() => {
|
||||
const plusIndex = this.shadowInputs.findIndex((i) => i.name === "+");
|
||||
if (plusIndex === -1) {
|
||||
!alerted && console.error("ERROR, no plus in shadow inputs", [...this.shadowInputs]);
|
||||
alerted = true;
|
||||
}
|
||||
else if (plusIndex !== this.shadowInputs.length - 1) {
|
||||
!alerted &&
|
||||
console.error("ERROR, plus is not last in shadow inputs", [...this.shadowInputs]);
|
||||
alerted = true;
|
||||
}
|
||||
else {
|
||||
alerted && console.error("BACK TO NOREMAL", [...this.shadowInputs]);
|
||||
alerted = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
static setUp(comfyClass) {
|
||||
BaseContextNode.setUp(comfyClass, ContextDynamicSwitchNode);
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
while (cloned.outputs.length > 1) {
|
||||
cloned.removeOutput(cloned.outputs.length - 1);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
getContextInputsList() {
|
||||
return this.shadowInputs;
|
||||
}
|
||||
onNodeCreated() {
|
||||
}
|
||||
onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
|
||||
let canConnect = true;
|
||||
if (super.onConnectInput) {
|
||||
canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex);
|
||||
}
|
||||
const allConnectedNodes = getConnectedInputNodes(this);
|
||||
if (canConnect && allConnectedNodes.includes(outputNode)) {
|
||||
rgthree.showMessage({
|
||||
id: "dynamic-context-looped",
|
||||
type: "warn",
|
||||
message: "You may not connect the same context node to a switch.",
|
||||
timeout: 5000,
|
||||
});
|
||||
canConnect = false;
|
||||
}
|
||||
if (canConnect && shouldPassThrough(outputNode, PassThroughFollowing.REROUTE_ONLY)) {
|
||||
const connectedNodes = getConnectedInputNodesAndFilterPassThroughs(outputNode, undefined, undefined, PassThroughFollowing.REROUTE_ONLY);
|
||||
if (connectedNodes.length && allConnectedNodes.find((n) => connectedNodes.includes(n))) {
|
||||
rgthree.showMessage({
|
||||
id: "dynamic-context-looped",
|
||||
type: "warn",
|
||||
message: "You may not connect the same context node to a switch, even through a reroute.",
|
||||
timeout: 5000,
|
||||
});
|
||||
canConnect = false;
|
||||
}
|
||||
}
|
||||
return canConnect;
|
||||
}
|
||||
configure(info) {
|
||||
super.configure(info);
|
||||
setTimeout(() => {
|
||||
this.shadowInputs = this.getAllShadowInputs();
|
||||
this.shadowInputs.push({ name: "+", type: "*" });
|
||||
this.updateLastInputsList();
|
||||
console.log(this.shadowInputs);
|
||||
}, 100);
|
||||
}
|
||||
moveContextInput(slotFrom, slotTo) {
|
||||
super.moveContextInput(slotFrom, slotTo);
|
||||
this.updateLastInputsList();
|
||||
}
|
||||
removeContextInput(slot) {
|
||||
super.removeContextInput(slot);
|
||||
this.updateLastInputsList();
|
||||
}
|
||||
addContextInput(name, type, slot = -1) {
|
||||
super.addContextInput(name, type, slot);
|
||||
this.updateLastInputsList();
|
||||
}
|
||||
updateLastInputsList() {
|
||||
this.lastInputsList = this.getAllInputsList();
|
||||
}
|
||||
connectSlotFromUpdateOrInput(data) {
|
||||
console.log(`connectSlotFromUpdateOrInput: ${data.name}`, data);
|
||||
if (data.duplicatesBefore.length) {
|
||||
console.log(`[Do Nothing] It has duplicatesBefore (${data.duplicatesBefore.join(",")}).`);
|
||||
this.updateLastInputsList();
|
||||
}
|
||||
else if (data.duplicatesAfter.length) {
|
||||
const from = this.shadowInputs.findIndex((i) => i.name.toLocaleUpperCase() === data.key);
|
||||
console.log(`[Move] Has duplicates after. ${from} -> ${data.shadowIndex}`);
|
||||
this.moveContextInput(from, data.shadowIndex);
|
||||
}
|
||||
else {
|
||||
console.log(`[Add] No dupes, so we can add it at ${data.shadowIndex}.`);
|
||||
this.addContextInput(data.name, data.type, data.shadowIndex);
|
||||
}
|
||||
}
|
||||
handleInputConnected(slotIndex) {
|
||||
var _a;
|
||||
console.log("--- handleInputConnected", slotIndex);
|
||||
const postInputsList = [...this.getAllInputsList()];
|
||||
const node = (_a = postInputsList.find((i) => i.slot === slotIndex)) === null || _a === void 0 ? void 0 : _a.node;
|
||||
if (!node) {
|
||||
console.error("hmmm... no node foun to handle connect.");
|
||||
return;
|
||||
}
|
||||
const inputsDataLists = postInputsList.filter((d) => d.slot == slotIndex && d.nodeIndex > 0 && d.type !== '*');
|
||||
for (const data of inputsDataLists) {
|
||||
this.connectSlotFromUpdateOrInput(data);
|
||||
}
|
||||
}
|
||||
handleInputDisconnected(slotIndex) {
|
||||
var _a;
|
||||
console.log("--- handleInputDisconnected", slotIndex);
|
||||
const preInputsList = [...this.lastInputsList];
|
||||
const node = (_a = preInputsList.find((i) => i.slot === slotIndex)) === null || _a === void 0 ? void 0 : _a.node;
|
||||
if (!node) {
|
||||
console.error("hmmmm... no node found to handle disconnect.");
|
||||
return;
|
||||
}
|
||||
const postInputsList = [...this.getAllInputsList()];
|
||||
const inputs = [...this.shadowInputs];
|
||||
console.log("postInputsList", postInputsList);
|
||||
let lastIndex = 0;
|
||||
for (let [index, data] of postInputsList.entries()) {
|
||||
data = this.getAllInputsList()[index];
|
||||
if (data.shadowIndex === -1 || data.nodeIndex === 0) {
|
||||
continue;
|
||||
}
|
||||
lastIndex++;
|
||||
const foundIndex = this.shadowInputs.findIndex((i) => i.name.toLocaleUpperCase() === data.key);
|
||||
console.log(data.name, foundIndex, data.shadowIndex);
|
||||
if (foundIndex !== data.shadowIndex) {
|
||||
this.moveContextInput(foundIndex, data.shadowIndex);
|
||||
}
|
||||
}
|
||||
for (let index = inputs.length - 1; index > lastIndex; index--) {
|
||||
const input = this.shadowInputs[index];
|
||||
if (input.type !== '*') {
|
||||
this.removeContextInput(index);
|
||||
}
|
||||
}
|
||||
console.log([...this.shadowInputs]);
|
||||
}
|
||||
updateFromUpstream(update, node, updatedSlotData) {
|
||||
var _a;
|
||||
console.log("----- ContextDynamicSwitchNode :: updateFromUpstream", update, updatedSlotData);
|
||||
const preInputsList = [...this.lastInputsList];
|
||||
const postInputsList = [...this.getAllInputsList()];
|
||||
if (shouldPassThrough(node)) {
|
||||
const connectedNodes = getConnectedNodesInfo(this, IoDirection.INPUT);
|
||||
const foundRerouteInfo = connectedNodes.find((n) => n.node === node);
|
||||
if (update == "connect") {
|
||||
this.handleInputConnected(foundRerouteInfo.originTravelFromSlot);
|
||||
}
|
||||
else if (update == "disconnect") {
|
||||
this.handleInputDisconnected(foundRerouteInfo.originTravelFromSlot);
|
||||
}
|
||||
else {
|
||||
throw new Error("Unexpected update type from pass through node: " + update);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (update) {
|
||||
case "connect": {
|
||||
const data = postInputsList.find((d) => {
|
||||
return d.node == node && d.nodeIndex === updatedSlotData.index;
|
||||
});
|
||||
if (!data) {
|
||||
throw new Error("Hmmm.. unfound input slot when connecting upstream.");
|
||||
}
|
||||
this.connectSlotFromUpdateOrInput(data);
|
||||
break;
|
||||
}
|
||||
case "disconnect":
|
||||
const preInputData = preInputsList.find((i) => {
|
||||
return i.node === node && i.nodeIndex == updatedSlotData.index;
|
||||
});
|
||||
if (!preInputData) {
|
||||
throw new Error("Hmmm... no matching input found in existing input list for disconnect.");
|
||||
}
|
||||
if (preInputData.duplicatesBefore.length) {
|
||||
console.log(`[Do Nothing] It was already duplicated before.`);
|
||||
this.updateLastInputsList();
|
||||
}
|
||||
else if (((_a = preInputData === null || preInputData === void 0 ? void 0 : preInputData.duplicatesAfter) === null || _a === void 0 ? void 0 : _a[0]) != null) {
|
||||
console.log(`[Move after] Not duplicated before, but is after.`);
|
||||
this.moveContextInput(preInputData.shadowIndex, preInputData.duplicatesAfter[0]);
|
||||
}
|
||||
else {
|
||||
console.log(`[Remove] ${preInputData.shadowIndex}.`, preInputData);
|
||||
this.removeContextInput(preInputData.shadowIndex);
|
||||
}
|
||||
break;
|
||||
case "move":
|
||||
break;
|
||||
case "update":
|
||||
const index = postInputsList.findIndex((d) => {
|
||||
return d.node == node && d.nodeIndex === updatedSlotData.index;
|
||||
});
|
||||
const pre = preInputsList[index];
|
||||
const post = postInputsList[index];
|
||||
console.log("preData", { ...pre });
|
||||
console.log("postData", { ...post });
|
||||
if (pre.shadowIndex == -1 && post.shadowIndex !== -1) {
|
||||
console.log(`[Add] Old name wasn't shown, but new is.`, post.name, post.shadowIndex);
|
||||
this.addContextInput(post.name, post.type, post.shadowIndex);
|
||||
}
|
||||
else if (pre.shadowIndex !== -1 && post.shadowIndex === -1) {
|
||||
console.log(`[Remove] Old name was shown, but new isn;t.`, post.name, post.shadowIndex);
|
||||
this.removeContextInput(pre.shadowIndex);
|
||||
}
|
||||
else if (post.shadowIndex > -1) {
|
||||
console.log(`[Rename] It's shown and has a new name.`, post.name, post.shadowIndex);
|
||||
this.renameContextInput(post.shadowIndex, post.name);
|
||||
}
|
||||
else {
|
||||
console.log(`[Do Nothing] It's shown and has a new name.`, post.name, post.shadowIndex);
|
||||
this.updateLastInputsList();
|
||||
}
|
||||
break;
|
||||
}
|
||||
console.log(this.shadowInputs);
|
||||
}
|
||||
getAllInputsList(indexToNodeOverride = {}) {
|
||||
var _a, _b, _c;
|
||||
const allConnectedInputsDataByName = {};
|
||||
const allConnectedInputsData = [];
|
||||
let currentShadowIndex = 0;
|
||||
for (const [slot, input] of (this.inputs || []).entries()) {
|
||||
const connectedNode = (_a = indexToNodeOverride[slot]) !== null && _a !== void 0 ? _a : (_b = getConnectedInputNodesAndFilterPassThroughs(this, this, slot)) === null || _b === void 0 ? void 0 : _b[0];
|
||||
if (connectedNode) {
|
||||
for (const inputData of connectedNode.provideInputsData()) {
|
||||
const key = inputData.name.toLocaleUpperCase();
|
||||
allConnectedInputsDataByName[key] = allConnectedInputsDataByName[key] || [];
|
||||
const existings = allConnectedInputsDataByName[key];
|
||||
let data = {
|
||||
node: connectedNode,
|
||||
slot,
|
||||
shadowIndexFull: allConnectedInputsData.length,
|
||||
shadowIndex: !existings.length ? currentShadowIndex : -1,
|
||||
shadowIndexIfShownSingularly: currentShadowIndex,
|
||||
nodeIndex: inputData.index,
|
||||
type: inputData.type,
|
||||
name: inputData.name,
|
||||
key,
|
||||
duplicatesBefore: allConnectedInputsDataByName[key].map((d) => d.shadowIndexFull),
|
||||
duplicatesAfter: [],
|
||||
};
|
||||
if (data.shadowIndex > -1) {
|
||||
currentShadowIndex++;
|
||||
}
|
||||
for (const existing of existings) {
|
||||
existing.duplicatesAfter.push(data.shadowIndexFull);
|
||||
}
|
||||
allConnectedInputsData.push(data);
|
||||
(_c = allConnectedInputsDataByName[key]) === null || _c === void 0 ? void 0 : _c.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allConnectedInputsData;
|
||||
}
|
||||
getAllShadowInputs() {
|
||||
const inputsDataMap = {
|
||||
BASE_CTX: { name: "base_ctx", type: "DYNAMIC_CONTEXT", index: 0 },
|
||||
};
|
||||
const baseNodes = getConnectedInputNodesAndFilterPassThroughs(this, this);
|
||||
for (const inputNode of baseNodes) {
|
||||
for (const inputData of inputNode.provideInputsData()) {
|
||||
const dataKey = inputData.name.toLocaleUpperCase();
|
||||
const existingData = inputsDataMap[dataKey];
|
||||
if (!existingData) {
|
||||
inputsDataMap[dataKey] = inputData;
|
||||
}
|
||||
else if (existingData.name !== inputData.name) {
|
||||
throw new Error(`Conflicting data for ${dataKey}. ${existingData.name} !== ${inputData.name}`);
|
||||
}
|
||||
else if (existingData.type !== inputData.type) {
|
||||
throw new Error(`Conflicting data for ${dataKey}. ${existingData.type} !== ${inputData.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.values(inputsDataMap).map((v, index) => Object.assign({ ...v }, { index }));
|
||||
}
|
||||
}
|
||||
ContextDynamicSwitchNode.title = "Dynamic Context Switch (rgthree)";
|
||||
ContextDynamicSwitchNode.type = "Dynamic Context Switch (rgthree)";
|
||||
ContextDynamicSwitchNode.comfyClass = "Dynamic Context Switch (rgthree)";
|
||||
const contextDynamicNodes = [ContextDynamicNode, ContextDynamicSwitchNode];
|
||||
app.registerExtension({
|
||||
name: "rgthree.ContextDynamic",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
for (const ctxClass of contextDynamicNodes) {
|
||||
if (nodeData.name === ctxClass.type) {
|
||||
console.log(nodeData.name);
|
||||
ctxClass.nodeData = nodeData;
|
||||
ctxClass.nodeType = nodeType;
|
||||
ctxClass.setUp(nodeType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import { addConnectionLayoutSupport } from "./utils.js";
|
||||
let hasShownAlertForUpdatingInt = false;
|
||||
app.registerExtension({
|
||||
name: "rgthree.DisplayInt",
|
||||
name: "rgthree.DisplayAny",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "Display Int (rgthree)") {
|
||||
if (nodeData.name === "Display Any (rgthree)" || nodeData.name === "Display Int (rgthree)") {
|
||||
nodeType.title_mode = LiteGraph.NO_TITLE;
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
@@ -12,16 +13,15 @@ app.registerExtension({
|
||||
this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget;
|
||||
this.showValueWidget.inputEl.readOnly = true;
|
||||
this.showValueWidget.serializeValue = async (node, index) => {
|
||||
node.widgets_values[index] = '';
|
||||
return '';
|
||||
node.widgets_values[index] = "";
|
||||
return "";
|
||||
};
|
||||
};
|
||||
addConnectionLayoutSupport(nodeType, app, [['Left'], ['Right']]);
|
||||
addConnectionLayoutSupport(nodeType, app, [["Left"], ["Right"]]);
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
var _a;
|
||||
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
|
||||
(_a = this.showValueWidget) === null || _a === void 0 ? void 0 : _a.value = message.text[0];
|
||||
this.showValueWidget.value = message.text[0];
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -0,0 +1,263 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
||||
import { NodeTypesString } from "./constants.js";
|
||||
import { addMenuItem } from "./utils.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
const MODE_ALWAYS = 0;
|
||||
const MODE_MUTE = 2;
|
||||
const MODE_BYPASS = 4;
|
||||
class FastActionsButton extends BaseAnyInputConnectedNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.isVirtualNode = true;
|
||||
this.serialize_widgets = true;
|
||||
this.widgetToData = new Map();
|
||||
this.nodeIdtoFunctionCache = new Map();
|
||||
this.executingFromShortcut = false;
|
||||
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);
|
||||
}
|
||||
configure(info) {
|
||||
super.configure(info);
|
||||
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);
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
cloned.properties["buttonText"] = "🎬 Action!";
|
||||
cloned.properties["shortcutKey"] = "";
|
||||
return cloned;
|
||||
}
|
||||
onAdded(graph) {
|
||||
window.addEventListener("keydown", this.keypressBound);
|
||||
window.addEventListener("keyup", this.keyupBound);
|
||||
}
|
||||
onRemoved() {
|
||||
window.removeEventListener("keydown", this.keypressBound);
|
||||
window.removeEventListener("keyup", this.keyupBound);
|
||||
}
|
||||
async onKeypress(event) {
|
||||
const target = event.target;
|
||||
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) {
|
||||
const target = event.target;
|
||||
if (target.localName == "input" || target.localName == "textarea") {
|
||||
return;
|
||||
}
|
||||
this.executingFromShortcut = false;
|
||||
}
|
||||
onPropertyChanged(property, value, _prevValue) {
|
||||
if (property == "buttonText") {
|
||||
this.buttonWidget.name = value;
|
||||
}
|
||||
if (property == "shortcutKey") {
|
||||
value = value.trim();
|
||||
this.properties["shortcutKey"] = (value && value[0].toLowerCase()) || "";
|
||||
}
|
||||
}
|
||||
handleLinkedNodesStabilization(linkedNodes) {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
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 = [];
|
||||
let indexOffset = 1;
|
||||
for (const [index, node] of linkedNodes.entries()) {
|
||||
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 && ((_a = this.widgetToData.get(widgetAtSlot)) === null || _a === void 0 ? void 0 : _a.comfy)) {
|
||||
indexOffset++;
|
||||
widgetAtSlot = this.widgets[index + indexOffset];
|
||||
}
|
||||
if (!widgetAtSlot || ((_c = (_b = this.widgetToData.get(widgetAtSlot)) === null || _b === void 0 ? void 0 : _b.node) === null || _c === void 0 ? void 0 : _c.id) !== node.id) {
|
||||
let widget = null;
|
||||
for (let i = index + indexOffset; i < this.widgets.length; i++) {
|
||||
if (((_e = (_d = this.widgetToData.get(this.widgets[i])) === null || _d === void 0 ? void 0 : _d.node) === null || _e === void 0 ? void 0 : _e.id) === node.id) {
|
||||
widget = this.widgets.splice(i, 1)[0];
|
||||
this.widgets.splice(index + indexOffset, 0, widget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!widget) {
|
||||
const exposedActions = node.constructor.exposedActions || [];
|
||||
widget = this.addWidget("combo", node.title, "None", "", {
|
||||
values: ["None", "Mute", "Bypass", "Enable", ...exposedActions],
|
||||
});
|
||||
widget.serializeValue = async (_node, _index) => {
|
||||
return widget === null || widget === void 0 ? void 0 : widget.value;
|
||||
};
|
||||
this.widgetToData.set(widget, { node });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = this.widgets.length - 1; i > linkedNodes.length + indexOffset - 1; i--) {
|
||||
const widgetAtSlot = this.widgets[i];
|
||||
if (widgetAtSlot && ((_f = this.widgetToData.get(widgetAtSlot)) === null || _f === void 0 ? void 0 : _f.comfy)) {
|
||||
continue;
|
||||
}
|
||||
this.removeWidget(widgetAtSlot);
|
||||
}
|
||||
}
|
||||
removeWidget(widgetOrSlot) {
|
||||
const widget = typeof widgetOrSlot === "number"
|
||||
? this.widgets[widgetOrSlot]
|
||||
: widgetOrSlot;
|
||||
if (widget && this.widgetToData.has(widget)) {
|
||||
this.widgetToData.delete(widget);
|
||||
}
|
||||
super.removeWidget(widgetOrSlot);
|
||||
}
|
||||
async executeConnectedNodes() {
|
||||
var _a;
|
||||
for (const widget of this.widgets) {
|
||||
if (widget == this.buttonWidget) {
|
||||
continue;
|
||||
}
|
||||
const action = widget.value;
|
||||
const { comfy, node } = (_a = this.widgetToData.get(widget)) !== null && _a !== void 0 ? _a : {};
|
||||
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 (node.handleAction) {
|
||||
await node.handleAction(action);
|
||||
}
|
||||
app.graph.change();
|
||||
continue;
|
||||
}
|
||||
console.warn("Fast Actions Button has a widget without correct data.");
|
||||
}
|
||||
}
|
||||
addComfyActionWidget(slot, value) {
|
||||
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["lastValue_"];
|
||||
}
|
||||
else if (widget.value.startsWith("REMOVE ")) {
|
||||
this.removeWidget(widget);
|
||||
}
|
||||
widget["lastValue_"] = widget.value;
|
||||
}, {
|
||||
values: ["None", "Queue Prompt", "REMOVE Comfy Action", "MOVE to end"],
|
||||
});
|
||||
widget["lastValue_"] = value;
|
||||
widget.serializeValue = async (_node, _index) => {
|
||||
return `comfy_app:${widget === null || widget === void 0 ? void 0 : 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;
|
||||
}
|
||||
onSerialize(o) {
|
||||
var _a;
|
||||
super.onSerialize && super.onSerialize(o);
|
||||
for (let [index, value] of (o.widgets_values || []).entries()) {
|
||||
if (((_a = this.widgets[index]) === null || _a === void 0 ? void 0 : _a.name) === "Comfy Action") {
|
||||
o.widgets_values[index] = `comfy_action:${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
static setUp(clazz) {
|
||||
BaseAnyInputConnectedNode.setUp(clazz);
|
||||
addMenuItem(clazz, app, {
|
||||
name: "➕ Append a Comfy Action",
|
||||
callback: (nodeArg) => {
|
||||
nodeArg.addComfyActionWidget();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
FastActionsButton.type = NodeTypesString.FAST_ACTIONS_BUTTON;
|
||||
FastActionsButton.title = NodeTypesString.FAST_ACTIONS_BUTTON;
|
||||
FastActionsButton["@buttonText"] = { type: "string" };
|
||||
FastActionsButton["@shortcutModifier"] = {
|
||||
type: "combo",
|
||||
values: ["ctrl", "alt", "shift"],
|
||||
};
|
||||
FastActionsButton["@shortcutKey"] = { type: "string" };
|
||||
FastActionsButton.collapsible = false;
|
||||
app.registerExtension({
|
||||
name: "rgthree.FastActionsButton",
|
||||
registerCustomNodes() {
|
||||
FastActionsButton.setUp(FastActionsButton);
|
||||
},
|
||||
loadedGraphNode(node) {
|
||||
if (node.type == FastActionsButton.title) {
|
||||
node._tempWidth = node.size[0];
|
||||
}
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,195 @@
|
||||
import { fixBadLinks } from "../link_fixer.js";
|
||||
import { getPngMetadata } from "../../../scripts/pnginfo.js";
|
||||
function wait(ms = 16, value) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(value);
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
const logger = {
|
||||
logTo: console,
|
||||
log: (...args) => {
|
||||
logger.logTo === console
|
||||
? console.log(...args)
|
||||
: (logger.logTo.innerText += args.join(",") + "\n");
|
||||
},
|
||||
};
|
||||
const findBadLinksLogger = {
|
||||
log: async (...args) => {
|
||||
logger.log(...args);
|
||||
},
|
||||
};
|
||||
export class LinkPage {
|
||||
constructor() {
|
||||
this.containerEl = document.querySelector(".box");
|
||||
this.figcaptionEl = document.querySelector("figcaption");
|
||||
this.outputeMessageEl = document.querySelector(".output");
|
||||
this.outputImageEl = document.querySelector(".output-image");
|
||||
this.btnFix = document.querySelector(".btn-fix");
|
||||
document.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
document.addEventListener("drop", (e) => {
|
||||
this.onDrop(e);
|
||||
});
|
||||
this.btnFix.addEventListener("click", (e) => {
|
||||
this.onFixClick(e);
|
||||
});
|
||||
}
|
||||
async onFixClick(e) {
|
||||
if (!this.graphResults || !this.graph) {
|
||||
this.updateUi("⛔ Fix button click without results.");
|
||||
return;
|
||||
}
|
||||
let graphFinalResults = fixBadLinks(this.graph, true);
|
||||
graphFinalResults = fixBadLinks(graphFinalResults.graph, true);
|
||||
if (graphFinalResults.patched || graphFinalResults.deleted) {
|
||||
graphFinalResults = fixBadLinks(graphFinalResults.graph, true);
|
||||
}
|
||||
this.graphFinalResults = graphFinalResults;
|
||||
await this.saveFixedWorkflow();
|
||||
if (graphFinalResults.hasBadLinks) {
|
||||
this.updateUi("⛔ Hmm... Still detecting bad links. Can you file an issue at https://github.com/rgthree/rgthree-comfy/issues with your image/workflow.");
|
||||
}
|
||||
else {
|
||||
this.updateUi("✅ Workflow fixed.<br><br><small>Please load new saved workflow json and double check linking and execution.</small>");
|
||||
}
|
||||
}
|
||||
async onDrop(event) {
|
||||
var _a, _b, _c, _d;
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.dataTransfer.files.length && ((_b = (_a = event.dataTransfer.files) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.type) !== "image/bmp") {
|
||||
await this.handleFile(event.dataTransfer.files[0]);
|
||||
return;
|
||||
}
|
||||
const validTypes = ["text/uri-list", "text/x-moz-url"];
|
||||
const match = [...event.dataTransfer.types].find((t) => validTypes.find((v) => t === v));
|
||||
if (match) {
|
||||
const uri = (_d = (_c = event.dataTransfer.getData(match)) === null || _c === void 0 ? void 0 : _c.split("\n")) === null || _d === void 0 ? void 0 : _d[0];
|
||||
if (uri) {
|
||||
await this.handleFile(await (await fetch(uri)).blob());
|
||||
}
|
||||
}
|
||||
}
|
||||
reset() {
|
||||
this.file = undefined;
|
||||
this.graph = undefined;
|
||||
this.graphResults = undefined;
|
||||
this.graphFinalResults = undefined;
|
||||
this.updateUi();
|
||||
}
|
||||
updateUi(msg) {
|
||||
this.outputeMessageEl.innerHTML = "";
|
||||
if (this.file && !this.containerEl.classList.contains("-has-file")) {
|
||||
this.containerEl.classList.add("-has-file");
|
||||
this.figcaptionEl.innerHTML = this.file.name || this.file.type;
|
||||
if (this.file.type === "application/json") {
|
||||
this.outputImageEl.src = "icon_file_json.png";
|
||||
}
|
||||
else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => (this.outputImageEl.src = reader.result);
|
||||
reader.readAsDataURL(this.file);
|
||||
}
|
||||
}
|
||||
else if (!this.file && this.containerEl.classList.contains("-has-file")) {
|
||||
this.containerEl.classList.remove("-has-file");
|
||||
this.outputImageEl.src = "";
|
||||
this.outputImageEl.removeAttribute("src");
|
||||
}
|
||||
if (this.graphResults) {
|
||||
this.containerEl.classList.add("-has-results");
|
||||
if (!this.graphResults.patched && !this.graphResults.deleted) {
|
||||
this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow.";
|
||||
}
|
||||
else {
|
||||
this.containerEl.classList.add("-has-fixable-results");
|
||||
this.outputeMessageEl.innerHTML = `⚠️ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.containerEl.classList.remove("-has-results");
|
||||
this.containerEl.classList.remove("-has-fixable-results");
|
||||
}
|
||||
if (msg) {
|
||||
this.outputeMessageEl.innerHTML = msg;
|
||||
}
|
||||
}
|
||||
async handleFile(file) {
|
||||
this.file = file;
|
||||
this.updateUi();
|
||||
let workflow = null;
|
||||
if (file.type.startsWith("image/")) {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
workflow = pngInfo === null || pngInfo === void 0 ? void 0 : pngInfo.workflow;
|
||||
}
|
||||
else if (file.type === "application/json" ||
|
||||
(file instanceof File && file.name.endsWith(".json"))) {
|
||||
workflow = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
if (!workflow) {
|
||||
this.updateUi("⛔ No workflow found in dropped item.");
|
||||
}
|
||||
else {
|
||||
try {
|
||||
this.graph = JSON.parse(workflow);
|
||||
}
|
||||
catch (e) {
|
||||
this.graph = undefined;
|
||||
}
|
||||
if (!this.graph) {
|
||||
this.updateUi("⛔ Invalid workflow found in dropped item.");
|
||||
}
|
||||
else {
|
||||
this.loadGraphData(this.graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
async loadGraphData(graphData) {
|
||||
this.graphResults = await fixBadLinks(graphData);
|
||||
this.updateUi();
|
||||
}
|
||||
async saveFixedWorkflow() {
|
||||
if (!this.graphFinalResults) {
|
||||
this.updateUi("⛔ Save w/o final graph patched.");
|
||||
return false;
|
||||
}
|
||||
let filename = this.file.name || 'workflow.json';
|
||||
let filenames = filename.split('.');
|
||||
filenames.pop();
|
||||
filename = filenames.join('.');
|
||||
filename += '_fixed.json';
|
||||
filename = prompt("Save workflow as:", filename);
|
||||
if (!filename)
|
||||
return false;
|
||||
if (!filename.toLowerCase().endsWith(".json")) {
|
||||
filename += ".json";
|
||||
}
|
||||
const json = JSON.stringify(this.graphFinalResults.graph, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.download = filename;
|
||||
anchor.href = url;
|
||||
anchor.style.display = 'none';
|
||||
document.body.appendChild(anchor);
|
||||
await wait();
|
||||
anchor.click();
|
||||
await wait();
|
||||
anchor.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>rgthree's comfy: Workflow Link Fixer</title>
|
||||
<style>
|
||||
html, body {
|
||||
}
|
||||
html {
|
||||
font-size: 100%;
|
||||
overflow-y: scroll;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit
|
||||
}
|
||||
|
||||
body {
|
||||
background: #222;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: calc(16 * 0.0625rem);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
padding-bottom: 64px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.box, figure, picture {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box {
|
||||
margin: 32px auto;
|
||||
max-width: 720px;
|
||||
padding: 16px 16px 32px;
|
||||
background: rgba(125,125,125,0.33);
|
||||
box-shadow: 0px 8px 10px rgba(0,0,0,0.85);
|
||||
border-radius: 8px;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.box > * {
|
||||
text-align: center;
|
||||
}
|
||||
.box > p {
|
||||
margin: 0 0 .6em;
|
||||
text-align: left;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
picture > img {
|
||||
display: block;
|
||||
height: 256px;
|
||||
min-width: 256px;
|
||||
border: 10px dashed rgba(125,125,125,0.66);
|
||||
border-radius: 32px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
figcaption {
|
||||
opacity: 0.66;
|
||||
padding: 4px;
|
||||
font-size: calc(11 * 0.0625rem);
|
||||
}
|
||||
|
||||
.-has-file picture > img {
|
||||
border-color: transparent;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.output {
|
||||
margin: 8px 8px 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(125,125,125,0.66);
|
||||
padding: 4px 8px;
|
||||
min-width: 80%;
|
||||
}
|
||||
|
||||
.output:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-fix {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: calc(24 * 0.0625rem);
|
||||
}
|
||||
.-has-fixable-results .btn-fix {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="box">
|
||||
<h1>rgthree's Workflow Link Fixer</h1>
|
||||
<p>Early versions of the reroute node would occasionally leave behind stale node-linking data in the graph, which could sometimes cause erratic workflow loading. This tool will look at the metadata and attempt to fix these errors.</p>
|
||||
<p>Drag and drop a comfy-generated image or workflow json into this window to check its serialized links and fix.</p>
|
||||
|
||||
<figure>
|
||||
<picture>
|
||||
<img class="output-image" />
|
||||
</picture>
|
||||
<figcaption></figcaption>
|
||||
</figure>
|
||||
|
||||
<div class="output"></div>
|
||||
|
||||
<button class="btn-fix">Fix & Save new workflow</button>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import {LinkPage} from './link_page.js';
|
||||
new LinkPage();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { RgthreeBaseNode } from "./base_node.js";
|
||||
import { applyMixins } from "./utils.js";
|
||||
class ImageInsetCrop extends RgthreeBaseNode {
|
||||
onAdded(graph) {
|
||||
const measurementWidget = this.widgets[0];
|
||||
let callback = measurementWidget.callback;
|
||||
measurementWidget.callback = (...args) => {
|
||||
this.setWidgetStep();
|
||||
callback && callback.apply(measurementWidget, [...args]);
|
||||
};
|
||||
this.setWidgetStep();
|
||||
}
|
||||
configure(info) {
|
||||
super.configure(info);
|
||||
this.setWidgetStep();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
async handleAction(action) {
|
||||
if (action === 'Reset Crop') {
|
||||
for (const widget of this.widgets) {
|
||||
if (['left', 'right', 'top', 'bottom'].includes(widget.name)) {
|
||||
widget.value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static setUp(clazz) {
|
||||
ImageInsetCrop.title = clazz.title;
|
||||
ImageInsetCrop.comfyClass = clazz.comfyClass;
|
||||
setTimeout(() => {
|
||||
ImageInsetCrop.category = clazz.category;
|
||||
});
|
||||
applyMixins(clazz, [RgthreeBaseNode, ImageInsetCrop]);
|
||||
}
|
||||
}
|
||||
ImageInsetCrop.type = '__OVERRIDE_ME__';
|
||||
ImageInsetCrop.comfyClass = '__OVERRIDE_ME__';
|
||||
ImageInsetCrop.exposedActions = ['Reset Crop'];
|
||||
ImageInsetCrop.maxResolution = 8192;
|
||||
app.registerExtension({
|
||||
name: "rgthree.ImageInsetCrop",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
|
||||
if (nodeData.name === "Image Inset Crop (rgthree)") {
|
||||
ImageInsetCrop.setUp(nodeType);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
var IoDirection;
|
||||
(function (IoDirection) {
|
||||
IoDirection[IoDirection["INPUT"] = 0] = "INPUT";
|
||||
IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT";
|
||||
})(IoDirection || (IoDirection = {}));
|
||||
function getNodeById(graph, id) {
|
||||
if (graph.getNodeById) {
|
||||
return graph.getNodeById(id);
|
||||
}
|
||||
graph = graph;
|
||||
return graph.nodes.find((n) => n.id === id);
|
||||
}
|
||||
function extendLink(link) {
|
||||
return {
|
||||
link: link,
|
||||
id: link[0],
|
||||
origin_id: link[1],
|
||||
origin_slot: link[2],
|
||||
target_id: link[3],
|
||||
target_slot: link[4],
|
||||
type: link[5],
|
||||
};
|
||||
}
|
||||
export function fixBadLinks(graph, fix = false, silent = false, logger = console) {
|
||||
var _a, _b;
|
||||
const patchedNodeSlots = {};
|
||||
const data = {
|
||||
patchedNodes: [],
|
||||
deletedLinks: [],
|
||||
};
|
||||
async function patchNodeSlot(node, ioDir, slot, linkId, op) {
|
||||
var _a, _b, _c;
|
||||
patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {};
|
||||
const patchedNode = patchedNodeSlots[node.id];
|
||||
if (ioDir == IoDirection.INPUT) {
|
||||
patchedNode["inputs"] = patchedNode["inputs"] || {};
|
||||
if (patchedNode["inputs"][slot] !== undefined) {
|
||||
!silent &&
|
||||
logger.log(` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"][slot]} Skipping.`);
|
||||
return false;
|
||||
}
|
||||
let linkIdToSet = op === "REMOVE" ? null : linkId;
|
||||
patchedNode["inputs"][slot] = linkIdToSet;
|
||||
if (fix) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
patchedNode["outputs"] = patchedNode["outputs"] || {};
|
||||
patchedNode["outputs"][slot] = patchedNode["outputs"][slot] || {
|
||||
links: [...(((_b = (_a = node.outputs) === null || _a === void 0 ? void 0 : _a[slot]) === null || _b === void 0 ? void 0 : _b.links) || [])],
|
||||
changes: {},
|
||||
};
|
||||
if (patchedNode["outputs"][slot]["changes"][linkId] !== undefined) {
|
||||
!silent &&
|
||||
logger.log(` > Already set ${node.id}.outputs[${slot}] to ${patchedNode["inputs"][slot]}! Skipping.`);
|
||||
return false;
|
||||
}
|
||||
patchedNode["outputs"][slot]["changes"][linkId] = op;
|
||||
if (op === "ADD") {
|
||||
let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId);
|
||||
if (linkIdIndex !== -1) {
|
||||
!silent && logger.log(` > Hmmm.. asked to add ${linkId} but it is already in list...`);
|
||||
return false;
|
||||
}
|
||||
patchedNode["outputs"][slot]["links"].push(linkId);
|
||||
if (fix) {
|
||||
node.outputs = node.outputs || [];
|
||||
node.outputs[slot] = node.outputs[slot] || {};
|
||||
node.outputs[slot].links = node.outputs[slot].links || [];
|
||||
node.outputs[slot].links.push(linkId);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId);
|
||||
if (linkIdIndex === -1) {
|
||||
!silent && logger.log(` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`);
|
||||
return false;
|
||||
}
|
||||
patchedNode["outputs"][slot]["links"].splice(linkIdIndex, 1);
|
||||
if (fix) {
|
||||
(_c = node.outputs) === null || _c === void 0 ? void 0 : _c[slot].links.splice(linkIdIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.patchedNodes.push(node);
|
||||
return true;
|
||||
}
|
||||
function nodeHasLinkId(node, ioDir, slot, linkId) {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
||||
let has = false;
|
||||
if (ioDir === IoDirection.INPUT) {
|
||||
let nodeHasIt = ((_b = (_a = node.inputs) === null || _a === void 0 ? void 0 : _a[slot]) === null || _b === void 0 ? void 0 : _b.link) === linkId;
|
||||
if ((_c = patchedNodeSlots[node.id]) === null || _c === void 0 ? void 0 : _c["inputs"]) {
|
||||
let patchedHasIt = patchedNodeSlots[node.id]["inputs"][slot] === linkId;
|
||||
if (fix && nodeHasIt !== patchedHasIt) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
has = patchedHasIt;
|
||||
}
|
||||
else {
|
||||
has = !!nodeHasIt;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let nodeHasIt = (_f = (_e = (_d = node.outputs) === null || _d === void 0 ? void 0 : _d[slot]) === null || _e === void 0 ? void 0 : _e.links) === null || _f === void 0 ? void 0 : _f.includes(linkId);
|
||||
if ((_j = (_h = (_g = patchedNodeSlots[node.id]) === null || _g === void 0 ? void 0 : _g["outputs"]) === null || _h === void 0 ? void 0 : _h[slot]) === null || _j === void 0 ? void 0 : _j["changes"][linkId]) {
|
||||
let patchedHasIt = (_k = patchedNodeSlots[node.id]["outputs"][slot]) === null || _k === void 0 ? void 0 : _k.links.includes(linkId);
|
||||
if (fix && nodeHasIt !== patchedHasIt) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
has = !!patchedHasIt;
|
||||
}
|
||||
else {
|
||||
has = !!nodeHasIt;
|
||||
}
|
||||
}
|
||||
return has;
|
||||
}
|
||||
function nodeHasAnyLink(node, ioDir, slot) {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
||||
let hasAny = false;
|
||||
if (ioDir === IoDirection.INPUT) {
|
||||
let nodeHasAny = ((_b = (_a = node.inputs) === null || _a === void 0 ? void 0 : _a[slot]) === null || _b === void 0 ? void 0 : _b.link) != null;
|
||||
if ((_c = patchedNodeSlots[node.id]) === null || _c === void 0 ? void 0 : _c["inputs"]) {
|
||||
let patchedHasAny = patchedNodeSlots[node.id]["inputs"][slot] != null;
|
||||
if (fix && nodeHasAny !== patchedHasAny) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
hasAny = patchedHasAny;
|
||||
}
|
||||
else {
|
||||
hasAny = !!nodeHasAny;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let nodeHasAny = (_f = (_e = (_d = node.outputs) === null || _d === void 0 ? void 0 : _d[slot]) === null || _e === void 0 ? void 0 : _e.links) === null || _f === void 0 ? void 0 : _f.length;
|
||||
if ((_j = (_h = (_g = patchedNodeSlots[node.id]) === null || _g === void 0 ? void 0 : _g["outputs"]) === null || _h === void 0 ? void 0 : _h[slot]) === null || _j === void 0 ? void 0 : _j["changes"]) {
|
||||
let patchedHasAny = (_k = patchedNodeSlots[node.id]["outputs"][slot]) === null || _k === void 0 ? void 0 : _k.links.length;
|
||||
if (fix && nodeHasAny !== patchedHasAny) {
|
||||
throw Error("Error. Expected node to match patched data.");
|
||||
}
|
||||
hasAny = !!patchedHasAny;
|
||||
}
|
||||
else {
|
||||
hasAny = !!nodeHasAny;
|
||||
}
|
||||
}
|
||||
return hasAny;
|
||||
}
|
||||
let links = [];
|
||||
if (!Array.isArray(graph.links)) {
|
||||
Object.values(graph.links).reduce((acc, v) => {
|
||||
acc[v.id] = v;
|
||||
return acc;
|
||||
}, links);
|
||||
}
|
||||
else {
|
||||
links = graph.links;
|
||||
}
|
||||
const linksReverse = [...links];
|
||||
linksReverse.reverse();
|
||||
for (let l of linksReverse) {
|
||||
if (!l)
|
||||
continue;
|
||||
const link = l.origin_slot != null ? l : extendLink(l);
|
||||
const originNode = getNodeById(graph, link.origin_id);
|
||||
const originHasLink = () => nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id);
|
||||
const patchOrigin = (op, id = link.id) => patchNodeSlot(originNode, IoDirection.OUTPUT, link.origin_slot, id, op);
|
||||
const targetNode = getNodeById(graph, link.target_id);
|
||||
const targetHasLink = () => nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id);
|
||||
const targetHasAnyLink = () => nodeHasAnyLink(targetNode, IoDirection.INPUT, link.target_slot);
|
||||
const patchTarget = (op, id = link.id) => patchNodeSlot(targetNode, IoDirection.INPUT, link.target_slot, id, op);
|
||||
const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`;
|
||||
const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`;
|
||||
if (!originNode || !targetNode) {
|
||||
if (!originNode && !targetNode) {
|
||||
!silent &&
|
||||
logger.log(`Link ${link.id} is invalid, ` +
|
||||
`both origin ${link.origin_id} and target ${link.target_id} do not exist`);
|
||||
}
|
||||
else if (!originNode) {
|
||||
!silent &&
|
||||
logger.log(`Link ${link.id} is funky... ` +
|
||||
`origin ${link.origin_id} does not exist, but target ${link.target_id} does.`);
|
||||
if (targetHasLink()) {
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`);
|
||||
patchTarget("REMOVE", -1);
|
||||
}
|
||||
}
|
||||
else if (!targetNode) {
|
||||
!silent &&
|
||||
logger.log(`Link ${link.id} is funky... ` +
|
||||
`target ${link.target_id} does not exist, but origin ${link.origin_id} does.`);
|
||||
if (originHasLink()) {
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`);
|
||||
patchOrigin("REMOVE");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (targetHasLink() || originHasLink()) {
|
||||
if (!originHasLink()) {
|
||||
!silent &&
|
||||
logger.log(`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`);
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`);
|
||||
patchOrigin("ADD");
|
||||
}
|
||||
else if (!targetHasLink()) {
|
||||
!silent &&
|
||||
logger.log(`${link.id} is funky... ${targetLog} is NOT correct (is ${(_b = (_a = targetNode.inputs) === null || _a === void 0 ? void 0 : _a[link.target_slot]) === null || _b === void 0 ? void 0 : _b.link}), but ${originLog} contains it`);
|
||||
if (!targetHasAnyLink()) {
|
||||
!silent && logger.log(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`);
|
||||
let patched = patchTarget("ADD");
|
||||
if (!patched) {
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`);
|
||||
patched = patchOrigin("REMOVE");
|
||||
}
|
||||
}
|
||||
else {
|
||||
!silent &&
|
||||
logger.log(` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`);
|
||||
patchOrigin("REMOVE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let l of linksReverse) {
|
||||
if (!l)
|
||||
continue;
|
||||
const link = l.origin_slot != null ? l : extendLink(l);
|
||||
const originNode = getNodeById(graph, link.origin_id);
|
||||
const targetNode = getNodeById(graph, link.target_id);
|
||||
if ((!originNode || !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) &&
|
||||
(!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id))) {
|
||||
!silent &&
|
||||
logger.log(`${link.id} is def invalid; BOTH origin node ${link.origin_id} ${originNode ? "is removed" : `doesn\'t have ${link.id}`} and ${link.origin_id} target node ${link.target_id ? "is removed" : `doesn\'t have ${link.id}`}.`);
|
||||
data.deletedLinks.push(link.id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (fix) {
|
||||
for (let i = data.deletedLinks.length - 1; i >= 0; i--) {
|
||||
!silent && logger.log(`Deleting link #${data.deletedLinks[i]}.`);
|
||||
if (graph.getNodeById) {
|
||||
delete graph.links[data.deletedLinks[i]];
|
||||
}
|
||||
else {
|
||||
graph = graph;
|
||||
const idx = graph.links.findIndex((l) => l && (l[0] === data.deletedLinks[i] || l.id === data.deletedLinks[i]));
|
||||
if (idx === -1) {
|
||||
logger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`);
|
||||
}
|
||||
logger.log(`splicing ${idx} from links`);
|
||||
graph.links.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
if (!graph.getNodeById) {
|
||||
graph.links = graph.links.filter((l) => !!l);
|
||||
}
|
||||
}
|
||||
if (!data.patchedNodes.length && !data.deletedLinks.length) {
|
||||
!silent && logger.log(`No bad links detected.`);
|
||||
return {
|
||||
hasBadLinks: false,
|
||||
fixed: false,
|
||||
graph,
|
||||
patched: data.patchedNodes.length,
|
||||
deleted: data.deletedLinks.length,
|
||||
};
|
||||
}
|
||||
!silent &&
|
||||
logger.log(`${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`);
|
||||
let hasBadLinks = !!(data.patchedNodes.length || data.deletedLinks.length);
|
||||
if (fix && !silent) {
|
||||
const rerun = fixBadLinks(graph, false, true);
|
||||
hasBadLinks = rerun.hasBadLinks;
|
||||
}
|
||||
return {
|
||||
hasBadLinks,
|
||||
fixed: !!hasBadLinks && fix,
|
||||
graph,
|
||||
patched: data.patchedNodes.length,
|
||||
deleted: data.deletedLinks.length,
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,20 @@ class MuterNode extends BaseNodeModeChanger {
|
||||
this.modeOn = MODE_ALWAYS;
|
||||
this.modeOff = MODE_MUTE;
|
||||
}
|
||||
async handleAction(action) {
|
||||
if (action === 'Mute all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOff(widget);
|
||||
}
|
||||
}
|
||||
else if (action === 'Enable all') {
|
||||
for (const widget of this.widgets) {
|
||||
this.forceWidgetOn(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MuterNode.exposedActions = ['Mute all', 'Enable all'];
|
||||
MuterNode.type = NodeTypesString.FAST_MUTER;
|
||||
MuterNode.title = NodeTypesString.FAST_MUTER;
|
||||
app.registerExtension({
|
||||
@@ -1,12 +1,16 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { addConnectionLayoutSupport, wait } from "./utils.js";
|
||||
import { addConnectionLayoutSupport } from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import { BaseCollectorNode } from './base_node_collector.js';
|
||||
import { NodeTypesString } from "./constants.js";
|
||||
class CollectorNode extends BaseCollectorNode {
|
||||
constructor(title = CollectorNode.title) {
|
||||
super(title);
|
||||
}
|
||||
}
|
||||
CollectorNode.type = "Node Collector (rgthree)";
|
||||
CollectorNode.title = "Node Collector (rgthree)";
|
||||
CollectorNode.legacyType = "Node Combiner (rgthree)";
|
||||
CollectorNode.type = NodeTypesString.NODE_COLLECTOR;
|
||||
CollectorNode.title = NodeTypesString.NODE_COLLECTOR;
|
||||
class CombinerNode extends CollectorNode {
|
||||
constructor(title = CombinerNode.title) {
|
||||
super(title);
|
||||
@@ -36,7 +40,7 @@ class CombinerNode extends CollectorNode {
|
||||
CombinerNode.legacyType = "Node Combiner (rgthree)";
|
||||
CombinerNode.title = "‼️ Node Combiner [DEPRECATED]";
|
||||
async function updateCombinerToCollector(node) {
|
||||
if (node.type === CollectorNode.legacyType) {
|
||||
if (node.type === CombinerNode.legacyType) {
|
||||
const newNode = new CollectorNode();
|
||||
if (node.title != CombinerNode.title) {
|
||||
newNode.title = node.title.replace('‼️ ', '');
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes, wait } from "./utils.js";
|
||||
import { BaseCollectorNode } from './base_node_collector.js';
|
||||
import { PassThroughFollowing, addConnectionLayoutSupport, addHelp, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, } from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
import { BaseCollectorNode } from "./base_node_collector.js";
|
||||
import { NodeTypesString, stripRgthree } from "./constants.js";
|
||||
const MODE_ALWAYS = 0;
|
||||
const MODE_MUTE = 2;
|
||||
@@ -9,33 +10,35 @@ const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
|
||||
class NodeModeRelay extends BaseCollectorNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
setTimeout(() => { this.stabilize(); }, 500);
|
||||
this.inputsPassThroughFollowing = PassThroughFollowing.ALL;
|
||||
setTimeout(() => {
|
||||
this.stabilize();
|
||||
}, 500);
|
||||
this.removeOutput(0);
|
||||
this.addOutput('REPEATER', '_NODE_REPEATER_', {
|
||||
color_on: '#Fc0',
|
||||
color_off: '#a80',
|
||||
this.addOutput("REPEATER", "_NODE_REPEATER_", {
|
||||
color_on: "#Fc0",
|
||||
color_off: "#a80",
|
||||
shape: LiteGraph.ARROW_SHAPE,
|
||||
});
|
||||
}
|
||||
onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) {
|
||||
var _a, _b;
|
||||
let canConnect = true;
|
||||
if (super.onConnectOutput) {
|
||||
canConnect = (_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
||||
}
|
||||
let nextNode = (_b = getConnectedOutputNodes(app, this, inputNode)[0]) !== null && _b !== void 0 ? _b : inputNode;
|
||||
let canConnect = (_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
||||
let nextNode = (_b = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0]) !== null && _b !== void 0 ? _b : inputNode;
|
||||
return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER;
|
||||
}
|
||||
onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot) {
|
||||
super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot);
|
||||
setTimeout(() => { this.stabilize(); }, 500);
|
||||
setTimeout(() => {
|
||||
this.stabilize();
|
||||
}, 500);
|
||||
}
|
||||
stabilize() {
|
||||
var _a;
|
||||
if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) {
|
||||
return;
|
||||
}
|
||||
const inputNodes = getConnectedInputNodes(app, this);
|
||||
const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this, this, -1, this.inputsPassThroughFollowing);
|
||||
let mode = undefined;
|
||||
for (const inputNode of inputNodes) {
|
||||
if (mode === undefined) {
|
||||
@@ -53,7 +56,7 @@ class NodeModeRelay extends BaseCollectorNode {
|
||||
}
|
||||
if (mode != null) {
|
||||
if ((_a = this.outputs) === null || _a === void 0 ? void 0 : _a.length) {
|
||||
const outputNodes = getConnectedOutputNodes(app, this);
|
||||
const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
|
||||
for (const outputNode of outputNodes) {
|
||||
outputNode.mode = mode;
|
||||
wait(16).then(() => {
|
||||
@@ -62,7 +65,9 @@ class NodeModeRelay extends BaseCollectorNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(() => { this.stabilize(); }, 500);
|
||||
setTimeout(() => {
|
||||
this.stabilize();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
NodeModeRelay.type = NodeTypesString.NODE_MODE_RELAY;
|
||||
@@ -74,12 +79,15 @@ NodeModeRelay.help = [
|
||||
`\n- When all connected input nodes are muted, the relay will set a connected repeater to mute.`,
|
||||
`\n- When all connected input nodes are bypassed, the relay will set a connected repeater to bypass.`,
|
||||
`\n- When any connected input nodes are active, the relay will set a connected repeater to active.`,
|
||||
].join(' ');
|
||||
].join(" ");
|
||||
app.registerExtension({
|
||||
name: "rgthree.NodeModeRepeaterHelper",
|
||||
registerCustomNodes() {
|
||||
addConnectionLayoutSupport(NodeModeRelay, app, [
|
||||
["Left", "Right"],
|
||||
["Right", "Left"],
|
||||
]);
|
||||
addHelp(NodeModeRelay, app);
|
||||
addConnectionLayoutSupport(NodeModeRelay, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
|
||||
NodeModeRelay.category = NodeModeRelay._category;
|
||||
},
|
||||
@@ -1,34 +1,37 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { BaseCollectorNode } from './base_node_collector.js';
|
||||
import { BaseCollectorNode } from "./base_node_collector.js";
|
||||
import { NodeTypesString, stripRgthree } from "./constants.js";
|
||||
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes } from "./utils.js";
|
||||
import { PassThroughFollowing, addConnectionLayoutSupport, addHelp, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, } from "./utils.js";
|
||||
class NodeModeRepeater extends BaseCollectorNode {
|
||||
constructor(title) {
|
||||
super(title);
|
||||
this.inputsPassThroughFollowing = PassThroughFollowing.ALL;
|
||||
this.hasRelayInput = false;
|
||||
this.hasTogglerOutput = false;
|
||||
this.removeOutput(0);
|
||||
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
|
||||
color_on: '#Fc0',
|
||||
color_off: '#a80',
|
||||
this.addOutput("OPT_CONNECTION", "*", {
|
||||
color_on: "#Fc0",
|
||||
color_off: "#a80",
|
||||
});
|
||||
}
|
||||
onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) {
|
||||
var _a;
|
||||
let canConnect = !this.hasRelayInput;
|
||||
if (super.onConnectOutput) {
|
||||
canConnect = canConnect && ((_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex));
|
||||
}
|
||||
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] || inputNode;
|
||||
return canConnect && (nextNode.type === NodeTypesString.FAST_MUTER || nextNode.type === NodeTypesString.FAST_BYPASSER);
|
||||
canConnect = canConnect && super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex);
|
||||
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 || ""));
|
||||
}
|
||||
onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
|
||||
var _a;
|
||||
let canConnect = true;
|
||||
if (super.onConnectInput) {
|
||||
canConnect = canConnect && ((_a = super.onConnectInput) === null || _a === void 0 ? void 0 : _a.call(this, inputIndex, outputType, outputSlot, outputNode, outputIndex));
|
||||
}
|
||||
let nextNode = getConnectedOutputNodes(app, this, outputNode)[0] || outputNode;
|
||||
let canConnect = (_a = super.onConnectInput) === null || _a === void 0 ? void 0 : _a.call(this, inputIndex, outputType, outputSlot, outputNode, outputIndex);
|
||||
let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, outputNode)[0] || outputNode;
|
||||
const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY;
|
||||
return canConnect && (!isNextNodeRelay || !this.hasTogglerOutput);
|
||||
}
|
||||
@@ -36,14 +39,15 @@ class NodeModeRepeater extends BaseCollectorNode {
|
||||
super.onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot);
|
||||
let hasTogglerOutput = false;
|
||||
let hasRelayInput = false;
|
||||
const outputNodes = getConnectedOutputNodes(app, this);
|
||||
const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
|
||||
for (const outputNode of outputNodes) {
|
||||
if ((outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_MUTER || (outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_BYPASSER) {
|
||||
if ((outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_MUTER ||
|
||||
(outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_BYPASSER) {
|
||||
hasTogglerOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const inputNodes = getConnectedInputNodes(app, this);
|
||||
const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this);
|
||||
for (const [index, inputNode] of inputNodes.entries()) {
|
||||
if ((inputNode === null || inputNode === void 0 ? void 0 : inputNode.type) === NodeTypesString.NODE_MODE_RELAY) {
|
||||
if (hasTogglerOutput) {
|
||||
@@ -53,8 +57,8 @@ class NodeModeRepeater extends BaseCollectorNode {
|
||||
else {
|
||||
hasRelayInput = true;
|
||||
if (this.inputs[index]) {
|
||||
this.inputs[index].color_on = '#FC0';
|
||||
this.inputs[index].color_off = '#a80';
|
||||
this.inputs[index].color_on = "#FC0";
|
||||
this.inputs[index].color_off = "#a80";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,18 +75,31 @@ class NodeModeRepeater extends BaseCollectorNode {
|
||||
}
|
||||
}
|
||||
else if (!this.outputs[0]) {
|
||||
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
|
||||
color_on: '#Fc0',
|
||||
color_off: '#a80',
|
||||
this.addOutput("OPT_CONNECTION", "*", {
|
||||
color_on: "#Fc0",
|
||||
color_off: "#a80",
|
||||
});
|
||||
}
|
||||
}
|
||||
onModeChange() {
|
||||
var _a, _b;
|
||||
super.onModeChange();
|
||||
const linkedNodes = getConnectedInputNodes(app, this);
|
||||
for (const node of linkedNodes) {
|
||||
if (node.type !== NodeTypesString.NODE_MODE_RELAY) {
|
||||
node.mode = this.mode;
|
||||
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 ((_a = app.graph._groups) === null || _a === void 0 ? void 0 : _a.length) {
|
||||
for (const group of app.graph._groups) {
|
||||
group.recomputeInsideNodes();
|
||||
if ((_b = group._nodes) === null || _b === void 0 ? void 0 : _b.includes(this)) {
|
||||
for (const node of group._nodes) {
|
||||
node.mode = this.mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +108,8 @@ NodeModeRepeater.type = NodeTypesString.NODE_MODE_REPEATER;
|
||||
NodeModeRepeater.title = NodeTypesString.NODE_MODE_REPEATER;
|
||||
NodeModeRepeater.help = [
|
||||
`When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`,
|
||||
`connected input nodes.`,
|
||||
`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.`,
|
||||
`\n`,
|
||||
`\n- Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}`,
|
||||
`or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly`,
|
||||
@@ -99,12 +117,15 @@ NodeModeRepeater.help = [
|
||||
`\n- 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)`,
|
||||
].join(' ');
|
||||
].join(" ");
|
||||
app.registerExtension({
|
||||
name: "rgthree.NodeModeRepeater",
|
||||
registerCustomNodes() {
|
||||
addConnectionLayoutSupport(NodeModeRepeater, app, [
|
||||
["Left", "Right"],
|
||||
["Right", "Left"],
|
||||
]);
|
||||
addHelp(NodeModeRepeater, app);
|
||||
addConnectionLayoutSupport(NodeModeRepeater, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater);
|
||||
NodeModeRepeater.category = NodeModeRepeater._category;
|
||||
},
|
||||
@@ -0,0 +1,38 @@
|
||||
import { app } from '../../scripts/app.js';
|
||||
import { addConnectionLayoutSupport } from './utils.js';
|
||||
import { PowerPrompt } from './base_power_prompt.js';
|
||||
let nodeData = null;
|
||||
app.registerExtension({
|
||||
name: 'rgthree.PowerPrompt',
|
||||
async beforeRegisterNodeDef(nodeType, passedNodeData, _app) {
|
||||
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.powerPrompt = new PowerPrompt(this, passedNodeData);
|
||||
};
|
||||
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
|
||||
}
|
||||
},
|
||||
async loadedGraphNode(node) {
|
||||
if (node.type === 'Power Prompt (rgthree)') {
|
||||
setTimeout(() => {
|
||||
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];
|
||||
node.outputs[0].name = nodeData.output_name[0] || node.outputs[0].type;
|
||||
node.outputs[0].color_on = undefined;
|
||||
node.outputs[0].color_off = undefined;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
||||
import { NodeTypesString } from "./constants.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
import { addHelp, getConnectedInputNodesAndFilterPassThroughs } from "./utils.js";
|
||||
const MODE_MUTE = 2;
|
||||
const MODE_ALWAYS = 0;
|
||||
class RandomUnmuterNode extends BaseAnyInputConnectedNode {
|
||||
constructor(title = RandomUnmuterNode.title) {
|
||||
super(title);
|
||||
this.modeOn = MODE_ALWAYS;
|
||||
this.modeOff = MODE_MUTE;
|
||||
this.tempEnabledNode = null;
|
||||
this.processingQueue = false;
|
||||
this.onQueueBound = this.onQueue.bind(this);
|
||||
this.onQueueEndBound = this.onQueueEnd.bind(this);
|
||||
this.onGraphtoPromptBound = this.onGraphtoPrompt.bind(this);
|
||||
this.onGraphtoPromptEndBound = this.onGraphtoPromptEnd.bind(this);
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
this.processingQueue = true;
|
||||
}
|
||||
onQueueEnd(event) {
|
||||
this.processingQueue = false;
|
||||
}
|
||||
onGraphtoPrompt(event) {
|
||||
if (!this.processingQueue) {
|
||||
return;
|
||||
}
|
||||
this.tempEnabledNode = null;
|
||||
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) {
|
||||
if (this.tempEnabledNode) {
|
||||
this.tempEnabledNode.mode = this.modeOff;
|
||||
this.tempEnabledNode = null;
|
||||
}
|
||||
}
|
||||
static setUp(clazz) {
|
||||
BaseAnyInputConnectedNode.setUp(clazz);
|
||||
addHelp(clazz);
|
||||
}
|
||||
handleLinkedNodesStabilization(linkedNodes) {
|
||||
}
|
||||
}
|
||||
RandomUnmuterNode.exposedActions = ['Mute all', 'Enable all'];
|
||||
RandomUnmuterNode.type = NodeTypesString.RANDOM_UNMUTER;
|
||||
RandomUnmuterNode.title = RandomUnmuterNode.type;
|
||||
RandomUnmuterNode.help = [
|
||||
`Use this node to unmute on of its inputs randomly when the graph is queued (and, immediately`,
|
||||
`mute it back).`,
|
||||
`\n`,
|
||||
`\n- NOTE: All input nodes MUST be muted to start; if not this node will not randomly unmute another.`,
|
||||
`\n(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.)`,
|
||||
`\n- 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.`,
|
||||
].join(" ");
|
||||
app.registerExtension({
|
||||
name: "rgthree.RandomUnmuter",
|
||||
registerCustomNodes() {
|
||||
RandomUnmuterNode.setUp(RandomUnmuterNode);
|
||||
},
|
||||
loadedGraphNode(node) {
|
||||
if (node.type == RandomUnmuterNode.title) {
|
||||
node._tempWidth = node.size[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
var _a;
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { rgthreeConfig } from "./rgthree_config.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
import { LAYOUT_CLOCKWISE, LAYOUT_LABEL_OPPOSITES, LAYOUT_LABEL_TO_DATA, addConnectionLayoutSupport, addMenuItem, getConnectedOutputNodesAndFilterPassThroughs, getSlotLinks, isValidConnection, } from "./utils.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
const rerouteConfig = ((_a = rgthreeConfig === null || rgthreeConfig === void 0 ? void 0 : rgthreeConfig['nodes']) === null || _a === void 0 ? void 0 : _a['reroute']) || {};
|
||||
let configWidth = Math.max(Math.round((Number(rerouteConfig['default_width']) || 40) / 10) * 10, 10);
|
||||
let configHeight = Math.max(Math.round((Number(rerouteConfig['default_height']) || 30) / 10) * 10, 10);
|
||||
while (configWidth * configHeight < 400) {
|
||||
configWidth += 10;
|
||||
configHeight += 10;
|
||||
}
|
||||
const configDefaultSize = [configWidth, configHeight];
|
||||
const configResizable = !!rerouteConfig['default_resizable'];
|
||||
let configLayout = rerouteConfig['default_layout'];
|
||||
if (!Array.isArray(configLayout)) {
|
||||
configLayout = ['Left', 'Right'];
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[configLayout[0]]) {
|
||||
configLayout[0] = 'Left';
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[configLayout[1]] || configLayout[0] == configLayout[1]) {
|
||||
configLayout[1] = LAYOUT_LABEL_OPPOSITES[configLayout[0]];
|
||||
}
|
||||
app.registerExtension({
|
||||
name: "rgthree.Reroute",
|
||||
registerCustomNodes() {
|
||||
class RerouteNode extends LGraphNode {
|
||||
constructor(title = RerouteNode.title) {
|
||||
var _a;
|
||||
super(title);
|
||||
this.configuring = true;
|
||||
this.defaultConnectionsLayout = configLayout;
|
||||
this.isVirtualNode = true;
|
||||
this.hideSlotLabels = true;
|
||||
this.setResizable((_a = this.properties['resizable']) !== null && _a !== void 0 ? _a : configResizable);
|
||||
this.size = RerouteNode.size;
|
||||
this.addInput("", "*");
|
||||
this.addOutput("", "*");
|
||||
setTimeout(() => this.applyNodeSize(), 20);
|
||||
}
|
||||
configure(info) {
|
||||
var _a;
|
||||
this.configuring = true;
|
||||
super.configure(info);
|
||||
this.setResizable((_a = this.properties['resizable']) !== null && _a !== void 0 ? _a : configResizable);
|
||||
this.applyNodeSize();
|
||||
this.configuring = false;
|
||||
}
|
||||
setResizable(resizable) {
|
||||
this.properties['resizable'] = !!resizable;
|
||||
this.resizable = this.properties['resizable'];
|
||||
}
|
||||
clone() {
|
||||
const cloned = super.clone();
|
||||
cloned.inputs[0].type = "*";
|
||||
cloned.outputs[0].type = "*";
|
||||
return cloned;
|
||||
}
|
||||
onConnectionsChange(type, _slotIndex, connected, _link_info, _ioSlot) {
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = [];
|
||||
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
||||
const linkId = this.outputs[0].links[i];
|
||||
const link = app.graph.links[linkId];
|
||||
linksToDisconnect.push(link);
|
||||
}
|
||||
for (const link of linksToDisconnect) {
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
node.disconnectInput(link.target_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.configuring) {
|
||||
return;
|
||||
}
|
||||
this.stabilize();
|
||||
if (type === LiteGraph.INPUT) {
|
||||
this.updateDownstream(connected ? 'connect' : 'disconnect', { index: 0, name: this.inputs[0].name });
|
||||
}
|
||||
}
|
||||
updateDownstream(update, updatedIndexes) {
|
||||
var _a;
|
||||
const nodes = getConnectedOutputNodesAndFilterPassThroughs(this, this, 0);
|
||||
for (const node of nodes) {
|
||||
(_a = node === null || node === void 0 ? void 0 : node.updateFromUpstream) === null || _a === void 0 ? void 0 : _a.call(node, update, this, updatedIndexes);
|
||||
}
|
||||
}
|
||||
onDrawForeground(ctx, canvas) {
|
||||
var _a, _b, _c;
|
||||
if ((_a = this.properties) === null || _a === void 0 ? void 0 : _a['showLabel']) {
|
||||
const low_quality = canvas.ds.scale < 0.6;
|
||||
if (low_quality || this.size[0] <= 10) {
|
||||
return;
|
||||
}
|
||||
const fontSize = Math.min(14, ((this.size[1] * 0.65) | 0));
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.font = `${fontSize}px Arial`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(String(this.title && this.title !== RerouteNode.title ? this.title : ((_c = (_b = this.outputs) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.type) || ''), this.size[0] / 2, (this.size[1] / 2), this.size[0] - 30);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
disconnectOutput(slot, targetNode) {
|
||||
return super.disconnectOutput(slot, targetNode);
|
||||
}
|
||||
stabilize() {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
if (this.configuring) {
|
||||
return;
|
||||
}
|
||||
let currentNode = this;
|
||||
let updateNodes = [];
|
||||
let input = null;
|
||||
let inputType = null;
|
||||
let inputNode = null;
|
||||
let inputNodeOutputSlot = null;
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode);
|
||||
const linkId = currentNode.inputs[0].link;
|
||||
if (linkId !== null) {
|
||||
const link = app.graph.links[linkId];
|
||||
const node = app.graph.getNodeById(link.origin_id);
|
||||
if (!node) {
|
||||
app.graph.removeLink(linkId);
|
||||
currentNode = null;
|
||||
break;
|
||||
}
|
||||
const type = node.constructor.type;
|
||||
if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) {
|
||||
if (node === this) {
|
||||
currentNode.disconnectInput(link.target_slot);
|
||||
currentNode = null;
|
||||
}
|
||||
else {
|
||||
currentNode = node;
|
||||
}
|
||||
}
|
||||
else {
|
||||
inputNode = node;
|
||||
inputNodeOutputSlot = link.origin_slot;
|
||||
input = (_a = node.outputs[inputNodeOutputSlot]) !== null && _a !== void 0 ? _a : null;
|
||||
inputType = (_b = input === null || input === void 0 ? void 0 : input.type) !== null && _b !== void 0 ? _b : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
currentNode = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const nodes = [this];
|
||||
let outputNodes = [];
|
||||
let outputType = null;
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop();
|
||||
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
||||
if (outputs.length) {
|
||||
for (const linkId of outputs) {
|
||||
const link = app.graph.links[linkId];
|
||||
if (!link)
|
||||
continue;
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
if (!node)
|
||||
continue;
|
||||
const type = node.constructor.type;
|
||||
if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) {
|
||||
nodes.push(node);
|
||||
updateNodes.push(node);
|
||||
}
|
||||
else {
|
||||
const output = (_d = (_c = node.inputs) === null || _c === void 0 ? void 0 : _c[link.target_slot]) !== null && _d !== void 0 ? _d : null;
|
||||
const nodeOutType = output === null || output === void 0 ? void 0 : output.type;
|
||||
if (nodeOutType == null) {
|
||||
console.warn(`[rgthree] Reroute - Connected node ${node.id} does not have type information for slot ${link.target_slot}. Skipping connection enforcement, but something is odd with that node.`);
|
||||
}
|
||||
else if (inputType &&
|
||||
inputType !== "*" &&
|
||||
nodeOutType !== "*" &&
|
||||
!isValidConnection(input, output)) {
|
||||
console.warn(`[rgthree] Reroute - Disconnecting connected node's input (${node.id}.${link.target_slot}) (${node.type}) because its type (${String(nodeOutType)}) does not match the reroute type (${String(inputType)})`);
|
||||
node.disconnectInput(link.target_slot);
|
||||
}
|
||||
else {
|
||||
if (outputType != null && outputType !== nodeOutType) {
|
||||
console.warn(`[rgthree] Reroute - Mismatching output types..`);
|
||||
}
|
||||
outputType = nodeOutType;
|
||||
outputNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
const displayType = inputType || outputType || "*";
|
||||
const color = LGraphCanvas.link_type_colors[displayType];
|
||||
for (const node of updateNodes) {
|
||||
node.outputs[0].type = inputType || "*";
|
||||
node.__outputType = displayType;
|
||||
node.outputs[0].name = (input === null || input === void 0 ? void 0 : input.name) || "";
|
||||
node.size = node.computeSize();
|
||||
(_f = (_e = node).applyNodeSize) === null || _f === void 0 ? void 0 : _f.call(_e);
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputNode && inputNodeOutputSlot != null) {
|
||||
const links = inputNode.outputs[inputNodeOutputSlot].links;
|
||||
for (const l of links || []) {
|
||||
const link = app.graph.links[l];
|
||||
if (link) {
|
||||
link.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
computeSize(out) {
|
||||
var _a;
|
||||
if (((_a = app.canvas.resizing_node) === null || _a === void 0 ? void 0 : _a.id) === this.id && rgthree.ctrlKey) {
|
||||
return [10, 10];
|
||||
}
|
||||
return super.computeSize(out);
|
||||
}
|
||||
onResize(size) {
|
||||
var _a;
|
||||
if (((_a = app.canvas.resizing_node) === null || _a === void 0 ? void 0 : _a.id) === this.id) {
|
||||
this.properties["size"] = [
|
||||
size[0],
|
||||
size[1],
|
||||
];
|
||||
if (size[0] < 40 || size[0] < 30) {
|
||||
this.setResizable(false);
|
||||
}
|
||||
}
|
||||
if (super.onResize) {
|
||||
super.onResize(size);
|
||||
}
|
||||
}
|
||||
applyNodeSize() {
|
||||
this.properties["size"] = this.properties["size"] || RerouteNode.size;
|
||||
this.properties["size"] = [
|
||||
Number(this.properties["size"][0]),
|
||||
Number(this.properties["size"][1]),
|
||||
];
|
||||
this.size = this.properties["size"];
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
RerouteNode.title = "Reroute (rgthree)";
|
||||
RerouteNode.category = "rgthree";
|
||||
RerouteNode._category = "rgthree";
|
||||
RerouteNode.title_mode = LiteGraph.NO_TITLE;
|
||||
RerouteNode.collapsable = false;
|
||||
RerouteNode.layout_slot_offset = 5;
|
||||
RerouteNode.size = configDefaultSize;
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: (node) => { var _a; return `${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['showLabel']) ? "Hide" : "Show"} Label/Title`; },
|
||||
property: 'showLabel',
|
||||
callback: async (node, value) => {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
},
|
||||
});
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: (node) => `${node.resizable ? 'No' : 'Allow'} Resizing`,
|
||||
callback: (node) => {
|
||||
node.setResizable(!node.resizable);
|
||||
node.size[0] = Math.max(40, node.size[0]);
|
||||
node.size[1] = Math.max(30, node.size[1]);
|
||||
node.applyNodeSize();
|
||||
},
|
||||
});
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Static Width",
|
||||
property: "size",
|
||||
subMenuOptions: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
}
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [Number(value), node.size[1]],
|
||||
callback: (node) => {
|
||||
node.setResizable(false);
|
||||
node.applyNodeSize();
|
||||
},
|
||||
});
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Static Height",
|
||||
property: "size",
|
||||
subMenuOptions: (() => {
|
||||
const options = [];
|
||||
for (let w = 8; w > 0; w--) {
|
||||
options.push(`${w * 10}`);
|
||||
}
|
||||
return options;
|
||||
})(),
|
||||
prepareValue: (value, node) => [node.size[0], Number(value)],
|
||||
callback: (node) => {
|
||||
node.setResizable(false);
|
||||
node.applyNodeSize();
|
||||
},
|
||||
});
|
||||
addConnectionLayoutSupport(RerouteNode, app, [
|
||||
["Left", "Right"],
|
||||
["Left", "Top"],
|
||||
["Left", "Bottom"],
|
||||
["Right", "Left"],
|
||||
["Right", "Top"],
|
||||
["Right", "Bottom"],
|
||||
["Top", "Left"],
|
||||
["Top", "Right"],
|
||||
["Top", "Bottom"],
|
||||
["Bottom", "Left"],
|
||||
["Bottom", "Right"],
|
||||
["Bottom", "Top"],
|
||||
], (node) => {
|
||||
node.applyNodeSize();
|
||||
});
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Rotate",
|
||||
subMenuOptions: [
|
||||
"Rotate 90° Clockwise",
|
||||
"Rotate 90° Counter-Clockwise",
|
||||
"Rotate 180°",
|
||||
null,
|
||||
"Flip Horizontally",
|
||||
"Flip Vertically",
|
||||
],
|
||||
callback: (node, value) => {
|
||||
const w = node.size[0];
|
||||
const h = node.size[1];
|
||||
node.properties["connections_layout"] = node.properties["connections_layout"] || node.defaultConnectionsLayout;
|
||||
const inputDirIndex = LAYOUT_CLOCKWISE.indexOf(node.properties["connections_layout"][0]);
|
||||
const outputDirIndex = LAYOUT_CLOCKWISE.indexOf(node.properties["connections_layout"][1]);
|
||||
if (value === null || value === void 0 ? void 0 : value.startsWith("Rotate 90°")) {
|
||||
node.size[0] = h;
|
||||
node.size[1] = w;
|
||||
if (value.includes("Counter")) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex - 1) % 4) + 4) % 4];
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex - 1) % 4) + 4) % 4];
|
||||
}
|
||||
else {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 1) % 4) + 4) % 4];
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 1) % 4) + 4) % 4];
|
||||
}
|
||||
}
|
||||
else if (value === null || value === void 0 ? void 0 : value.startsWith("Rotate 180°")) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4];
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
else if (value === null || value === void 0 ? void 0 : value.startsWith("Flip Horizontally")) {
|
||||
if (["Left", "Right"].includes(node.properties["connections_layout"][0])) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
if (["Left", "Right"].includes(node.properties["connections_layout"][1])) {
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
}
|
||||
else if (value === null || value === void 0 ? void 0 : value.startsWith("Flip Vertically")) {
|
||||
if (["Top", "Bottom"].includes(node.properties["connections_layout"][0])) {
|
||||
node.properties["connections_layout"][0] =
|
||||
LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
if (["Top", "Bottom"].includes(node.properties["connections_layout"][1])) {
|
||||
node.properties["connections_layout"][1] =
|
||||
LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4];
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
addMenuItem(RerouteNode, app, {
|
||||
name: "Clone New Reroute...",
|
||||
subMenuOptions: [
|
||||
"Before",
|
||||
"After",
|
||||
],
|
||||
callback: async (node, value) => {
|
||||
const clone = node.clone();
|
||||
const pos = [...node.pos];
|
||||
if (value === 'Before') {
|
||||
clone.pos = [pos[0] - 20, pos[1] - 20];
|
||||
app.graph.add(clone);
|
||||
await wait();
|
||||
const inputLinks = getSlotLinks(node.inputs[0]);
|
||||
for (const inputLink of inputLinks) {
|
||||
const link = inputLink.link;
|
||||
const linkedNode = app.graph.getNodeById(link.origin_id);
|
||||
if (linkedNode) {
|
||||
linkedNode.connect(0, clone, 0);
|
||||
}
|
||||
}
|
||||
clone.connect(0, node, 0);
|
||||
}
|
||||
else {
|
||||
clone.pos = [pos[0] + 20, pos[1] + 20];
|
||||
app.graph.add(clone);
|
||||
await wait();
|
||||
const outputLinks = getSlotLinks(node.outputs[0]);
|
||||
node.connect(0, clone, 0);
|
||||
for (const outputLink of outputLinks) {
|
||||
const link = outputLink.link;
|
||||
const linkedNode = app.graph.getNodeById(link.target_id);
|
||||
if (linkedNode) {
|
||||
clone.connect(0, linkedNode, link.target_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
|
||||
RerouteNode.category = RerouteNode._category;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
.rgthree-top-messages-container {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container > div {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
padding: 4px;
|
||||
margin-top: -100px; /* re-set by JS */
|
||||
opacity: 0;
|
||||
transition: all 0.33s ease-in-out;
|
||||
z-index: 3;
|
||||
}
|
||||
.rgthree-top-messages-container > div:last-child {
|
||||
z-index: 2;
|
||||
}
|
||||
.rgthree-top-messages-container > div:not(.-show) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container > div.-show {
|
||||
opacity: 1;
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container > div.-show {
|
||||
opacity: 1;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container > div > div {
|
||||
position: relative;
|
||||
background: #353535;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: fit-content;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.88);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container [type="warn"]::before {
|
||||
content: '⚠️';
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
font-size: 18px;
|
||||
margin-right: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container a {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: #fc0;
|
||||
margin-left: 4px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rgthree-top-messages-container a:hover {
|
||||
color: #fc0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Fix node selector being crazy long b/c of array types. */
|
||||
.litegraph.litesearchbox input,
|
||||
.litegraph.litesearchbox select {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
/* There's no reason for this z-index to be so high. It layers on top of things it shouldn't,
|
||||
(like pythongssss' image gallery, etc.) */
|
||||
.comfy-multiline-input {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
.comfy-multiline-input:focus {
|
||||
z-index: 3 !important;
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { rgthreeConfig } from "./rgthree_config.js";
|
||||
import { fixBadLinks } from "./link_fixer.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
export var LogLevel;
|
||||
(function (LogLevel) {
|
||||
LogLevel[LogLevel["IMPORTANT"] = 1] = "IMPORTANT";
|
||||
LogLevel[LogLevel["ERROR"] = 2] = "ERROR";
|
||||
LogLevel[LogLevel["WARN"] = 3] = "WARN";
|
||||
LogLevel[LogLevel["INFO"] = 4] = "INFO";
|
||||
LogLevel[LogLevel["DEBUG"] = 5] = "DEBUG";
|
||||
})(LogLevel || (LogLevel = {}));
|
||||
const LogLevelToMethod = {
|
||||
[LogLevel.IMPORTANT]: "log",
|
||||
[LogLevel.ERROR]: "error",
|
||||
[LogLevel.WARN]: "warn",
|
||||
[LogLevel.INFO]: "info",
|
||||
[LogLevel.DEBUG]: "debug",
|
||||
};
|
||||
const LogLevelToCSS = {
|
||||
[LogLevel.IMPORTANT]: "font-weight:bold; color:blue;",
|
||||
[LogLevel.ERROR]: "",
|
||||
[LogLevel.WARN]: "",
|
||||
[LogLevel.INFO]: "",
|
||||
[LogLevel.DEBUG]: "font-style: italic;",
|
||||
};
|
||||
let GLOBAL_LOG_LEVEL = LogLevel.DEBUG;
|
||||
class Logger {
|
||||
log(level, message, ...args) {
|
||||
if (level <= GLOBAL_LOG_LEVEL) {
|
||||
const css = LogLevelToCSS[level] || "";
|
||||
console[LogLevelToMethod[level]](`%c${message}`, css, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
class LogSession {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.logger = new Logger();
|
||||
}
|
||||
log(levelOrMessage, message, ...args) {
|
||||
let level = typeof levelOrMessage === "string" ? LogLevel.INFO : levelOrMessage;
|
||||
if (typeof levelOrMessage === "string") {
|
||||
message = levelOrMessage;
|
||||
}
|
||||
this.logger.log(level, `${this.name || ""}${message ? " " + message : ""}`, ...args);
|
||||
}
|
||||
debug(message, ...args) {
|
||||
this.log(LogLevel.DEBUG, message, ...args);
|
||||
}
|
||||
info(message, ...args) {
|
||||
this.log(LogLevel.INFO, message, ...args);
|
||||
}
|
||||
error(message, ...args) {
|
||||
this.log(LogLevel.ERROR, message, ...args);
|
||||
}
|
||||
newSession(name) {
|
||||
return new LogSession(`${this.name}${name}`);
|
||||
}
|
||||
}
|
||||
class Rgthree {
|
||||
async clearAllMessages() {
|
||||
let container = document.querySelector('.rgthree-top-messages-container');
|
||||
container && (container.innerHTML = '');
|
||||
}
|
||||
async showMessage(data) {
|
||||
let container = document.querySelector('.rgthree-top-messages-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.classList.add('rgthree-top-messages-container');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
await this.hideMessage(data.id);
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.setAttribute('type', data.type || 'info');
|
||||
const message = document.createElement('span');
|
||||
message.innerText = data.message;
|
||||
messageContainer.appendChild(message);
|
||||
for (let a = 0; a < (data.actions || []).length; a++) {
|
||||
const action = data.actions[a];
|
||||
if (a > 0) {
|
||||
const sep = document.createElement('span');
|
||||
sep.innerHTML = ' | ';
|
||||
messageContainer.appendChild(sep);
|
||||
}
|
||||
const actionEl = document.createElement('a');
|
||||
actionEl.innerText = action.label;
|
||||
if (action.href) {
|
||||
actionEl.target = '_blank';
|
||||
actionEl.href = action.href;
|
||||
}
|
||||
if (action.callback) {
|
||||
actionEl.onclick = (e) => {
|
||||
return action.callback(e);
|
||||
};
|
||||
}
|
||||
messageContainer.appendChild(actionEl);
|
||||
}
|
||||
const messageAnimContainer = document.createElement('div');
|
||||
messageAnimContainer.setAttribute('msg-id', data.id);
|
||||
messageAnimContainer.appendChild(messageContainer);
|
||||
container.appendChild(messageAnimContainer);
|
||||
await wait(64);
|
||||
messageAnimContainer.style.marginTop = `-${messageAnimContainer.offsetHeight}px`;
|
||||
await wait(64);
|
||||
messageAnimContainer.classList.add('-show');
|
||||
if (data.timeout) {
|
||||
await wait(data.timeout);
|
||||
this.hideMessage(data.id);
|
||||
}
|
||||
}
|
||||
async hideMessage(id) {
|
||||
const msg = document.querySelector(`.rgthree-top-messages-container > [msg-id="${id}"]`);
|
||||
if (msg === null || msg === void 0 ? void 0 : msg.classList.contains('-show')) {
|
||||
msg.classList.remove('-show');
|
||||
await wait(750);
|
||||
}
|
||||
msg && msg.remove();
|
||||
}
|
||||
constructor() {
|
||||
this.ctrlKey = false;
|
||||
this.altKey = false;
|
||||
this.metaKey = false;
|
||||
this.shiftKey = false;
|
||||
this.logger = new LogSession("[rgthree]");
|
||||
this.monitorBadLinksAlerted = false;
|
||||
this.monitorLinkTimeout = null;
|
||||
this.eventsToFns = new Map();
|
||||
window.addEventListener("keydown", (e) => {
|
||||
this.ctrlKey = !!e.ctrlKey;
|
||||
this.altKey = !!e.altKey;
|
||||
this.metaKey = !!e.metaKey;
|
||||
this.shiftKey = !!e.shiftKey;
|
||||
});
|
||||
window.addEventListener("keyup", (e) => {
|
||||
this.ctrlKey = !!e.ctrlKey;
|
||||
this.altKey = !!e.altKey;
|
||||
this.metaKey = !!e.metaKey;
|
||||
this.shiftKey = !!e.shiftKey;
|
||||
});
|
||||
const that = this;
|
||||
const queuePrompt = app.queuePrompt;
|
||||
app.queuePrompt = async function () {
|
||||
that.fireEvent('queue', {});
|
||||
let promise = queuePrompt.apply(app, [...arguments]);
|
||||
that.fireEvent('queue-end', {});
|
||||
return promise;
|
||||
};
|
||||
const graphToPrompt = app.graphToPrompt;
|
||||
app.graphToPrompt = async function () {
|
||||
that.fireEvent('graph-to-prompt', {});
|
||||
let promise = graphToPrompt.apply(app, [...arguments]);
|
||||
await promise;
|
||||
that.fireEvent('graph-to-prompt-end', {});
|
||||
return promise;
|
||||
};
|
||||
const clean = app.clean;
|
||||
app.clean = function () {
|
||||
that.clearAllMessages();
|
||||
clean && clean.call(app, ...arguments);
|
||||
};
|
||||
const loadGraphData = app.loadGraphData;
|
||||
app.loadGraphData = function (graph) {
|
||||
if (that.monitorLinkTimeout) {
|
||||
clearTimeout(that.monitorLinkTimeout);
|
||||
that.monitorLinkTimeout = null;
|
||||
}
|
||||
that.clearAllMessages();
|
||||
let graphCopy;
|
||||
try {
|
||||
graphCopy = JSON.parse(JSON.stringify(graph));
|
||||
}
|
||||
catch (e) {
|
||||
graphCopy = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
var _a, _b;
|
||||
const wasLoadingAborted = (_b = (_a = document.querySelector('.comfy-modal-content')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.includes('Loading aborted due');
|
||||
const graphToUse = wasLoadingAborted ? (graphCopy || graph) : app.graph;
|
||||
const fixBadLinksResult = fixBadLinks(graphToUse);
|
||||
if (fixBadLinksResult.hasBadLinks) {
|
||||
that.showMessage({
|
||||
id: 'bad-links',
|
||||
type: 'warn',
|
||||
message: 'The workflow you\'ve loaded may have connection/linking data that could be fixed.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open fixer',
|
||||
href: '/extensions/rgthree-comfy/html/links.html',
|
||||
},
|
||||
{
|
||||
label: 'Fix in place',
|
||||
href: '/extensions/rgthree-comfy/html/links.html',
|
||||
callback: (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (confirm('This will attempt to fix in place. Please make sure to have a saved copy of your workflow.')) {
|
||||
const fixBadLinksResult = fixBadLinks(graphToUse, true);
|
||||
if (!fixBadLinksResult.hasBadLinks) {
|
||||
that.hideMessage('bad-links');
|
||||
alert('Success! It\'s possible some valid links may have been affected. Please check and verify your workflow.');
|
||||
wasLoadingAborted && app.loadGraphData(fixBadLinksResult.graph);
|
||||
if (rgthreeConfig['monitor_bad_links']) {
|
||||
that.monitorLinkTimeout = setTimeout(() => {
|
||||
that.monitorBadLinks();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
else if (rgthreeConfig['monitor_bad_links']) {
|
||||
that.monitorLinkTimeout = setTimeout(() => {
|
||||
that.monitorBadLinks();
|
||||
}, 5000);
|
||||
}
|
||||
}, 100);
|
||||
loadGraphData && loadGraphData.call(app, ...arguments);
|
||||
};
|
||||
wait(100).then(() => {
|
||||
this.injectRgthreeCss();
|
||||
});
|
||||
}
|
||||
injectRgthreeCss() {
|
||||
let link = document.createElement("link");
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = 'extensions/rgthree-comfy/rgthree.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
addEventListener(event, fn) {
|
||||
if (!this.eventsToFns.has(event)) {
|
||||
this.eventsToFns.set(event, new Set());
|
||||
}
|
||||
this.eventsToFns.get(event).add(fn);
|
||||
}
|
||||
removeEventListener(event, fn) {
|
||||
if (this.eventsToFns.has(event)) {
|
||||
this.eventsToFns.get(event).delete(fn);
|
||||
}
|
||||
}
|
||||
fireEvent(event, data) {
|
||||
if (this.eventsToFns.has(event)) {
|
||||
for (let fn of this.eventsToFns.get(event)) {
|
||||
const e = new CustomEvent(event, { detail: data });
|
||||
fn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
setLogLevel(level) {
|
||||
GLOBAL_LOG_LEVEL = level;
|
||||
}
|
||||
log(levelOrMessage, message, ...args) {
|
||||
this.logger.log(levelOrMessage, message, ...args);
|
||||
}
|
||||
newLogSession(name) {
|
||||
return this.logger.newSession(name);
|
||||
}
|
||||
monitorBadLinks() {
|
||||
const badLinksFound = fixBadLinks(app.graph);
|
||||
if (badLinksFound.hasBadLinks && !this.monitorBadLinksAlerted) {
|
||||
this.monitorBadLinksAlerted = true;
|
||||
alert(`Problematic links just found in live data. Can you save your workflow and file a bug with the last few steps you took to trigger this at https://github.com/rgthree/rgthree-comfy/issues. Thank you!`);
|
||||
}
|
||||
else if (!badLinksFound.hasBadLinks) {
|
||||
this.monitorBadLinksAlerted = false;
|
||||
}
|
||||
this.monitorLinkTimeout = setTimeout(() => {
|
||||
this.monitorBadLinks();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
export const rgthree = new Rgthree();
|
||||
window.rgthree = rgthree;
|
||||
+33
-18
@@ -1,6 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
const LAST_SEED_BUTTON_LABEL = '♻️ (Use Last Queued Seed)';
|
||||
const LAST_SEED_BUTTON_LABEL = "♻️ (Use Last Queued Seed)";
|
||||
const SPECIAL_SEED_RANDOM = -1;
|
||||
const SPECIAL_SEED_INCREMENT = -2;
|
||||
const SPECIAL_SEED_DECREMENT = -3;
|
||||
@@ -11,29 +11,43 @@ class SeedControl {
|
||||
this.serializedCtx = {};
|
||||
this.lastSeedValue = null;
|
||||
this.node = node;
|
||||
this.node.constructor.exposedActions = ["Randomize Each Time", "Use Last Queued Seed"];
|
||||
const handleAction = this.node.handleAction;
|
||||
this.node.handleAction = async (action) => {
|
||||
handleAction && handleAction.call(this.node, action);
|
||||
if (action === "Randomize Each Time") {
|
||||
this.seedWidget.value = SPECIAL_SEED_RANDOM;
|
||||
}
|
||||
else if (action === "Use Last Queued Seed") {
|
||||
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
|
||||
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
|
||||
this.lastSeedButton.disabled = true;
|
||||
}
|
||||
};
|
||||
this.node.properties = this.node.properties || {};
|
||||
for (const [i, w] of this.node.widgets.entries()) {
|
||||
if (w.name === 'seed') {
|
||||
if (w.name === "seed") {
|
||||
this.seedWidget = w;
|
||||
}
|
||||
else if (w.name === 'control_after_generate') {
|
||||
else if (w.name === "control_after_generate") {
|
||||
this.node.widgets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (!this.seedWidget) {
|
||||
throw new Error('Something\'s wrong; expected seed widget');
|
||||
throw new Error("Something's wrong; expected seed widget");
|
||||
}
|
||||
const randMax = Math.min(1125899906842624, this.seedWidget.options.max);
|
||||
const randMin = Math.max(0, this.seedWidget.options.min);
|
||||
const randomRange = (randMax - Math.max(0, randMin)) / (this.seedWidget.options.step / 10);
|
||||
this.node.addWidget('button', '🎲 Randomize Each Time', null, () => {
|
||||
this.node.addWidget("button", "🎲 Randomize Each Time", null, () => {
|
||||
this.seedWidget.value = SPECIAL_SEED_RANDOM;
|
||||
}, { serialize: false });
|
||||
this.node.addWidget('button', '🎲 New Fixed Random', null, () => {
|
||||
this.seedWidget.value = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
this.node.addWidget("button", "🎲 New Fixed Random", null, () => {
|
||||
this.seedWidget.value =
|
||||
Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
}, { serialize: false });
|
||||
this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
|
||||
this.seedWidget.value = this.lastSeed;
|
||||
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
|
||||
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
|
||||
this.lastSeedButton.disabled = true;
|
||||
}, { width: 50, serialize: false });
|
||||
@@ -44,16 +58,17 @@ class SeedControl {
|
||||
inputSeed: this.seedWidget.value,
|
||||
};
|
||||
if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed)) {
|
||||
if (typeof this.lastSeed === 'number' && !SPECIAL_SEEDS.includes(this.lastSeed)) {
|
||||
if (typeof this.lastSeed === "number" && !SPECIAL_SEEDS.includes(this.lastSeed)) {
|
||||
if (inputSeed === SPECIAL_SEED_INCREMENT) {
|
||||
this.serializedCtx.seedUsed = this.lastSeed + 1;
|
||||
}
|
||||
else if (inputSeed === SPECIAL_SEED_INCREMENT) {
|
||||
else if (inputSeed === SPECIAL_SEED_DECREMENT) {
|
||||
this.serializedCtx.seedUsed = this.lastSeed - 1;
|
||||
}
|
||||
}
|
||||
if (!this.serializedCtx.seedUsed || SPECIAL_SEEDS.includes(this.serializedCtx.seedUsed)) {
|
||||
this.serializedCtx.seedUsed = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
this.serializedCtx.seedUsed =
|
||||
Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -85,14 +100,14 @@ class SeedControl {
|
||||
options.splice(options.length - 1, 0, {
|
||||
content: "Show/Hide Last Seed Value",
|
||||
callback: (_value, _options, _event, _parentMenu, _node) => {
|
||||
this.node.properties['showLastSeed'] = !this.node.properties['showLastSeed'];
|
||||
if (this.node.properties['showLastSeed']) {
|
||||
this.node.properties["showLastSeed"] = !this.node.properties["showLastSeed"];
|
||||
if (this.node.properties["showLastSeed"]) {
|
||||
this.addLastSeedValue();
|
||||
}
|
||||
else {
|
||||
this.removeLastSeedValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -101,11 +116,11 @@ class SeedControl {
|
||||
return;
|
||||
this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", { multiline: true }], app).widget;
|
||||
this.lastSeedValue.inputEl.readOnly = true;
|
||||
this.lastSeedValue.inputEl.style.fontSize = '0.75rem';
|
||||
this.lastSeedValue.inputEl.style.textAlign = 'center';
|
||||
this.lastSeedValue.inputEl.style.fontSize = "0.75rem";
|
||||
this.lastSeedValue.inputEl.style.textAlign = "center";
|
||||
this.lastSeedValue.serializeValue = async (node, index) => {
|
||||
node.widgets_values[index] = '';
|
||||
return '';
|
||||
node.widgets_values[index] = "";
|
||||
return "";
|
||||
};
|
||||
this.node.computeSize();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export function getResolver(timeout = 5000) {
|
||||
const resolver = {};
|
||||
resolver.id = generateId(8);
|
||||
resolver.completed = false;
|
||||
resolver.resolved = false;
|
||||
resolver.rejected = false;
|
||||
resolver.promise = new Promise((resolve, reject) => {
|
||||
resolver.reject = () => {
|
||||
resolver.completed = true;
|
||||
resolver.rejected = true;
|
||||
reject();
|
||||
};
|
||||
resolver.resolve = (data) => {
|
||||
resolver.completed = true;
|
||||
resolver.resolved = true;
|
||||
resolve(data);
|
||||
};
|
||||
});
|
||||
resolver.timeout = setTimeout(() => {
|
||||
if (!resolver.completed) {
|
||||
resolver.reject();
|
||||
}
|
||||
}, timeout);
|
||||
return resolver;
|
||||
}
|
||||
export function wait(ms = 16, value) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(value);
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
function dec2hex(dec) {
|
||||
return dec.toString(16).padStart(2, "0");
|
||||
}
|
||||
export function generateId(length) {
|
||||
const arr = new Uint8Array(length / 2);
|
||||
crypto.getRandomValues(arr);
|
||||
return Array.from(arr, dec2hex).join('');
|
||||
}
|
||||
+587
@@ -0,0 +1,587 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { wait } from "./shared_utils.js";
|
||||
const oldApiGetNodeDefs = api.getNodeDefs;
|
||||
api.getNodeDefs = async function () {
|
||||
const defs = await oldApiGetNodeDefs.call(api);
|
||||
this.dispatchEvent(new CustomEvent("fresh-node-defs", { detail: defs }));
|
||||
return defs;
|
||||
};
|
||||
export var IoDirection;
|
||||
(function (IoDirection) {
|
||||
IoDirection[IoDirection["INPUT"] = 0] = "INPUT";
|
||||
IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT";
|
||||
})(IoDirection || (IoDirection = {}));
|
||||
const PADDING = 0;
|
||||
export const LAYOUT_LABEL_TO_DATA = {
|
||||
Left: [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]],
|
||||
Right: [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]],
|
||||
Top: [LiteGraph.UP, [0.5, 0], [0, PADDING]],
|
||||
Bottom: [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]],
|
||||
};
|
||||
export const LAYOUT_LABEL_OPPOSITES = {
|
||||
Left: "Right",
|
||||
Right: "Left",
|
||||
Top: "Bottom",
|
||||
Bottom: "Top",
|
||||
};
|
||||
export const LAYOUT_CLOCKWISE = ["Top", "Right", "Bottom", "Left"];
|
||||
export function addMenuItem(node, _app, config) {
|
||||
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
|
||||
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
|
||||
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
|
||||
let idx = menuOptions
|
||||
.slice()
|
||||
.reverse()
|
||||
.findIndex((option) => option === null || option === void 0 ? void 0 : option.isRgthree);
|
||||
if (idx == -1) {
|
||||
idx = menuOptions.findIndex((option) => option === null || option === void 0 ? void 0 : option.content.includes("Shape")) + 1;
|
||||
if (!idx) {
|
||||
idx = menuOptions.length - 1;
|
||||
}
|
||||
menuOptions.splice(idx, 0, null);
|
||||
idx++;
|
||||
}
|
||||
else {
|
||||
idx = menuOptions.length - idx;
|
||||
}
|
||||
const subMenuOptions = typeof config.subMenuOptions === 'function' ? config.subMenuOptions(this) : config.subMenuOptions;
|
||||
menuOptions.splice(idx, 0, {
|
||||
content: typeof config.name == "function" ? config.name(this) : config.name,
|
||||
has_submenu: !!(subMenuOptions === null || subMenuOptions === void 0 ? void 0 : subMenuOptions.length),
|
||||
isRgthree: true,
|
||||
callback: (value, _options, event, parentMenu, _node) => {
|
||||
if (!!(subMenuOptions === null || subMenuOptions === void 0 ? void 0 : subMenuOptions.length)) {
|
||||
new LiteGraph.ContextMenu(subMenuOptions.map((option) => (option ? { content: option } : null)), {
|
||||
event,
|
||||
parentMenu,
|
||||
callback: (subValue, _options, _event, _parentMenu, _node) => {
|
||||
if (config.property) {
|
||||
this.properties = this.properties || {};
|
||||
this.properties[config.property] = config.prepareValue
|
||||
? config.prepareValue(subValue.content, this)
|
||||
: subValue.content;
|
||||
}
|
||||
config.callback && config.callback(this, subValue === null || subValue === void 0 ? void 0 : subValue.content);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (config.property) {
|
||||
this.properties = this.properties || {};
|
||||
this.properties[config.property] = config.prepareValue
|
||||
? config.prepareValue(this.properties[config.property], this)
|
||||
: !this.properties[config.property];
|
||||
}
|
||||
config.callback && config.callback(this, value === null || value === void 0 ? void 0 : value.content);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
export function addConnectionLayoutSupport(node, app, options = [
|
||||
["Left", "Right"],
|
||||
["Right", "Left"],
|
||||
], callback) {
|
||||
addMenuItem(node, app, {
|
||||
name: "Connections Layout",
|
||||
property: "connections_layout",
|
||||
subMenuOptions: options.map((option) => option[0] + (option[1] ? " -> " + option[1] : "")),
|
||||
prepareValue: (value, node) => {
|
||||
var _a;
|
||||
const values = value.split(" -> ");
|
||||
if (!values[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) {
|
||||
values[1] = LAYOUT_LABEL_OPPOSITES[values[0]];
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[values[0]] || !LAYOUT_LABEL_TO_DATA[values[1]]) {
|
||||
throw new Error(`New Layout invalid: [${values[0]}, ${values[1]}]`);
|
||||
}
|
||||
return values;
|
||||
},
|
||||
callback: (node) => {
|
||||
callback && callback(node);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
},
|
||||
});
|
||||
node.prototype.getConnectionPos = function (isInput, slotNumber, out) {
|
||||
return getConnectionPosForLayout(this, isInput, slotNumber, out);
|
||||
};
|
||||
}
|
||||
export function setConnectionsLayout(node, newLayout) {
|
||||
var _a;
|
||||
newLayout = newLayout || node.defaultConnectionsLayout || ["Left", "Right"];
|
||||
if (!newLayout[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) {
|
||||
newLayout[1] = LAYOUT_LABEL_OPPOSITES[newLayout[0]];
|
||||
}
|
||||
if (!LAYOUT_LABEL_TO_DATA[newLayout[0]] || !LAYOUT_LABEL_TO_DATA[newLayout[1]]) {
|
||||
throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`);
|
||||
}
|
||||
node.properties = node.properties || {};
|
||||
node.properties["connections_layout"] = newLayout;
|
||||
}
|
||||
export function setConnectionsCollapse(node, collapseConnections = null) {
|
||||
node.properties = node.properties || {};
|
||||
collapseConnections =
|
||||
collapseConnections !== null ? collapseConnections : !node.properties["collapse_connections"];
|
||||
node.properties["collapse_connections"] = collapseConnections;
|
||||
}
|
||||
export function getConnectionPosForLayout(node, isInput, slotNumber, out) {
|
||||
var _a, _b, _c;
|
||||
out = out || new Float32Array(2);
|
||||
node.properties = node.properties || {};
|
||||
const layout = node.properties["connections_layout"] || node.defaultConnectionsLayout || ["Left", "Right"];
|
||||
const collapseConnections = node.properties["collapse_connections"] || false;
|
||||
const offset = (_a = node.constructor.layout_slot_offset) !== null && _a !== void 0 ? _a : LiteGraph.NODE_SLOT_HEIGHT * 0.5;
|
||||
let side = isInput ? layout[0] : layout[1];
|
||||
const otherSide = isInput ? layout[1] : layout[0];
|
||||
let data = LAYOUT_LABEL_TO_DATA[side];
|
||||
const slotList = node[isInput ? "inputs" : "outputs"];
|
||||
const cxn = slotList[slotNumber];
|
||||
if (!cxn) {
|
||||
console.log("No connection found.. weird", isInput, slotNumber);
|
||||
return out;
|
||||
}
|
||||
if (cxn.disabled) {
|
||||
if (cxn.color_on !== "#666665") {
|
||||
cxn._color_on_org = cxn._color_on_org || cxn.color_on;
|
||||
cxn._color_off_org = cxn._color_off_org || cxn.color_off;
|
||||
}
|
||||
cxn.color_on = "#666665";
|
||||
cxn.color_off = "#666665";
|
||||
}
|
||||
else if (cxn.color_on === "#666665") {
|
||||
cxn.color_on = cxn._color_on_org || undefined;
|
||||
cxn.color_off = cxn._color_off_org || undefined;
|
||||
}
|
||||
const displaySlot = collapseConnections
|
||||
? 0
|
||||
: slotNumber -
|
||||
slotList.reduce((count, ioput, index) => {
|
||||
count += index < slotNumber && ioput.hidden ? 1 : 0;
|
||||
return count;
|
||||
}, 0);
|
||||
cxn.dir = data[0];
|
||||
if (node.size[0] == 10 &&
|
||||
["Left", "Right"].includes(side) &&
|
||||
["Top", "Bottom"].includes(otherSide)) {
|
||||
side = otherSide === "Top" ? "Bottom" : "Top";
|
||||
}
|
||||
else if (node.size[1] == 10 &&
|
||||
["Top", "Bottom"].includes(side) &&
|
||||
["Left", "Right"].includes(otherSide)) {
|
||||
side = otherSide === "Left" ? "Right" : "Left";
|
||||
}
|
||||
if (side === "Left") {
|
||||
if (node.flags.collapsed) {
|
||||
var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
|
||||
out[0] = node.pos[0];
|
||||
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
|
||||
}
|
||||
else {
|
||||
toggleConnectionLabel(cxn, !isInput || collapseConnections || !!node.hideSlotLabels);
|
||||
out[0] = node.pos[0] + offset;
|
||||
if ((_b = node.constructor) === null || _b === void 0 ? void 0 : _b.type.includes("Reroute")) {
|
||||
out[1] = node.pos[1] + node.size[1] * 0.5;
|
||||
}
|
||||
else {
|
||||
out[1] =
|
||||
node.pos[1] +
|
||||
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
|
||||
(node.constructor.slot_start_y || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (side === "Right") {
|
||||
if (node.flags.collapsed) {
|
||||
var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
|
||||
out[0] = node.pos[0] + w;
|
||||
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
|
||||
}
|
||||
else {
|
||||
toggleConnectionLabel(cxn, isInput || collapseConnections || !!node.hideSlotLabels);
|
||||
out[0] = node.pos[0] + node.size[0] + 1 - offset;
|
||||
if ((_c = node.constructor) === null || _c === void 0 ? void 0 : _c.type.includes("Reroute")) {
|
||||
out[1] = node.pos[1] + node.size[1] * 0.5;
|
||||
}
|
||||
else {
|
||||
out[1] =
|
||||
node.pos[1] +
|
||||
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
|
||||
(node.constructor.slot_start_y || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (side === "Top") {
|
||||
if (!cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
cxn.label = " ";
|
||||
}
|
||||
out[0] = node.pos[0] + node.size[0] * 0.5;
|
||||
out[1] = node.pos[1] + offset;
|
||||
}
|
||||
else if (side === "Bottom") {
|
||||
if (!cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
cxn.label = " ";
|
||||
}
|
||||
out[0] = node.pos[0] + node.size[0] * 0.5;
|
||||
out[1] = node.pos[1] + node.size[1] - offset;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function toggleConnectionLabel(cxn, hide = true) {
|
||||
if (hide) {
|
||||
if (!cxn.has_old_label) {
|
||||
cxn.has_old_label = true;
|
||||
cxn.old_label = cxn.label;
|
||||
}
|
||||
cxn.label = " ";
|
||||
}
|
||||
else if (!hide && cxn.has_old_label) {
|
||||
cxn.has_old_label = false;
|
||||
cxn.label = cxn.old_label;
|
||||
cxn.old_label = undefined;
|
||||
}
|
||||
return cxn;
|
||||
}
|
||||
export function addHelp(nodeCtor, comfyApp) {
|
||||
addMenuItem(nodeCtor, comfyApp || app, {
|
||||
name: "🛟 Node Help",
|
||||
property: "help",
|
||||
callback: (node) => {
|
||||
alert(node.help || nodeCtor.help);
|
||||
},
|
||||
});
|
||||
}
|
||||
export var PassThroughFollowing;
|
||||
(function (PassThroughFollowing) {
|
||||
PassThroughFollowing[PassThroughFollowing["ALL"] = 0] = "ALL";
|
||||
PassThroughFollowing[PassThroughFollowing["NONE"] = 1] = "NONE";
|
||||
PassThroughFollowing[PassThroughFollowing["REROUTE_ONLY"] = 2] = "REROUTE_ONLY";
|
||||
})(PassThroughFollowing || (PassThroughFollowing = {}));
|
||||
export function shouldPassThrough(node, passThroughFollowing = PassThroughFollowing.ALL) {
|
||||
var _a;
|
||||
const type = (_a = node === null || node === void 0 ? void 0 : node.constructor) === null || _a === void 0 ? void 0 : _a.type;
|
||||
if (!type || passThroughFollowing === PassThroughFollowing.NONE) {
|
||||
return false;
|
||||
}
|
||||
if (passThroughFollowing === PassThroughFollowing.REROUTE_ONLY) {
|
||||
return type.includes("Reroute");
|
||||
}
|
||||
return (type.includes("Reroute") || type.includes("Node Combiner") || type.includes("Node Collector"));
|
||||
}
|
||||
export function filterOutPassthroughNodes(nodes, passThroughFollowing = PassThroughFollowing.ALL) {
|
||||
return nodes.filter((n) => !shouldPassThrough(n, passThroughFollowing));
|
||||
}
|
||||
export function getConnectedInputNodes(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) {
|
||||
return getConnectedNodesInfo(startNode, IoDirection.INPUT, currentNode, slot, passThroughFollowing).map(n => n.node);
|
||||
}
|
||||
export function getConnectedInputNodesAndFilterPassThroughs(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) {
|
||||
return filterOutPassthroughNodes(getConnectedInputNodes(startNode, currentNode, slot, passThroughFollowing), passThroughFollowing);
|
||||
}
|
||||
export function getConnectedOutputNodes(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) {
|
||||
return getConnectedNodesInfo(startNode, IoDirection.OUTPUT, currentNode, slot, passThroughFollowing).map(n => n.node);
|
||||
}
|
||||
export function getConnectedOutputNodesAndFilterPassThroughs(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) {
|
||||
return filterOutPassthroughNodes(getConnectedOutputNodes(startNode, currentNode, slot, passThroughFollowing), passThroughFollowing);
|
||||
}
|
||||
export function getConnectedNodesInfo(startNode, dir = IoDirection.INPUT, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL, originTravelFromSlot) {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
currentNode = currentNode || startNode;
|
||||
let rootNodes = [];
|
||||
const slotsToRemove = [];
|
||||
if (startNode === currentNode || shouldPassThrough(currentNode, passThroughFollowing)) {
|
||||
let linkIds;
|
||||
if (dir == IoDirection.OUTPUT) {
|
||||
if (slot != null && slot > -1) {
|
||||
linkIds = (_b = (_a = currentNode.outputs[slot]) === null || _a === void 0 ? void 0 : _a.links) !== null && _b !== void 0 ? _b : [];
|
||||
}
|
||||
else {
|
||||
linkIds = ((_c = currentNode.outputs) === null || _c === void 0 ? void 0 : _c.flatMap((i) => i.links)) || [];
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (slot != null && slot > -1) {
|
||||
linkIds = [(_e = (_d = currentNode.inputs[slot]) === null || _d === void 0 ? void 0 : _d.link) !== null && _e !== void 0 ? _e : -1].filter(i => i > -1);
|
||||
}
|
||||
else {
|
||||
linkIds = ((_f = currentNode.inputs) === null || _f === void 0 ? void 0 : _f.map((i) => i.link)) || [];
|
||||
}
|
||||
}
|
||||
let graph = app.graph;
|
||||
for (const linkId of linkIds) {
|
||||
let link = null;
|
||||
if (typeof linkId == "number") {
|
||||
link = graph.links[linkId];
|
||||
}
|
||||
if (!link) {
|
||||
continue;
|
||||
}
|
||||
const travelFromSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot;
|
||||
const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id;
|
||||
const travelToSlot = dir == IoDirection.OUTPUT ? link.target_slot : link.origin_slot;
|
||||
originTravelFromSlot = originTravelFromSlot != null ? originTravelFromSlot : travelFromSlot;
|
||||
const originNode = graph.getNodeById(connectedId);
|
||||
if (!link) {
|
||||
console.error("No connected node found... weird");
|
||||
continue;
|
||||
}
|
||||
if (rootNodes.some((n) => n.node == originNode)) {
|
||||
console.log(`${startNode.title} (${startNode.id}) seems to have two links to ${originNode.title} (${originNode.id}). One may be stale: ${linkIds.join(", ")}`);
|
||||
}
|
||||
else {
|
||||
rootNodes.push({ node: originNode, travelFromSlot, travelToSlot, originTravelFromSlot });
|
||||
if (shouldPassThrough(originNode, passThroughFollowing)) {
|
||||
for (const foundNode of getConnectedNodesInfo(startNode, dir, originNode, undefined, undefined, originTravelFromSlot)) {
|
||||
if (!rootNodes.map(n => n.node).includes(foundNode.node)) {
|
||||
rootNodes.push(foundNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rootNodes;
|
||||
}
|
||||
export function followConnectionUntilType(node, dir, slotNum, skipSelf = false) {
|
||||
const slots = dir === IoDirection.OUTPUT ? node.outputs : node.inputs;
|
||||
if (!slots || !slots.length) {
|
||||
return null;
|
||||
}
|
||||
let type = null;
|
||||
if (slotNum) {
|
||||
if (!slots[slotNum]) {
|
||||
return null;
|
||||
}
|
||||
type = getTypeFromSlot(slots[slotNum], dir, skipSelf);
|
||||
}
|
||||
else {
|
||||
for (const slot of slots) {
|
||||
type = getTypeFromSlot(slot, dir, skipSelf);
|
||||
if (type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
function getTypeFromSlot(slot, dir, skipSelf = false) {
|
||||
let graph = app.graph;
|
||||
let type = slot === null || slot === void 0 ? void 0 : slot.type;
|
||||
if (!skipSelf && type != null && type != "*") {
|
||||
return { type: type, label: slot === null || slot === void 0 ? void 0 : slot.label, name: slot === null || slot === void 0 ? void 0 : slot.name };
|
||||
}
|
||||
const links = getSlotLinks(slot);
|
||||
for (const link of links) {
|
||||
const connectedId = dir == IoDirection.OUTPUT ? link.link.target_id : link.link.origin_id;
|
||||
const connectedSlotNum = dir == IoDirection.OUTPUT ? link.link.target_slot : link.link.origin_slot;
|
||||
const connectedNode = graph.getNodeById(connectedId);
|
||||
const connectedSlots = dir === IoDirection.OUTPUT ? connectedNode.inputs : connectedNode.outputs;
|
||||
let connectedSlot = connectedSlots[connectedSlotNum];
|
||||
if ((connectedSlot === null || connectedSlot === void 0 ? void 0 : connectedSlot.type) != null && (connectedSlot === null || connectedSlot === void 0 ? void 0 : connectedSlot.type) != "*") {
|
||||
return {
|
||||
type: connectedSlot.type,
|
||||
label: connectedSlot === null || connectedSlot === void 0 ? void 0 : connectedSlot.label,
|
||||
name: connectedSlot === null || connectedSlot === void 0 ? void 0 : connectedSlot.name
|
||||
};
|
||||
}
|
||||
else if ((connectedSlot === null || connectedSlot === void 0 ? void 0 : connectedSlot.type) == "*") {
|
||||
return followConnectionUntilType(connectedNode, dir);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export async function replaceNode(existingNode, typeOrNewNode, inputNameMap) {
|
||||
const existingCtor = existingNode.constructor;
|
||||
const newNode = typeof typeOrNewNode === "string" ? LiteGraph.createNode(typeOrNewNode) : typeOrNewNode;
|
||||
if (existingNode.title != existingCtor.title) {
|
||||
newNode.title = existingNode.title;
|
||||
}
|
||||
newNode.pos = [...existingNode.pos];
|
||||
newNode.properties = { ...existingNode.properties };
|
||||
const oldComputeSize = [...existingNode.computeSize()];
|
||||
const oldSize = [
|
||||
existingNode.size[0] === oldComputeSize[0] ? null : existingNode.size[0],
|
||||
existingNode.size[1] === oldComputeSize[1] ? null : existingNode.size[1]
|
||||
];
|
||||
let setSizeIters = 0;
|
||||
const setSizeFn = () => {
|
||||
const newComputesize = newNode.computeSize();
|
||||
newNode.size[0] = Math.max(oldSize[0] || 0, newComputesize[0]);
|
||||
newNode.size[1] = Math.max(oldSize[1] || 0, newComputesize[1]);
|
||||
setSizeIters++;
|
||||
if (setSizeIters > 10) {
|
||||
requestAnimationFrame(setSizeFn);
|
||||
}
|
||||
};
|
||||
setSizeFn();
|
||||
const links = [];
|
||||
for (const [index, output] of existingNode.outputs.entries()) {
|
||||
for (const linkId of output.links || []) {
|
||||
const link = app.graph.links[linkId];
|
||||
if (!link)
|
||||
continue;
|
||||
const targetNode = app.graph.getNodeById(link.target_id);
|
||||
links.push({ node: newNode, slot: output.name, targetNode, targetSlot: link.target_slot });
|
||||
}
|
||||
}
|
||||
for (const [index, input] of existingNode.inputs.entries()) {
|
||||
const linkId = input.link;
|
||||
if (linkId) {
|
||||
const link = app.graph.links[linkId];
|
||||
const originNode = app.graph.getNodeById(link.origin_id);
|
||||
links.push({
|
||||
node: originNode,
|
||||
slot: link.origin_slot,
|
||||
targetNode: newNode,
|
||||
targetSlot: (inputNameMap === null || inputNameMap === void 0 ? void 0 : inputNameMap.has(input.name))
|
||||
? inputNameMap.get(input.name)
|
||||
: input.name || index,
|
||||
});
|
||||
}
|
||||
}
|
||||
app.graph.add(newNode);
|
||||
await wait();
|
||||
for (const link of links) {
|
||||
link.node.connect(link.slot, link.targetNode, link.targetSlot);
|
||||
}
|
||||
await wait();
|
||||
app.graph.remove(existingNode);
|
||||
newNode.size = newNode.computeSize();
|
||||
newNode.setDirtyCanvas(true, true);
|
||||
return newNode;
|
||||
}
|
||||
export function getOriginNodeByLink(linkId) {
|
||||
let node = null;
|
||||
if (linkId != null) {
|
||||
const link = app.graph.links[linkId];
|
||||
node = link != null && app.graph.getNodeById(link.origin_id);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
export function applyMixins(original, constructors) {
|
||||
constructors.forEach((baseCtor) => {
|
||||
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
|
||||
Object.defineProperty(original.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null));
|
||||
});
|
||||
});
|
||||
}
|
||||
export function getSlotLinks(inputOrOutput) {
|
||||
var _a;
|
||||
const links = [];
|
||||
if (!inputOrOutput) {
|
||||
return links;
|
||||
}
|
||||
if ((_a = inputOrOutput.links) === null || _a === void 0 ? void 0 : _a.length) {
|
||||
const output = inputOrOutput;
|
||||
for (const linkId of output.links || []) {
|
||||
const link = app.graph.links[linkId];
|
||||
if (link) {
|
||||
links.push({ id: linkId, link: link });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputOrOutput.link) {
|
||||
const input = inputOrOutput;
|
||||
const link = app.graph.links[input.link];
|
||||
if (link) {
|
||||
links.push({ id: input.link, link: link });
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
export async function matchLocalSlotsToServer(node, direction, serverNodeData) {
|
||||
var _a, _b, _c;
|
||||
const serverSlotNames = direction == IoDirection.INPUT
|
||||
? Object.keys(((_a = serverNodeData.input) === null || _a === void 0 ? void 0 : _a.optional) || {})
|
||||
: serverNodeData.output_name;
|
||||
const serverSlotTypes = direction == IoDirection.INPUT
|
||||
? Object.values(((_b = serverNodeData.input) === null || _b === void 0 ? void 0 : _b.optional) || {}).map((i) => i[0])
|
||||
: serverNodeData.output;
|
||||
const slots = direction == IoDirection.INPUT ? node.inputs : node.outputs;
|
||||
let firstIndex = slots.findIndex((o, i) => i !== serverSlotNames.indexOf(o.name));
|
||||
if (firstIndex > -1) {
|
||||
const links = {};
|
||||
slots.map((slot) => {
|
||||
var _a;
|
||||
links[slot.name] = links[slot.name] || [];
|
||||
(_a = links[slot.name]) === null || _a === void 0 ? void 0 : _a.push(...getSlotLinks(slot));
|
||||
});
|
||||
for (const [index, serverSlotName] of serverSlotNames.entries()) {
|
||||
const currentNodeSlot = slots.map((s) => s.name).indexOf(serverSlotName);
|
||||
if (currentNodeSlot > -1) {
|
||||
if (currentNodeSlot != index) {
|
||||
const splicedItem = slots.splice(currentNodeSlot, 1)[0];
|
||||
slots.splice(index, 0, splicedItem);
|
||||
}
|
||||
}
|
||||
else if (currentNodeSlot === -1) {
|
||||
const splicedItem = {
|
||||
name: serverSlotName,
|
||||
type: serverSlotTypes[index],
|
||||
links: [],
|
||||
};
|
||||
slots.splice(index, 0, splicedItem);
|
||||
}
|
||||
}
|
||||
if (slots.length > serverSlotNames.length) {
|
||||
for (let i = slots.length - 1; i > serverSlotNames.length - 1; i--) {
|
||||
if (direction == IoDirection.INPUT) {
|
||||
node.disconnectInput(i);
|
||||
node.removeInput(i);
|
||||
}
|
||||
else {
|
||||
node.disconnectOutput(i);
|
||||
node.removeOutput(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, slotLinks] of Object.entries(links)) {
|
||||
let currentNodeSlot = slots.map((s) => s.name).indexOf(name);
|
||||
if (currentNodeSlot > -1) {
|
||||
for (const linkData of slotLinks) {
|
||||
if (direction == IoDirection.INPUT) {
|
||||
linkData.link.target_slot = currentNodeSlot;
|
||||
}
|
||||
else {
|
||||
linkData.link.origin_slot = currentNodeSlot;
|
||||
const nextNode = app.graph.getNodeById(linkData.link.target_id);
|
||||
if (nextNode && ((_c = nextNode.constructor) === null || _c === void 0 ? void 0 : _c.type.includes("Reroute"))) {
|
||||
nextNode.stabilize && nextNode.stabilize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export function isValidConnection(ioA, ioB) {
|
||||
if (!ioA || !ioB) {
|
||||
return false;
|
||||
}
|
||||
const typeA = String(ioA.type);
|
||||
const typeB = String(ioB.type);
|
||||
let isValid = LiteGraph.isValidConnection(typeA, typeB);
|
||||
if (!isValid) {
|
||||
let areCombos = (typeA.includes(',') && typeB === 'COMBO') || (typeA === 'COMBO' && typeB.includes(','));
|
||||
if (areCombos) {
|
||||
const nameA = ioA.name.toUpperCase().replace('_NAME', '').replace('CKPT', 'MODEL');
|
||||
const nameB = ioB.name.toUpperCase().replace('_NAME', '').replace('CKPT', 'MODEL');
|
||||
isValid = nameA.includes(nameB) || nameB.includes(nameA);
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
const oldIsValidConnection = LiteGraph.isValidConnection;
|
||||
LiteGraph.isValidConnection = function (typeA, typeB) {
|
||||
let isValid = oldIsValidConnection.call(LiteGraph, typeA, typeB);
|
||||
if (!isValid) {
|
||||
typeA = String(typeA);
|
||||
typeB = String(typeB);
|
||||
let areCombos = (typeA.includes(',') && typeB === 'COMBO')
|
||||
|| (typeA === 'COMBO' && typeB.includes(','));
|
||||
isValid = areCombos;
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
Reference in New Issue
Block a user