Files

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",
}