Skip to content

Commit c6ffbe8

Browse files
committed
fix: support split update animation starts
VChart bar update can split one diffAttrs handoff into channel subset animations. The first fix matched the snapshot by object identity, so split planner subsets missed it. Consume the Graphic-owned start snapshot by key while preserving stale-snapshot guards. Constraint: Split planners replace context.diffAttrs before running each update step Rejected: Match only original diffAttrs identity | misses valid channel subset planners Rejected: Store VRender snapshot on consumer context | leaks internals to VChart/VGrammar Confidence: high Scope-risk: moderate Directive: Keep update start snapshots on Graphic and consume them only by matching keys Tested: vrender-animate animation-runtime-attribute targeted test Tested: vrender-animate rushx test Tested: vrender-core state animation and transition targeted tests Tested: rush compile for vrender-core and vrender-animate Tested: targeted eslint; perf smoke; git diff --check Not-tested: VChart browser consumer smoke
1 parent 53e7418 commit c6ffbe8

3 files changed

Lines changed: 99 additions & 13 deletions

File tree

packages/vrender-animate/__tests__/unit/animation-runtime-attribute.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,85 @@ describe('D3 pre-handoff animation runtime', () => {
991991
expect(rect.getFinalAttribute().y).toBe(next.y);
992992
});
993993

