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;
}
};