diff --git a/.changeset/public-shoes-bathe.md b/.changeset/public-shoes-bathe.md
new file mode 100644
index 000000000..e1ee800cb
--- /dev/null
+++ b/.changeset/public-shoes-bathe.md
@@ -0,0 +1,13 @@
+---
+'@antv/g-plugin-canvaskit-renderer': minor
+'@antv/g-plugin-canvas-renderer': minor
+'@antv/g-plugin-device-renderer': minor
+'@antv/g-plugin-html-renderer': minor
+'@antv/g-plugin-image-loader': minor
+'@antv/g-plugin-svg-renderer': minor
+'@antv/g-plugin-a11y': minor
+'@antv/g-plugin-yoga': minor
+'@antv/g-lite': minor
+---
+
+perf: element event batch triggering
diff --git a/__tests__/demos/perf/benchmark-panel.ts b/__tests__/demos/perf/benchmark-panel.ts
new file mode 100644
index 000000000..322cc2e30
--- /dev/null
+++ b/__tests__/demos/perf/benchmark-panel.ts
@@ -0,0 +1,90 @@
+export interface BenchmarkResult {
+ [key: string]: string | number;
+}
+
+export class BenchmarkPanel {
+ private container: HTMLDivElement;
+ private title: string;
+ private configInfo: string | null = null;
+
+ constructor(title: string) {
+ this.title = title;
+
+ // Create result container on page
+ this.container = document.createElement('div');
+ this.container.style.position = 'absolute';
+ this.container.style.bottom = '10px';
+ this.container.style.left = '10px';
+ this.container.style.backgroundColor = 'rgba(0,0,0,0.8)';
+ this.container.style.color = 'white';
+ this.container.style.padding = '10px';
+ this.container.style.borderRadius = '4px';
+ this.container.style.fontFamily = 'monospace';
+ this.container.style.fontSize = '12px';
+ this.container.style.zIndex = '1000';
+ this.container.style.maxWidth = '500px';
+ this.container.id = 'benchmark-results-panel';
+
+ document.body.appendChild(this.container);
+
+ // Show initial status
+ this.showRunningStatus();
+ }
+
+ showRunningStatus(configInfo?: string) {
+ // Store config info for later use in updateResultsDisplay
+ this.configInfo = configInfo || null;
+
+ this.container.innerHTML = `
+
${this.title}
+ ${configInfo ? `${configInfo}
` : ''}
+ Running benchmark tests... Please wait.
+ `;
+ }
+
+ updateResultsDisplay(results: BenchmarkResult[]) {
+ // Debug: log the actual structure of results
+ console.log('Benchmark results structure:', results);
+
+ // Generate table dynamically based on the keys in the first result
+ let tableHtml =
+ '';
+ tableHtml += '';
+
+ if (results && results.length > 0) {
+ const firstResult = results[0];
+ Object.keys(firstResult).forEach((key) => {
+ tableHtml += `${key} | `;
+ });
+ }
+
+ tableHtml += '
';
+
+ if (results && results.length > 0) {
+ // Create table rows for each result
+ results.forEach((result, index) => {
+ // Alternate row colors
+ const bgColor =
+ index % 2 === 0 ? 'rgba(50, 50, 50, 0.3)' : 'rgba(70, 70, 70, 0.3)';
+ tableHtml += ``;
+
+ Object.keys(result).forEach((key) => {
+ const value = result[key];
+ tableHtml += `${value} | `;
+ });
+
+ tableHtml += '
';
+ });
+ } else {
+ tableHtml += 'No results available |
';
+ }
+
+ tableHtml += '
';
+
+ this.container.innerHTML = `
+ ${this.title}
+ ${this.configInfo ? `${this.configInfo}
` : ''}
+ ${tableHtml}
+ `;
+ }
+}
diff --git a/__tests__/demos/perf/custom-event.ts b/__tests__/demos/perf/custom-event.ts
new file mode 100644
index 000000000..a89aefb1f
--- /dev/null
+++ b/__tests__/demos/perf/custom-event.ts
@@ -0,0 +1,145 @@
+import { Canvas, ElementEvent, Rect, Group, CustomEvent } from '@antv/g';
+import * as tinybench from 'tinybench';
+import * as lil from 'lil-gui';
+import { BenchmarkPanel, BenchmarkResult } from './benchmark-panel';
+
+/**
+ * Custom Event Performance Test
+ * Compare performance between sharing a single event instance and creating new event instances each time
+ */
+export async function customEvent(context: { canvas: Canvas; gui: lil.GUI }) {
+ const { canvas, gui } = context;
+ console.log(canvas);
+
+ await canvas.ready;
+
+ const { width, height } = canvas.getConfig();
+ const root = new Group();
+ let count = 1e4;
+ let rects: { x: number; y: number; size: number; el: Rect }[] = [];
+
+ // Shared event instance
+ const sharedEvent = new CustomEvent(ElementEvent.BOUNDS_CHANGED);
+
+ function render() {
+ root.destroyChildren();
+ rects = [];
+
+ for (let i = 0; i < count; i++) {
+ const x = Math.random() * width;
+ const y = Math.random() * height;
+ const size = 10 + Math.random() * 40;
+
+ const rect = new Rect({
+ style: {
+ x,
+ y,
+ width: size,
+ height: size,
+ fill: 'white',
+ stroke: '#000',
+ lineWidth: 1,
+ },
+ });
+ root.appendChild(rect);
+ rects[i] = { x, y, size, el: rect };
+ }
+ }
+
+ render();
+ canvas.appendChild(root);
+
+ // benchmark
+ // ----------
+ const bench = new tinybench.Bench({
+ name: 'Custom Event Performance Comparison',
+ time: 1e3,
+ iterations: 100,
+ });
+
+ // Test performance of shared event instance
+ // Update event properties each time to simulate realistic usage
+ bench.add('Shared Event Instance', () => {
+ rects.forEach((rect) => {
+ // Update event properties to simulate realistic usage
+ sharedEvent.detail = {
+ affectChildren: true,
+ timestamp: performance.now(),
+ };
+ rect.el.dispatchEvent(sharedEvent);
+ });
+ });
+
+ // Test performance of creating new event instances each time
+ bench.add('New Event Instance', () => {
+ rects.forEach((rect) => {
+ const event = new CustomEvent(ElementEvent.BOUNDS_CHANGED);
+ event.detail = {
+ affectChildren: true,
+ timestamp: performance.now(),
+ };
+ rect.el.dispatchEvent(event);
+ });
+ });
+
+ // Test performance of creating same number of events but dispatching only once on root
+ bench.add('Create Events, Dispatch Once on Root', () => {
+ const events = [];
+ // Create same number of event instances
+ for (let i = 0; i < count; i++) {
+ const event = new CustomEvent(ElementEvent.BOUNDS_CHANGED);
+ event.detail = {
+ affectChildren: true,
+ timestamp: performance.now(),
+ };
+ events.push(event);
+ }
+ // But dispatch only once on root
+ root.dispatchEvent(events[0]);
+ });
+
+ // Test performance of dispatching event on each element without sharing event instance
+ bench.add('Dispatch on Each Element', () => {
+ rects.forEach((rect) => {
+ const event = new CustomEvent(ElementEvent.BOUNDS_CHANGED);
+ event.detail = {
+ affectChildren: true,
+ timestamp: performance.now(),
+ };
+ rect.el.dispatchEvent(event);
+ });
+ });
+
+ // Create benchmark panel
+ const benchmarkPanel = new BenchmarkPanel(bench.name);
+
+ // Show initial status with object count
+ benchmarkPanel.showRunningStatus(`Object Count: ${count}`);
+
+ // ----------
+
+ // GUI
+ const config = {
+ objectCount: count,
+ runBenchmark: async () => {
+ benchmarkPanel.showRunningStatus(`Object Count: ${count}`);
+
+ setTimeout(async () => {
+ await bench.run();
+ console.log(bench.name);
+ console.table(bench.table());
+
+ benchmarkPanel.updateResultsDisplay(
+ bench.table() as unknown as BenchmarkResult[],
+ );
+ }, 1e2);
+ },
+ };
+
+ gui.add(config, 'objectCount', 100, 50000, 100).onChange((value) => {
+ count = value;
+ render();
+ });
+
+ gui.add(config, 'runBenchmark');
+}
diff --git a/__tests__/demos/perf/index.ts b/__tests__/demos/perf/index.ts
index c1b8001ec..7883404c1 100644
--- a/__tests__/demos/perf/index.ts
+++ b/__tests__/demos/perf/index.ts
@@ -7,3 +7,4 @@ export { canvasApi } from './canvas-api';
export { javascript } from './javascript';
export { event } from './event';
export { destroyEvent } from './destroy-event';
+export { customEvent } from './custom-event';
diff --git a/__tests__/demos/perf/javascript.ts b/__tests__/demos/perf/javascript.ts
index 7f8037dcc..0cda651ce 100644
--- a/__tests__/demos/perf/javascript.ts
+++ b/__tests__/demos/perf/javascript.ts
@@ -13,7 +13,7 @@ export async function javascript(context: { canvas: Canvas; gui: lil.GUI }) {
// ----------
const bench = new tinybench.Bench({
name: 'javascript benchmark',
- time: 1e2,
+ time: 1e3,
});
const array = [
'stroke',
@@ -98,6 +98,64 @@ export async function javascript(context: { canvas: Canvas; gui: lil.GUI }) {
// bench.add('typeof - isNil', async () => {
// !(typeof value === 'undefined' || value === null);
// });
+
+ // region attr assign ---------------------------------------
+ // Performance comparison: direct property access vs method calls
+ class TestClass {
+ public prop1: number = 0;
+ private _prop2: number = 0;
+
+ setProp2(value: number) {
+ this._prop2 = value;
+ }
+
+ getProp2() {
+ return this._prop2;
+ }
+ }
+
+ const testObj = new TestClass();
+ const iterations = 1000000;
+
+ bench.add('attr assign - Direct property assignment', () => {
+ for (let i = 0; i < iterations; i++) {
+ testObj.prop1 = i;
+ }
+ });
+
+ bench.add('attr assign - Method call assignment', () => {
+ for (let i = 0; i < iterations; i++) {
+ testObj.setProp2(i);
+ }
+ });
+
+ bench.add('attr assign - Direct property access', () => {
+ let sum = 0;
+ for (let i = 0; i < iterations; i++) {
+ sum += testObj.prop1;
+ }
+ return sum;
+ });
+
+ bench.add('attr assign - Method call access', () => {
+ let sum = 0;
+ for (let i = 0; i < iterations; i++) {
+ sum += testObj.getProp2();
+ }
+ return sum;
+ });
+ // endregion ---------------------------------------
+
+ // region typeof ---------------------------------------
+ const testTypeof = undefined;
+ bench.add('typeof - typeof', async () => {
+ typeof testTypeof !== 'undefined';
+ });
+ bench.add('typeof - !==', async () => {
+ testTypeof !== undefined;
+ });
+ // endregion ---------------------------------------
+
// bench.add('@antv/util - isNil', async () => {
// !isNil(value);
// });
@@ -114,10 +172,7 @@ export async function javascript(context: { canvas: Canvas; gui: lil.GUI }) {
await bench.run();
- console.log(bench.name);
console.table(bench.table());
- console.log(bench.results);
- console.log(bench.tasks);
// ----------
}
diff --git a/__tests__/integration/snapshots/2d/webgl/zIndex.png b/__tests__/integration/snapshots/2d/webgl/zIndex.png
index 7c945f273..d3122aae4 100644
Binary files a/__tests__/integration/snapshots/2d/webgl/zIndex.png and b/__tests__/integration/snapshots/2d/webgl/zIndex.png differ
diff --git a/__tests__/unit/css/properties/transform.spec.ts b/__tests__/unit/css/properties/transform.spec.ts
index fb61030c7..50b3e23e0 100644
--- a/__tests__/unit/css/properties/transform.spec.ts
+++ b/__tests__/unit/css/properties/transform.spec.ts
@@ -240,6 +240,7 @@ describe('CSSPropertyTransform', () => {
cy: 10,
r: 50,
transform: 'scale(0)',
+ transformOrigin: 'center',
},
});
diff --git a/__tests__/unit/dom/event.spec.ts b/__tests__/unit/dom/event.spec.ts
index 326b501ad..40cad8ec5 100644
--- a/__tests__/unit/dom/event.spec.ts
+++ b/__tests__/unit/dom/event.spec.ts
@@ -220,6 +220,7 @@ describe('Event API', () => {
ul.appendChild(li2);
const event = new CustomEvent('build', { detail: { prop1: 'xx' } });
+
// delegate to parent
ul.addEventListener('build', (e) => {
expect(e.target).toBe(li1);
diff --git a/__tests__/unit/services/rendering-service-hooks.spec.ts b/__tests__/unit/services/rendering-service-hooks.spec.ts
new file mode 100644
index 000000000..ae2cfb766
--- /dev/null
+++ b/__tests__/unit/services/rendering-service-hooks.spec.ts
@@ -0,0 +1,167 @@
+import { Canvas, IRenderer } from '@antv/g';
+import { Renderer as SVGRenderer } from '@antv/g-svg';
+
+describe('RenderingService Hooks', () => {
+ let canvas: Canvas;
+ let renderer: IRenderer;
+
+ beforeEach(async () => {
+ renderer = new SVGRenderer();
+
+ const $container = document.createElement('div');
+ $container.id = 'container';
+ document.body.appendChild($container);
+
+ canvas = new Canvas({
+ container: 'container',
+ width: 100,
+ height: 100,
+ renderer,
+ });
+
+ // Wait for Canvas initialization to complete
+ if (canvas.ready) {
+ await canvas.ready;
+ }
+ });
+
+ afterEach(() => {
+ if (canvas) {
+ canvas.destroy();
+ }
+
+ // Clean up DOM
+ const $container = document.getElementById('container');
+ if ($container) {
+ $container.remove();
+ }
+ });
+
+ it('should register init hooks correctly', () => {
+ const renderingService = canvas.getRenderingService();
+
+ // Test init hook registration
+ const initCallback = jest.fn();
+ renderingService.hooks.init.tap('TestInit', initCallback);
+
+ // Verify the hook is registered by calling it directly
+ renderingService.hooks.init.call();
+ expect(initCallback).toHaveBeenCalled();
+ });
+
+ it('should call render related hooks during rendering', (done) => {
+ const renderingService = canvas.getRenderingService();
+
+ // Test render related hooks
+ const beginFrameCallback = jest.fn();
+ const endFrameCallback = jest.fn();
+
+ renderingService.hooks.beginFrame.tap('TestBeginFrame', beginFrameCallback);
+ renderingService.hooks.endFrame.tap('TestEndFrame', endFrameCallback);
+
+ // Trigger rendering
+ canvas.render();
+
+ // Use setTimeout to ensure async operations are completed
+ setTimeout(() => {
+ try {
+ // Verify hooks were called
+ expect(beginFrameCallback).toHaveBeenCalled();
+ expect(endFrameCallback).toHaveBeenCalled();
+ done();
+ } catch (error) {
+ done(error);
+ }
+ }, 100);
+ });
+
+ it('should call pointer event hooks', () => {
+ const renderingService = canvas.getRenderingService();
+
+ // Test pointer event hooks
+ const pointerDownCallback = jest.fn();
+ const pointerUpCallback = jest.fn();
+ const pointerMoveCallback = jest.fn();
+
+ renderingService.hooks.pointerDown.tap('TestPointerDown', pointerDownCallback);
+ renderingService.hooks.pointerUp.tap('TestPointerUp', pointerUpCallback);
+ renderingService.hooks.pointerMove.tap('TestPointerMove', pointerMoveCallback);
+
+ // Trigger events with proper event objects
+ const mockPointerEvent = {
+ clientX: 10,
+ clientY: 10,
+ type: 'pointerdown'
+ } as any;
+
+ renderingService.hooks.pointerDown.call(mockPointerEvent);
+ mockPointerEvent.type = 'pointerup';
+ renderingService.hooks.pointerUp.call(mockPointerEvent);
+ mockPointerEvent.type = 'pointermove';
+ renderingService.hooks.pointerMove.call(mockPointerEvent);
+
+ // Verify hooks were called
+ expect(pointerDownCallback).toHaveBeenCalledWith(mockPointerEvent);
+ expect(pointerUpCallback).toHaveBeenCalledWith(mockPointerEvent);
+ expect(pointerMoveCallback).toHaveBeenCalledWith(mockPointerEvent);
+ });
+
+ it('should call pick hooks', () => {
+ const renderingService = canvas.getRenderingService();
+
+ // Test pick hooks - test the hook registration and direct call
+ const pickCallback = jest.fn((result) => {
+ return result;
+ });
+
+ renderingService.hooks.pickSync.tap('TestPick', pickCallback);
+
+ // Create test data that matches the expected PickingResult structure
+ const testData = {
+ position: {
+ x: 10,
+ y: 10,
+ viewportX: 10,
+ viewportY: 10,
+ clientX: 10,
+ clientY: 10
+ },
+ picked: [],
+ topmost: true,
+ };
+
+ // Call the hook directly with test data
+ const result = renderingService.hooks.pickSync.call(testData);
+
+ // Verify hooks were called with correct data
+ expect(pickCallback).toHaveBeenCalled();
+ expect(result).toEqual(testData);
+ });
+
+ it('should support waterfall hooks', () => {
+ const renderingService = canvas.getRenderingService();
+
+ // Test waterfall hooks (like dirtycheck) - test the hook registration and direct call
+ const dirtyCheckCallback1 = jest.fn((object) => {
+ // First callback just passes through the object
+ return object;
+ });
+
+ const dirtyCheckCallback2 = jest.fn((object) => {
+ // Second callback also passes through the object
+ return object;
+ });
+
+ renderingService.hooks.dirtycheck.tap('TestDirtyCheck1', dirtyCheckCallback1);
+ renderingService.hooks.dirtycheck.tap('TestDirtyCheck2', dirtyCheckCallback2);
+
+ // Call the hook directly with test data
+ const testData = null;
+ const result = renderingService.hooks.dirtycheck.call(testData);
+
+ // Verify all callbacks were called in the correct order
+ expect(dirtyCheckCallback1).toHaveBeenCalled();
+ expect(dirtyCheckCallback2).toHaveBeenCalled();
+ expect(result).toBe(testData); // Waterfall should pass through the final result
+ });
+});
\ No newline at end of file
diff --git a/packages/g-lite/src/Canvas.ts b/packages/g-lite/src/Canvas.ts
index e574b53d5..aa1057af4 100644
--- a/packages/g-lite/src/Canvas.ts
+++ b/packages/g-lite/src/Canvas.ts
@@ -496,7 +496,6 @@ export class Canvas extends EventTarget implements ICanvas {
}
render(frame?: XRFrame) {
- // console.log('render ----------------------');
if (frame) {
beforeRenderEvent.detail = frame;
afterRenderEvent.detail = frame;
@@ -512,7 +511,7 @@ export class Canvas extends EventTarget implements ICanvas {
);
const renderingService = this.getRenderingService();
- renderingService.render(this.getConfig(), frame, () => {
+ renderingService.render(this, frame, () => {
// trigger actual rerender event
// @see https://github.com/antvis/G/issues/1268
this.dispatchEvent(
diff --git a/packages/g-lite/src/css/StyleValueRegistry.ts b/packages/g-lite/src/css/StyleValueRegistry.ts
index 229030fa4..3017a9b4b 100644
--- a/packages/g-lite/src/css/StyleValueRegistry.ts
+++ b/packages/g-lite/src/css/StyleValueRegistry.ts
@@ -831,7 +831,6 @@ export class DefaultStyleValueRegistry implements StyleValueRegistry {
}
if (needUpdateGeometry) {
- object.geometry.dirty = true;
object.dirty(true, true);
if (!options.forceUpdateGeometry) {
diff --git a/packages/g-lite/src/display-objects/DisplayObject.ts b/packages/g-lite/src/display-objects/DisplayObject.ts
index d58493312..335b2f656 100644
--- a/packages/g-lite/src/display-objects/DisplayObject.ts
+++ b/packages/g-lite/src/display-objects/DisplayObject.ts
@@ -278,7 +278,7 @@ export class DisplayObject<
/**
* called when attributes get changed or initialized
*/
- internalSetAttribute(
+ private internalSetAttribute(
name: Key,
value: StyleProps[Key],
parseOptions: Partial = {},
@@ -297,8 +297,6 @@ export class DisplayObject<
// redraw at next frame
this.dirty();
- // return;
-
const newParsedValue = this.parsedStyle[name as string];
if (this.isConnected) {
attrModifiedEvent.relatedNode = this as IElement;
diff --git a/packages/g-lite/src/dom/CustomEvent.ts b/packages/g-lite/src/dom/CustomEvent.ts
index 15ea14b6c..9867273c3 100644
--- a/packages/g-lite/src/dom/CustomEvent.ts
+++ b/packages/g-lite/src/dom/CustomEvent.ts
@@ -1,7 +1,8 @@
import { FederatedEvent } from './FederatedEvent';
/**
- * @see https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
+ * @link https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events
*
* @example
const event = new CustomEvent('build', { detail: { prop1: 'xx' } });
@@ -12,14 +13,16 @@ import { FederatedEvent } from './FederatedEvent';
circle.dispatchEvent(event);
*/
-export class CustomEvent extends FederatedEvent {
- constructor(eventName: string, object?: object) {
+export class CustomEvent<
+ O extends { detail?: any } = any,
+> extends FederatedEvent {
+ constructor(eventName: string, options?: O) {
super(null);
this.type = eventName;
- this.detail = object;
+ this.detail = options?.detail;
// compatible with G 3.0
- Object.assign(this, object);
+ Object.assign(this, options);
}
}
diff --git a/packages/g-lite/src/dom/Element.ts b/packages/g-lite/src/dom/Element.ts
index 56a886982..9469e355e 100644
--- a/packages/g-lite/src/dom/Element.ts
+++ b/packages/g-lite/src/dom/Element.ts
@@ -99,24 +99,31 @@ export class Element<
dirty: false,
};
- /**
- * @param flag - default `true`, whether the object needs to be updated
- * @param updateShape - default `false`, whether the bounding box of the object is updated
- */
- dirty(flag = true, updateShape = false) {
- this.renderable.dirty = flag;
- if (updateShape) {
- this.renderable.boundsDirty = flag;
- this.renderable.renderBoundsDirty = flag;
- }
- }
-
geometry: Geometry = {
contentBounds: undefined,
renderBounds: undefined,
dirty: true,
};
+ /**
+ * Marks the element as dirty, indicating it needs re-rendering or relayout.
+ *
+ * @param styleFlag - Whether to update style state (default: true).
+ * When true, sets `renderable.dirty` to true.
+ * @param layoutFlag - Optional. When provided, updates layout-related dirty flags:
+ * - `renderable.boundsDirty`
+ * - `renderable.renderBoundsDirty`
+ * - `geometry.dirty`
+ */
+ dirty(styleFlag = true, layoutFlag?: boolean) {
+ this.renderable.dirty = styleFlag;
+ if (layoutFlag !== undefined) {
+ this.renderable.boundsDirty = layoutFlag;
+ this.renderable.renderBoundsDirty = layoutFlag;
+ this.geometry.dirty = layoutFlag;
+ }
+ }
+
cullable: Cullable = {
strategy: Strategy.Standard,
visibilityPlaneMask: -1,
@@ -632,8 +639,6 @@ export class Element<
setAttribute(
attributeName: Key,
value: StyleProps[Key],
- force?: boolean,
- memoize?: boolean,
) {
this.attributes[attributeName] = value;
}
diff --git a/packages/g-lite/src/dom/EventTarget.ts b/packages/g-lite/src/dom/EventTarget.ts
index 82e715ee8..dd4184da1 100644
--- a/packages/g-lite/src/dom/EventTarget.ts
+++ b/packages/g-lite/src/dom/EventTarget.ts
@@ -13,7 +13,7 @@ import type {
/**
* Objects that can receive events and may have listeners for them.
* eg. Element, Canvas, DisplayObject
- * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
+ * @docs https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
*/
export class EventTarget implements IEventTarget {
/**
@@ -102,10 +102,10 @@ export class EventTarget implements IEventTarget {
* @alias dispatchEvent
*/
emit(eventName: string, object: object) {
- this.dispatchEvent(new CustomEvent(eventName, object));
+ this.dispatchEvent(new CustomEvent(eventName, { detail: object }));
}
- dispatchEventToSelf(e: T) {
+ private dispatchEventToSelf(e: T) {
e.target ||= this;
e.currentTarget = this;
this.emitter.emit(e.type, e);
diff --git a/packages/g-lite/src/dom/MutationEvent.ts b/packages/g-lite/src/dom/MutationEvent.ts
index d794ad548..87c6c6b85 100644
--- a/packages/g-lite/src/dom/MutationEvent.ts
+++ b/packages/g-lite/src/dom/MutationEvent.ts
@@ -1,6 +1,9 @@
import { FederatedEvent } from './FederatedEvent';
import type { ElementEvent, IElement } from './interfaces';
+/**
+ * @deprecated https://developer.chrome.com/blog/mutation-events-deprecation
+ */
export class MutationEvent extends FederatedEvent {
static readonly ADDITION: number = 2;
static readonly MODIFICATION: number = 1;
diff --git a/packages/g-lite/src/dom/MutationObserver.ts b/packages/g-lite/src/dom/MutationObserver.ts
new file mode 100644
index 000000000..774791369
--- /dev/null
+++ b/packages/g-lite/src/dom/MutationObserver.ts
@@ -0,0 +1,19 @@
+import type { IElement } from './interfaces';
+
+/**
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
+ */
+export interface MutationRecord {
+ type: MutationRecordType;
+ target: IElement;
+ addedNodes?: IElement[];
+ attributeName?: string;
+ attributeNamespace?: string;
+ nextSibling?: IElement;
+ oldValue?: string;
+ previousSibling?: IElement;
+ removedNodes?: IElement[];
+
+ // HACK
+ _boundsChangeData?: { affectChildren: boolean };
+}
diff --git a/packages/g-lite/src/dom/index.ts b/packages/g-lite/src/dom/index.ts
index dd35b5d46..9037b0549 100644
--- a/packages/g-lite/src/dom/index.ts
+++ b/packages/g-lite/src/dom/index.ts
@@ -9,4 +9,5 @@ export * from './FederatedPointerEvent';
export * from './FederatedWheelEvent';
export * from './interfaces';
export * from './MutationEvent';
+export * from './MutationObserver';
export * from './Node';
diff --git a/packages/g-lite/src/plugins/PrepareRendererPlugin.ts b/packages/g-lite/src/plugins/PrepareRendererPlugin.ts
index 066255b6d..dd2b104ef 100644
--- a/packages/g-lite/src/plugins/PrepareRendererPlugin.ts
+++ b/packages/g-lite/src/plugins/PrepareRendererPlugin.ts
@@ -2,8 +2,12 @@ import type RBush from 'rbush';
import { runtime } from '../global-runtime';
import type { RBushNodeAABB } from '../components';
import { DisplayObject } from '../display-objects';
-import type { FederatedEvent } from '../dom';
-import { ElementEvent } from '../dom';
+import {
+ type FederatedEvent,
+ type CustomEvent,
+ type MutationRecord,
+ ElementEvent,
+} from '../dom';
import type { RenderingPlugin, RenderingPluginContext } from '../services';
import { raf } from '../utils';
import { AABB } from '../shapes';
@@ -12,7 +16,7 @@ export class PrepareRendererPlugin implements RenderingPlugin {
static tag = 'Prepare';
private rBush: RBush;
- private syncTasks = new Map();
+ private mutationRecords: MutationRecord[] = [];
private ricSyncRTreeId: number;
private isFirstTimeRendering = true;
private syncing = false;
@@ -26,13 +30,17 @@ export class PrepareRendererPlugin implements RenderingPlugin {
this.rBush = rBushRoot;
const handleAttributeChanged = (e: FederatedEvent) => {
- renderingService.dirtify();
+ renderingService.dirty();
};
- const handleBoundsChanged = (e: FederatedEvent) => {
- this.syncTasks.set(e.target as DisplayObject, e.detail.affectChildren);
+ const handleBoundsChanged = (
+ e: CustomEvent<{ detail: MutationRecord[] }>,
+ ) => {
+ const records = e.detail;
+ // ! WARN: push is used instead of direct assignment because syncTasks are processed asynchronously.
+ this.mutationRecords.push(...records);
- renderingService.dirtify();
+ renderingService.dirty();
};
const handleMounted = (e: FederatedEvent) => {
@@ -54,10 +62,8 @@ export class PrepareRendererPlugin implements RenderingPlugin {
this.rBush.remove(rBushNode.aabb);
}
- this.syncTasks.delete(object);
-
runtime.sceneGraphService.dirtyToRoot(object);
- renderingService.dirtify();
+ renderingService.dirty();
};
renderingService.hooks.init.tap(PrepareRendererPlugin.tag, () => {
@@ -81,7 +87,16 @@ export class PrepareRendererPlugin implements RenderingPlugin {
ElementEvent.BOUNDS_CHANGED,
handleBoundsChanged,
);
- this.syncTasks.clear();
+
+ this.mutationRecords = [];
+
+ if (
+ this.ricSyncRTreeId &&
+ runtime.globalThis.requestIdleCallback &&
+ runtime.globalThis.cancelIdleCallback
+ ) {
+ runtime.globalThis.cancelIdleCallback(this.ricSyncRTreeId);
+ }
});
const ric =
@@ -112,8 +127,6 @@ export class PrepareRendererPlugin implements RenderingPlugin {
}
private syncNode(node: DisplayObject, force = false) {
- if (!node.isConnected) return;
-
const rBushNode = node.rBushNode;
// clear dirty node
@@ -161,7 +174,7 @@ export class PrepareRendererPlugin implements RenderingPlugin {
}
private syncRTree(force = false) {
- if (!force && (this.syncing || this.syncTasks.size === 0)) {
+ if (!force && (this.syncing || this.mutationRecords.length === 0)) {
return;
}
@@ -172,7 +185,7 @@ export class PrepareRendererPlugin implements RenderingPlugin {
const synced = new Set();
const sync = (node: DisplayObject) => {
- if (!synced.has(node) && node.renderable) {
+ if (node.isConnected && !synced.has(node) && node.renderable) {
const aabb = this.syncNode(node, force);
if (aabb) {
bulk.push(aabb);
@@ -181,17 +194,26 @@ export class PrepareRendererPlugin implements RenderingPlugin {
}
};
- this.syncTasks.forEach((affectChildren, node) => {
- if (affectChildren) {
- node.forEach(sync);
+ // TODO: Logical redundancy, repeated traversal
+ const recordCount = this.mutationRecords.length;
+ for (let i = 0; i < recordCount; i++) {
+ const record = this.mutationRecords[i];
+ const { _boundsChangeData, target } = record;
+ if (!target.isConnected) {
+ continue;
}
- let parent = node;
+ if (_boundsChangeData?.affectChildren) {
+ target.forEach(sync);
+ }
+
+ let parent = target;
while (parent) {
- sync(parent);
+ sync(parent as DisplayObject);
parent = parent.parentElement as DisplayObject;
}
- });
+ }
+ this.mutationRecords = [];
// use bulk inserting, which is ~2-3 times faster
// @see https://github.com/mourner/rbush#bulk-inserting-data
diff --git a/packages/g-lite/src/services/RenderingService.ts b/packages/g-lite/src/services/RenderingService.ts
index 07c3da0b8..8f3b0f995 100644
--- a/packages/g-lite/src/services/RenderingService.ts
+++ b/packages/g-lite/src/services/RenderingService.ts
@@ -18,6 +18,7 @@ import {
} from '../utils';
import type { RenderingContext } from './RenderingContext';
import { RenderReason } from './RenderingContext';
+import type { Canvas } from '../Canvas';
export type RenderingPluginContext = CanvasContext & GlobalRuntime;
@@ -167,19 +168,16 @@ export class RenderingService {
);
}
- render(
- canvasConfig: Partial,
- frame: XRFrame,
- rerenderCallback: () => void,
- ) {
+ render(canvas: Canvas, frame: XRFrame, rerenderCallback: () => void) {
+ const canvasConfig = canvas.getConfig();
+ const { renderingContext } = this.context;
+
this.stats.total = 0;
this.stats.rendered = 0;
this.zIndexCounter = 0;
- const { renderingContext } = this.context;
-
this.globalRuntime.sceneGraphService.syncHierarchy(renderingContext.root);
- this.globalRuntime.sceneGraphService.triggerPendingEvents();
+ this.globalRuntime.sceneGraphService.notifyMutationObservers(canvas);
if (renderingContext.renderReasons.size && this.inited) {
// @ts-ignore
@@ -330,10 +328,10 @@ export class RenderingService {
destroy() {
this.inited = false;
this.hooks.destroy.call();
- this.globalRuntime.sceneGraphService.clearPendingEvents();
+ this.globalRuntime.sceneGraphService.clearMutationObserverData();
}
- dirtify() {
+ dirty() {
// need re-render
this.context.renderingContext.renderReasons.add(
RenderReason.DISPLAY_OBJECT_CHANGED,
diff --git a/packages/g-lite/src/services/SceneGraphService.ts b/packages/g-lite/src/services/SceneGraphService.ts
index 54e045aa4..ab9c1cbf9 100644
--- a/packages/g-lite/src/services/SceneGraphService.ts
+++ b/packages/g-lite/src/services/SceneGraphService.ts
@@ -6,8 +6,15 @@ import {
updateLocalTransform,
updateWorldTransform,
} from '../components';
-import type { CustomElement, DisplayObject } from '../display-objects';
-import type { Element, IChildNode, IElement, INode, IParentNode } from '../dom';
+import { CustomElement, DisplayObject } from '../display-objects';
+import type {
+ Element,
+ IChildNode,
+ IElement,
+ INode,
+ IParentNode,
+ MutationRecord,
+} from '../dom';
import { CustomEvent } from '../dom/CustomEvent';
import { ElementEvent } from '../dom/interfaces';
import { MutationEvent } from '../dom/MutationEvent';
@@ -16,6 +23,7 @@ import { AABB, Rectangle } from '../shapes';
import { Shape } from '../types';
import { findClosestClipPathTarget, isInFragment } from '../utils';
import type { SceneGraphService } from './interfaces';
+import type { Canvas } from '../Canvas';
const reparentEvent = new MutationEvent(
ElementEvent.REPARENT,
@@ -57,17 +65,13 @@ const $setEulerAngles_InvParentRot = quat.create();
const $rotateLocal = quat.create();
const $rotate_ParentInvertRotation = quat.create();
-const $triggerPendingEvents_detail = { affectChildren: true };
-
/**
* update transform in scene graph
*
* @see https://community.khronos.org/t/scene-graphs/50542/7
*/
export class DefaultSceneGraphService implements SceneGraphService {
- // target -> affectChildren
- private pendingEvents = new Map();
- private boundsChangedEvent = new CustomEvent(ElementEvent.BOUNDS_CHANGED);
+ private mutationsMap: Map = new Map();
constructor(private runtime: GlobalRuntime) {}
@@ -141,7 +145,7 @@ export class DefaultSceneGraphService implements SceneGraphService {
if (isAttachToFragment) return;
if (isChildFragment) {
- this.dirtifyFragment(child);
+ this.dirtyFragment(child);
} else {
const transform = (child as unknown as Element).transformable;
if (transform) {
@@ -909,11 +913,11 @@ export class DefaultSceneGraphService implements SceneGraphService {
}
dirtyWorldTransform(element: INode, transform: Transform) {
- this.dirtifyWorldInternal(element, transform);
+ this.dirtyWorldInternal(element, transform);
this.dirtyToRoot(element, true);
}
- private dirtifyWorldInternal(element: INode, transform: Transform) {
+ private dirtyWorldInternal(element: INode, transform: Transform) {
const enableAttributeUpdateOptimization =
element.ownerDocument?.defaultView?.getConfig()?.future
?.experimentalAttributeUpdateOptimization === true;
@@ -926,7 +930,7 @@ export class DefaultSceneGraphService implements SceneGraphService {
element.childNodes.forEach((child) => {
const childTransform = (child as Element).transformable;
- this.dirtifyWorldInternal(child as IElement, childTransform);
+ this.dirtyWorldInternal(child as IElement, childTransform);
});
}
}
@@ -956,10 +960,25 @@ export class DefaultSceneGraphService implements SceneGraphService {
this.informDependentDisplayObjects(element as DisplayObject);
- this.pendingEvents.set(element as DisplayObject, affectChildren);
+ let mutation = this.mutationsMap.get(element as DisplayObject);
+ if (!mutation) {
+ mutation = {
+ type: 'attributes',
+ target: element as DisplayObject,
+ _boundsChangeData: {
+ affectChildren,
+ },
+ };
+ this.mutationsMap.set(element as DisplayObject, mutation);
+ } else {
+ mutation._boundsChangeData = {
+ affectChildren:
+ mutation._boundsChangeData.affectChildren || affectChildren,
+ };
+ }
}
- dirtifyFragment(element: INode) {
+ dirtyFragment(element: INode) {
const transform = (element as Element).transformable;
if (transform) {
transform.dirtyFlag = true;
@@ -969,81 +988,22 @@ export class DefaultSceneGraphService implements SceneGraphService {
const length = element.childNodes.length;
for (let i = 0; i < length; i++) {
- this.dirtifyFragment(element.childNodes[i]);
- }
-
- if (element.nodeName === Shape.FRAGMENT) {
- this.pendingEvents.set(element as DisplayObject, false);
+ this.dirtyFragment(element.childNodes[i]);
}
}
- triggerPendingEvents() {
- const triggered = new Set();
- let enableCancelEventPropagation: boolean;
- let enableAttributeUpdateOptimization: boolean;
-
- const trigger = (element: DisplayObject, detail) => {
- if (
- !element.isConnected ||
- triggered.has(element) ||
- (element.nodeName as Shape) === Shape.FRAGMENT
- ) {
- return;
- }
-
- this.boundsChangedEvent.detail = detail;
- this.boundsChangedEvent.target = element;
- if (element.isMutationObserved) {
- element.dispatchEvent(this.boundsChangedEvent);
- } else {
- if (enableCancelEventPropagation === undefined) {
- enableCancelEventPropagation =
- element.ownerDocument.defaultView?.getConfig()?.future
- ?.experimentalCancelEventPropagation === true;
- }
-
- element.ownerDocument.defaultView.dispatchEvent(
- this.boundsChangedEvent,
- true,
- enableCancelEventPropagation,
- );
- }
-
- triggered.add(element);
- };
-
- this.pendingEvents.forEach((affectChildren, element) => {
- if ((element.nodeName as Shape) === Shape.FRAGMENT) {
- return;
- }
-
- if (enableAttributeUpdateOptimization === undefined) {
- enableAttributeUpdateOptimization =
- element.ownerDocument?.defaultView?.getConfig()?.future
- ?.experimentalAttributeUpdateOptimization === true;
- }
-
- $triggerPendingEvents_detail.affectChildren = affectChildren;
- if (enableAttributeUpdateOptimization) {
- trigger(element, $triggerPendingEvents_detail);
- } else {
- // eslint-disable-next-line no-lonely-if
- if (affectChildren) {
- element.forEach((e: DisplayObject) => {
- trigger(e, $triggerPendingEvents_detail);
- });
- } else {
- trigger(element, $triggerPendingEvents_detail);
- }
- }
+ notifyMutationObservers(canvas: Canvas) {
+ const event = new CustomEvent(ElementEvent.BOUNDS_CHANGED, {
+ detail: Array.from(this.mutationsMap.values()),
});
- triggered.clear();
- this.clearPendingEvents();
+ canvas.dispatchEvent(event, true, true);
+
+ this.clearMutationObserverData();
}
- clearPendingEvents() {
- this.pendingEvents.clear();
+ clearMutationObserverData() {
+ this.mutationsMap.clear();
}
private displayObjectDependencyMap: WeakMap<
diff --git a/packages/g-lite/src/services/interfaces.ts b/packages/g-lite/src/services/interfaces.ts
index 8414b1b30..3ea176acb 100644
--- a/packages/g-lite/src/services/interfaces.ts
+++ b/packages/g-lite/src/services/interfaces.ts
@@ -1,12 +1,12 @@
import type { mat4, quat, vec2, vec3 } from 'gl-matrix';
import type { Transform } from '../components';
-import type { IElement, INode, IParentNode } from '../dom';
+import type { ICanvas, IElement, INode, IParentNode } from '../dom';
import type { AABB, Rectangle } from '../shapes';
import type { DisplayObject } from '../display-objects';
export interface SceneGraphService {
- triggerPendingEvents: () => void;
- clearPendingEvents: () => void;
+ notifyMutationObservers: (canvas: ICanvas) => void;
+ clearMutationObserverData: () => void;
updateDisplayObjectDependency: (
name: string,
oldPath: DisplayObject,
diff --git a/packages/g-plugin-a11y/src/A11yPlugin.ts b/packages/g-plugin-a11y/src/A11yPlugin.ts
index 786c5b0cb..b6d40e721 100644
--- a/packages/g-plugin-a11y/src/A11yPlugin.ts
+++ b/packages/g-plugin-a11y/src/A11yPlugin.ts
@@ -1,7 +1,9 @@
import type {
DisplayObject,
FederatedEvent,
+ CustomEvent,
MutationEvent,
+ MutationRecord,
RenderingPlugin,
RenderingPluginContext,
Text,
@@ -59,17 +61,23 @@ export class A11yPlugin implements RenderingPlugin {
}
};
- const handleBoundsChanged = (e: MutationEvent) => {
- const object = e.target as DisplayObject;
- if (enableExtractingText && !this.isSVG()) {
- const nodes =
- object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
-
- nodes.forEach((node: DisplayObject) => {
- if (node.nodeName === Shape.TEXT) {
- this.textExtractor.updateAttribute('modelMatrix', node as Text);
- }
- });
+ const handleBoundsChanged = (
+ e: CustomEvent<{ detail: MutationRecord[] }>,
+ ) => {
+ const records = e.detail;
+ for (let i = 0; i < records.length; i++) {
+ const record = records[i];
+ const object = record.target as DisplayObject;
+ if (enableExtractingText && !this.isSVG()) {
+ const nodes =
+ object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
+
+ nodes.forEach((node: DisplayObject) => {
+ if (node.nodeName === Shape.TEXT) {
+ this.textExtractor.updateAttribute('modelMatrix', node as Text);
+ }
+ });
+ }
}
};
diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Image.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Image.ts
index 088bbbb38..b4b03a266 100644
--- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Image.ts
+++ b/packages/g-plugin-canvas-renderer/src/shapes/styles/Image.ts
@@ -55,7 +55,7 @@ export class ImageRenderer extends DefaultRenderer {
// rerender
object.dirty();
- object.ownerDocument.defaultView.context.renderingService.dirtify();
+ object.ownerDocument.defaultView.context.renderingService.dirty();
})
.catch((reason) => {
console.error(reason);
@@ -103,7 +103,7 @@ export class ImageRenderer extends DefaultRenderer {
// rerender
object.dirty();
- object.ownerDocument.defaultView.context.renderingService.dirtify();
+ object.ownerDocument.defaultView.context.renderingService.dirty();
},
object,
)
diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/helper.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/helper.ts
index 4b121f03d..569427b69 100644
--- a/packages/g-plugin-canvas-renderer/src/shapes/styles/helper.ts
+++ b/packages/g-plugin-canvas-renderer/src/shapes/styles/helper.ts
@@ -75,7 +75,7 @@ export function getPattern(
() => {
// set dirty rectangle flag
object.dirty();
- canvasContext.renderingService.dirtify();
+ canvasContext.renderingService.dirty();
},
);
diff --git a/packages/g-plugin-canvaskit-renderer/src/CanvaskitRendererPlugin.ts b/packages/g-plugin-canvaskit-renderer/src/CanvaskitRendererPlugin.ts
index 504623360..3c8f472bd 100644
--- a/packages/g-plugin-canvaskit-renderer/src/CanvaskitRendererPlugin.ts
+++ b/packages/g-plugin-canvaskit-renderer/src/CanvaskitRendererPlugin.ts
@@ -328,7 +328,7 @@ export class CanvaskitRendererPlugin implements RenderingPlugin {
() => {
// set dirty rectangle flag
object.renderable.dirty = true;
- this.context.renderingService.dirtify();
+ this.context.renderingService.dirty();
},
);
diff --git a/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts b/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts
index 28393af8e..3919cf29b 100644
--- a/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts
+++ b/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts
@@ -3,7 +3,9 @@ import type {
DataURLOptions,
DisplayObject,
FederatedEvent,
+ CustomEvent,
MutationEvent,
+ MutationRecord,
RenderingPlugin,
RenderingPluginContext,
} from '@antv/g-lite';
@@ -205,15 +207,21 @@ export class RenderGraphPlugin implements RenderingPlugin {
}
};
- const handleBoundsChanged = (e: MutationEvent) => {
+ const handleBoundsChanged = (
+ e: CustomEvent<{ detail: MutationRecord[] }>,
+ ) => {
if (this.swapChain) {
- const object = e.target as DisplayObject;
- const nodes =
- object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
-
- nodes.forEach((node: DisplayObject) => {
- this.batchManager.updateAttribute(node, 'modelMatrix', null);
- });
+ const records = e.detail;
+ for (let i = 0; i < records.length; i++) {
+ const record = records[i];
+ const object = record.target as DisplayObject;
+ const nodes =
+ object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
+
+ nodes.forEach((node: DisplayObject) => {
+ this.batchManager.updateAttribute(node, 'modelMatrix', null);
+ });
+ }
}
};
diff --git a/packages/g-plugin-device-renderer/src/TexturePool.ts b/packages/g-plugin-device-renderer/src/TexturePool.ts
index a75cc2b13..b0109d47b 100644
--- a/packages/g-plugin-device-renderer/src/TexturePool.ts
+++ b/packages/g-plugin-device-renderer/src/TexturePool.ts
@@ -70,7 +70,7 @@ export class TexturePool {
if (!isString(src)) {
texture.setImageData([src]);
texture.emit(TextureEvent.LOADED);
- this.context.renderingService.dirtify();
+ this.context.renderingService.dirty();
} else {
// @see https://github.com/antvis/g/issues/938
const image = this.context.config.createImage();
@@ -80,7 +80,7 @@ export class TexturePool {
const onSuccess = (bitmap: ImageBitmap | HTMLImageElement) => {
this.textureCache[id].setImageData([bitmap]);
this.textureCache[id].emit(TextureEvent.LOADED);
- this.context.renderingService.dirtify();
+ this.context.renderingService.dirty();
if (successCallback) {
successCallback(this.textureCache[id], bitmap);
}
diff --git a/packages/g-plugin-device-renderer/src/drawcalls/Image.ts b/packages/g-plugin-device-renderer/src/drawcalls/Image.ts
index ef13c3fa6..44cad95b3 100644
--- a/packages/g-plugin-device-renderer/src/drawcalls/Image.ts
+++ b/packages/g-plugin-device-renderer/src/drawcalls/Image.ts
@@ -72,7 +72,7 @@ export class ImageDrawcall extends Instanced {
// this.calculateWithAspectRatio(object, width, height);
// // set dirty rectangle flag
// object.renderable.dirty = true;
- // this.context.renderingService.dirtify();
+ // this.context.renderingService.dirty();
// }
// });
// },
diff --git a/packages/g-plugin-device-renderer/src/drawcalls/Instanced.ts b/packages/g-plugin-device-renderer/src/drawcalls/Instanced.ts
index bcb058026..785fdacf4 100644
--- a/packages/g-plugin-device-renderer/src/drawcalls/Instanced.ts
+++ b/packages/g-plugin-device-renderer/src/drawcalls/Instanced.ts
@@ -183,7 +183,7 @@ export abstract class Instanced {
this.geometry.meshes.forEach((mesh) => {
mesh.renderable.dirty = true;
});
- this.context.renderingService.dirtify();
+ this.context.renderingService.dirty();
});
}
@@ -192,7 +192,7 @@ export abstract class Instanced {
this.material.meshes.forEach((mesh) => {
mesh.renderable.dirty = true;
});
- this.context.renderingService.dirtify();
+ this.context.renderingService.dirty();
});
}
diff --git a/packages/g-plugin-html-renderer/src/HTMLRenderingPlugin.ts b/packages/g-plugin-html-renderer/src/HTMLRenderingPlugin.ts
index 1fca2bd6a..5a1e67bfa 100644
--- a/packages/g-plugin-html-renderer/src/HTMLRenderingPlugin.ts
+++ b/packages/g-plugin-html-renderer/src/HTMLRenderingPlugin.ts
@@ -1,10 +1,12 @@
import {
DisplayObject,
FederatedEvent,
+ CustomEvent,
GlobalRuntime,
HTML,
ICamera,
MutationEvent,
+ MutationRecord,
RenderingPlugin,
RenderingPluginContext,
CanvasEvent,
@@ -101,17 +103,23 @@ export class HTMLRenderingPlugin implements RenderingPlugin {
}
};
- const handleBoundsChanged = (e: MutationEvent) => {
- const object = e.target as HTML;
- const nodes =
- object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
-
- nodes.forEach((node: HTML) => {
- if (node.nodeName === Shape.HTML) {
- const $el = this.getOrCreateEl(node);
- setTransform(node, $el);
- }
- });
+ const handleBoundsChanged = (
+ e: CustomEvent<{ detail: MutationRecord[] }>,
+ ) => {
+ const records = e.detail;
+ for (let i = 0; i < records.length; i++) {
+ const record = records[i];
+ const object = record.target as DisplayObject;
+ const nodes =
+ object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
+
+ nodes.forEach((node: DisplayObject) => {
+ if (node.nodeName === Shape.HTML) {
+ const $el = this.getOrCreateEl(node);
+ setTransform(node as HTML, $el);
+ }
+ });
+ }
};
const handleCanvasResize = () => {
diff --git a/packages/g-plugin-image-loader/src/LoadImagePlugin.ts b/packages/g-plugin-image-loader/src/LoadImagePlugin.ts
index 66bbbae2e..25aab0a24 100644
--- a/packages/g-plugin-image-loader/src/LoadImagePlugin.ts
+++ b/packages/g-plugin-image-loader/src/LoadImagePlugin.ts
@@ -44,9 +44,8 @@ export class LoadImagePlugin implements RenderingPlugin {
calculateWithAspectRatio(object, width, height);
}
- // set dirty rectangle flag
- object.renderable.dirty = true;
- renderingService.dirtify();
+ object.dirty();
+ renderingService.dirty();
},
);
}
@@ -75,9 +74,8 @@ export class LoadImagePlugin implements RenderingPlugin {
calculateWithAspectRatio(object, width, height);
}
- // set dirty rectangle flag
- object.renderable.dirty = true;
- renderingService.dirtify();
+ object.dirty();
+ renderingService.dirty();
})
.catch(() => {
//
diff --git a/packages/g-plugin-svg-renderer/src/SVGRendererPlugin.ts b/packages/g-plugin-svg-renderer/src/SVGRendererPlugin.ts
index eb43b2567..c5cad7764 100644
--- a/packages/g-plugin-svg-renderer/src/SVGRendererPlugin.ts
+++ b/packages/g-plugin-svg-renderer/src/SVGRendererPlugin.ts
@@ -1,8 +1,10 @@
import {
DisplayObject,
FederatedEvent,
+ CustomEvent,
LinearGradient,
MutationEvent,
+ MutationRecord,
RadialGradient,
RenderingPlugin,
RenderingPluginContext,
@@ -230,49 +232,55 @@ export class SVGRendererPlugin implements RenderingPlugin {
attribtues.push(attrName);
};
- const handleGeometryBoundsChanged = (e: MutationEvent) => {
- const target = e.target as DisplayObject;
-
- const nodes =
- target.nodeName === Shape.FRAGMENT ? target.childNodes : [target];
- nodes.forEach((object: DisplayObject) => {
- // @ts-ignore
- const $el = object.elementSVG?.$el;
+ const handleGeometryBoundsChanged = (
+ e: CustomEvent<{ detail: MutationRecord[] }>,
+ ) => {
+ const records = e.detail;
+ for (let i = 0; i < records.length; i++) {
+ const record = records[i];
+ const object = record.target as DisplayObject;
+ const nodes =
+ object.nodeName === Shape.FRAGMENT ? object.childNodes : [object];
+
+ nodes.forEach((node: DisplayObject) => {
+ // @ts-ignore
+ const $el = object.elementSVG?.$el;
- const { fill, stroke, clipPath } = object.parsedStyle;
+ const { fill, stroke, clipPath } = object.parsedStyle;
- if (fill && !isCSSRGB(fill)) {
- this.defElementManager.createOrUpdateGradientAndPattern(
- object,
- $el,
- fill,
- 'fill',
- this,
- );
- }
- if (stroke && !isCSSRGB(stroke)) {
- this.defElementManager.createOrUpdateGradientAndPattern(
- object,
- $el,
- stroke,
- 'stroke',
- this,
- );
- }
- if (clipPath) {
- const parentInvert = mat4.invert(
- mat4.create(),
- object.getWorldTransform(),
- );
+ if (fill && !isCSSRGB(fill)) {
+ this.defElementManager.createOrUpdateGradientAndPattern(
+ object,
+ $el,
+ fill,
+ 'fill',
+ this,
+ );
+ }
+ if (stroke && !isCSSRGB(stroke)) {
+ this.defElementManager.createOrUpdateGradientAndPattern(
+ object,
+ $el,
+ stroke,
+ 'stroke',
+ this,
+ );
+ }
+ if (clipPath) {
+ const parentInvert = mat4.invert(
+ mat4.create(),
+ object.getWorldTransform(),
+ );
- const clipPathId = `${CLIP_PATH_PREFIX + clipPath.entity}-${object.entity}`;
- const $def = this.defElementManager.getDefElement();
- const $existed = $def.querySelector(`#${clipPathId}`);
- if ($existed) {
- this.applyTransform($existed, parentInvert);
+ const clipPathId = `${CLIP_PATH_PREFIX + clipPath.entity}-${object.entity}`;
+ const $def = this.defElementManager.getDefElement();
+ const $existed = $def.querySelector(`#${clipPathId}`);
+ if ($existed) {
+ this.applyTransform($existed, parentInvert);
+ }
}
- }
- });
+ });
+ }
};
renderingService.hooks.init.tap(SVGRendererPlugin.tag, () => {
diff --git a/packages/g-plugin-yoga/src/YogaPlugin.ts b/packages/g-plugin-yoga/src/YogaPlugin.ts
index f8e67edec..c0250adee 100644
--- a/packages/g-plugin-yoga/src/YogaPlugin.ts
+++ b/packages/g-plugin-yoga/src/YogaPlugin.ts
@@ -2,7 +2,9 @@ import type {
CSSUnitValue,
DisplayObject,
FederatedEvent,
+ CustomEvent,
MutationEvent,
+ MutationRecord,
RenderingPlugin,
RenderingPluginContext,
} from '@antv/g-lite';
@@ -140,13 +142,19 @@ export class YogaPlugin implements RenderingPlugin {
}
};
- const handleBoundsChanged = (e: FederatedEvent) => {
- const object = e.target as DisplayObject;
- // skip if this object mounted on another scenegraph root
- if (object.ownerDocument?.documentElement !== renderingContext.root) {
- return;
+ const handleBoundsChanged = (
+ e: CustomEvent<{ detail: MutationRecord[] }>,
+ ) => {
+ const records = e.detail;
+ for (let i = 0; i < records.length; i++) {
+ const record = records[i];
+ const object = record.target as DisplayObject;
+ // skip if this object mounted on another scenegraph root
+ if (object.ownerDocument?.documentElement !== renderingContext.root) {
+ return;
+ }
+ this.needRecalculateLayout = true;
}
- this.needRecalculateLayout = true;
};
renderingService.hooks.init.tap(YogaPlugin.tag, () => {
diff --git a/site/examples/shape/text/demo/text.js b/site/examples/shape/text/demo/text.js
index 864d106ab..2bdc123c3 100644
--- a/site/examples/shape/text/demo/text.js
+++ b/site/examples/shape/text/demo/text.js
@@ -197,6 +197,7 @@ const fillStrokeConfig = {
textDecorationLine: 'none',
textDecorationColor: 'none',
textDecorationStyle: 'solid',
+ textDecorationThickness: 1,
};
fillStrokeFolder.addColor(fillStrokeConfig, 'fill').onChange((color) => {
text.attr('fill', color);
@@ -255,6 +256,11 @@ fillStrokeFolder
.onChange((textDecorationStyle) => {
text.attr('textDecorationStyle', textDecorationStyle);
});
+fillStrokeFolder
+ .add(fillStrokeConfig, 'textDecorationThickness', 1, 10)
+ .onChange((textDecorationThickness) => {
+ text.attr('textDecorationThickness', textDecorationThickness);
+ });
fillStrokeFolder
.addColor(fillStrokeConfig, 'textDecorationColor')
.onChange((color) => {