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 += ``; + }); + } + + 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 += ``; + }); + + tableHtml += ''; + }); + } else { + tableHtml += ''; + } + + tableHtml += '
${key}
${value}
No results available
'; + + 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) => {