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, ) {