From 562da7cd906d3da0eec200d09d44f5c6ae5372b2 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 25 Feb 2026 21:24:52 +1300 Subject: [PATCH] feat(43-02): integrate Router into connection menus, execution, and data flow ConnectionDropMenu.tsx: - Add Router entry to all 8 connection drop menu arrays - Router appears as last item (utility node) in each menu - Uses git-branch SVG icon for routing/branching concept workflowStore.ts: - Import executeRouter from execution module - Add "router" case to executeWorkflow switch (after easeCurve) - Add "router" case to regenerateNode switch (after easeCurve) execution/index.ts: - Export executeRouter from simpleNodeExecutors connectedInputs.ts: - Add optional visited parameter to getConnectedInputsPure for circular protection - Add Router passthrough logic before getSourceOutput call - Router nodes recursively fetch upstream data and route by handle type - Prevents infinite loops with visited Set tracking - Handle all 6 data types: image, text, video, audio, 3d, easeCurve RouterNode.tsx (bugfix): - Add missing id prop to BaseNode (from 43-01) Co-Authored-By: Claude Opus 4.6 --- src/components/ConnectionDropMenu.tsx | 96 +++++++++++++++++++++++++++ src/components/nodes/RouterNode.tsx | 1 + src/store/execution/index.ts | 1 + src/store/utils/connectedInputs.ts | 57 ++++++++++++---- src/store/workflowStore.ts | 7 ++ 5 files changed, 148 insertions(+), 14 deletions(-) diff --git a/src/components/ConnectionDropMenu.tsx b/src/components/ConnectionDropMenu.tsx index 32a4a324..09578009 100644 --- a/src/components/ConnectionDropMenu.tsx +++ b/src/components/ConnectionDropMenu.tsx @@ -88,6 +88,18 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; const TEXT_TARGET_OPTIONS: MenuOption[] = [ @@ -154,6 +166,18 @@ const TEXT_TARGET_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; // Define which nodes can provide sources for handle types (when dragging to a target handle) @@ -203,6 +227,18 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; const TEXT_SOURCE_OPTIONS: MenuOption[] = [ @@ -242,6 +278,18 @@ const TEXT_SOURCE_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; // Video can only connect to videoStitch, generateVideo (video-to-video), or output nodes @@ -300,6 +348,18 @@ const VIDEO_TARGET_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; // GenerateVideo and VideoStitch nodes produce video output @@ -340,6 +400,18 @@ const VIDEO_SOURCE_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; // Audio target options (nodes that accept audio input) @@ -371,6 +443,18 @@ const AUDIO_TARGET_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; // Audio source options (nodes that produce audio output) @@ -393,6 +477,18 @@ const AUDIO_SOURCE_OPTIONS: MenuOption[] = [ ), }, + { + type: "router", + label: "Router", + icon: ( + + + + + + + ), + }, ]; // 3D target options (nodes that accept 3D input) diff --git a/src/components/nodes/RouterNode.tsx b/src/components/nodes/RouterNode.tsx index b55a8b09..68df4c02 100644 --- a/src/components/nodes/RouterNode.tsx +++ b/src/components/nodes/RouterNode.tsx @@ -56,6 +56,7 @@ export const RouterNode = memo(({ id, data, selected }: NodeProps) return ( ): ConnectedInputs { + const _visited = visited || new Set(); + if (_visited.has(nodeId)) return { images: [], videos: [], audio: [], model3d: null, text: null, dynamicInputs: {}, easeCurve: null }; + _visited.add(nodeId); const images: string[] = []; const videos: string[] = []; const audio: string[] = []; let model3d: string | null = null; let text: string | null = null; const dynamicInputs: Record = {}; + let easeCurve: ConnectedInputs["easeCurve"] = null; // Get the target node to check for inputSchema const targetNode = nodes.find((n) => n.id === nodeId); @@ -163,6 +168,29 @@ export function getConnectedInputsPure( const sourceNode = nodes.find((n) => n.id === edge.source); if (!sourceNode) return; + // Router passthrough — traverse upstream to find actual data source + if (sourceNode.type === "router") { + const routerInputs = getConnectedInputsPure(sourceNode.id, nodes, edges, _visited); + // Determine which type this edge carries based on the source handle + const edgeType = edge.sourceHandle; // Will be "image", "text", "video", "audio", "3d", or "easeCurve" + + if (edgeType === "image" || (!edgeType && isImageHandle(edge.sourceHandle))) { + images.push(...routerInputs.images); + } else if (edgeType === "text" || (!edgeType && isTextHandle(edge.sourceHandle))) { + if (routerInputs.text) text = routerInputs.text; + } else if (edgeType === "video") { + videos.push(...routerInputs.videos); + } else if (edgeType === "audio") { + audio.push(...routerInputs.audio); + } else if (edgeType === "3d") { + if (routerInputs.model3d) model3d = routerInputs.model3d; + } else if (edgeType === "easeCurve") { + // EaseCurve passthrough + if (routerInputs.easeCurve) easeCurve = routerInputs.easeCurve; + } + return; // Skip normal getSourceOutput processing for this edge + } + const handleId = edge.targetHandle; const { type, value } = getSourceOutput( sourceNode, @@ -201,19 +229,20 @@ export function getConnectedInputsPure( } }); - // Extract easeCurve data from parent EaseCurve node - let easeCurve: ConnectedInputs["easeCurve"] = null; - const easeCurveEdge = edges.find( - (e) => e.target === nodeId && e.targetHandle === "easeCurve" - ); - if (easeCurveEdge) { - const sourceNode = nodes.find((n) => n.id === easeCurveEdge.source); - if (sourceNode?.type === "easeCurve") { - const sourceData = sourceNode.data as EaseCurveNodeData; - easeCurve = { - bezierHandles: sourceData.bezierHandles, - easingPreset: sourceData.easingPreset, - }; + // Extract easeCurve data from parent EaseCurve node (if not already set by router passthrough) + if (!easeCurve) { + const easeCurveEdge = edges.find( + (e) => e.target === nodeId && e.targetHandle === "easeCurve" + ); + if (easeCurveEdge) { + const sourceNode = nodes.find((n) => n.id === easeCurveEdge.source); + if (sourceNode?.type === "easeCurve") { + const sourceData = sourceNode.data as EaseCurveNodeData; + easeCurve = { + bezierHandles: sourceData.bezierHandles, + easingPreset: sourceData.easingPreset, + }; + } } } diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 9d1c9590..45cafc2f 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -77,6 +77,7 @@ import { executeVideoTrim, executeVideoFrameGrab, executeGlbViewer, + executeRouter, } from "./execution"; import type { NodeExecutionContext } from "./execution"; export type { LevelGroup } from "./utils/executionUtils"; @@ -1008,6 +1009,9 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ case "videoFrameGrab": await executeVideoFrameGrab(executionCtx); break; + case "router": + await executeRouter(executionCtx); + break; } }; // End of executeSingleNode helper @@ -1326,6 +1330,9 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ case "videoFrameGrab": await executeVideoFrameGrab(executionCtx); break; + case "router": + await executeRouter(executionCtx); + break; } };