Skip to content

Commit 710c6a8

Browse files
committed
fix: keep rect alias fallbacks transient
Removed attr fallback targets must be transient. Generic state planning cannot derive rect geometry aliases from key names. Rect now computes width/height/x1/y1 fallbacks from the full static target snapshot. Those fallback values remain in extraAnimateAttrs and never enter static truth. Constraint: D3 static truth remains baseAttributes plus resolvedStatePatch. Constraint: Animation fallback values must stay transient. Rejected: Keep global alias suppression | leaks rect semantics into generic planning. Rejected: Write computed aliases into target attrs | would pollute static truth. Confidence: high Scope-risk: moderate Directive: Geometry defaults do not belong in StateTransitionOrchestrator. Directive: Shape aliases must use full static target context. Tested: packages/vrender-core rushx test --runInBand Tested: packages/vrender-animate rushx test --runInBand Tested: rush compile -t @visactor/vrender-core -t @visactor/vrender-animate Tested: eslint targeted changed files Tested: git diff --check
1 parent e7dd936 commit 710c6a8

5 files changed

Lines changed: 450 additions & 31 deletions

File tree

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

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,204 @@ describe('D3 pre-handoff animation runtime', () => {
426426
expect(rect.getFinalAttribute().fillOpacity).toBeUndefined();
427427
});
428428

