Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/assets/guide/en/asd/Upgrade_Guide/Upgrade_to_1_1_0.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ VRender 1.1.0 makes component, plugin, and custom animation registration boundar
- Custom animation can register `basic`, `richtext`, `disappear`, `story`, or the full surface.
- The `poptip` plugin is explicit. Use `loadPoptip()` or `installPoptipToApp(app)`; it is not part of the default bootstrap.

### Custom Runtime Contributions Use a Runtime Installer

If an upper layer injects renderer contributions, draw interceptors, or picker contributions, use the runtime contribution installer instead of directly managing VRender DI/container refresh order:

```ts
import { installRuntimeContributionModule } from '@visactor/vrender/entries/runtime-contribution';

installRuntimeContributionModule(customContributionModule, {
targets: ['graphic-renderer', 'draw-contribution']
});
```

When `app` is omitted, VRender records the module as a pending runtime contribution and refreshes any shared apps that already exist. It does not create a new app. New apps created later install the pending module after their default bootstrap, so replacement modules can see built-in contribution tokens before calling `rebind`.

When an app already exists or is passed by the host, pass it explicitly:

```ts
installRuntimeContributionModule(customContributionModule, {
app,
targets: ['graphic-renderer']
});
```

Use `{ picker: CanvasPickerContribution }` in `targets` when the module registers picker contributions. The same module object is loaded once per binding context, so it is safe for an upper layer to call the installer before app creation and again after resolving the app.

## Breaking Changes

- `graphic.stateProxy` has been removed. Use `StateDefinition.resolver` for dynamic state styles.
Expand Down Expand Up @@ -457,6 +482,15 @@ installPoptipToApp(app);

This API requires an explicit `app` argument so caller mistakes fail early. If the App has not been created yet and you only need to register poptip capability globally, use `loadPoptip()`.

### When should I use `installRuntimeContributionModule()`?

Use it for upper-layer custom renderer contribution modules, for example table split-border rendering, custom draw interceptors, or chart/table picker extensions. It handles both states:

- no app yet: save the runtime contribution module for future app bootstrap and refresh existing shared apps if any;
- app already exists: reinstall the requested app registry targets for that app.

It is not a replacement for default VRender bootstrap and does not auto-load arbitrary optional features.

## Not Included in the 1.1.0 Stable Contract

