diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 2ce73194b..e6a28fc7a 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -321,6 +321,40 @@ const VIDEO_CANVAS_INSERT_MENU_ITEMS: VideoCanvasInsertMenuItem[] = [ }, ]; +const VIDEO_WORKFLOW_QUICK_CONNECT_ICON_MAP: Partial< + Record +> = { + "text-node": FileText, + "input-image": FileImage, + "video-input": Film, + "novel-input": FileText, + "video-analyze": Scissors, + "storyboard-node": GitBranch, + "storyboard-chart-node": GitBranch, + "table-editor-node": Table2, + "gen-image": Palette, + "gen-video": Video, + "gen-music": Music2, + "motion-control": Clapperboard, + "camera-movement": Clapperboard, + "professional-camera": Wand2, + "inpaint-crop": Scissors, + "inpaint-stitch": Scissors, + "local-save": Save, + preview: Eye, + "custom-agent": Bot, + "rh-app": Box, + "rh-comfy": Box, + "jimeng-super-resolution": Sparkles, + "doodle-canvas": PenTool, + "canvas-node": Layers, + "image-compare": Layers, +}; + +function getVideoWorkflowQuickConnectIcon(type: VideoWorkflowTemplateType) { + return VIDEO_WORKFLOW_QUICK_CONNECT_ICON_MAP[type] || Plus; +} + function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } @@ -1004,6 +1038,41 @@ export default function CanvasEditorPage({ params }: PageProps) { : [], [selectedVideoWorkflowType] ); + const selectedWorkflowQuickConnectAnchor = useMemo(() => { + if ( + !isVideoMode || + !selectedVideoWorkflowNode || + selectedVideoWorkflowQuickConnects.length === 0 + ) { + return null; + } + + const nodeLeft = selectedVideoWorkflowNode.position.x * viewport.zoom + viewport.x; + const nodeTop = selectedVideoWorkflowNode.position.y * viewport.zoom + viewport.y; + const nodeWidth = selectedVideoWorkflowNode.size.width * viewport.zoom; + const anchorX = nodeLeft + nodeWidth; + const anchorY = + nodeTop + (selectedVideoWorkflowNode.size.height * viewport.zoom) / 2; + const openLeft = viewportWidth > 0 && anchorX + 260 > viewportWidth - 32; + + return { + x: anchorX, + y: anchorY, + openLeft, + }; + }, [ + isVideoMode, + selectedVideoWorkflowNode, + selectedVideoWorkflowQuickConnects.length, + viewport.x, + viewport.y, + viewport.zoom, + viewportWidth, + ]); + const selectedWorkflowQuickConnectSourceNode = + selectedWorkflowQuickConnectAnchor && selectedVideoWorkflowNode + ? selectedVideoWorkflowNode + : null; const workflowGraph = useMemo( () => createWorkflowGraphContext(nodes, edges), [edges, nodes] @@ -7343,40 +7412,97 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null} {isVideoMode && - selectedVideoWorkflowDefinition && - (selectedVideoWorkflowQuickConnects.length > 0 || selectedWorkflowIsActionable) ? ( -
- {selectedVideoWorkflowQuickConnects.length > 0 ? ( - - -
-
- {selectedVideoWorkflowDefinition.label} - - 常用下游分支会放在这里,点击后自动放到右侧并连线。 - -
- - Quick Connect - -
-
- - {selectedVideoWorkflowQuickConnects.map((item) => ( - - ))} - -
- ) : null} + selectedWorkflowQuickConnectAnchor && + selectedWorkflowQuickConnectSourceNode && + selectedVideoWorkflowQuickConnects.length > 0 && + !videoCanvasInsertMenu ? ( +
+ + {selectedVideoWorkflowQuickConnects.map((item, index, items) => { + const total = items.length; + const y = (index - (total - 1) / 2) * 72; + const x = 96 + Math.abs(y) * 0.28; + const direction = selectedWorkflowQuickConnectAnchor.openLeft ? -1 : 1; + const targetX = x * direction; + const cp1X = 36 * direction; + const cp2X = (x - 26) * direction; + return ( + + ); + })} + + + + {selectedVideoWorkflowQuickConnects.map((item, index, items) => { + const total = items.length; + const y = (index - (total - 1) / 2) * 72; + const x = 96 + Math.abs(y) * 0.28; + const direction = selectedWorkflowQuickConnectAnchor.openLeft ? -1 : 1; + const Icon = getVideoWorkflowQuickConnectIcon(item.type); + return ( + + ); + })} +
+ ) : null} + + {isVideoMode && + selectedVideoWorkflowDefinition && + selectedWorkflowIsActionable ? ( +
{selectedWorkflowIsActionable ? (