Browse Source

feat(44-02): register Switch in WorkflowCanvas and connection menus

- Import and register SwitchNode in nodeTypes
- Add switch case to getNodeHandles (generic-input, dynamic outputs)
- Add resolveSwitchHandle function for input type resolution
- Add Switch-specific validation in isValidConnection
- Add switch case to handleMenuSelect (generic-input target)
- Add switch support to findCompatibleHandle
- Add switch minimap color (#8b5cf6 violet-500)
- Add Switch to all 8 ConnectionDropMenu arrays (IMAGE/TEXT/VIDEO/AUDIO SOURCE/TARGET)
- Import HandleType for type resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
b22335e317
  1. 63
      src/components/ConnectionDropMenu.tsx
  2. 66
      src/components/WorkflowCanvas.tsx

63
src/components/ConnectionDropMenu.tsx

@ -100,6 +100,15 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
]; ];
const TEXT_TARGET_OPTIONS: MenuOption[] = [ const TEXT_TARGET_OPTIONS: MenuOption[] = [
@ -178,6 +187,15 @@ const TEXT_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</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)
@ -239,6 +257,15 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
]; ];
const TEXT_SOURCE_OPTIONS: MenuOption[] = [ const TEXT_SOURCE_OPTIONS: MenuOption[] = [
@ -360,6 +387,15 @@ const VIDEO_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
]; ];
// GenerateVideo and VideoStitch nodes produce video output // GenerateVideo and VideoStitch nodes produce video output
@ -412,6 +448,15 @@ const VIDEO_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
]; ];
// Audio target options (nodes that accept audio input) // Audio target options (nodes that accept audio input)
@ -455,6 +500,15 @@ const AUDIO_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
]; ];
// Audio source options (nodes that produce audio output) // Audio source options (nodes that produce audio output)
@ -489,6 +543,15 @@ const AUDIO_SOURCE_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
]; ];
// 3D target options (nodes that accept 3D input) // 3D target options (nodes that accept 3D input)

66
src/components/WorkflowCanvas.tsx