429+
test('clearing explicit height state on y/y1 rect animates to computed layout height', () => {
430+
const { group, ticker, graphicService } = createStageHarness('state-runtime-clear-explicit-height-alias');
431+
const rect = createRect({
432+
x: 0,
433+
y: 0,
434+
y1: 100,
435+
width: 20,
436+
height: undefined,
437+
visible: true
438+
} as any);
439+
bindGraphicService(rect as any, graphicService);
440+
rect.setFinalAttributes({ ...rect.attribute });
441+
group.appendChild(rect);
442+
443+
rect.states = {
444+
hover: {
445+
height: 60
446+
}
447+
} as any;
448+
rect.stateAnimateConfig = {
449+
duration: 100,
450+
easing: 'linear'
451+
} as any;
452+
453+
rect.useStates(['hover'], true);
454+
tick(ticker, 100);
455+
expect(rect.attribute.height).toBe(60);
456+
expect((rect as any).baseAttributes.height).toBeUndefined();
457+
458+
rect.useStates([], true);
459+
tick(ticker, 50);
460+
461+
expect(rect.attribute.height).toBeGreaterThan(60);
462+
expect(rect.attribute.height).toBeLessThan(100);
463+
expect(boundsSize(rect).height).toBeGreaterThan(60);
464+
expect((rect as any).baseAttributes.height).toBeUndefined();
465+
expect(rect.getFinalAttribute().height).toBeUndefined();
466+
467+
tick(ticker, 100);
468+
expect(rect.attribute.height).toBeUndefined();
469+
expect(rect.attribute.y).toBe(0);
470+
expect(rect.attribute.y1).toBe(100);
471+
expect(boundsSize(rect).height).toBeGreaterThan(90);
472+
expect((rect as any).baseAttributes.height).toBeUndefined();
473+
expect(rect.getFinalAttribute().height).toBeUndefined();
474+
});
475+
476+
test('clearing explicit width state on x/x1 rect animates to computed layout width', () => {
477+
const { group, ticker, graphicService } = createStageHarness('state-runtime-clear-explicit-width-alias');
478+
const rect = createRect({
479+
x: 0,
480+
x1: 100,
481+
y: 0,
482+
height: 20,
483+
width: undefined,
484+
visible: true
485+
} as any);
486+
bindGraphicService(rect as any, graphicService);
487+
rect.setFinalAttributes({ ...rect.attribute });
488+
group.appendChild(rect);
489+
490+
rect.states = {
491+
hover: {
492+
width: 60
493+
}
494+
} as any;
495+
rect.stateAnimateConfig = {
496+
duration: 100,
497+
easing: 'linear'
498+
} as any;
499+
500+
rect.useStates(['hover'], true);
501+
tick(ticker, 100);
502+
expect(rect.attribute.width).toBe(60);
503+
expect((rect as any).baseAttributes.width).toBeUndefined();
504+
505+
rect.useStates([], true);
506+
tick(ticker, 50);
507+
508+
expect(rect.attribute.width).toBeGreaterThan(60);
509+
expect(rect.attribute.width).toBeLessThan(100);
510+
expect(boundsSize(rect).width).toBeGreaterThan(60);
511+
expect((rect as any).baseAttributes.width).toBeUndefined();
512+
expect(rect.getFinalAttribute().width).toBeUndefined();
513+
514+
tick(ticker, 100);
515+
expect(rect.attribute.width).toBeUndefined();
516+
expect(rect.attribute.x).toBe(0);
517+
expect(rect.attribute.x1).toBe(100);
518+
expect(boundsSize(rect).width).toBeGreaterThan(90);
519+
expect((rect as any).baseAttributes.width).toBeUndefined();
520+
expect(rect.getFinalAttribute().width).toBeUndefined();
521+
});
522+
523+
test('switching from explicit height state to another state animates to computed layout height', () => {
524+
const { group, ticker, graphicService } = createStageHarness('state-runtime-switch-explicit-height-alias');
525+
const rect = createRect({
526+
x: 0,
527+
y: 0,
528+
y1: 100,
529+
width: 20,
530+
height: undefined,
531+
visible: true
532+
} as any);
533+
bindGraphicService(rect as any, graphicService);
534+
rect.setFinalAttributes({ ...rect.attribute });
535+
group.appendChild(rect);
536+
537+
rect.states = {
538+
hover: {
539+
height: 60
540+
},
541+
selected: {
542+
fillOpacity: 0.5
543+
}
544+
} as any;
545+
rect.stateAnimateConfig = {
546+
duration: 100,
547+
easing: 'linear'
548+
} as any;
549+
550+
rect.useStates(['hover'], true);
551+
tick(ticker, 100);
552+
expect(rect.attribute.height).toBe(60);
553+
554+
rect.useStates(['selected'], true);
555+
tick(ticker, 50);
556+
557+
expect(rect.attribute.height).toBeGreaterThan(60);
558+
expect(rect.attribute.height).toBeLessThan(100);
559+
expect(rect.attribute.fillOpacity).toBeGreaterThan(0.5);
560+
expect(rect.attribute.fillOpacity).toBeLessThan(1);
561+
expect(boundsSize(rect).height).toBeGreaterThan(60);
562+
expect((rect as any).baseAttributes.height).toBeUndefined();
563+
expect(rect.getFinalAttribute().height).toBeUndefined();
564+
565+
tick(ticker, 100);
566+
expect(rect.attribute.height).toBeUndefined();
567+
expect(rect.attribute.y).toBe(0);
568+
expect(rect.attribute.y1).toBe(100);
569+
expect(rect.attribute.fillOpacity).toBe(0.5);
570+
expect(boundsSize(rect).height).toBeGreaterThan(90);
571+
expect((rect as any).baseAttributes.height).toBeUndefined();
572+
expect(rect.getFinalAttribute().height).toBeUndefined();
573+
});
574+
575+
test('switching from explicit width state to another state animates to computed layout width', () => {
576+
const { group, ticker, graphicService } = createStageHarness('state-runtime-switch-explicit-width-alias');
577+
const rect = createRect({
578+
x: 0,
579+
x1: 100,
580+
y: 0,
581+
height: 20,
582+
width: undefined,
583+
visible: true
584+
} as any);
585+
bindGraphicService(rect as any, graphicService);
586+
rect.setFinalAttributes({ ...rect.attribute });
587+
group.appendChild(rect);
588+
589+
rect.states = {
590+
hover: {
591+
width: 60
592+
},
593+
selected: {
594+
fillOpacity: 0.5
595+
}
596+
} as any;
597+
rect.stateAnimateConfig = {
598+
duration: 100,
599+
easing: 'linear'
600+
} as any;
601+
602+
rect.useStates(['hover'], true);
603+
tick(ticker, 100);
604+
expect(rect.attribute.width).toBe(60);
605+
606+
rect.useStates(['selected'], true);
607+
tick(ticker, 50);
608+
609+
expect(rect.attribute.width).toBeGreaterThan(60);
610+
expect(rect.attribute.width).toBeLessThan(100);
611+
expect(rect.attribute.fillOpacity).toBeGreaterThan(0.5);
612+
expect(rect.attribute.fillOpacity).toBeLessThan(1);
613+
expect(boundsSize(rect).width).toBeGreaterThan(60);
614+
expect((rect as any).baseAttributes.width).toBeUndefined();
615+
expect(rect.getFinalAttribute().width).toBeUndefined();
616+
617+
tick(ticker, 100);
618+
expect(rect.attribute.width).toBeUndefined();
619+
expect(rect.attribute.x).toBe(0);
620+
expect(rect.attribute.x1).toBe(100);
621+
expect(rect.attribute.fillOpacity).toBe(0.5);
622+
expect(boundsSize(rect).width).toBeGreaterThan(90);
623+
expect((rect as any).baseAttributes.width).toBeUndefined();
624+
expect(rect.getFinalAttribute().width).toBeUndefined();
625+
});
626+
429627
test('animate.to restores static truth after completion and keeps baseAttributes untouched', () => {
430628
const { group, ticker, graphicService } = createStageHarness('self-to');
431629
const rect = createAnimatedRect(graphicService);

packages/vrender-core/__tests__/unit/graphic/state-transition-orchestrator.test.ts

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,27 @@ describe('StateTransitionOrchestrator', () => {
204204
});
205205
});
206206