994+
test('executor update consumes start snapshots after split planners replace diffAttrs by channel subset', () => {
995+
const { group, ticker, graphicService } = createStageHarness('executor-update-split-start-snapshot');
996+
const initial = {
997+
x: 10,
998+
y: 50,
999+
width: 20,
1000+
height: 10,
1001+
visible: true
1002+
};
1003+
const next = {
1004+
...initial,
1005+
x: 100,
1006+
y: 3,
1007+
height: 34
1008+
};
1009+
const rect = createRect(initial);
1010+
bindGraphicService(rect as any, graphicService);
1011+
rect.setFinalAttributes(initial);
1012+
group.appendChild(rect);
1013+
1014+
const diffAttrs = {
1015+
x: next.x,
1016+
y: next.y,
1017+
height: next.height
1018+
};
1019+
(rect as any).context = {
1020+
animationState: 'update',
1021+
diffState: 'update',
1022+
data: [{ id: 'China_share_global_co2' }],
1023+
diffAttrs,
1024+
finalAttrs: next
1025+
};
1026+
1027+
rect.setAttributesAndPreventAnimate(diffAttrs);
1028+
rect.setFinalAttributes(next);
1029+
1030+
(rect as any).context.diffAttrs = {
1031+
y: next.y,
1032+
height: next.height
1033+
};
1034+
(rect as any).executeAnimation({
1035+
type: 'update',
1036+
duration: 300,
1037+
easing: 'linear'
1038+
});
1039+
1040+
tick(ticker, 150);
1041+
expect(rect.attribute.y).toBeGreaterThan(next.y);
1042+
expect(rect.attribute.y).toBeLessThan(initial.y);
1043+
expect(rect.attribute.height).toBeGreaterThan(initial.height);
1044+
expect(rect.attribute.height).toBeLessThan(next.height);
1045+
1046+
tick(ticker, 150);
1047+
expect(rect.attribute.y).toBe(next.y);
1048+
expect(rect.attribute.height).toBe(next.height);
1049+
1050+
(rect as any).context.diffAttrs = {
1051+
x: next.x
1052+
};
1053+
(rect as any).executeAnimation({
1054+
type: 'update',
1055+
duration: 300,
1056+
easing: 'linear'
1057+
});
1058+
1059+
tick(ticker, 150);
1060+
expect(rect.attribute.x).toBeGreaterThan(initial.x);
1061+
expect(rect.attribute.x).toBeLessThan(next.x);
1062+
1063+
tick(ticker, 150);
1064+
expect(rect.attribute.x).toBe(next.x);
1065+
expect((rect as any).baseAttributes.x).toBe(next.x);
1066+
expect((rect as any).baseAttributes.y).toBe(next.y);
1067+
expect((rect as any).baseAttributes.height).toBe(next.height);
1068+
expect(rect.getFinalAttribute().x).toBe(next.x);
1069+
expect(rect.getFinalAttribute().y).toBe(next.y);
1070+
expect(rect.getFinalAttribute().height).toBe(next.height);
1071+
});
1072+
9941073
test('superseded executor update cannot commit stale layout when it ends late', () => {
9951074
const { group, ticker, graphicService } = createStageHarness('executor-update-stale-end');
9961075
const threeItemLayout = {

packages/vrender-animate/src/custom/update.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,13 @@ export class Update extends ACustomAnimate<Record<string, number>> {
6666
const targetContext = this.target.context ?? ({} as any);
6767
let { diffAttrs = {} } = targetContext;
6868
const { options } = this.params as any;
69-
const rawDiffAttrs = diffAttrs;
7069

7170
diffAttrs = filterExcludedChannels(diffAttrs, options?.excludeChannels);
7271

7372
this.props = diffAttrs;
7473
const consumeTransientFromAttrs = (this.target as any).consumeTransientFromAttrsBeforePreventAnimate;
7574
this.updateFromAttrs =
76-
typeof consumeTransientFromAttrs === 'function'
77-
? consumeTransientFromAttrs.call(this.target, rawDiffAttrs, diffAttrs)
78-
: null;
75+
typeof consumeTransientFromAttrs === 'function' ? consumeTransientFromAttrs.call(this.target, diffAttrs) : null;
7976
this.clipPathSyncKeys = Object.keys(diffAttrs).filter(key => clipPathGeometryAttrs[key]);
8077
this.clipPathSyncDisabled = !this.clipPathSyncKeys.length;
8178
this.syncParentClipPathToTarget();

packages/vrender-core/src/graphic/graphic.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,10 +1343,14 @@ export abstract class Graphic<T extends Partial<IGraphicAttribute> = Partial<IGr
13431343
return;
13441344
}
13451345

1346-
let fromAttrs: Record<string, any> | null =
1347-
this.transientFromAttrsBeforePreventAnimateDiffAttrs === diffAttrs
1348-
? this.transientFromAttrsBeforePreventAnimate ?? null
1349-
: null;
1346+
const sameDiffAttrs = this.transientFromAttrsBeforePreventAnimateDiffAttrs === diffAttrs;
1347+
let fromAttrs: Record<string, any> | null = sameDiffAttrs
1348+
? this.transientFromAttrsBeforePreventAnimate ?? null
1349+
: null;
1350+
if (!sameDiffAttrs) {
1351+
this.transientFromAttrsBeforePreventAnimate = null;
1352+
this.transientFromAttrsBeforePreventAnimateDiffAttrs = null;
1353+
}
13501354
let captured = false;
13511355
for (let i = 0; i < keys.length; i++) {
13521356
const key = keys[i];
@@ -1371,14 +1375,20 @@ export abstract class Graphic<T extends Partial<IGraphicAttribute> = Partial<IGr
13711375
}
13721376
}
13731377

1374-
protected consumeTransientFromAttrsBeforePreventAnimate(
1375-
rawDiffAttrs: Record<string, any>,
1376-
diffAttrs: Record<string, any>
1377-
): Record<string, any> | null {
1378+
protected consumeTransientFromAttrsBeforePreventAnimate(diffAttrs: Record<string, any>): Record<string, any> | null {
13781379
const transientFromAttrs = this.transientFromAttrsBeforePreventAnimate;
1379-
if (!transientFromAttrs || this.transientFromAttrsBeforePreventAnimateDiffAttrs !== rawDiffAttrs) {
1380+
const sourceDiffAttrs = this.transientFromAttrsBeforePreventAnimateDiffAttrs;
1381+
if (!transientFromAttrs || !sourceDiffAttrs) {
13801382
return null;
13811383
}
1384+
for (const key in diffAttrs) {
1385+
if (
1386+
Object.prototype.hasOwnProperty.call(diffAttrs, key) &&
1387+
(!Object.prototype.hasOwnProperty.call(sourceDiffAttrs, key) || !isEqual(sourceDiffAttrs[key], diffAttrs[key]))
1388+
) {
1389+
return null;
1390+
}
1391+
}
13821392

13831393
let fromAttrs: Record<string, any> | null = null;
13841394
let remaining = false;

0 commit comments

Comments
 (0)