@ -41,6 +41,7 @@ import {
VideoTrimNode, VideoTrimNode,
VideoFrameGrabNode, VideoFrameGrabNode,
RouterNode, RouterNode,
SwitchNode,
} from "./nodes"; } from "./nodes";
// Lazy-load GLBViewerNode to avoid bundling three.js for users who don't use 3D nodes // Lazy-load GLBViewerNode to avoid bundling three.js for users who don't use 3D nodes
@ -51,7 +52,7 @@ import { MultiSelectToolbar } from "./MultiSelectToolbar";
import { EdgeToolbar } from "./EdgeToolbar"; import { EdgeToolbar } from "./EdgeToolbar";
import { GlobalImageHistory } from "./GlobalImageHistory"; import { GlobalImageHistory } from "./GlobalImageHistory";
import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay"; import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay";
import { NodeType, NanoBananaNodeData } from "@/types"; import { NodeType, NanoBananaNodeData, HandleType } from "@/types";
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; import { defaultNodeDimensions } from "@/store/utils/nodeDefaults";
import { detectAndSplitGrid } from "@/utils/gridSplitter"; import { detectAndSplitGrid } from "@/utils/gridSplitter";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
@ -82,6 +83,7 @@ const nodeTypes: NodeTypes = {
videoTrim: VideoTrimNode, videoTrim: VideoTrimNode,
videoFrameGrab: VideoFrameGrabNode, videoFrameGrab: VideoFrameGrabNode,
router: RouterNode, router: RouterNode,
switch: SwitchNode,
glbViewer: GLBViewerNode, glbViewer: GLBViewerNode,
}; };
@ -158,6 +160,10 @@ const getNodeHandles = (nodeType: string): { inputs: string[]; outputs: string[]
return { inputs: ["video"], outputs: ["image"] }; return { inputs: ["video"], outputs: ["image"] };
case "router": case "router":
return { inputs: ["image", "text", "video", "audio", "3d", "easeCurve", "generic-input"], outputs: ["image", "text", "video", "audio", "3d", "easeCurve", "generic-output"] }; return { inputs: ["image", "text", "video", "audio", "3d", "easeCurve", "generic-input"], outputs: ["image", "text", "video", "audio", "3d", "easeCurve", "generic-output"] };
case "switch":
// Switch has one input handle (generic-input when disconnected, typed when connected)
// Output handles are dynamic based on switches array, all matching inputType
return { inputs: ["generic-input"], outputs: [] }; // Outputs handled dynamically in SwitchNode
case "glbViewer": case "glbViewer":
return { inputs: ["3d"], outputs: ["image"] }; return { inputs: ["3d"], outputs: ["image"] };
default: default:
@ -330,6 +336,21 @@ export function WorkflowCanvas() {
const sourceType = getHandleType(connection.sourceHandle); const sourceType = getHandleType(connection.sourceHandle);
const targetType = getHandleType(connection.targetHandle); const targetType = getHandleType(connection.targetHandle);
// Switch input: accept any type (generic-input handle)
const targetNode = nodes.find((n) => n.id === connection.target);
const sourceNode = nodes.find((n) => n.id === connection.source);
if (targetNode?.type === "switch" && connection.targetHandle === "generic-input") return true;
// Switch output: the type is determined by inputType stored in node data
if (sourceNode?.type === "switch") {
const switchData = sourceNode.data as { inputType?: string | null };
if (switchData.inputType && targetType) {
return switchData.inputType === targetType;
}
// If inputType not set yet, allow connection (will be resolved)
return true;
}
// If we can't determine types, allow the connection // If we can't determine types, allow the connection
if (!sourceType || !targetType) return true; if (!sourceType || !targetType) return true;
@ -432,6 +453,23 @@ export function WorkflowCanvas() {
return conn; return conn;
}; };
// For Switch nodes, resolve generic-input to the source's handle type and update inputType
const resolveSwitchHandle = (conn: Connection): Connection => {
const targetNode = nodes.find((n) => n.id === conn.target);
if (targetNode?.type !== "switch") return conn;
// If targeting the generic-input handle, resolve to the source handle type
if (conn.targetHandle === "generic-input") {
const sourceType = getHandleType(conn.sourceHandle);
if (sourceType) {
// Update the Switch node's inputType in data so output handles render
updateNodeData(conn.target, { inputType: sourceType as HandleType });
return { ...conn, targetHandle: sourceType };
}
}
return conn;
};
// Get all selected nodes // Get all selected nodes
const selectedNodes = nodes.filter((node) => node.selected); const selectedNodes = nodes.filter((node) => node.selected);
const sourceNode = nodes.find((node) => node.id === connection.source); const sourceNode = nodes.find((node) => node.id === connection.source);
@ -447,6 +485,7 @@ export function WorkflowCanvas() {
let resolved = resolveImageCompareHandle(connection, batchUsed); let resolved = resolveImageCompareHandle(connection, batchUsed);
resolved = resolveRouterHandle(resolved); resolved = resolveRouterHandle(resolved);
resolved = resolveRouterSourceHandle(resolved); resolved = resolveRouterSourceHandle(resolved);
resolved = resolveSwitchHandle(resolved);
// Resolve videoStitch handles for batch connections // Resolve videoStitch handles for batch connections
const tgtNode = nodes.find((n) => n.id === resolved.target); const tgtNode = nodes.find((n) => n.id === resolved.target);
if (tgtNode?.type === "videoStitch" && resolved.targetHandle?.startsWith("video-")) { if (tgtNode?.type === "videoStitch" && resolved.targetHandle?.startsWith("video-")) {
@ -501,6 +540,7 @@ export function WorkflowCanvas() {
let resolved = resolveImageCompareHandle(multiConnection, batchUsed); let resolved = resolveImageCompareHandle(multiConnection, batchUsed);
resolved = resolveRouterHandle(resolved); resolved = resolveRouterHandle(resolved);
resolved = resolveRouterSourceHandle(resolved); resolved = resolveRouterSourceHandle(resolved);
resolved = resolveSwitchHandle(resolved);
if (resolved.targetHandle) batchUsed.add(resolved.targetHandle); if (resolved.targetHandle) batchUsed.add(resolved.targetHandle);
if (isValidConnection(resolved)) { if (isValidConnection(resolved)) {
onConnect(resolved); onConnect(resolved);
@ -511,6 +551,7 @@ export function WorkflowCanvas() {
let resolved = resolveImageCompareHandle(connection); let resolved = resolveImageCompareHandle(connection);
resolved = resolveRouterHandle(resolved); resolved = resolveRouterHandle(resolved);
resolved = resolveRouterSourceHandle(resolved); resolved = resolveRouterSourceHandle(resolved);
resolved = resolveSwitchHandle(resolved);
onConnect(resolved); onConnect(resolved);
} }
}, },
@ -586,6 +627,20 @@ export function WorkflowCanvas() {
return handleType; return handleType;
} }
// Switch accepts any type on input, outputs match inputType
if (node.type === "switch" && needInput) {
return "generic-input";
}
if (node.type === "switch" && !needInput) {
const switchData = node.data as { switches?: Array<{ id: string; enabled: boolean }> };
// Return first enabled switch output handle ID
if (switchData.switches && switchData.switches.length > 0) {
const firstEnabled = switchData.switches.find(s => s.enabled);
if (firstEnabled) return firstEnabled.id;
}
return null;
}
// Fall back to static handles // Fall back to static handles
const staticHandles = getNodeHandles(node.type || ""); const staticHandles = getNodeHandles(node.type || "");
const handleList = needInput ? staticHandles.inputs : staticHandles.outputs; const handleList = needInput ? staticHandles.inputs : staticHandles.outputs;
@ -883,6 +938,13 @@ export function WorkflowCanvas() {
targetHandleId = handleType; targetHandleId = handleType;
sourceHandleIdForNewNode = handleType; sourceHandleIdForNewNode = handleType;
} }
} else if (nodeType === "switch") {
// Switch accepts any type on generic-input, outputs match that type
targetHandleId = "generic-input";
// Switch outputs use dynamic handle IDs (switch entry IDs)
// For connection purposes, we'll connect to the first switch output
// The resolveSwitchHandle will update inputType when input connects
sourceHandleIdForNewNode = null; // Switch outputs are dynamic, no static handle ID
} else if (handleType === "image") { } else if (handleType === "image") {
if (nodeType === "annotation" || nodeType === "output" || nodeType === "splitGrid" || nodeType === "outputGallery" || nodeType === "imageCompare") { if (nodeType === "annotation" || nodeType === "output" || nodeType === "splitGrid" || nodeType === "outputGallery" || nodeType === "imageCompare") {
targetHandleId = "image"; targetHandleId = "image";
@ -1811,6 +1873,8 @@ export function WorkflowCanvas() {
return "#38bdf8"; // sky-400 (image from video) return "#38bdf8"; // sky-400 (image from video)
case "router": case "router":
return "#6b7280"; // neutral-500 (gray/slate utility theme) return "#6b7280"; // neutral-500 (gray/slate utility theme)
case "switch":
return "#8b5cf6"; // violet-500 (distinct from Router)
case "glbViewer": case "glbViewer":
return "#0ea5e9"; // sky-500 (3D viewport) return "#0ea5e9"; // sky-500 (3D viewport)
default: default:

Loading…
Cancel
Save