From f0f555ad0b72adefe0455a84189559ed50a7024c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 5 Aug 2026 13:25:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A7=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E5=9B=BE=E5=B1=82=E6=81=A2=E5=A4=8D=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在缺少权威素材类型时保留历史音视频媒体类型 拆分图层恢复中的嵌套状态判定 补充旧音视频恢复回归测试 --- .../ImageCanvasEditorModel.test.ts | 27 ++++++ .../image-editor/ImageCanvasEditorModel.ts | 82 +++++++++++++------ 2 files changed, 84 insertions(+), 25 deletions(-) diff --git a/src/components/image-editor/ImageCanvasEditorModel.test.ts b/src/components/image-editor/ImageCanvasEditorModel.test.ts index d3e0a14cc..dd897d021 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.test.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.test.ts @@ -602,6 +602,33 @@ describe('ImageCanvasEditorModel', () => { } }); + it('preserves legacy audio and video media types when assetKind is absent', () => { + for (const mediaType of ['video', 'audio'] as const) { + const hydrated = hydrateLayer( + { + layerId: `legacy-${mediaType}-layer`, + resourceId: `legacy-${mediaType}-resource`, + title: `旧${mediaType}`, + sourceType: 'uploaded', + mediaType, + }, + new Map([ + [ + `legacy-${mediaType}-resource`, + { + imageSrc: + mediaType === 'video' + ? '/legacy/cutscene.mp4' + : '/legacy/theme.mp3', + }, + ], + ]), + ); + + expect(hydrated?.mediaType).toBe(mediaType); + } + }); + it('recovers the normal source model after the owner payload removes an internal model', () => { const hydrated = hydrateLayer( { diff --git a/src/components/image-editor/ImageCanvasEditorModel.ts b/src/components/image-editor/ImageCanvasEditorModel.ts index cef504081..2bc6743b6 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.ts @@ -761,11 +761,15 @@ export function hydrateLayer( ) { return null; } - const imageSequenceFrames = isFormalCharacterAnimation - ? resourceImageSequenceFrames - : isSelfContainedLocalResource || snapshot.mediaType === 'image-sequence' - ? snapshotImageSequenceFrames - : []; + let imageSequenceFrames: NonNullable = []; + if (isFormalCharacterAnimation) { + imageSequenceFrames = resourceImageSequenceFrames; + } else if ( + isSelfContainedLocalResource || + snapshot.mediaType === 'image-sequence' + ) { + imageSequenceFrames = snapshotImageSequenceFrames; + } const firstSequenceFrame = imageSequenceFrames[0]?.imageSrc ?? ''; const src = isFormalCharacterAnimation ? firstSequenceFrame || resource?.imageSrc || '' @@ -786,16 +790,37 @@ export function hydrateLayer( ) { return null; } + let resourcePersistenceState: CanvasLayer['resourcePersistenceState']; + if (resource) { + resourcePersistenceState = 'registered'; + } else if (isSelfContainedLocalResource) { + resourcePersistenceState = 'self-contained-local'; + } else if (resourceId.startsWith('local-')) { + resourcePersistenceState = 'unresolved-local'; + } + let sourceType: CanvasLayer['sourceType'] = 'uploaded'; + if (isCanvasSourceType(resource?.sourceType)) { + sourceType = resource.sourceType; + } else if (isCanvasSourceType(snapshot.sourceType)) { + sourceType = snapshot.sourceType; + } + const mediaType = resolveHydratedLayerMediaType( + snapshot, + assetKind, + isSelfContainedLocalResource, + ); + let imageSequenceDurationMs: number | undefined; + if (isFormalCharacterAnimation) { + imageSequenceDurationMs = + audioDurationOrNull(resource?.imageSequenceDurationMs) ?? undefined; + } else if (isSelfContainedLocalResource) { + imageSequenceDurationMs = + audioDurationOrNull(snapshot.imageSequenceDurationMs) ?? undefined; + } return { id: layerId, resourceId, - resourcePersistenceState: resource - ? 'registered' - : isSelfContainedLocalResource - ? 'self-contained-local' - : resourceId.startsWith('local-') - ? 'unresolved-local' - : undefined, + resourcePersistenceState, title, src, x: numberFromSnapshot(snapshot.x, 0), @@ -813,14 +838,8 @@ export function hydrateLayer( }; })(), zIndex: numberFromSnapshot(snapshot.zIndex, 1), - sourceType: isCanvasSourceType(resource?.sourceType) - ? resource.sourceType - : isCanvasSourceType(snapshot.sourceType) - ? snapshot.sourceType - : 'uploaded', - mediaType: isSelfContainedLocalResource - ? 'image-sequence' - : resolveCanvasMediaTypeFromAssetKind(assetKind), + sourceType, + mediaType, thumbnailSrc: isFormalCharacterAnimation ? firstSequenceFrame || null : stringOrNull(snapshot.thumbnailSrc), @@ -860,11 +879,7 @@ export function hydrateLayer( ? undefined : (audioDurationOrNull(snapshot.durationSeconds) ?? audioDurationOrNull(resource?.durationSeconds)), - imageSequenceDurationMs: isFormalCharacterAnimation - ? audioDurationOrNull(resource?.imageSequenceDurationMs) - : isSelfContainedLocalResource - ? audioDurationOrNull(snapshot.imageSequenceDurationMs) - : undefined, + imageSequenceDurationMs, sourceResourceId: isFormalCharacterAnimation ? stringOrNull(resource?.sourceResourceId) : (stringOrNull(snapshot.sourceResourceId) ?? @@ -887,6 +902,23 @@ export function hydrateLayer( }; } +function resolveHydratedLayerMediaType( + snapshot: EditorProjectLayerSnapshot, + assetKind: CanvasAssetKind | null, + isSelfContainedLocalResource: boolean, +): CanvasMediaType { + if (isSelfContainedLocalResource) { + return 'image-sequence'; + } + if (assetKind) { + return resolveCanvasMediaTypeFromAssetKind(assetKind); + } + if (snapshot.mediaType === 'video' || snapshot.mediaType === 'audio') { + return snapshot.mediaType; + } + return 'image'; +} + function isSelfContainedLegacyLocalImageSequence( snapshot: EditorProjectLayerSnapshot, ) {