506 lines
18 KiB
Python
506 lines
18 KiB
Python
import json
|
|
|
|
try:
|
|
from comfy_execution.graph import ExecutionBlocker
|
|
except ImportError:
|
|
ExecutionBlocker = None
|
|
|
|
|
|
class AnyType(str):
|
|
def __ne__(self, other):
|
|
return False
|
|
|
|
def __eq__(self, other):
|
|
return True
|
|
|
|
def __hash__(self):
|
|
return hash(str(self))
|
|
|
|
|
|
any_type = AnyType("*")
|
|
|
|
_NOT_PROVIDED = object()
|
|
|
|
SLOTS = ("a", "b", "c", "d", "e")
|
|
|
|
|
|
class LenientSwitch:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
pass_if_tooltip = (
|
|
"Per-slot truthiness test (NOT a CONDITIONING input). "
|
|
"When connected, this value decides whether the matching source_X is "
|
|
"forwarded; if unconnected, source_X is tested against itself."
|
|
)
|
|
bypass_tooltip = (
|
|
"When ON, slots whose pass_if_X is connected AND falsy are skipped "
|
|
"WITHOUT evaluating their source_X, so that source's upstream chain "
|
|
"does not run (lazy evaluation). Slots whose pass_if_X is unconnected "
|
|
"are always evaluated (their winner can only be known at run time). "
|
|
"Note: nodes are not visually bypassed like rgthree; they simply do "
|
|
"not execute."
|
|
)
|
|
return {
|
|
"required": {
|
|
"source_a": (
|
|
any_type,
|
|
{
|
|
"tooltip": "Pass-through source for slot A (any type).",
|
|
"lazy": True,
|
|
},
|
|
),
|
|
"treat_zero_as_false": ("BOOLEAN", {"default": True}),
|
|
"treat_empty_string_as_false": ("BOOLEAN", {"default": True}),
|
|
"evaluate_boolean": ("BOOLEAN", {"default": True}),
|
|
"treat_empty_list_as_false": ("BOOLEAN", {"default": True}),
|
|
"block_on_all_false": ("BOOLEAN", {"default": False}),
|
|
"bypass_unselected": (
|
|
"BOOLEAN",
|
|
{"default": False, "tooltip": bypass_tooltip},
|
|
),
|
|
},
|
|
"optional": {
|
|
"source_b": (
|
|
any_type,
|
|
{
|
|
"tooltip": "Optional pass-through source for slot B (any type).",
|
|
"lazy": True,
|
|
},
|
|
),
|
|
"source_c": (
|
|
any_type,
|
|
{
|
|
"tooltip": "Optional pass-through source for slot C (any type).",
|
|
"lazy": True,
|
|
},
|
|
),
|
|
"source_d": (
|
|
any_type,
|
|
{
|
|
"tooltip": "Optional pass-through source for slot D (any type).",
|
|
"lazy": True,
|
|
},
|
|
),
|
|
"source_e": (
|
|
any_type,
|
|
{
|
|
"tooltip": "Optional pass-through source for slot E (any type).",
|
|
"lazy": True,
|
|
},
|
|
),
|
|
"pass_if_a": (any_type, {"tooltip": pass_if_tooltip}),
|
|
"pass_if_b": (any_type, {"tooltip": pass_if_tooltip}),
|
|
"pass_if_c": (any_type, {"tooltip": pass_if_tooltip}),
|
|
"pass_if_d": (any_type, {"tooltip": pass_if_tooltip}),
|
|
"pass_if_e": (any_type, {"tooltip": pass_if_tooltip}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (any_type, "BOOLEAN")
|
|
RETURN_NAMES = ("output", "matched")
|
|
FUNCTION = "run"
|
|
CATEGORY = "Lenient Switch"
|
|
|
|
@staticmethod
|
|
def _is_truthy(
|
|
value, treat_zero, treat_empty_string, evaluate_boolean, treat_empty_list
|
|
):
|
|
if value is None:
|
|
return False
|
|
|
|
if isinstance(value, bool):
|
|
if evaluate_boolean:
|
|
return bool(value)
|
|
return True
|
|
|
|
if isinstance(value, (int, float)):
|
|
if treat_zero and value == 0:
|
|
return False
|
|
return True
|
|
|
|
if isinstance(value, str):
|
|
if treat_empty_string and len(value) == 0:
|
|
return False
|
|
return True
|
|
|
|
if isinstance(value, (list, tuple)):
|
|
if treat_empty_list and len(value) == 0:
|
|
return False
|
|
return True
|
|
|
|
return True
|
|
|
|
def check_lazy_status(
|
|
self,
|
|
treat_zero_as_false,
|
|
treat_empty_string_as_false,
|
|
evaluate_boolean,
|
|
treat_empty_list_as_false,
|
|
block_on_all_false,
|
|
bypass_unselected,
|
|
**kwargs,
|
|
):
|
|
# All source_* are declared lazy. Returning a name asks ComfyUI to
|
|
# evaluate that source's upstream; never returning it leaves the upstream
|
|
# un-run (the "bypass" effect). Only connected sources appear in kwargs,
|
|
# so we never request an unconnected slot.
|
|
connected = [slot for slot in SLOTS if f"source_{slot}" in kwargs]
|
|
if not bypass_unselected:
|
|
return [f"source_{slot}" for slot in connected]
|
|
|
|
needed = []
|
|
for slot in connected:
|
|
pass_if = kwargs.get(f"pass_if_{slot}", _NOT_PROVIDED)
|
|
# Bypass only slots whose pass_if is connected AND falsy (guaranteed
|
|
# losers). An unconnected pass_if means the condition is the source
|
|
# itself, knowable only at run time, so that source must be evaluated.
|
|
if pass_if is _NOT_PROVIDED or self._is_truthy(
|
|
pass_if,
|
|
treat_zero_as_false,
|
|
treat_empty_string_as_false,
|
|
evaluate_boolean,
|
|
treat_empty_list_as_false,
|
|
):
|
|
needed.append(f"source_{slot}")
|
|
return needed
|
|
|
|
def run(
|
|
self,
|
|
source_a,
|
|
treat_zero_as_false,
|
|
treat_empty_string_as_false,
|
|
evaluate_boolean,
|
|
treat_empty_list_as_false,
|
|
block_on_all_false,
|
|
bypass_unselected,
|
|
**kwargs,
|
|
):
|
|
sources = {
|
|
"a": source_a,
|
|
"b": kwargs.get("source_b", _NOT_PROVIDED),
|
|
"c": kwargs.get("source_c", _NOT_PROVIDED),
|
|
"d": kwargs.get("source_d", _NOT_PROVIDED),
|
|
"e": kwargs.get("source_e", _NOT_PROVIDED),
|
|
}
|
|
conditions = {
|
|
slot: kwargs.get(f"pass_if_{slot}", _NOT_PROVIDED) for slot in SLOTS
|
|
}
|
|
|
|
for slot in SLOTS:
|
|
src = sources[slot]
|
|
if src is _NOT_PROVIDED:
|
|
continue
|
|
|
|
cond = conditions[slot]
|
|
test_value = src if cond is _NOT_PROVIDED else cond
|
|
|
|
if self._is_truthy(
|
|
test_value,
|
|
treat_zero_as_false,
|
|
treat_empty_string_as_false,
|
|
evaluate_boolean,
|
|
treat_empty_list_as_false,
|
|
):
|
|
return (src, True)
|
|
|
|
if block_on_all_false and ExecutionBlocker is not None:
|
|
return (ExecutionBlocker(None), False)
|
|
return (None, False)
|
|
|
|
|
|
_SELECT_CHOICES = ["none", "A", "B", "C", "D", "E"]
|
|
|
|
|
|
class SimpleSelectorSwitch:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
label_tooltip = (
|
|
"Free-form per-slot memo (e.g. 'SDXL base'). Not used by the node."
|
|
)
|
|
|
|
def label_field(letter):
|
|
return (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"multiline": False,
|
|
"placeholder": f"{letter} label (memo)",
|
|
"tooltip": label_tooltip,
|
|
},
|
|
)
|
|
|
|
return {
|
|
"required": {
|
|
"select": (
|
|
_SELECT_CHOICES,
|
|
{
|
|
"default": "none",
|
|
"tooltip": (
|
|
"Pick which source to forward. 'none' forwards nothing "
|
|
"(see block_on_none_selected)."
|
|
),
|
|
},
|
|
),
|
|
"label_a": label_field("A"),
|
|
"label_b": label_field("B"),
|
|
"label_c": label_field("C"),
|
|
"label_d": label_field("D"),
|
|
"label_e": label_field("E"),
|
|
"block_on_none_selected": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": (
|
|
"When 'none' is selected, emit ExecutionBlocker so downstream "
|
|
"nodes are skipped instead of receiving None."
|
|
),
|
|
},
|
|
),
|
|
"bypass_unselected": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": (
|
|
"When ON, only the selected slot's source_X is evaluated; the "
|
|
"upstream chains feeding the other (and, with 'none', all) "
|
|
"sources do not run (lazy evaluation). Note: nodes are not "
|
|
"visually bypassed like rgthree; they simply do not execute."
|
|
),
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"source_a": (
|
|
any_type,
|
|
{"tooltip": "Source for slot A (any type).", "lazy": True},
|
|
),
|
|
"source_b": (
|
|
any_type,
|
|
{"tooltip": "Source for slot B (any type).", "lazy": True},
|
|
),
|
|
"source_c": (
|
|
any_type,
|
|
{"tooltip": "Source for slot C (any type).", "lazy": True},
|
|
),
|
|
"source_d": (
|
|
any_type,
|
|
{"tooltip": "Source for slot D (any type).", "lazy": True},
|
|
),
|
|
"source_e": (
|
|
any_type,
|
|
{"tooltip": "Source for slot E (any type).", "lazy": True},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (any_type, "STRING")
|
|
RETURN_NAMES = ("output", "label")
|
|
FUNCTION = "run"
|
|
CATEGORY = "Lenient Switch"
|
|
|
|
def check_lazy_status(
|
|
self, select, block_on_none_selected, bypass_unselected, **kwargs
|
|
):
|
|
# source_* are lazy; only the names returned here get their upstream run.
|
|
# Only connected sources appear in kwargs, so unconnected slots are never
|
|
# requested.
|
|
connected = [slot for slot in SLOTS if f"source_{slot}" in kwargs]
|
|
if not bypass_unselected:
|
|
return [f"source_{slot}" for slot in connected]
|
|
if select == "none":
|
|
return []
|
|
slot = select.lower()
|
|
return [f"source_{slot}"] if slot in connected else []
|
|
|
|
def run(self, select, block_on_none_selected, bypass_unselected, **kwargs):
|
|
if select == "none":
|
|
if block_on_none_selected and ExecutionBlocker is not None:
|
|
blocker = ExecutionBlocker(None)
|
|
return (blocker, blocker)
|
|
return (None, "")
|
|
|
|
slot = select.lower()
|
|
src = kwargs.get(f"source_{slot}", _NOT_PROVIDED)
|
|
label = kwargs.get(f"label_{slot}", "")
|
|
if src is _NOT_PROVIDED:
|
|
return (None, label)
|
|
return (src, label)
|
|
|
|
|
|
_SELECTOR_DEFAULT = json.dumps({"select": "none", "labels": ["", "", "", "", ""]})
|
|
|
|
|
|
class SimpleSelectorSwitchAdvanced:
|
|
"""SimpleSelectorSwitch reworked around a single custom selector widget.
|
|
|
|
The frontend (`web/simple_selector_group_bypass.js`) registers a custom
|
|
widget type **LENIENT_SELECTOR** via `getCustomWidgets` and renders the
|
|
`selector` input as an exclusive-checkbox radio list (slots A-E plus
|
|
`none`) with a per-slot editable label. The widget's value is a JSON string
|
|
`{"select": "a".."e"|"none", "labels": [..5 strings..]}`; `run` and
|
|
`check_lazy_status` parse it. This replaces the old per-slot `select` COMBO
|
|
+ `label_a`-`label_e` STRING widgets with ONE first-class custom widget, so
|
|
the Vue (Nodes 2.0) renderer never disposes/rebuilds it away and there are no
|
|
hidden widgets to hide. `RETURN_TYPES`/`RETURN_NAMES`/`FUNCTION`/`CATEGORY`
|
|
are inherited from SimpleSelectorSwitch (same `(output, label)` outputs).
|
|
|
|
`bypass_unselected_groups` is a JS-only toggle (rgthree FastGroupsBypasser-
|
|
style group bypass keyed on the per-slot labels); Python ignores it. The
|
|
plain SimpleSelectorSwitch stays pure Python with no frontend. See CLAUDE.md.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"selector": (
|
|
"LENIENT_SELECTOR",
|
|
{
|
|
"default": _SELECTOR_DEFAULT,
|
|
"tooltip": (
|
|
"Exclusive-checkbox selector (slots A-E + none) with a "
|
|
"per-slot editable label. Click a row to select; "
|
|
"double-click a slot row to edit its label (the label "
|
|
"also names the canvas group for bypass_unselected_groups). "
|
|
'Value is JSON: {"select": ..., "labels": [..]}.'
|
|
),
|
|
},
|
|
),
|
|
"block_on_none_selected": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": (
|
|
"When 'none' is selected, emit ExecutionBlocker so downstream "
|
|
"nodes are skipped instead of receiving None."
|
|
),
|
|
},
|
|
),
|
|
"bypass_unselected": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": (
|
|
"When ON, only the selected slot's source_X is evaluated; the "
|
|
"upstream chains feeding the other (and, with 'none', all) "
|
|
"sources do not run (lazy evaluation). Note: nodes are not "
|
|
"visually bypassed like rgthree; they simply do not execute."
|
|
),
|
|
},
|
|
),
|
|
"bypass_unselected_groups": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": (
|
|
"JS-only, rgthree FastGroupsBypasser-style group bypass. When "
|
|
"ON, the canvas group whose name matches each slot's label is "
|
|
"set to BYPASS (mode 4) for every UNSELECTED slot and to ALWAYS "
|
|
"for the selected slot. Handled entirely by the frontend; the "
|
|
"Python run/check_lazy_status ignore this toggle. Orthogonal to "
|
|
"bypass_unselected (lazy eval). When OFF, referenced groups are "
|
|
"restored to ALWAYS."
|
|
),
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"source_a": (
|
|
any_type,
|
|
{"tooltip": "Source for slot A (any type).", "lazy": True},
|
|
),
|
|
"source_b": (
|
|
any_type,
|
|
{"tooltip": "Source for slot B (any type).", "lazy": True},
|
|
),
|
|
"source_c": (
|
|
any_type,
|
|
{"tooltip": "Source for slot C (any type).", "lazy": True},
|
|
),
|
|
"source_d": (
|
|
any_type,
|
|
{"tooltip": "Source for slot D (any type).", "lazy": True},
|
|
),
|
|
"source_e": (
|
|
any_type,
|
|
{"tooltip": "Source for slot E (any type).", "lazy": True},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (any_type, "STRING")
|
|
RETURN_NAMES = ("output", "label")
|
|
FUNCTION = "run"
|
|
CATEGORY = "Lenient Switch"
|
|
|
|
@staticmethod
|
|
def _parse_selector(selector):
|
|
"""Parse the LENIENT_SELECTOR JSON value into (select, labels).
|
|
|
|
`select` is lowercased to 'a'..'e'/'none'; `labels` is always a list of
|
|
exactly 5 strings. Tolerates malformed/empty input (falls back to none).
|
|
"""
|
|
try:
|
|
data = json.loads(selector) if isinstance(selector, str) else selector
|
|
except (ValueError, TypeError):
|
|
data = None
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
select = str(data.get("select", "none")).lower()
|
|
labels = data.get("labels")
|
|
if not isinstance(labels, list):
|
|
labels = []
|
|
labels = [str(x) for x in labels[:5]]
|
|
labels += [""] * (5 - len(labels))
|
|
return select, labels
|
|
|
|
def check_lazy_status(
|
|
self,
|
|
selector,
|
|
block_on_none_selected,
|
|
bypass_unselected,
|
|
bypass_unselected_groups,
|
|
**kwargs,
|
|
):
|
|
select, _ = self._parse_selector(selector)
|
|
connected = [slot for slot in SLOTS if f"source_{slot}" in kwargs]
|
|
if not bypass_unselected:
|
|
return [f"source_{slot}" for slot in connected]
|
|
if select == "none":
|
|
return []
|
|
return [f"source_{select}"] if select in connected else []
|
|
|
|
def run(
|
|
self,
|
|
selector,
|
|
block_on_none_selected,
|
|
bypass_unselected,
|
|
bypass_unselected_groups,
|
|
**kwargs,
|
|
):
|
|
select, labels = self._parse_selector(selector)
|
|
if select == "none":
|
|
if block_on_none_selected and ExecutionBlocker is not None:
|
|
blocker = ExecutionBlocker(None)
|
|
return (blocker, blocker)
|
|
return (None, "")
|
|
|
|
src = kwargs.get(f"source_{select}", _NOT_PROVIDED)
|
|
label = labels[SLOTS.index(select)] if select in SLOTS else ""
|
|
if src is _NOT_PROVIDED:
|
|
return (None, label)
|
|
return (src, label)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"LenientSwitch": LenientSwitch,
|
|
"SimpleSelectorSwitch": SimpleSelectorSwitch,
|
|
"SimpleSelectorSwitchAdvanced": SimpleSelectorSwitchAdvanced,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"LenientSwitch": "Lenient Switch",
|
|
"SimpleSelectorSwitch": "Simple Selector (Switch)",
|
|
"SimpleSelectorSwitchAdvanced": "Simple Selector (Switch) Advanced",
|
|
}
|