Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
562da7cd90
  1. 96
      src/components/ConnectionDropMenu.tsx
  2. 1
      src/components/nodes/RouterNode.tsx
  3. 1
      src/store/execution/index.ts
  4. 57
      src/store/utils/connectedInputs.ts
  5. 7
      src/store/workflowStore.ts

96
src/components/ConnectionDropMenu.tsx

@ -88,6 +88,18 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
const TEXT_TARGET_OPTIONS: MenuOption[] = [ const TEXT_TARGET_OPTIONS: MenuOption[] = [
@ -154,6 +166,18 @@ const TEXT_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
// Define which nodes can provide sources for handle types (when dragging to a target handle) // Define which nodes can provide sources for handle types (when dragging to a target handle)
@ -203,6 +227,18 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
const TEXT_SOURCE_OPTIONS: MenuOption[] = [ const TEXT_SOURCE_OPTIONS: MenuOption[] = [
@ -242,6 +278,18 @@ const TEXT_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
// Video can only connect to videoStitch, generateVideo (video-to-video), or output nodes // Video can only connect to videoStitch, generateVideo (video-to-video), or output nodes
@ -300,6 +348,18 @@ const VIDEO_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
// GenerateVideo and VideoStitch nodes produce video output // GenerateVideo and VideoStitch nodes produce video output
@ -340,6 +400,18 @@ const VIDEO_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
// Audio target options (nodes that accept audio input) // Audio target options (nodes that accept audio input)
@ -371,6 +443,18 @@ const AUDIO_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
// Audio source options (nodes that produce audio output) // Audio source options (nodes that produce audio output)
@ -393,6 +477,18 @@ const AUDIO_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
]; ];
// 3D target options (nodes that accept 3D input) // 3D target options (nodes that accept 3D input)

1
src/components/nodes/RouterNode.tsx

@ -56,6 +56,7 @@ export const RouterNode = memo(({ id, data, selected }: NodeProps<WorkflowNode>)
return ( return (
<BaseNode <BaseNode
id={id}
title="Router" title="Router"
customTitle={nodeData.customTitle} customTitle={nodeData.customTitle}
comment={nodeData.comment} comment={nodeData.comment}

1
src/store/execution/index.ts

@ -17,6 +17,7 @@ export {
executeOutputGallery, executeOutputGallery,
executeImageCompare, executeImageCompare,
executeGlbViewer, executeGlbViewer,
executeRouter,
} from "./simpleNodeExecutors"; } from "./simpleNodeExecutors";
export { executeNanoBanana } from "./nanoBananaExecutor"; export { executeNanoBanana } from "./nanoBananaExecutor";

57
src/store/utils/connectedInputs.ts

@ -123,14 +123,19 @@ function getSourceOutput(
export function getConnectedInputsPure( export function getConnectedInputsPure(
nodeId: string, nodeId: string,
nodes: WorkflowNode[], nodes: WorkflowNode[],
edges: WorkflowEdge[] edges: WorkflowEdge[],
visited?: Set<string>
): ConnectedInputs { ): ConnectedInputs {
const _visited = visited || new Set<string>();
if (_visited.has(nodeId)) return { images: [], videos: [], audio: [], model3d: null, text: null, dynamicInputs: {}, easeCurve: null };
_visited.add(nodeId);
const images: string[] = []; const images: string[] = [];
const videos: string[] = []; const videos: string[] = [];
const audio: string[] = []; const audio: string[] = [];
let model3d: string | null = null; let model3d: string | null = null;
let text: string | null = null; let text: string | null = null;
const dynamicInputs: Record<string, string | string[]> = {}; const dynamicInputs: Record<string, string | string[]> = {};
let easeCurve: ConnectedInputs["easeCurve"] = null;
// Get the target node to check for inputSchema // Get the target node to check for inputSchema
const targetNode = nodes.find((n) => n.id === nodeId); const targetNode = nodes.find((n) => n.id === nodeId);
@ -163,6 +168,29 @@ export function getConnectedInputsPure(
const sourceNode = nodes.find((n) => n.id === edge.source); const sourceNode = nodes.find((n) => n.id === edge.source);
if (!sourceNode) return; 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 handleId = edge.targetHandle;
const { type, value } = getSourceOutput( const { type, value } = getSourceOutput(
sourceNode, sourceNode,
@ -201,19 +229,20 @@ export function getConnectedInputsPure(
} }
}); });
// Extract easeCurve data from parent EaseCurve node // Extract easeCurve data from parent EaseCurve node (if not already set by router passthrough)
let easeCurve: ConnectedInputs["easeCurve"] = null; if (!easeCurve) {
const easeCurveEdge = edges.find( const easeCurveEdge = edges.find(
(e) => e.target === nodeId && e.targetHandle === "easeCurve" (e) => e.target === nodeId && e.targetHandle === "easeCurve"
); );
if (easeCurveEdge) { if (easeCurveEdge) {
const sourceNode = nodes.find((n) => n.id === easeCurveEdge.source); const sourceNode = nodes.find((n) => n.id === easeCurveEdge.source);
if (sourceNode?.type === "easeCurve") { if (sourceNode?.type === "easeCurve") {
const sourceData = sourceNode.data as EaseCurveNodeData; const sourceData = sourceNode.data as EaseCurveNodeData;
easeCurve = { easeCurve = {
bezierHandles: sourceData.bezierHandles, bezierHandles: sourceData.bezierHandles,
easingPreset: sourceData.easingPreset, easingPreset: sourceData.easingPreset,
}; };
}
} }
} }

7
src/store/workflowStore.ts

@ -77,6 +77,7 @@ import {
executeVideoTrim, executeVideoTrim,
executeVideoFrameGrab, executeVideoFrameGrab,
executeGlbViewer, executeGlbViewer,
executeRouter,
} from "./execution"; } from "./execution";
import type { NodeExecutionContext } from "./execution"; import type { NodeExecutionContext } from "./execution";
export type { LevelGroup } from "./utils/executionUtils"; export type { LevelGroup } from "./utils/executionUtils";
@ -1008,6 +1009,9 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
case "videoFrameGrab": case "videoFrameGrab":
await executeVideoFrameGrab(executionCtx); await executeVideoFrameGrab(executionCtx);
break; break;
case "router":
await executeRouter(executionCtx);
break;
} }
}; // End of executeSingleNode helper }; // End of executeSingleNode helper
@ -1326,6 +1330,9 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
case "videoFrameGrab": case "videoFrameGrab":
await executeVideoFrameGrab(executionCtx); await executeVideoFrameGrab(executionCtx);
break; break;
case "router":
await executeRouter(executionCtx);
break;
} }
}; };

Loading…
Cancel
Save