- Tier 1 claims for Taro, Feishu, or TT.
Expand Down
25 changes: 25 additions & 0 deletions docs/assets/guide/zh/asd/Upgrade_Guide/Upgrade_to_1_1_0.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ export function registerVRenderBasicAnimationProfile() {
- custom animation 可以选择 `basic`、`richtext`、`disappear`、`story` 或 full register。
- `poptip` 插件需要通过 `loadPoptip()` 或 `installPoptipToApp(app)` 显式触发,不属于默认 bootstrap。

### 自定义 Runtime Contribution 使用统一 Installer

如果上层需要注入 renderer contribution、draw interceptor 或 picker contribution,应使用 VRender 的 runtime contribution installer,而不是自己维护 DI/container 刷新顺序:

```ts
import { installRuntimeContributionModule } from '@visactor/vrender/entries/runtime-contribution';

installRuntimeContributionModule(customContributionModule, {
targets: ['graphic-renderer', 'draw-contribution']
});
```

未传 `app` 时,VRender 会把 module 记录为 pending runtime contribution,并刷新当前已经存在的 shared app。它不会创建新 app;后续新建 app 会在默认 bootstrap 完成后安装这份 pending module,保证 replacement module 在执行 `rebind` 前可以看到内置 contribution token。

如果 app 已经存在,或由宿主传入,应显式传入 app:

```ts
installRuntimeContributionModule(customContributionModule, {
app,
targets: ['graphic-renderer']
});
```

如果 module 注册 picker contribution,在 `targets` 中加入 `{ picker: CanvasPickerContribution }`。同一个 module object 对同一个 binding context 只会加载一次,因此上层可以在 app 创建前调用一次,并在拿到 app 后再次调用。

## 破坏性变化

- `graphic.stateProxy` 已移除。动态状态样式应迁移到 `StateDefinition.resolver`。
Expand Down
62 changes: 62 additions & 0 deletions docs/refactor/bundle-size/VRENDER_ON_DEMAND_CAPABILITY_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,68 @@ export function registerVRenderStoryCustomAnimations() {
- 如果使用 static profile,上层应通过不同入口文件、构建条件或明确 profile 导出承载选择,而不是在一个文件里静态导入所有 optional group。
- 如果用户选择发生在运行时,使用下一节的动态加载方式。

## 自定义 Runtime Contribution Module

上层如果需要注入 renderer contribution、draw interceptor 或 picker contribution,应使用 VRender 的 runtime contribution installer,而不是直接维护 legacy/runtime container 与 app registry 的刷新顺序。

Public entry:

```ts
import { installRuntimeContributionModule } from '@visactor/vrender/entries/runtime-contribution';
```

基础用法:

```ts
installRuntimeContributionModule(customContributionModule, {
targets: ['graphic-renderer']
});
```

语义:

- 如果未传 `app`,module 会登记为 pending runtime contribution。VRender 不会创建新 app,也不会在默认 graphics 尚未安装前提前执行 replacement module。
- 当前已经存在的 shared app 会立即安装该 module。
- 后续通过 `@visactor/vrender` app creator 创建的新 app,会在默认 bootstrap 完成后安装 pending module,保证 replacement module 可以看到内置 contribution token。
- 默认也会安装到 legacy binding context,保持旧兼容路径可用;需要禁用时传 `legacy: false`。
- 如果传入 `app`,VRender 只把 module 安装到该 app,不会同时扫描其他 shared app。
- 同一个 module object 对同一个 binding context 只会加载一次,避免 append-style contribution 重复绑定;app-scoped reinstall 仍可以重复调用。

`targets` 控制已有 app 需要刷新的 registry:

| Target | 作用 |
| --- | --- |
| `'graphic-renderer'` | 重新安装 runtime graphic renderer,并调用 renderer `reInit()` 刷新 render contribution |
| `'draw-contribution'` | 重新安装 draw interceptor contribution |
| `{ picker: CanvasPickerContribution }` | 重新安装指定 picker contribution |

VTable 这类同时注入 rect/group/image/text render contribution、draw interceptor 和 chart picker 的 profile,可以收口成:

```ts
import { CanvasPickerContribution } from '@visactor/vrender-kits/picker/contributions/constants';
import { installRuntimeContributionModule } from '@visactor/vrender/entries/runtime-contribution';

export function installVTableRuntimeContributions(app?: IApp) {
installRuntimeContributionModule(splitModule, {
app,
targets: ['graphic-renderer', 'draw-contribution', { picker: CanvasPickerContribution }]
});

installRuntimeContributionModule(textMeasureModule, {
app,
targets: ['graphic-renderer']
});
}
```

推荐调用方式:

1. 在 profile 顶层调用一次,覆盖 app 尚未创建时的 runtime 状态。
2. 在拿到 app 后、`app.createStage()` 前再调用一次,覆盖 app 已存在或 shared app 被其他上层提前创建的场景。
3. 不要依赖顶层 import 顺序来判断 renderer 是否已经安装;该判断由 VRender installer 承担。
4. 不要把业务 contribution 放进 VRender 默认 bootstrap。full/default 行为保持完整,但业务可选能力应由上层 profile 显式安装。
5. 业务方如果要替换内置 contribution,仍应使用稳定 token 的 `rebind(Token).to(CustomContribution)`;不要只追加同类型 contribution,否则内置 contribution 仍会参与绘制。

## 上层动态加载方式

如果上层希望把 optional 能力拆成异步 chunk,可以使用动态 import。推荐仍然保留一个统一入口:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ describe('AnimateExtension close-out', () => {

target.restoreStaticAttribute();

expect(restore).toHaveBeenCalledWith({ type: AttributeUpdateType.ANIMATE_END });
expect(restore).toHaveBeenCalledWith({ type: AttributeUpdateType.ANIMATE_END }, undefined);
});

test('should pass excluded key table to the standard static truth restore path', () => {
const restore = jest.fn();
const target: any = createTarget({
_restoreAttributeFromStaticTruth: restore
});

target.restoreStaticAttribute({ x: true });

expect(restore).toHaveBeenCalledWith({ type: AttributeUpdateType.ANIMATE_END }, { x: true });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,77 @@ describe('D3 pre-handoff animation runtime', () => {
expect(rect.getFinalAttribute().y).toBeCloseTo(200, 6);
});

test('parallel update siblings should not restore active sibling attrs when the shorter channel ends', () => {
const { group, ticker, graphicService } = createStageHarness('parallel-update-sibling-restore');
const initial = {
x: 100,
y: 40,
width: 10,
height: 10,
visible: true
};
const finalAttrs = {
...initial,
x: 400,
y: 200
};
const rect = createRect(initial);
bindGraphicService(rect as any, graphicService);
rect.setFinalAttributes(initial);
group.appendChild(rect);

const diffAttrs = {
x: finalAttrs.x,
y: finalAttrs.y
};
(rect as any).context = {
diffState: 'update',
animationState: 'update',
data: [{ id: 'parallel-update' }],
diffAttrs,
finalAttrs
};
rect.setFinalAttributes(finalAttrs);

(rect as any).applyAnimationState(
['update'],
[
{
name: 'update_0',
animation: [
{
type: 'update',
duration: 1000,
easing: 'linear',
options: { excludeChannels: ['y'] }
},
{
channel: ['y'],
duration: 600,
easing: 'linear'
}
]
}
]
);

tick(ticker, 599);
expect(rect.attribute.x).toBeCloseTo(279.7, 5);
expect(rect.attribute.y).toBeGreaterThan(initial.y);
expect(rect.attribute.y).toBeLessThan(finalAttrs.y);

tick(ticker, 2);
expect(rect.attribute.x).toBeCloseTo(280.3, 5);
expect(rect.attribute.x).toBeGreaterThan(initial.x);
expect(rect.attribute.y).toBeCloseTo(finalAttrs.y, 5);

tick(ticker, 399);
expect(rect.attribute.x).toBeCloseTo(400, 5);
expect(rect.attribute.y).toBeCloseTo(finalAttrs.y, 5);
expect((rect as any).baseAttributes.x).toBeCloseTo(400, 5);
expect((rect as any).baseAttributes.y).toBeCloseTo(finalAttrs.y, 5);
});

test('non-animation update commits diffAttrs without going through animation executor', () => {
const oldAttrs = {
x: 0,
Expand Down
4 changes: 2 additions & 2 deletions packages/vrender-animate/src/animate-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export class AnimateExtension {
target.setAttributesAndPreventAnimate(finalAttribute, false, { type: AttributeUpdateType.ANIMATE_BIND });
}

restoreStaticAttribute(): void {
(this as any)._restoreAttributeFromStaticTruth({ type: AttributeUpdateType.ANIMATE_END });
restoreStaticAttribute(excludedKeys?: Record<string, true>): void {
(this as any)._restoreAttributeFromStaticTruth({ type: AttributeUpdateType.ANIMATE_END }, excludedKeys);
}

/**
Expand Down
42 changes: 40 additions & 2 deletions packages/vrender-animate/src/animate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,44 @@ function removeKeysFromRecord<T extends Record<string, any> | undefined>(record:
return nextRecord as T;
}

type ExcludedAttrKeys = Record<string, true>;

function collectActiveSiblingAttrKeys(target: any, currentAnimate: IAnimate): ExcludedAttrKeys | undefined {
const getTrackedAnimates = target?.getTrackedAnimates;
if (typeof getTrackedAnimates === 'function') {
const trackedAnimates = getTrackedAnimates.call(target) as Map<string | number, IAnimate> | undefined;
if (trackedAnimates && trackedAnimates.size <= 1) {
return undefined;
}
}

const forEachTrackedAnimate = target?.forEachTrackedAnimate;
if (typeof forEachTrackedAnimate !== 'function') {
return undefined;
}

let keys: ExcludedAttrKeys | undefined;
const currentEndProps = currentAnimate.getEndProps?.();
forEachTrackedAnimate.call(target, (animate: IAnimate) => {
if (animate === currentAnimate || animate.status === AnimateStatus.END) {
return;
}
const endProps = animate.getEndProps?.();
if (!endProps) {
return;
}
for (const key in endProps) {
if (
Object.prototype.hasOwnProperty.call(endProps, key) &&
!(currentEndProps && Object.prototype.hasOwnProperty.call(currentEndProps, key))
) {
(keys ?? (keys = Object.create(null)))[key] = true;
}
}
});
return keys;
}

export class Animate implements IAnimate {
readonly id: string | number;
status: AnimateStatus;
Expand Down Expand Up @@ -157,7 +195,7 @@ export class Animate implements IAnimate {
this.onRemove(() => {
this.stop();
if (!(this as any).__skipRestoreStaticAttributeOnRemove) {
trackerTarget.restoreStaticAttribute();
trackerTarget.restoreStaticAttribute(collectActiveSiblingAttrKeys(trackerTarget, this));
}
trackerTarget.untrackAnimate(this.id);
});
Expand Down Expand Up @@ -641,7 +679,7 @@ export class Animate implements IAnimate {
this.onEnd();
this.status = AnimateStatus.END;
const trackerTarget = this.target as any;
trackerTarget.restoreStaticAttribute();
trackerTarget.restoreStaticAttribute(collectActiveSiblingAttrKeys(trackerTarget, this));
(this as any).__skipRestoreStaticAttributeOnRemove = true;
return;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/vrender-animate/src/custom/static-truth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export function commitAnimationStaticAttrs(
}

(target as any).setFinalAttributes(commitAttrs);
target.setAttributes(commitAttrs as any, false, { type: AttributeUpdateType.ANIMATE_END });
const commitStaticAttributes = (target as any)._commitAnimationStaticAttributes;
if (typeof commitStaticAttributes === 'function') {
commitStaticAttributes.call(target, commitAttrs, { type: AttributeUpdateType.ANIMATE_END });
} else {
target.setAttributes(commitAttrs as any, false, { type: AttributeUpdateType.ANIMATE_END });
}
return true;
}
Loading
Loading