feat(canvas): 画布工作台 - 新增生成副本并修复大型画布性能、素材同步与删除校验

统一图片与视频副本的生成落点并保留参考连接。\n优化大型画布节点虚拟化、媒体懒加载、拖拽与详情进入路径。\n修复素材主版本 ID 同步及删除时的画布独立引用校验。
This commit is contained in:
ddCat committed 2026-09-01 14:57:05 +08:00
1 parent 434f63e153
commit 3f4a9ea3d0
45 files changed
+1348 -286

No files matched your search

+44 -3
View File
@@ -76,7 +76,7 @@ import { CanvasFreeformEmptyState, CanvasLinkedProjectEmptyState, CanvasShortDra
import { resolveCanvasEmptyStateKind } from "@/lib/canvas/canvas-starter";
import { failedImageBatchChildren, markImageBatchRetrying, reconcileImageBatchRoot, restoreUnsubmittedImageBatchChild } from "@/lib/canvas/canvas-image-batch-retry";
import { createCanvasNode, getInputSummary, isHiddenBatchChild, persistCanvasWorkspaceMode, readCanvasWorkspaceMode } from "@/lib/canvas/canvas-project-domain";
import { stampCanvasNodeChanges } from "@/lib/canvas/canvas-node-timestamps";
import { stampCanvasNodeChanges, updateCanvasNode, updateCanvasNodes } from "@/lib/canvas/canvas-node-timestamps";
import { canvasAssetHandoffAttempt, finalizeCanvasAssetHandoff, uninsertedCanvasAssetHandoffPayloads } from "@/lib/canvas/canvas-asset-handoff";
import { batchSourceRestriction } from "@/lib/canvas/canvas-batch-connection";
import { deriveStoryboardPipelineProgress } from "@/lib/canvas/canvas-storyboard-progress";
@@ -92,7 +92,7 @@ import { CanvasProjectMediaDialogs } from "./canvas-project-media-dialogs";
import { CanvasProjectSelectionToolbar } from "./canvas-project-selection-toolbar";
import { CanvasProjectStatusDialogs } from "./canvas-project-status-dialogs";
import { CanvasProjectWorldLayers } from "./canvas-project-world-layers";
import { CanvasNodeActionContext } from "@/components/canvas/canvas-node-action-context";
import { CanvasNodeActionContext, type CanvasNodeActionContextValue } from "@/components/canvas/canvas-node-action-context";
import { bringCanvasNodeToFront, type CanvasNodeStackOrder } from "@/lib/canvas/canvas-node-stack-order";
import { PortraitClearanceModal } from "@/components/canvas/portrait-clearance/portrait-clearance-modal";
import { CanvasNodeGraphContext, type CanvasNodeGraphContextValue } from "@/components/canvas/canvas-node-graph-context";
@@ -131,6 +131,7 @@ import {
type CanvasFolderStyle,
type CanvasFolderTheme,
type CanvasNodeData,
type CanvasNodeMetadata,
type CanvasMediaPerformanceMode,
type StoryboardColumn,
type StoryboardShotCount,
@@ -1386,6 +1387,45 @@ function InfiniteCanvasPage() {
setToolbarNodeId(null);
setPortraitClearanceNodeId(node.id);
}, []);
const duplicateNodeFromContent = useCallback((node: CanvasNodeData) => duplicateNode(node.id), [duplicateNode]);
const deleteNodeFromContent = useCallback((node: CanvasNodeData) => deleteNodes(new Set([node.id])), [deleteNodes]);
const updateNodeFromContent = useCallback((nodeId: string, update: (node: CanvasNodeData) => CanvasNodeData) => {
setNodesState((current) => {
const next = updateCanvasNode(current, nodeId, update);
nodesRef.current = next;
return next;
});
}, []);
const pendingMediaUpdatesRef = useRef(new Map<string, (node: CanvasNodeData) => CanvasNodeData>());
const mediaUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const updateMediaNodeFromContent = useCallback((nodeId: string, update: (node: CanvasNodeData) => CanvasNodeData) => {
const previous = pendingMediaUpdatesRef.current.get(nodeId);
pendingMediaUpdatesRef.current.set(nodeId, previous ? (node) => update(previous(node)) : update);
if (mediaUpdateTimerRef.current) return;
mediaUpdateTimerRef.current = setTimeout(() => {
const updates = pendingMediaUpdatesRef.current;
pendingMediaUpdatesRef.current = new Map();
mediaUpdateTimerRef.current = null;
if (!updates.size) return;
setNodesState((current) => {
const next = updateCanvasNodes(current, updates);
nodesRef.current = next;
return next;
});
}, 120);
}, []);
const updateNodeMetadataFromContent = useCallback((nodeId: string, patch: CanvasNodeMetadata) => {
updateNodeFromContent(nodeId, (node) => ({ ...node, metadata: { ...node.metadata, ...patch } }));
}, [updateNodeFromContent]);
const canvasNodeActions = useMemo<CanvasNodeActionContextValue>(() => ({
download: downloadNodeImage,
duplicate: duplicateNodeFromContent,
deleteNode: deleteNodeFromContent,
updateMetadata: updateNodeMetadataFromContent,
updateNode: updateNodeFromContent,
updateMediaNode: updateMediaNodeFromContent,
openPortraitClearance,
}), [deleteNodeFromContent, downloadNodeImage, duplicateNodeFromContent, openPortraitClearance, updateMediaNodeFromContent, updateNodeFromContent, updateNodeMetadataFromContent]);
const { agentSnapshot, agentUndoCount, applyAgentOps, canUndoAgentOps, dismissLastAgentChange, lastAgentChange, undoAgentOps, viewLastAgentChange } = useCanvasAgentOperations({
projectId,
domainProjectId: currentProject?.projectId,
@@ -2202,7 +2242,7 @@ function InfiniteCanvasPage() {
onFileDragLeave={handleFileDragLeave}
onFileDragOver={handleFileDragOver}
>
<CanvasNodeActionContext.Provider value={{ download: downloadNodeImage, duplicate: (node) => duplicateNode(node.id), deleteNode: (node) => deleteNodes(new Set([node.id])), updateMetadata: (nodeId, patch) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...patch } } : node))), resizeNode: (nodeId, size) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, width: size.width, height: size.height } : node))), openPortraitClearance }}>
<CanvasNodeActionContext.Provider value={canvasNodeActions}>
<CanvasNodeGraphContext.Provider value={nodeGraphContext}>
<CanvasProjectWorldLayers
projectId={projectId}
@@ -2562,6 +2602,7 @@ function InfiniteCanvasPage() {
onRedo={redoCanvas}
onPaste={pasteAtPosition}
onCopyNode={(nodeId) => copyNodesToClipboard(new Set([nodeId]))}
onCreateGenerationCopy={(nodeId) => duplicateNode(nodeId, "copy")}
onDuplicate={duplicateNode}
onDeleteNode={(nodeId) => deleteNodes(new Set([nodeId]))}
onDeleteConnection={deleteConnection}