import type { CanvasGenerationDialogState, CanvasHistoryAction, CanvasHistorySnapshot, CanvasLayer, GenerateDialogState, } from './ImageCanvasEditorTypes'; const CANVAS_HISTORY_ACTION_LABELS: Record< CanvasHistoryAction['type'], string > = { 'move-image': '移动图片', 'move-generation-result': '移动生成结果', 'delete-image': '删除图片', 'delete-generation-result': '删除生成结果', 'cut-image': '剪切图片', 'paste-image': '粘贴图片', 'duplicate-image': '复制图片', 'add-image': '添加图片', 'upload-image': '上传图片', 'generate-image': '生成图片', 'expand-image': '扩展图片', 'remove-background': '移除背景', 'split-atlas': '拆分图集', 'replace-image': '替换图片', 'show-image': '显示图片', 'hide-image': '隐藏图片', 'change-layer-order': '调整图片层级', 'group-images': '组合图片', 'ungroup-images': '取消组合', 'lock-image': '锁定图片', 'unlock-image': '解锁图片', 'flip-image': '翻转图片', 'change-asset-kind': '修改素材类型', 'change-viewport': '调整画布视图', }; const PROTECTED_CANVAS_HISTORY_ACTION_TYPES = new Set< CanvasHistoryAction['type'] >([ 'paste-image', 'duplicate-image', 'add-image', 'upload-image', 'generate-image', 'expand-image', 'remove-background', 'split-atlas', 'replace-image', ]); export function formatCanvasHistoryAction(action: CanvasHistoryAction): string { return CANVAS_HISTORY_ACTION_LABELS[action.type]; } export function isProtectedCanvasHistoryAction( action: CanvasHistoryAction, ): boolean { return PROTECTED_CANVAS_HISTORY_ACTION_TYPES.has(action.type); } function hasSameStableMediaReference( current: { src: string; objectKey?: string | null; assetObjectId?: string | null; }, target: { src: string; objectKey?: string | null; assetObjectId?: string | null; }, ): boolean { if (current.objectKey && target.objectKey) { return current.objectKey === target.objectKey; } if (current.assetObjectId && target.assetObjectId) { return current.assetObjectId === target.assetObjectId; } return current.src === target.src; } function hasSameImageSequenceFrames( current: CanvasLayer, target: CanvasLayer, ): boolean { const currentFrames = current.imageSequenceFrames ?? []; const targetFrames = target.imageSequenceFrames ?? []; if (currentFrames.length !== targetFrames.length) { return false; } return currentFrames.every((currentFrame, index) => { const targetFrame = targetFrames[index]; return ( targetFrame !== undefined && currentFrame.frameIndex === targetFrame.frameIndex && hasSameStableMediaReference( { src: currentFrame.imageSrc, objectKey: currentFrame.objectKey, assetObjectId: currentFrame.assetObjectId, }, { src: targetFrame.imageSrc, objectKey: targetFrame.objectKey, assetObjectId: targetFrame.assetObjectId, }, ) ); }); } function hasSameLayerContent( current: CanvasLayer, target: CanvasLayer, ): boolean { return ( (current.mediaType ?? 'image') === (target.mediaType ?? 'image') && hasSameStableMediaReference(current, target) && hasSameImageSequenceFrames(current, target) ); } function cloneDialogPlaceholder( dialog: GenerateDialogState, ): GenerateDialogState['placeholder'] { return dialog.placeholder ? { ...dialog.placeholder } : undefined; } function mergeLayerForHistoryRestore( target: CanvasLayer, current: CanvasLayer | undefined, restoreAssetKind: boolean, ): CanvasLayer { if (!current) { return { ...target }; } const resourceAssetKind = current.resourceAssetKind ?? target.resourceAssetKind; const restoresLayerOverride = target.assetKindOverride !== undefined || current.assetKindOverride !== undefined; const assetKindOverride = restoreAssetKind ? (target.assetKindOverride ?? null) : current.assetKindOverride; return { ...current, x: target.x, y: target.y, zIndex: target.zIndex, groupId: target.groupId, ...(resourceAssetKind !== undefined ? { resourceAssetKind } : {}), ...(restoresLayerOverride ? { assetKindOverride } : {}), assetKind: restoreAssetKind ? restoresLayerOverride ? (assetKindOverride ?? resourceAssetKind ?? null) : target.assetKind : current.assetKind, hidden: target.hidden, locked: target.locked, flipX: target.flipX, flipY: target.flipY, }; } function mergeDialogPlaceholderForHistoryRestore( target: GenerateDialogState, current: GenerateDialogState, ): GenerateDialogState['placeholder'] { if (!current.placeholder) { return cloneDialogPlaceholder(target); } if (!target.placeholder) { return cloneDialogPlaceholder(current); } return { ...current.placeholder, x: target.placeholder.x, y: target.placeholder.y, }; } function mergeDialogForHistoryRestore< TTarget extends GenerateDialogState, TCurrent extends GenerateDialogState, >(target: TTarget, current: TCurrent | undefined): TTarget | TCurrent { if (!current) { return { ...target, placeholder: cloneDialogPlaceholder(target), }; } return { ...current, composerOpen: target.composerOpen, placeholder: mergeDialogPlaceholderForHistoryRestore(target, current), }; } export function mergeCanvasHistorySnapshotForRestore({ current, target, assetKindLayerIds, }: { current: CanvasHistorySnapshot; target: CanvasHistorySnapshot; assetKindLayerIds?: ReadonlySet; }): CanvasHistorySnapshot { const currentLayerById = new Map( current.layers.map((layer) => [layer.id, layer] as const), ); const currentDialogById = new Map(); for (const dialog of current.inactiveGenerateDialogs) { currentDialogById.set(dialog.id, dialog); } if (current.generateDialog?.id) { currentDialogById.set(current.generateDialog.id, current.generateDialog); } const mergeTargetDialog = (dialog: T): T => mergeDialogForHistoryRestore( dialog, dialog.id ? currentDialogById.get(dialog.id) : undefined, ) as T; return { ...target, layers: target.layers.map((layer) => mergeLayerForHistoryRestore( layer, currentLayerById.get(layer.id), assetKindLayerIds?.has(layer.id) ?? true, ), ), generateDialog: target.generateDialog ? mergeTargetDialog(target.generateDialog) : null, inactiveGenerateDialogs: target.inactiveGenerateDialogs.map( (dialog): CanvasGenerationDialogState => mergeTargetDialog(dialog), ), }; } export function canRestoreCanvasHistorySnapshotWithoutContentLoss({ current, target, }: { current: CanvasHistorySnapshot; target: CanvasHistorySnapshot; }): boolean { const targetLayerById = new Map( target.layers.map((layer) => [layer.id, layer] as const), ); const preservesLayers = current.layers.every((currentLayer) => { const targetLayer = targetLayerById.get(currentLayer.id); if (!targetLayer) { return false; } if (!currentLayer.hidden && targetLayer.hidden) { return false; } return hasSameLayerContent(currentLayer, targetLayer); }); if (!preservesLayers) { return false; } const getDialogIds = (snapshot: CanvasHistorySnapshot): Set => new Set( [ snapshot.generateDialog?.id, ...snapshot.inactiveGenerateDialogs.map((dialog) => dialog.id), ].filter((dialogId): dialogId is string => Boolean(dialogId)), ); const targetDialogIds = getDialogIds(target); return [...getDialogIds(current)].every((dialogId) => targetDialogIds.has(dialogId), ); }