diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index 6445f2741..816e69bba 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -43,6 +43,7 @@ type CanvasNodeProps = { batchClosing?: boolean; batchOpening?: boolean; batchRecovering?: boolean; + batchPrimary?: boolean; batchMotion?: { x: number; y: number; index: number }; onMouseDown: (event: React.MouseEvent, nodeId: string) => void; onHoverStart: (nodeId: string) => void; @@ -108,6 +109,7 @@ export const CanvasNode = React.memo(function CanvasNode({ batchClosing = false, batchOpening = false, batchRecovering = false, + batchPrimary = false, batchMotion, onMouseDown, onHoverStart, @@ -461,7 +463,7 @@ export const CanvasNode = React.memo(function CanvasNode({ {resourceLabel ? : null} {hasMediaContent && !readOnly ? : null} {isBatchRoot ? onToggleBatch?.(data.id)} /> : null} - {isBatchChild && !readOnly ? onSetBatchPrimary?.(data)} /> : null} + {isBatchChild && !readOnly ? onSetBatchPrimary?.(data)} /> : null} {data.metadata?.locked ? : null} ) : null} @@ -802,11 +804,11 @@ function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number; ); } -function BatchPrimaryBadge({ visible, theme, onSelect }: { visible: boolean; theme: CanvasTheme; onSelect: () => void }) { +function BatchPrimaryBadge({ visible, selected, theme, onSelect }: { visible: boolean; selected: boolean; theme: CanvasTheme; onSelect: () => void }) { return ( - ); } diff --git a/web/src/lib/canvas/canvas-project-domain.ts b/web/src/lib/canvas/canvas-project-domain.ts index c6a2ed635..f728d5ee9 100644 --- a/web/src/lib/canvas/canvas-project-domain.ts +++ b/web/src/lib/canvas/canvas-project-domain.ts @@ -296,6 +296,30 @@ export function sameNodeSemanticData(left: CanvasNodeData, right: CanvasNodeData return left.id === right.id && left.type === right.type && left.title === right.title && left.parentId === right.parentId && left.width === right.width && left.height === right.height && left.metadata === right.metadata; } +export function applyBatchPrimaryImage(root: CanvasNodeData, primary: CanvasNodeData): CanvasNodeData { + // 主图是根节点对媒体的完整代理;可选字段也必须显式覆盖,避免残留上一张图的存储身份。 + return { + ...root, + width: primary.width, + height: primary.height, + metadata: { + ...root.metadata, + primaryImageId: primary.id, + content: primary.metadata?.content, + storageKey: primary.metadata?.storageKey, + status: primary.metadata?.status, + naturalWidth: primary.metadata?.naturalWidth, + naturalHeight: primary.metadata?.naturalHeight, + bytes: primary.metadata?.bytes, + mimeType: primary.metadata?.mimeType, + errorDetails: primary.metadata?.errorDetails, + generationErrorCode: primary.metadata?.generationErrorCode, + failedPromptFingerprint: primary.metadata?.failedPromptFingerprint, + freeResize: primary.metadata?.freeResize, + }, + }; +} + export function removeCanvasNodes(nodes: CanvasNodeData[], requestedIds: Set) { const removedIds = new Set(requestedIds); nodes.forEach((node) => { @@ -327,17 +351,8 @@ export function removeCanvasNodes(nodes: CanvasNodeData[], requestedIds: Set item.id === primaryImageId); - return { - ...cleaned, - metadata: { - ...cleaned.metadata, - batchChildIds: childIds, - primaryImageId, - content: primaryNode?.metadata?.content || cleaned.metadata?.content, - naturalWidth: primaryNode?.metadata?.naturalWidth || cleaned.metadata?.naturalWidth, - naturalHeight: primaryNode?.metadata?.naturalHeight || cleaned.metadata?.naturalHeight, - }, - }; + const batchRoot = { ...cleaned, metadata: { ...cleaned.metadata, batchChildIds: childIds, primaryImageId } }; + return primaryNode ? applyBatchPrimaryImage(batchRoot, primaryNode) : batchRoot; }); return { removedIds, nodes: nextNodes }; } diff --git a/web/src/pages/canvas/canvas-project-world-layers.tsx b/web/src/pages/canvas/canvas-project-world-layers.tsx index 83b3637da..41a754e9d 100644 --- a/web/src/pages/canvas/canvas-project-world-layers.tsx +++ b/web/src/pages/canvas/canvas-project-world-layers.tsx @@ -127,6 +127,7 @@ export function CanvasProjectWorldLayers(props: CanvasProjectWorldLayersProps) { batchClosing={Boolean(node.metadata?.batchRootId && props.collapsingBatchIds.has(node.metadata.batchRootId))} batchOpening={props.openingBatchIds.has(node.id)} batchRecovering={props.collapsingBatchIds.has(node.id)} + batchPrimary={Boolean(node.metadata?.batchRootId && props.nodeById.get(node.metadata.batchRootId)?.metadata?.primaryImageId === node.id)} batchMotion={props.batchMotionById.get(node.id)} showImageInfo={props.showImageInfo} reduceMediaEffects={props.reduceMediaEffects || props.isNodeDragging || props.mediaEffectsDisabledNodeId === node.id} diff --git a/web/src/pages/canvas/use-canvas-node-editor.ts b/web/src/pages/canvas/use-canvas-node-editor.ts index 6cd12f76e..de8b43267 100644 --- a/web/src/pages/canvas/use-canvas-node-editor.ts +++ b/web/src/pages/canvas/use-canvas-node-editor.ts @@ -5,7 +5,7 @@ import { saveAs } from "file-saver"; import { NODE_DEFAULT_SIZE } from "@/constant/canvas"; import { FRAME_COLLAPSED_HEIGHT, FRAME_COLLAPSED_WIDTH, getFrameChildIds, isFrameNode } from "@/lib/canvas/canvas-frame"; -import { applyNodeConfigPatch } from "@/lib/canvas/canvas-project-domain"; +import { applyBatchPrimaryImage, applyNodeConfigPatch } from "@/lib/canvas/canvas-project-domain"; import { audioExtension, imageExtension, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation"; import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error"; import { ensureCanvasNodeAsset } from "@/services/project-asset-sync"; @@ -117,7 +117,7 @@ export function useCanvasNodeEditor({ setNodes((current) => current.map((node) => node.id === rootId - ? { ...node, width: child.width, height: child.height, metadata: { ...node.metadata, content: child.metadata?.content, primaryImageId: child.id, naturalWidth: child.metadata?.naturalWidth, naturalHeight: child.metadata?.naturalHeight, freeResize: child.metadata?.freeResize } } + ? applyBatchPrimaryImage(node, child) : node, ), );