Skip to content

Commit 90366ca

Browse files
authored
fix(animation): preserve dirty state and legacy targets (#822)
1 parent 6f810d6 commit 90366ca

21 files changed

Lines changed: 827 additions & 242 deletions

packages/cocos-cli-types/__tests__/__snapshots__/dts-snapshot.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5881,6 +5881,7 @@ export declare interface IAnimationOperationOptions {
58815881
export declare interface IAnimationOperationResult {
58825882
state: 'success' | 'failure';
58835883
result: boolean;
5884+
undoRecorded?: boolean;
58845885
reason?: string;
58855886
}
58865887
export declare interface IAnimationPlayStateOptions {
@@ -5932,6 +5933,7 @@ export declare interface IAnimationRootResult {
59325933
}
59335934
export declare interface IAnimationSaveOptions {
59345935
saveScene?: boolean;
5936+
target?: string;
59355937
}
59365938
export declare interface IAnimationService extends IServiceEvents {
59375939
enter(options: IAnimationEnterOptions): Promise<IAnimationStateInfo>;
@@ -5964,6 +5966,7 @@ export declare interface IAnimationStateInfo {
59645966
time: number;
59655967
playState: AnimationPlayState_2;
59665968
dirty: boolean;
5969+
sceneDirty: boolean;
59675970
selection: string[];
59685971
restoreSelectionOnExit: boolean;
59695972
}

src/core/scene/common/animation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ export interface IAnimationStateInfo {
370370
playState: AnimationPlayState;
371371
/** 当前 animation authoring session 相对进入/保存 baseline 是否有未保存修改。 */
372372
dirty: boolean;
373+
/** 当前 Scene 相对进入/保存 baseline 是否有未保存修改;不包含当前 Animation scope。 */
374+
sceneDirty: boolean;
373375
/** 当前 selection paths。 */
374376
selection: string[];
375377
/** 退出 session 时默认是否恢复进入前的 selection。 */
@@ -500,6 +502,8 @@ export interface IAnimationOperationOptions {
500502
export interface IAnimationOperationResult {
501503
state: 'success' | 'failure';
502504
result: boolean;
505+
/** 成功操作是否创建了 animation scoped Undo 历史;失败时省略。 */
506+
undoRecorded?: boolean;
503507
reason?: string;
504508
}
505509

@@ -508,6 +512,10 @@ export interface IAnimationSaveOptions {
508512
* 保存 clip 成功后同步保存当前 scene/prefab 资源,并用普通 editor save 语义清理 shared undo dirty。
509513
*/
510514
saveScene?: boolean;
515+
/**
516+
* 将当前 clip 内容保存到指定的新资源路径,不保存宿主 scene。
517+
*/
518+
target?: string;
511519
}
512520

513521
/**

src/core/scene/scene-process/service/animation.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export class AnimationService extends BaseService<Record<string, any>> implement
209209
time: 0,
210210
playState: 'stop',
211211
dirty: false,
212+
sceneDirty: Service.Undo.isDirty(),
212213
selection,
213214
restoreSelectionOnExit: true,
214215
};
@@ -224,6 +225,7 @@ export class AnimationService extends BaseService<Record<string, any>> implement
224225
time: this._curEditTime,
225226
playState: this._playState,
226227
dirty: this._isAnimationSessionDirty(this._session),
228+
sceneDirty: this._isSceneSessionDirty(this._session),
227229
selection,
228230
restoreSelectionOnExit: this._session.restoreSelectionOnExit,
229231
};
@@ -535,7 +537,8 @@ export class AnimationService extends BaseService<Record<string, any>> implement
535537
this._animationStates.create(session.clipUuid, clip);
536538
await this.setTime({ time: this._curEditTime });
537539
const after = shouldRecordUndo ? captureAnimationClipSnapshot(clip, propertyMetadataContext) : null;
538-
if (before && after && !animationClipSnapshotsEqual(before, after)) {
540+
const undoRecorded = Boolean(before && after && !animationClipSnapshotsEqual(before, after));
541+
if (undoRecorded && before && after) {
539542
const undoCommand = new AnimationClipSnapshotCommand({
540543
clipUuid: session.clipUuid,
541544
before,
@@ -565,17 +568,27 @@ export class AnimationService extends BaseService<Record<string, any>> implement
565568
return {
566569
state: 'success',
567570
result: true,
571+
undoRecorded,
568572
};
569573
}
570574

571575
async save(options: IAnimationSaveOptions = {}): Promise<boolean> {
572576
const session = requireAnimationSession(this._session);
573577
const state = await this._getAnimationState(session.clipUuid);
574578
const rootNode = this._getSessionRootNode();
579+
ensureClipEvents(state.clip);
580+
if (options.target) {
581+
return await saveAnimationServiceClip({
582+
session,
583+
rootNode,
584+
clip: state.clip,
585+
target: options.target,
586+
});
587+
}
588+
575589
const propertyMetadataContext = createAnimationPropertyCurveMetadataContext(rootNode);
576590
const savedSnapshot = captureAnimationClipSnapshot(state.clip, propertyMetadataContext);
577591
const animationDirtyAtSave = this._isAnimationSessionDirty(session);
578-
ensureClipEvents(state.clip);
579592
this._markSelfSavedClipRefresh(session.clipUuid);
580593
let saved = false;
581594
try {
@@ -884,6 +897,16 @@ export class AnimationService extends BaseService<Record<string, any>> implement
884897
return Service.Undo.hasScopedDifferenceAfterCheckpoint(session.undoBaseline, scope);
885898
}
886899

900+
private _isSceneSessionDirty(session: IAnimationSession): boolean {
901+
if (session.globalDirtyAtEnter) {
902+
return true;
903+
}
904+
return Service.Undo.hasDifferenceOutsideScope(
905+
session.undoBaseline,
906+
this._createAnimationUndoScope(session.clipUuid),
907+
);
908+
}
909+
887910
private _createAnimationUndoScope(clipUuid: string): Partial<IUndoScope> {
888911
return {
889912
assetUuid: clipUuid,

src/core/scene/scene-process/service/animation/clip-snapshot.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import type { AnimationClip } from 'cc';
1+
import type { AnimationClip, Asset } from 'cc';
22
import type {
33
IAnimationAuxiliaryCurveDump,
44
IAnimationCurveDump,
5+
IAnimationCurveKeyDump,
56
IAnimationEmbeddedPlayerDump,
67
IAnimationEmbeddedPlayerGroup,
78
IAnimationEventDump,
9+
IAnimationValue,
810
} from '../../../common';
11+
import {
12+
loadAnimationAssetValue,
13+
queryAnimationAssetCtor,
14+
queryAnimationAssetUuid,
15+
} from './asset-value';
916
import { dumpAuxiliaryCurves, replaceAuxiliaryCurves } from './auxiliary-curve';
1017
import {
1118
dumpEmbeddedPlayers,
@@ -34,6 +41,9 @@ export interface IAnimationClipSnapshot {
3441
auxiliaryCurves: Record<string, IAnimationAuxiliaryCurveDump>;
3542
}
3643

44+
type AnimationAssetCtor = new () => Asset;
45+
type PendingAnimationAssetLoads = Map<AnimationAssetCtor, Map<string, Promise<Asset>>>;
46+
3747
export function captureAnimationClipSnapshot(clip: AnimationClip, options: IPropertyCurveMetadataContext = {}): IAnimationClipSnapshot {
3848
const sample = getClipSample(clip);
3949
const events = queryClipEvents(clip) || [];
@@ -56,24 +66,30 @@ export function captureAnimationClipSnapshot(clip: AnimationClip, options: IProp
5666

5767
export async function restoreAnimationClipSnapshot(clip: AnimationClip, snapshot: IAnimationClipSnapshot): Promise<void> {
5868
const previous = captureAnimationClipSnapshot(clip);
69+
const pendingAssetLoads: PendingAnimationAssetLoads = new Map();
5970
try {
60-
await applyAnimationClipSnapshot(clip, snapshot);
71+
await applyAnimationClipSnapshot(clip, snapshot, pendingAssetLoads);
6172
} catch (error) {
6273
try {
63-
await applyAnimationClipSnapshot(clip, previous);
74+
await applyAnimationClipSnapshot(clip, previous, pendingAssetLoads);
6475
} catch (restoreError) {
6576
console.error('[Animation] rollback failed animation clip snapshot restore:', restoreError);
6677
}
6778
throw error;
6879
}
6980
}
7081

71-
async function applyAnimationClipSnapshot(clip: AnimationClip, snapshot: IAnimationClipSnapshot): Promise<void> {
82+
async function applyAnimationClipSnapshot(
83+
clip: AnimationClip,
84+
snapshot: IAnimationClipSnapshot,
85+
pendingAssetLoads: PendingAnimationAssetLoads,
86+
): Promise<void> {
7287
(clip as any).duration = snapshot.duration;
7388
(clip as any).sample = snapshot.sample;
7489
(clip as any).speed = snapshot.speed;
7590
(clip as any).wrapMode = snapshot.wrapMode;
76-
if (!replacePropertyCurves(clip, snapshot.curves)) {
91+
const curves = await hydrateAnimationAssetCurveValues(snapshot.curves, pendingAssetLoads);
92+
if (!replacePropertyCurves(clip, curves)) {
7793
throw new Error('Failed to restore animation property curves.');
7894
}
7995
restoreEvents(clip, snapshot);
@@ -86,6 +102,86 @@ async function applyAnimationClipSnapshot(clip: AnimationClip, snapshot: IAnimat
86102
}
87103
}
88104

105+
async function hydrateAnimationAssetCurveValues(
106+
curves: IAnimationCurveDump[],
107+
pendingAssetLoads: PendingAnimationAssetLoads,
108+
): Promise<IAnimationCurveDump[]> {
109+
return await Promise.all(curves.map(async (curve) => {
110+
if (!Array.isArray(curve.keyframes) || curve.keyframes.length === 0) {
111+
return curve;
112+
}
113+
114+
let changed = false;
115+
const keyframes = await Promise.all(curve.keyframes.map(async (keyframe) => {
116+
const value = await hydrateAnimationAssetKeyframeValue(curve, keyframe, pendingAssetLoads);
117+
if (value === keyframe.dump.value) {
118+
return keyframe;
119+
}
120+
changed = true;
121+
return {
122+
...keyframe,
123+
dump: {
124+
...keyframe.dump,
125+
value: value as IAnimationValue,
126+
},
127+
};
128+
}));
129+
130+
return changed ? { ...curve, keyframes } : curve;
131+
}));
132+
}
133+
134+
async function hydrateAnimationAssetKeyframeValue(
135+
curve: IAnimationCurveDump,
136+
keyframe: IAnimationCurveKeyDump,
137+
pendingAssetLoads: PendingAnimationAssetLoads,
138+
): Promise<unknown> {
139+
const assetCtor = queryAnimationAssetKeyframeCtor(curve, keyframe);
140+
const value = keyframe.dump.value as unknown;
141+
if (!assetCtor || value === null || value === undefined || value instanceof assetCtor) {
142+
return value;
143+
}
144+
145+
const uuid = queryAnimationAssetUuid(value);
146+
if (!uuid) {
147+
return value;
148+
}
149+
150+
return await loadAnimationAssetOnce(assetCtor, uuid, pendingAssetLoads);
151+
}
152+
153+
function queryAnimationAssetKeyframeCtor(
154+
curve: IAnimationCurveDump,
155+
keyframe: IAnimationCurveKeyDump,
156+
): AnimationAssetCtor | null {
157+
if (keyframe.dump.type) {
158+
const keyframeCtor = queryAnimationAssetCtor({ type: { value: keyframe.dump.type } });
159+
if (keyframeCtor) {
160+
return keyframeCtor;
161+
}
162+
}
163+
return curve.type ? queryAnimationAssetCtor({ type: curve.type }) : null;
164+
}
165+
166+
function loadAnimationAssetOnce(
167+
assetCtor: AnimationAssetCtor,
168+
uuid: string,
169+
pendingAssetLoads: PendingAnimationAssetLoads,
170+
): Promise<Asset> {
171+
let ctorLoads = pendingAssetLoads.get(assetCtor);
172+
if (!ctorLoads) {
173+
ctorLoads = new Map();
174+
pendingAssetLoads.set(assetCtor, ctorLoads);
175+
}
176+
177+
let pending = ctorLoads.get(uuid);
178+
if (!pending) {
179+
pending = loadAnimationAssetValue(assetCtor, uuid);
180+
ctorLoads.set(uuid, pending);
181+
}
182+
return pending;
183+
}
184+
89185
export function animationClipSnapshotsEqual(left: IAnimationClipSnapshot, right: IAnimationClipSnapshot): boolean {
90186
return JSON.stringify(left) === JSON.stringify(right);
91187
}

src/core/scene/scene-process/service/animation/operation-normalizer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ async function normalizePropertyKeyOperation(
4545
const keyData = operation.keyData ?? operation.curveData;
4646
if (operation.value !== undefined) {
4747
const value = await normalizeProvidedAnimationPropertyOperationValue(context.rootNode, context.rootPath, operation, {
48-
queryNodeByUuid: (uuid) => getNodeByUuid(uuid),
49-
queryNodePath: (node) => getNodePath(node),
48+
queryNodeByUuid: getNodeByUuid,
49+
queryNodePath: getNodePath,
5050
});
5151
return { ...operation, keyData, value };
5252
}

src/core/scene/scene-process/service/animation/property-curve.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -364,23 +364,18 @@ function applyPropertyMetadata(
364364
}
365365

366366
function resolveRelativeNodePath(context: IPropertyCurveOperationContext, operation: IPropertyTarget): string | null {
367-
if (operation.nodeUuid) {
368-
return findRelativeNodePathByUuid(context.rootNode, operation.nodeUuid);
369-
}
370-
371367
const nodePath = normalizePath(operation.nodePath || '');
372-
if (!nodePath) {
373-
return '';
374-
}
375-
376-
const rootPath = normalizePath(context.rootPath);
377-
if (nodePath === rootPath) {
378-
return '';
379-
}
380-
if (rootPath && nodePath.startsWith(`${rootPath}/`)) {
381-
return nodePath.slice(rootPath.length + 1);
368+
if (nodePath) {
369+
const rootPath = normalizePath(context.rootPath);
370+
if (nodePath === rootPath) {
371+
return '';
372+
}
373+
if (rootPath && nodePath.startsWith(`${rootPath}/`)) {
374+
return nodePath.slice(rootPath.length + 1);
375+
}
376+
return context.rootNode.getChildByPath(nodePath) ? nodePath : null;
382377
}
383-
return context.rootNode.getChildByPath(nodePath) ? nodePath : null;
378+
return operation.nodeUuid ? findRelativeNodePathByUuid(context.rootNode, operation.nodeUuid) : '';
384379
}
385380

386381
function findRelativeNodePathByUuid(node: Node, uuid: string, prefix = ''): string | null {

src/core/scene/scene-process/service/animation/property-value.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,14 @@ function resolveOperationRelativeNodePath(
9191
operation: { nodeUuid?: string; nodePath?: string },
9292
options: { queryNodeByUuid: (uuid: string) => Node | null; queryNodePath: (node: Node) => string },
9393
): string | null {
94+
if (operation.nodePath) {
95+
return toRelativeNodePath(rootNode, rootPath, operation.nodePath);
96+
}
9497
const node = options.queryNodeByUuid(operation.nodeUuid || '');
9598
if (node) {
9699
return toRelativeNodePath(rootNode, rootPath, options.queryNodePath(node));
97100
}
98-
return toRelativeNodePath(rootNode, rootPath, operation.nodePath || '');
101+
return toRelativeNodePath(rootNode, rootPath, '');
99102
}
100103

101104
function toRelativeNodePath(rootNode: Node, rootPath: string, nodePath: string): string | null {

src/core/scene/scene-process/service/animation/service-save.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,26 @@ export async function saveAnimationServiceClip(options: {
1111
session: IAnimationSession;
1212
rootNode: Node;
1313
clip: AnimationClip;
14+
target?: string;
1415
}): Promise<boolean> {
15-
const { session, rootNode, clip } = options;
16+
const { session, rootNode, clip, target } = options;
17+
if (target) {
18+
if (isSkeletonClip(session.clipUuid, rootNode)) {
19+
throw new Error('Save As is not supported for skeletal animation clips.');
20+
}
21+
22+
const content = EditorExtends.serialize(clip);
23+
const assetInfo = await Rpc.getInstance().request('assetManager', 'createAsset', [{
24+
target,
25+
content,
26+
overwrite: true,
27+
}]);
28+
if (!assetInfo) {
29+
throw new Error(`Animation clip Save As failed: ${target}`);
30+
}
31+
return true;
32+
}
33+
1634
if (isSkeletonClip(session.clipUuid, rootNode)) {
1735
await saveSkeletonAnimationMeta(session.clipUuid, clip);
1836
return true;

src/core/scene/scene-process/service/animation/service-target.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,6 @@ export function resolveAnimationTargetNode(
7676
}
7777

7878
export function resolveAnimationFrameQueryNode(options: IAnimationQueryPropertyValueAtFrameOptions, session: IAnimationSession): Node {
79-
const nodeByUuid = getNodeByUuid(options.nodeUuid || '');
80-
if (nodeByUuid) {
81-
return nodeByUuid;
82-
}
83-
8479
if (options.nodePath) {
8580
const path = options.nodePath;
8681
if (path === session.rootPath || path.startsWith(`${session.rootPath}/`)) {
@@ -99,7 +94,13 @@ export function resolveAnimationFrameQueryNode(options: IAnimationQueryPropertyV
9994
if (nodeByPath) {
10095
return nodeByPath;
10196
}
102-
} else {
97+
}
98+
99+
const nodeByUuid = getNodeByUuid(options.nodeUuid || '');
100+
if (nodeByUuid) {
101+
return nodeByUuid;
102+
}
103+
if (!options.nodePath) {
103104
const rootNode = getNodeByPath(session.rootPath);
104105
if (rootNode) {
105106
return rootNode;

0 commit comments

Comments
 (0)