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>
3.7 KiB
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 inputin_<i>(*) → outputout_<i>(*). - The node always returns a length-
MAX_LANEStuple; 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
laneslane 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
AnyTypeso any wire connects. - (Phase 3 polish) compact reroute-pill look / optional per-lane labels.
6. Edge cases
- Empty lane →
ExecutionBlocker(downstream skipped). A legitimateNonevalue is treated as empty (reroute values are objects/tensors, effectively neverNone). - 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_LANESrequested → capped (logged in UI). - Mixed types across lanes is fine — each lane is independent
*.
7. Code shape
gates/anytype.py(new) — sharedAnyType("*")+ANY(textgate can dedupe onto this later; not touched now).gates/reroute_node.py(new) — purebuild_outputs(values, max_lanes, blocker)+MultiReroutenode (lazyExecutionBlockerimport for testability).web/multi_reroute.js(new) — dynamic lane slots + +/− buttons + persistence.- root
__init__.py— additive merge of the node mapping.
8. Testing
- pytest:
anytypeequals-everything;build_outputsforwards connected lanes and blocks empty ones (length == MAX_LANES); nodeRETURN_TYPESlength + 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.