Files
ComfyUI-Dataset-Gates/docs/plans/2026-06-21-multi-reroute-design.md
T
Ethanfel ce371ffe13 Add Multi-Reroute (Rail) design + implementation plan
Multi-lane any-type pass-through node ("rail"): in_i -> out_i, empty lane
-> ExecutionBlocker; +/- to add/remove lanes (bottom in P1, top with
wiring-preserving remap in P2). Pure build_outputs + shared AnyType; lazy
comfy import keeps it unit-testable. No IS_CHANGED (transparent passthrough).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:27:05 +02:00

3.7 KiB
Raw Blame History

Multi-Reroute (Rail) — Design

Date: 2026-06-21 Status: Approved (brainstorming complete, ready for implementation plan)

1. Purpose

A single node holding N parallel pass-through lanes (a "rail"), so you can run tidy bundles of wires across the graph instead of dropping many separate Reroute nodes. Each lane forwards any type; you grow/shrink the rail with +/ at either end.

Seventh node in the ComfyUI-Datasete-Gates suite.

2. Approach

A real pass-through node with any-type lanes (AnyType("*")). Lane i's input is forwarded to lane i's output. An unconnected lane outputs an ExecutionBlocker so nothing downstream of an unused lane runs. (Not the frontend-only virtual-reroute trick — simpler and robust across all types; the trade-off is slots read as * instead of adapting to the wired type.)

3. IO

  • Up to MAX_LANES (32) lanes, each: optional input in_<i> (*) → output out_<i> (*).
  • The node always returns a length-MAX_LANES tuple; the frontend shows only the active lanes (default 4). Wired output indices are stable, so unshown trailing outputs are simply unconnected.
RETURN_TYPES = (ANY,) * MAX_LANES        RETURN_NAMES = ("out_1", …, "out_32")
INPUT_TYPES  = {"optional": {"in_1": (ANY,), …}}

No IS_CHANGED override — a reroute should be transparent/cacheable (re-runs only when an input value actually changes).

4. Run logic

def run(self, **kwargs):
    blocker = ExecutionBlocker(None)
    return tuple(
        kwargs.get(f"in_{i+1}") if kwargs.get(f"in_{i+1}") is not None else blocker
        for i in range(MAX_LANES)
    )

Lane-count-agnostic: connected lanes forward their value; empty lanes block. The visible lane count is purely a frontend concern.

5. Frontend (web/multi_reroute.js)

  • Render lanes lane rows (input + output pair), default 4; persist the count in a hidden widget so reload restores the rail (the "use raw widgets_values to add slots before link rewiring" pattern already used in this repo).
  • +/ buttons:
    • Bottom add/remove (Phase 1): reveal/hide the next/last lane pair — trivial and wiring-safe (only the end moves).
    • Top add/remove (Phase 2): insert/remove a lane at the top while preserving the other lanes' wiring — requires capturing links and re-mapping slot indices (rgthree-style). Kept separate so a bug here can't scramble existing rails.
  • Lanes use the shared AnyType so any wire connects.
  • (Phase 3 polish) compact reroute-pill look / optional per-lane labels.

6. Edge cases

  • Empty lane → ExecutionBlocker (downstream skipped). A legitimate None value is treated as empty (reroute values are objects/tensors, effectively never None).
  • Removing a lane is from the end in Phase 1 (indices stay stable → links intact). Mid/top removal is Phase 2 with remap.
  • More than MAX_LANES requested → capped (logged in UI).
  • Mixed types across lanes is fine — each lane is independent *.

7. Code shape

  • gates/anytype.py (new) — shared AnyType("*") + ANY (textgate can dedupe onto this later; not touched now).
  • gates/reroute_node.py (new) — pure build_outputs(values, max_lanes, blocker) + MultiReroute node (lazy ExecutionBlocker import for testability).
  • web/multi_reroute.js (new) — dynamic lane slots + +/ buttons + persistence.
  • root __init__.py — additive merge of the node mapping.

8. Testing

  • pytest: anytype equals-everything; build_outputs forwards connected lanes and blocks empty ones (length == MAX_LANES); node RETURN_TYPES length + all-*.
  • Manual (live): add/remove lanes (bottom, then top), wire mixed types through, confirm values pass and reload restores the rail; empty lanes don't trigger downstream.