feat(canvas): 画布节点 - 恢复批量创作表与转换能力

This commit is contained in:
ddCat committed 2026-09-15 20:56:19 +08:00
1 parent f59b59b3e6
commit c57f1b36ae
36 files changed
+3341 -34

No files matched your search

@@ -6,6 +6,7 @@ import { latchCanvasConnectionApproach, type CanvasConnectionApproach } from "@/
import type { PendingConnectionCreate } from "@/components/canvas/canvas-workspace-overlays";
import { getNodeSpec } from "@/constant/canvas";
import { batchSourceRestriction, buildBatchConnectionCreateRequest, hasBatchConnectionCandidate, planBatchConnections, type CanvasBatchConnectionPreview } from "@/lib/canvas/canvas-batch-connection";
import { batchReferenceHandleAtY } from "@/lib/canvas/canvas-batch-table";
import { connectedNodeCenterFromEdgeDrop } from "@/lib/canvas/canvas-connected-node-placement";
import { canvasConnectionError } from "@/lib/canvas/canvas-connection-policy";
import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor, isHiddenBatchChild, normalizeConnection, storyboardHandleAtY, storyboardPromptTemplateMetadata, storyboardRowFromHandle } from "@/lib/canvas/canvas-project-domain";
@@ -227,7 +228,7 @@ export function useCanvasConnectionController({
setContextMenu(null);
}, [config, connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config, pending: PendingConnectionCreate, workflowProvider?: "runninghub") => {
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.BatchTable | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config | CanvasNodeType.MediaConversion, pending: PendingConnectionCreate, workflowProvider?: "runninghub") => {
const nodeType = type;
if (nodeType === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine, tldrawLicenseKey)) {
message.error("当前生产构建未配置 tldraw License Key,不能创建 tldraw 绘图");
@@ -319,8 +320,7 @@ export function useCanvasConnectionController({
setConnections(nextConnections);
setSelectedNodeIds(new Set([newNode.id]));
setSelectedConnectionId(null);
if (nodeType === CanvasNodeType.Script) setDialogNodeId(null);
else if (nodeType !== CanvasNodeType.Text && nodeType !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
if (nodeType !== CanvasNodeType.Text && nodeType !== CanvasNodeType.Script && nodeType !== CanvasNodeType.BatchTable && nodeType !== CanvasNodeType.Audio && nodeType !== CanvasNodeType.MediaConversion) setDialogNodeId(newNode.id);
const skippedCount = batchPlan.skipped.length;
const duplicateCount = batchPlan.duplicates.length;
const suffix = skippedCount || duplicateCount ? `,跳过 ${skippedCount + duplicateCount} 个` : "";
@@ -386,13 +386,12 @@ export function useCanvasConnectionController({
setSelectedNodeIds(new Set([newNode.id]));
setSelectedConnectionId(null);
if (nodeType === CanvasNodeType.Drawing) setDrawingNodeId(newNode.id);
else if (nodeType === CanvasNodeType.Script) setDialogNodeId(null);
else if (nodeType !== CanvasNodeType.Text && nodeType !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
else if (nodeType !== CanvasNodeType.Text && nodeType !== CanvasNodeType.Script && nodeType !== CanvasNodeType.BatchTable && nodeType !== CanvasNodeType.Audio && nodeType !== CanvasNodeType.MediaConversion) setDialogNodeId(newNode.id);
closeConnectionCreateMenu();
setConnecting(null);
}, [closeConnectionCreateMenu, config, connectionsRef, defaultDrawingEngine, message, nodesRef, projectId, runtimeStatuses, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, tldrawLicenseKey]);
const getConnectionCreateDisabledReason = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config, pending: PendingConnectionCreate, workflowProvider?: "runninghub") => {
const getConnectionCreateDisabledReason = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.BatchTable | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config | CanvasNodeType.MediaConversion, pending: PendingConnectionCreate, workflowProvider?: "runninghub") => {
const nodeType = type;
if (nodeType === CanvasNodeType.Config) {
if (workflowProvider && !workflowProviderPluginEnabled(runtimeStatuses, workflowProvider)) return "RunningHub 工作流插件未启用";
@@ -426,8 +425,8 @@ export function useCanvasConnectionController({
.reverse()
.forEach((node) => {
const scrollTop = scriptScrollTopById[node.id] || 0;
const targetHandleId = node.type === CanvasNodeType.Script ? storyboardHandleAtY(node, world.y, scrollTop) : undefined;
if (node.type === CanvasNodeType.Script && !targetHandleId) return;
const targetHandleId = node.type === CanvasNodeType.Script ? storyboardHandleAtY(node, world.y, scrollTop) : node.type === CanvasNodeType.BatchTable ? batchReferenceHandleAtY(node, world.y, handleRadius) : undefined;
if ((node.type === CanvasNodeType.Script || node.type === CanvasNodeType.BatchTable) && !targetHandleId) return;
// Ordinary nodes expose one centered input/output port. Only
// storyboard rows have a meaningful vertical target position.
const targetAnchorRatio = undefined;
@@ -473,8 +472,8 @@ export function useCanvasConnectionController({
.reverse()
.forEach((node) => {
const scrollTop = scriptScrollTopById[node.id] || 0;
const targetHandleId = node.type === CanvasNodeType.Script ? storyboardHandleAtY(node, world.y, scrollTop) : undefined;
if (node.type === CanvasNodeType.Script && !targetHandleId) return;
const targetHandleId = node.type === CanvasNodeType.Script ? storyboardHandleAtY(node, world.y, scrollTop) : node.type === CanvasNodeType.BatchTable ? batchReferenceHandleAtY(node, world.y, handleRadius) : undefined;
if ((node.type === CanvasNodeType.Script || node.type === CanvasNodeType.BatchTable) && !targetHandleId) return;
const targetAnchorRatio = undefined;
const anchor = getConnectionTargetAnchor(node, current, targetHandleId, scrollTop);
const dx = world.x - anchor.x;
@@ -646,12 +645,21 @@ export function useCanvasConnectionController({
// reset, an old quick-create menu can coexist with the new draft line;
// pointerup then sees the stale pending menu and silently aborts.
if (pendingConnectionCreateRef.current) closeConnectionCreateMenu();
if (connectingParamsRef.current) setConnecting(null);
if (batchConnectionPreviewRef.current && handleType === "target") {
commitBatchConnection(batchConnectionPreviewRef.current.sourceNodeIds, nodeId, handleId, anchorRatio);
clearBatchConnection();
return;
}
// A batch table exposes real target pins inside its content. When a normal
// image output is dragged onto one, commit that active connection instead
// of starting a new connection session from the target pin.
const activeConnection = connectingParamsRef.current;
if (activeConnection && handleType === "target" && activeConnection.nodeId !== nodeId) {
connectNodes(activeConnection, nodeId, handleId, anchorRatio);
setConnecting(null);
return;
}
if (activeConnection) setConnecting(null);
if (batchConnectionPreviewRef.current) clearBatchConnection();
connectingPointerIdRef.current = event.pointerId;
connectingPointerStartRef.current = { x: event.clientX, y: event.clientY };
@@ -660,7 +668,21 @@ export function useCanvasConnectionController({
setConnectionTargetNodeId(null);
setConnectionTargetAnchorRatio(undefined);
setSelectedConnectionId(null);
}, [clearBatchConnection, closeConnectionCreateMenu, commitBatchConnection, screenToCanvas, setConnecting, setSelectedConnectionId]);
}, [clearBatchConnection, closeConnectionCreateMenu, commitBatchConnection, connectNodes, screenToCanvas, setConnecting, setSelectedConnectionId]);
const handleConnectDrop = useCallback((event: ReactPointerEvent, nodeId: string, handleId?: string) => {
event.preventDefault();
event.stopPropagation();
if (batchConnectionPreviewRef.current) {
commitBatchConnection(batchConnectionPreviewRef.current.sourceNodeIds, nodeId, handleId);
clearBatchConnection();
return;
}
const current = connectingParamsRef.current;
if (!current || current.nodeId === nodeId) return;
connectNodes(current, nodeId, handleId);
setConnecting(null);
}, [clearBatchConnection, commitBatchConnection, connectNodes, setConnecting]);
useEffect(() => {
const cancelPendingPointerMove = () => {
@@ -799,6 +821,7 @@ export function useCanvasConnectionController({
createConnectedNode,
getConnectionCreateDisabledReason,
handleConnectStart,
handleConnectDrop,
handleBatchConnectionTargetClick,
batchConnectionPreview,
beginBatchConnectionMode,