207+
test('should not globally treat geometry names as optional aliases', () => {
208+
const orchestrator = new StateTransitionOrchestrator<any>();
209+
210+
const plan = orchestrator.analyzeTransition(
211+
{},
212+
{
213+
height: undefined
214+
},
215+
[],
216+
true,
217+
{
218+
isClear: true,
219+
getDefaultAttribute: (key: string) => (key === 'height' ? 0 : undefined)
220+
}
221+
);
222+
223+
expect(plan.animateAttrs).toEqual({
224+
height: 0
225+
});
226+
});
227+
207228
test('should not materialize undefined rect geometry aliases to theme defaults during clear transitions', () => {
208229
const orchestrator = new StateTransitionOrchestrator<any>();
209230

@@ -235,16 +256,59 @@ describe('StateTransitionOrchestrator', () => {
235256
}
236257
);
237258

238-
expect(verticalPlan.animateAttrs).toEqual({
259+
expect(verticalPlan.animateAttrs).toHaveProperty('height', 0);
260+
expect(verticalPlan.animateAttrs).toHaveProperty('x1', 0);
261+
262+
const verticalRectPlan = orchestrator.analyzeTransition(
263+
{},
264+
{
265+
lineWidth: 0,
266+
fillOpacity: undefined,
267+
x: 150,
268+
y: 0,
269+
y1: 320,
270+
width: 140,
271+
x1: undefined,
272+
height: undefined
273+
},
274+
[],
275+
true,
276+
{
277+
isClear: true,
278+
getDefaultAttribute: (key: string) => {
279+
if (key === 'lineWidth' || key === 'height' || key === 'x1') {
280+
return 0;
281+
}
282+
if (key === 'fillOpacity') {
283+
return 1;
284+
}
285+
return undefined;
286+
},
287+
shouldSkipDefaultAttribute: (key: string) => key === 'height' || key === 'x1'
288+
}
289+
);
290+
291+
expect(verticalRectPlan.animateAttrs).toEqual({
239292
lineWidth: 0,
240293
fillOpacity: 1,
241294
x: 150,
242295
y: 0,
243296
y1: 320,
244297
width: 140
245298
});
246-
expect(verticalPlan.animateAttrs).not.toHaveProperty('height');
247-
expect(verticalPlan.animateAttrs).not.toHaveProperty('x1');
299+
expect(verticalRectPlan.animateAttrs).not.toHaveProperty('height');
300+
expect(verticalRectPlan.animateAttrs).not.toHaveProperty('x1');
301+
302+
expect(verticalPlan.animateAttrs).toEqual({
303+
lineWidth: 0,
304+
fillOpacity: 1,
305+
x: 150,
306+
y: 0,
307+
y1: 320,
308+
width: 140,
309+
x1: 0,
310+
height: 0
311+
});
248312

249313
const horizontalPlan = orchestrator.analyzeTransition(
250314
{},
@@ -274,16 +338,59 @@ describe('StateTransitionOrchestrator', () => {
274338
}
275339
);
276340

277-
expect(horizontalPlan.animateAttrs).toEqual({
341+
expect(horizontalPlan.animateAttrs).toHaveProperty('width', 0);
342+
expect(horizontalPlan.animateAttrs).toHaveProperty('y1', 0);
343+
344+
const horizontalRectPlan = orchestrator.analyzeTransition(
345+
{},
346+
{
347+
lineWidth: 0,
348+
fillOpacity: undefined,
349+
x: 10,
350+
x1: 210,
351+
y: 5,
352+
height: 40,
353+
width: undefined,
354+
y1: undefined
355+
},
356+
[],
357+
true,
358+
{
359+
isClear: true,
360+
getDefaultAttribute: (key: string) => {
361+
if (key === 'lineWidth' || key === 'width' || key === 'y1') {
362+
return 0;
363+
}
364+
if (key === 'fillOpacity') {
365+
return 1;
366+
}
367+
return undefined;
368+
},
369+
shouldSkipDefaultAttribute: (key: string) => key === 'width' || key === 'y1'
370+
}
371+
);
372+
373+
expect(horizontalRectPlan.animateAttrs).toEqual({
278374
lineWidth: 0,
279375
fillOpacity: 1,
280376
x: 10,
281377
x1: 210,
282378
y: 5,
283379
height: 40
284380
});
285-
expect(horizontalPlan.animateAttrs).not.toHaveProperty('width');
286-
expect(horizontalPlan.animateAttrs).not.toHaveProperty('y1');
381+
expect(horizontalRectPlan.animateAttrs).not.toHaveProperty('width');
382+
expect(horizontalRectPlan.animateAttrs).not.toHaveProperty('y1');
383+
384+
expect(horizontalPlan.animateAttrs).toEqual({
385+
lineWidth: 0,
386+
fillOpacity: 1,
387+
x: 10,
388+
x1: 210,
389+
y: 5,
390+
height: 40,
391+
width: 0,
392+
y1: 0
393+
});
287394
});
288395

289396
test('should allow animateConfig to append additional no-animate attrs', () => {
@@ -348,4 +455,32 @@ describe('StateTransitionOrchestrator', () => {
348455
});
349456
expect(plan.targetAttrs).not.toHaveProperty('fillOpacity');
350457
});
458+
459+
test('should allow extra animation attrs to replace an undefined clear target transiently', () => {
460+
const orchestrator = new StateTransitionOrchestrator<any>();
461+
462+
const plan = orchestrator.analyzeTransition(
463+
{},
464+
{
465+
height: undefined
466+
},
467+
[],
468+
true,
469+
{
470+
isClear: true,
471+
getDefaultAttribute: (key: string) => (key === 'height' ? 0 : undefined),
472+
shouldSkipDefaultAttribute: (key: string) => key === 'height',
473+
extraAnimateAttrs: {
474+
height: 100
475+
}
476+
}
477+
);
478+
479+
expect(plan.targetAttrs).toEqual({
480+
height: undefined
481+
});
482+
expect(plan.animateAttrs).toEqual({
483+
height: 100
484+
});
485+
});
351486
});

0 commit comments

Comments
 (0)