修复旧媒体图层恢复类型

在缺少权威素材类型时保留历史音视频媒体类型

拆分图层恢复中的嵌套状态判定

补充旧音视频恢复回归测试
This commit is contained in:
2026-08-05 13:25:17 +08:00
parent 4d76e36d06
commit f0f555ad0b
2 changed files with 84 additions and 25 deletions
@@ -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(
{
@@ -761,11 +761,15 @@ export function hydrateLayer(
) {
return null;
}
const imageSequenceFrames = isFormalCharacterAnimation
? resourceImageSequenceFrames
: isSelfContainedLocalResource || snapshot.mediaType === 'image-sequence'
? snapshotImageSequenceFrames
: [];
let imageSequenceFrames: NonNullable<CanvasLayer['imageSequenceFrames']> = [];
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,
) {