From 0e911c2ad9355b3e705248cfe6058b7e6091cd44 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 15 Jul 2024 19:58:58 +0800 Subject: [PATCH] refactor: refactor canvas, support switch renderer (#6034) * chore: update pnpm workspace * refactor(runtime): refactor canvas, support setRenderer * refactor(runtime): graph adpat canvas, remove get/set background * refactor: adjust $layer property * refactor: refactor fullscreen plugin * refactor: base-element add context property * refactor: canvas add getRenderer API --- .../__tests__/utils/create.ts | 4 +- .../__tests__/utils/to-match-svg-snapshot.ts | 8 +- .../src/elements/base-node-3d.ts | 2 +- .../g6/__tests__/demos/case-indented-tree.ts | 8 +- packages/g6/__tests__/main.ts | 2 +- .../g6/__tests__/unit/runtime/canvas.spec.ts | 6 +- .../unit/runtime/graph/graph.spec.ts | 4 - packages/g6/__tests__/utils/create.ts | 4 +- .../__tests__/utils/to-match-svg-snapshot.ts | 13 +- packages/g6/src/behaviors/drag-element.ts | 3 +- packages/g6/src/elements/base-element.ts | 12 +- packages/g6/src/elements/combos/base-combo.ts | 4 +- packages/g6/src/elements/nodes/html.ts | 10 +- packages/g6/src/global.d.ts | 12 + packages/g6/src/plugins/fullscreen/index.ts | 17 +- packages/g6/src/runtime/canvas.ts | 371 +++++++----------- packages/g6/src/runtime/graph.ts | 41 +- pnpm-workspace.yaml | 2 +- 18 files changed, 221 insertions(+), 302 deletions(-) create mode 100644 packages/g6/src/global.d.ts diff --git a/packages/cli/template-extension/__tests__/utils/create.ts b/packages/cli/template-extension/__tests__/utils/create.ts index 2dc93d6998..4d9ee18d18 100644 --- a/packages/cli/template-extension/__tests__/utils/create.ts +++ b/packages/cli/template-extension/__tests__/utils/create.ts @@ -1,4 +1,3 @@ -import type { IRenderer } from '@antv/g'; import { resetEntityCounter } from '@antv/g'; import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Renderer as SVGRenderer } from '@antv/g-svg'; @@ -41,12 +40,11 @@ export function createGraphCanvas( } as unknown as HTMLCanvasElement; const context = new OffscreenCanvasContext(offscreenNodeCanvas); - const instance = getRenderer(renderer) as any as IRenderer; return { container, width, height, - renderer: () => instance, + renderer: () => getRenderer(renderer), document: container.ownerDocument, offscreenCanvas: offscreenNodeCanvas, }; diff --git a/packages/cli/template-extension/__tests__/utils/to-match-svg-snapshot.ts b/packages/cli/template-extension/__tests__/utils/to-match-svg-snapshot.ts index 26fce9fa1d..fde14db92b 100644 --- a/packages/cli/template-extension/__tests__/utils/to-match-svg-snapshot.ts +++ b/packages/cli/template-extension/__tests__/utils/to-match-svg-snapshot.ts @@ -99,7 +99,11 @@ export async function toMatchSnapshot( detail?: string, options: ToMatchSVGSnapshotOptions = {}, ) { - return await toMatchSVGSnapshot(Object.values(graph.getCanvas().canvas), ...getSnapshotDir(dir, detail), options); + return await toMatchSVGSnapshot( + Object.values(graph.getCanvas().getLayers()), + ...getSnapshotDir(dir, detail), + options, + ); } export async function toMatchAnimation( @@ -126,7 +130,7 @@ export async function toMatchAnimation( animation.currentTime = frame; await sleep(32); const result = await toMatchSVGSnapshot( - Object.values(graph.getCanvas().canvas), + Object.values(graph.getCanvas().getCanvases().canvas), ...getSnapshotDir(dir, `${detail}-${frame}`), options, ); diff --git a/packages/g6-extension-3d/src/elements/base-node-3d.ts b/packages/g6-extension-3d/src/elements/base-node-3d.ts index 86f09620e1..f9aaddac8e 100644 --- a/packages/g6-extension-3d/src/elements/base-node-3d.ts +++ b/packages/g6-extension-3d/src/elements/base-node-3d.ts @@ -23,7 +23,7 @@ export abstract class BaseNode3D extends BaseNod public type = 'node-3d'; protected get plugin() { - const renderer = this.attributes.context!.canvas.renderers['main']; + const renderer = this.context.canvas.getRenderer('main'); const plugin = renderer.getPlugin('device-renderer'); return plugin as unknown as Plugin; } diff --git a/packages/g6/__tests__/demos/case-indented-tree.ts b/packages/g6/__tests__/demos/case-indented-tree.ts index 82f738f576..22d5d6a996 100644 --- a/packages/g6/__tests__/demos/case-indented-tree.ts +++ b/packages/g6/__tests__/demos/case-indented-tree.ts @@ -78,7 +78,7 @@ export const caseIndentedTree: TestCase = async (context) => { } protected get childrenData() { - return this.attributes.context!.model.getChildrenData(this.id); + return this.context!.model.getChildrenData(this.id); } protected getKeyStyle(attributes: Required): RectStyleProps { @@ -147,7 +147,7 @@ export const caseIndentedTree: TestCase = async (context) => { this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => { event.stopPropagation(); - attributes.context!.graph.emit(TreeEvent.COLLAPSE_EXPAND, { + this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, { id: this.id, collapsed: false, }); @@ -184,7 +184,7 @@ export const caseIndentedTree: TestCase = async (context) => { this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => { event.stopPropagation(); - attributes.context!.graph.emit(TreeEvent.COLLAPSE_EXPAND, { + this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, { id: this.id, collapsed: !attributes.collapsed, }); @@ -221,7 +221,7 @@ export const caseIndentedTree: TestCase = async (context) => { this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => { event.stopPropagation(); - attributes.context!.graph.emit(TreeEvent.ADD_CHILD, { id: this.id }); + this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id }); }); } diff --git a/packages/g6/__tests__/main.ts b/packages/g6/__tests__/main.ts index 8e3042a325..99f7649a5d 100644 --- a/packages/g6/__tests__/main.ts +++ b/packages/g6/__tests__/main.ts @@ -64,7 +64,7 @@ async function render() { // render const { Renderer, Demo, Animation, Theme } = options; const canvas = createGraphCanvas($container, 500, 500, Renderer); - await canvas.init(); + await canvas.ready; const testCase = demos[Demo as keyof typeof demos]; if (!testCase) return; diff --git a/packages/g6/__tests__/unit/runtime/canvas.spec.ts b/packages/g6/__tests__/unit/runtime/canvas.spec.ts index 6c302e430c..a0f6a7835a 100644 --- a/packages/g6/__tests__/unit/runtime/canvas.spec.ts +++ b/packages/g6/__tests__/unit/runtime/canvas.spec.ts @@ -4,11 +4,7 @@ import { createGraphCanvas } from '@@/utils'; describe('Canvas', () => { const svg = createGraphCanvas(null, 500, 500, 'svg'); beforeAll(async () => { - await svg.init(); - }); - - it('getRendererType', () => { - expect(svg.getRendererType()).toBe('svg'); + await svg.ready; }); it('context', () => { diff --git a/packages/g6/__tests__/unit/runtime/graph/graph.spec.ts b/packages/g6/__tests__/unit/runtime/graph/graph.spec.ts index 5cd3eca6b1..678d16eaeb 100644 --- a/packages/g6/__tests__/unit/runtime/graph/graph.spec.ts +++ b/packages/g6/__tests__/unit/runtime/graph/graph.spec.ts @@ -43,10 +43,6 @@ describe('Graph', () => { }); }); - it('setBackground/getBackground', () => { - expect(graph.getBackground()).toEqual('#ffffff'); - }); - it('getSize', () => { expect(graph.getSize()).toEqual([500, 500]); }); diff --git a/packages/g6/__tests__/utils/create.ts b/packages/g6/__tests__/utils/create.ts index 0a88bd5795..71a2a26bb7 100644 --- a/packages/g6/__tests__/utils/create.ts +++ b/packages/g6/__tests__/utils/create.ts @@ -3,7 +3,6 @@ import { Graph } from '@/src'; import { Circle } from '@/src/elements'; import { Canvas } from '@/src/runtime/canvas'; import type { Node, Point } from '@/src/types'; -import type { IRenderer } from '@antv/g'; import { resetEntityCounter } from '@antv/g'; import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Renderer as SVGRenderer } from '@antv/g-svg'; @@ -60,12 +59,11 @@ export function createGraphCanvas( } as unknown as HTMLCanvasElement; const context = new OffscreenCanvasContext(offscreenNodeCanvas); - const instance = getRenderer(renderer) as any as IRenderer; return new Canvas({ container, width, height, - renderer: () => instance, + renderer: () => getRenderer(renderer), ...extraOptions, }); } diff --git a/packages/g6/__tests__/utils/to-match-svg-snapshot.ts b/packages/g6/__tests__/utils/to-match-svg-snapshot.ts index 59e732bbd5..f3cdf3ede9 100644 --- a/packages/g6/__tests__/utils/to-match-svg-snapshot.ts +++ b/packages/g6/__tests__/utils/to-match-svg-snapshot.ts @@ -1,4 +1,5 @@ import type { Graph, IAnimateEvent } from '@/src'; +import type { CanvasLayer } from '@/src/types'; import type { Canvas, IAnimation } from '@antv/g'; import chalk from 'chalk'; import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; @@ -21,7 +22,7 @@ const formatSVG = (svg: string, keepSVGElementId: boolean) => { // @see https://jestjs.io/docs/26.x/expect#expectextendmatchers export async function toMatchSVGSnapshot( - gCanvas: Canvas | Canvas[], + gCanvas: Record, dir: string, name: string, options: ToMatchSVGSnapshotOptions = {}, @@ -32,15 +33,15 @@ export async function toMatchSVGSnapshot( const namePath = join(dir, name); const actualPath = join(dir, `${name}-actual.${fileFormat}`); const expectedPath = join(dir, `${name}.${fileFormat}`); - const gCanvases = Array.isArray(gCanvas) ? gCanvas : [gCanvas]; let actual: string = ''; // Clone - const svg = (gCanvases[0].getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement; + const svg = (gCanvas.main.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement; const gRoot = svg.querySelector('#g-root'); - gCanvases.slice(1).forEach((gCanvas) => { + Object.entries(gCanvas).forEach(([key, gCanvas]) => { + if (key === 'main') return; const dom = (gCanvas.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement; // @ts-expect-error dom is SVGElement gRoot?.append(...(dom.querySelector('#g-root')?.childNodes || [])); @@ -99,7 +100,7 @@ export async function toMatchSnapshot( detail?: string, options: ToMatchSVGSnapshotOptions = {}, ) { - return await toMatchSVGSnapshot(Object.values(graph.getCanvas().canvas), ...getSnapshotDir(dir, detail), options); + return await toMatchSVGSnapshot(graph.getCanvas().getLayers(), ...getSnapshotDir(dir, detail), options); } export async function toMatchAnimation( @@ -127,7 +128,7 @@ export async function toMatchAnimation( await sleep(32); const result = await toMatchSVGSnapshot( - Object.values(graph.getCanvas().canvas), + graph.getCanvas().getLayers(), ...getSnapshotDir(dir, `${detail}-${frame}`), options, ); diff --git a/packages/g6/src/behaviors/drag-element.ts b/packages/g6/src/behaviors/drag-element.ts index 6a5c9393dc..c63b76daa2 100644 --- a/packages/g6/src/behaviors/drag-element.ts +++ b/packages/g6/src/behaviors/drag-element.ts @@ -376,11 +376,10 @@ export class DragElement extends BaseBehavior { } else { this.shadow = new Rect({ style: { + $layer: 'transient', ...shadowStyle, ...positionStyle, pointerEvents: 'none', - // @ts-expect-error $layer is not in the type definition - $layer: 'transient', }, }); this.context.canvas.appendChild(this.shadow); diff --git a/packages/g6/src/elements/base-element.ts b/packages/g6/src/elements/base-element.ts index 76227f9e27..9ae25ac2be 100644 --- a/packages/g6/src/elements/base-element.ts +++ b/packages/g6/src/elements/base-element.ts @@ -1,9 +1,13 @@ -import { IAnimation } from '@antv/g'; -import { Keyframe } from '../types'; -import type { BaseShapeStyleProps } from './shapes'; +import type { IAnimation } from '@antv/g'; +import type { RuntimeContext } from '../runtime/types'; +import type { BaseElementStyleProps, Keyframe } from '../types'; import { BaseShape } from './shapes'; -export abstract class BaseElement extends BaseShape { +export abstract class BaseElement extends BaseShape { + protected get context(): RuntimeContext { + return this.attributes.context!; + } + protected get parsedAttributes() { return this.attributes as Required; } diff --git a/packages/g6/src/elements/combos/base-combo.ts b/packages/g6/src/elements/combos/base-combo.ts index 58c0d4516e..e99515d0eb 100644 --- a/packages/g6/src/elements/combos/base-combo.ts +++ b/packages/g6/src/elements/combos/base-combo.ts @@ -194,7 +194,7 @@ export abstract class BaseCombo): number { - const ancestors = attributes.context!.model.getAncestorsData(this.id, COMBO_KEY) || []; + const ancestors = this.context.model.getAncestorsData(this.id, COMBO_KEY) || []; return ancestors.length; } @@ -231,7 +231,7 @@ export abstract class BaseCombo, container: Group = this) { diff --git a/packages/g6/src/elements/nodes/html.ts b/packages/g6/src/elements/nodes/html.ts index 4065d0f72b..dabf5bad8e 100644 --- a/packages/g6/src/elements/nodes/html.ts +++ b/packages/g6/src/elements/nodes/html.ts @@ -50,7 +50,7 @@ export class HTML extends BaseNode { private rootPointerEvent = new FederatedPointerEvent(null); private get eventService() { - return this.attributes.context!.canvas.context.eventService; + return this.context.canvas.context.eventService; } private get events() { @@ -105,7 +105,7 @@ export class HTML extends BaseNode { } private forwardEvents = (nativeEvent: PointerEvent) => { - const canvas = this.attributes.context!.canvas.main; + const canvas = this.context.canvas; const iCanvas = canvas.context.renderingContext.root!.ownerDocument!.defaultView!; const normalizedEvents = this.normalizeToPointerEvent(nativeEvent, iCanvas); @@ -232,11 +232,7 @@ export class HTML extends BaseNode { let x: number; let y: number; const { offsetX, offsetY, clientX, clientY } = nativeEvent; - if ( - this.attributes.context?.canvas.main.context.config.supportsCSSTransform && - !isNil(offsetX) && - !isNil(offsetY) - ) { + if (this.context.canvas.context.config.supportsCSSTransform && !isNil(offsetX) && !isNil(offsetY)) { x = offsetX; y = offsetY; } else { diff --git a/packages/g6/src/global.d.ts b/packages/g6/src/global.d.ts new file mode 100644 index 0000000000..ebb3c094ae --- /dev/null +++ b/packages/g6/src/global.d.ts @@ -0,0 +1,12 @@ +import '@antv/g'; + +declare module '@antv/g' { + interface BaseStyleProps { + /** + * 图形所在的图层,默认为 'main'。 + * + * The layer where the shape is located, default is 'main'. + */ + $layer?: string; + } +} diff --git a/packages/g6/src/plugins/fullscreen/index.ts b/packages/g6/src/plugins/fullscreen/index.ts index 61c9537377..3b7078b96b 100644 --- a/packages/g6/src/plugins/fullscreen/index.ts +++ b/packages/g6/src/plugins/fullscreen/index.ts @@ -58,7 +58,10 @@ export class Fullscreen extends BasePlugin { private shortcut: Shortcut; + private style: HTMLStyleElement; + private $el = this.context.canvas.getContainer()!; + private graphSize: [number, number] = [0, 0]; constructor(context: RuntimeContext, options: FullscreenOptions) { @@ -68,7 +71,13 @@ export class Fullscreen extends BasePlugin { this.bindEvents(); - this.$el.style.backgroundColor = this.context.graph.getBackground()!; + this.style = document.createElement('style'); + document.head.appendChild(this.style); + this.style.innerHTML = ` + :not(:root):fullscreen::backdrop { + background: transparent; + } + `; } private bindEvents() { @@ -150,6 +159,12 @@ export class Fullscreen extends BasePlugin { super.update(options); this.bindEvents(); } + + public destroy(): void { + this.exit(); + this.style.remove(); + super.destroy(); + } } /** diff --git a/packages/g6/src/runtime/canvas.ts b/packages/g6/src/runtime/canvas.ts index a2dcaccf8d..d818b2aa1b 100644 --- a/packages/g6/src/runtime/canvas.ts +++ b/packages/g6/src/runtime/canvas.ts @@ -1,17 +1,15 @@ -import type { Cursor, DisplayObject, CanvasConfig as GCanvasConfig, IAnimation, IRenderer } from '@antv/g'; +import type { DisplayObject, CanvasConfig as GCanvasConfig, IChildNode } from '@antv/g'; import { CanvasEvent, Canvas as GCanvas } from '@antv/g'; import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop'; -import { createDOM, isFunction, isString } from '@antv/util'; +import { createDOM } from '@antv/util'; import type { CanvasOptions } from '../spec/canvas'; -import type { PointObject } from '../types'; -import type { CanvasLayer } from '../types/canvas'; +import type { CanvasLayer } from '../types'; import { getBBoxSize, getCombinedBBox } from '../utils/bbox'; export interface CanvasConfig - extends Pick { + extends Pick { renderer?: CanvasOptions['renderer']; - background?: string; } export interface DataURLOptions { @@ -40,153 +38,93 @@ export interface DataURLOptions { encoderOptions: number; } -/** - * @deprecated this canvas will be replace by layered canvas - */ -export class Canvas { - protected config: CanvasConfig; +const layersName: CanvasLayer[] = ['background', 'main', 'label', 'transient']; - public background!: GCanvas; - public main!: GCanvas; - public label!: GCanvas; - public transient!: GCanvas; +export class Canvas extends GCanvas { + private extends: { + config: CanvasConfig; + renderer: CanvasOptions['renderer']; + renderers: Record; + layers: Record; + }; - public get canvas() { - return { - main: this.main, - label: this.label, - transient: this.transient, - background: this.background, + public getLayer(layer: CanvasLayer) { + return this.extends.layers[layer]; + } + + /** + * 获取所有图层 + * + * Get all layers + * @returns 图层 Layer + */ + public getLayers() { + return this.extends.layers; + } + + /** + * 获取渲染器 + * + * Get renderer + * @param layer - 图层 Layer + * @returns 渲染器 Renderer + */ + public getRenderer(layer: CanvasLayer) { + return this.extends.renderers[layer]; + } + + constructor(config: CanvasConfig) { + const { renderer, background, cursor, ...restConfig } = config; + const renderers = createRenderers(renderer); + + // init main canvas + super({ ...restConfig, supportsMutipleCanvasesInOneContainer: true, cursor, renderer: renderers.main }); + + const layers = Object.fromEntries( + layersName.map((layer) => { + if (layer === 'main') return [layer, this]; + + const canvas = new GCanvas({ + ...restConfig, + supportsMutipleCanvasesInOneContainer: true, + renderer: renderers[layer], + background: layer === 'background' ? background : undefined, + }); + + return [layer, canvas]; + }), + ) as Record; + + configCanvasDom(layers); + + this.extends = { + config, + renderer, + renderers, + layers, }; } - public get document() { - return this.main.document; - } - - public renderers!: Record; - - private initialized = false; - - constructor(config: CanvasConfig) { - this.config = config; - } - - public async init() { - if (this.initialized) return; - - const { renderer: getRenderer, background, ...restConfig } = this.config; - const names: CanvasLayer[] = ['main', 'label', 'transient', 'background']; - - const { renderers, canvas } = names.reduce( - (acc, name) => { - const renderer = isFunction(getRenderer) ? getRenderer?.(name) : new CanvasRenderer(); - - if (name === 'main') { - renderer.registerPlugin( - new DragNDropPlugin({ - isDocumentDraggable: true, - isDocumentDroppable: true, - dragstartDistanceThreshold: 10, - dragstartTimeThreshold: 100, - }), - ); - } else { - renderer.unregisterPlugin(renderer.getPlugin('dom-interaction')); - } - - const canvas = new GCanvas({ - renderer, - supportsMutipleCanvasesInOneContainer: true, - ...restConfig, - }); - - acc.renderers[name] = renderer; - acc.canvas[name] = canvas; - this[name] = canvas; - - return acc; - }, - { renderers: {}, canvas: {} } as { - renderers: Record; - canvas: Record; - }, - ); - - this.renderers = renderers; - - Object.entries(canvas).forEach(([name, canvas]) => { - const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement; - - domElement.style.position = 'absolute'; - domElement.style.outline = 'none'; - domElement.tabIndex = 1; - - if (name !== 'main') domElement.style.pointerEvents = 'none'; - }); - - this.setBackground(); - - await Promise.all(Object.values(this.canvas).map((canvas) => canvas.ready)); - - this.initialized = true; - } - - public getRendererType(layer: CanvasLayer = 'main') { - const plugins = this.renderers[layer].getPlugins(); - - for (const plugin of plugins) { - if (plugin.name === 'canvas-renderer') return 'canvas'; - if (plugin.name === 'svg-renderer') return 'svg'; - if (plugin.name === 'device-renderer') return 'gpu'; - } - - return 'unknown'; - } - - public get context() { - return this.main.context; - } - - public getDevice() { - // @ts-expect-error deviceRendererPlugin is private - return this.main.context?.deviceRendererPlugin?.getDevice(); - } - - public getConfig() { - return this.config; - } - - public setBackground(background = this.config.background) { - this.config.background = background; - } - - public setCursor(cursor: Cursor) { - this.config.cursor = cursor; - Object.values(this.canvas).forEach((canvas) => { - canvas.setCursor(cursor); - }); - } - - public getSize(): [number, number] { - return [this.config.width || 0, this.config.height || 0]; + public get ready() { + return Promise.all([ + super.ready, + ...Object.entries(this.getLayers()).map(([layer, canvas]) => + layer === 'main' ? Promise.resolve() : canvas.ready, + ), + ]); } public resize(width: number, height: number) { - this.config.width = width; - this.config.height = height; - Object.values(this.canvas).forEach((canvas) => { - canvas.resize(width, height); + Object.assign(this.extends.config, { width, height }); + Object.values(this.getLayers()).forEach((canvas) => { + if (canvas === this) super.resize(width, height); + else canvas.resize(width, height); }); } - public getCamera() { - return this.main.getCamera(); - } - public getBounds() { return getCombinedBBox( - Object.values(this.canvas) + Object.values(this.getLayers()) .map((canvas) => canvas.document.documentElement) .filter((el) => el.childNodes.length > 0) .map((el) => el.getBounds()), @@ -194,48 +132,38 @@ export class Canvas { } public getContainer() { - const container = this.config.container!; - - return isString(container) ? document.getElementById(container!) : container; + const container = this.extends.config.container!; + return typeof container === 'string' ? document.getElementById(container!) : container; } - public appendChild(child: T): T { - const layer = (child.style?.$layer || 'main') as CanvasLayer; - - return this[layer].appendChild(child); + public getSize(): [number, number] { + return [this.extends.config.width || 0, this.extends.config.height || 0]; } - public getContextService() { - return this.main.getContextService(); + public appendChild(child: T, index?: number): T { + const layer = ((child as unknown as DisplayObject).style?.$layer || 'main') as CanvasLayer; + if (layer === 'main') return super.appendChild(child, index); + return this.getLayer(layer).appendChild(child, index); } - public viewport2Client(viewport: PointObject) { - return this.main.viewport2Client(viewport); - } + public setRenderer(renderer: any) { + if (renderer === this.extends.renderer) return; + const renderers = createRenderers(renderer); + this.extends.renderers = renderers; - public viewport2Canvas(viewport: PointObject) { - return this.main.viewport2Canvas(viewport); - } - - public client2Viewport(client: PointObject) { - return this.main.client2Viewport(client); - } - - public canvas2Viewport(canvas: PointObject) { - return this.main.canvas2Viewport(canvas); + Object.entries(renderers).forEach(([layer, instance]) => { + if (layer === 'main') super.setRenderer(instance); + else this.getLayer(layer as CanvasLayer).setRenderer(instance); + }); } public async toDataURL(options: Partial = {}) { const devicePixelRatio = window.devicePixelRatio || 1; const { mode = 'viewport', ...restOptions } = options; - - let startX = 0; - let startY = 0; - let width = 0; - let height = 0; + let [startX, startY, width, height] = [0, 0, 0, 0]; if (mode === 'viewport') { - [width, height] = [this.config.width || 0, this.config.height || 0]; + [width, height] = this.getSize(); } else if (mode === 'overall') { const bounds = this.getBounds(); const size = getBBoxSize(bounds); @@ -251,28 +179,28 @@ export class Canvas { renderer: new CanvasRenderer(), devicePixelRatio, container, - background: this.config.background, + background: this.extends.config.background, }); await offscreenCanvas.ready; - offscreenCanvas.appendChild(this.background.getRoot().cloneNode(true)); - offscreenCanvas.appendChild(this.main.getRoot().cloneNode(true)); + offscreenCanvas.appendChild(this.getLayer('background').getRoot().cloneNode(true)); + offscreenCanvas.appendChild(this.getRoot().cloneNode(true)); // Handle label canvas - const label = this.label.getRoot().cloneNode(true); + const label = this.getLayer('label').getRoot().cloneNode(true); const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 }); - const currentCanvasPosition = this.main.viewport2Canvas({ x: 0, y: 0 }); + const currentCanvasPosition = this.viewport2Canvas({ x: 0, y: 0 }); label.translate([ currentCanvasPosition.x - originCanvasPosition.x, currentCanvasPosition.y - originCanvasPosition.y, ]); - label.scale(1 / this.main.getCamera().getZoom()); + label.scale(1 / this.getCamera().getZoom()); offscreenCanvas.appendChild(label); - offscreenCanvas.appendChild(this.transient.getRoot().cloneNode(true)); + offscreenCanvas.appendChild(this.getLayer('transient').getRoot().cloneNode(true)); - const camera = this.main.getCamera(); + const camera = this.getCamera(); const offscreenCamera = offscreenCanvas.getCamera(); if (mode === 'viewport') { @@ -289,7 +217,7 @@ export class Canvas { const contextService = offscreenCanvas.getContextService(); return new Promise((resolve) => { - offscreenCanvas.on(CanvasEvent.RERENDER, async () => { + offscreenCanvas.addEventListener(CanvasEvent.RERENDER, async () => { // 等待图片渲染完成 / Wait for the image to render await new Promise((r) => setTimeout(r, 300)); const url = await contextService.toDataURL(restOptions); @@ -298,57 +226,60 @@ export class Canvas { }); } - public destroy() { - this.config = {}; - // @ts-expect-error force delete - this.renderers = {}; - Object.entries(this.canvas).forEach(([name, canvas]) => { + public destroy(cleanUp?: boolean, skipTriggerEvent?: boolean) { + Object.values(this.getLayers()).forEach((canvas) => { const camera = canvas.getCamera(); - // @ts-expect-error landmark is private - if (camera.landmarks?.length) { - camera.cancelLandmarkAnimation(); - } - - destroyCanvas(canvas); - // @ts-expect-error force delete - this[name] = undefined; + camera.cancelLandmarkAnimation(); + if (canvas === this) super.destroy(cleanUp, skipTriggerEvent); + else canvas.destroy(cleanUp, skipTriggerEvent); }); } } /** - * G Canvas destroy 未处理动画对象,导致内存泄漏 + * 创建渲染器 * - * G Canvas destroy does not handle animation objects, causing memory leaks - * @param canvas GCanvas - * @remarks - * 这些操作都应该在 G 中完成,这里只是一个临时的解决方案 - * 此操作大概能在测试环节降低 10~20% 的内存占用(从 2800MB 降低到 2200MB) - * - * These operations should be completed in G, this is just a temporary solution - * This operation can reduce memory usage by 10-20% in the test environment (from 2800MB to 2200MB) + * Create renderers + * @param renderer - 渲染器创建器 Renderer creator + * @returns 渲染器 Renderer */ -function destroyCanvas(canvas: GCanvas) { - canvas.destroy(); +function createRenderers(renderer: CanvasConfig['renderer']) { + return Object.fromEntries( + layersName.map((layer) => { + const instance = renderer?.(layer) || new CanvasRenderer(); - // 移除相机事件 / Remove camera events - const camera = canvas.getCamera(); - camera.eventEmitter.removeAllListeners(); + if (layer === 'main') { + instance.registerPlugin( + new DragNDropPlugin({ + isDocumentDraggable: true, + isDocumentDroppable: true, + dragstartDistanceThreshold: 10, + dragstartTimeThreshold: 100, + }), + ); + } else { + instance.unregisterPlugin(instance.getPlugin('dom-interaction')); + } - canvas.document.timeline.destroy(); - // @ts-expect-error private property - const { animationsWithPromises } = canvas.document.timeline; - // 释放 target 对象图形 / Release target object graphics - animationsWithPromises.forEach((animation: IAnimation) => { - if (animation.effect.target) animation.effect.target = null; - // @ts-expect-error private property - if (animation.effect.computedTiming) animation.effect.computedTiming = null; - }); - - // @ts-expect-error private property - canvas.document.timeline.animationsWithPromises = []; - // @ts-expect-error private property - canvas.document.timeline.rafCallbacks = []; - // @ts-expect-error private property - canvas.document.timeline = null; + return [layer, instance]; + }), + ) as Record; +} + +/** + * 配置画布 DOM + * + * Configure canvas DOM + * @param layers - 画布 Canvas + */ +function configCanvasDom(layers: Record) { + Object.entries(layers).forEach(([layer, canvas]) => { + const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement; + + domElement.style.position = 'absolute'; + domElement.style.outline = 'none'; + domElement.tabIndex = 1; + + if (layer !== 'main') domElement.style.pointerEvents = 'none'; + }); } diff --git a/packages/g6/src/runtime/graph.ts b/packages/g6/src/runtime/graph.ts index fa6e6b534f..0d731acdc8 100644 --- a/packages/g6/src/runtime/graph.ts +++ b/packages/g6/src/runtime/graph.ts @@ -3,7 +3,6 @@ import type { AABB, BaseStyleProps } from '@antv/g'; import { debounce, isEqual, isFunction, isNumber, isObject, isString, omit } from '@antv/util'; import { COMBO_KEY, GraphEvent } from '../constants'; import type { Plugin } from '../plugins/types'; -import { getExtension } from '../registry'; import type { BehaviorOptions, ComboData, @@ -119,12 +118,10 @@ export class Graph extends EventEmitter { * @apiCategory option */ public setOptions(options: GraphOptions): void { - const { background, behaviors, combo, data, edge, height, layout, node, plugins, theme, transforms, width } = - options; + const { behaviors, combo, data, edge, height, layout, node, plugins, theme, transforms, width, renderer } = options; Object.assign(this.options, options); - if (background) this.setBackground(background); if (behaviors) this.setBehaviors(behaviors); if (combo) this.setCombo(combo); if (data) this.setData(data); @@ -136,29 +133,7 @@ export class Graph extends EventEmitter { if (transforms) this.setTransforms(transforms); if (isNumber(width) || isNumber(height)) this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0); - } - - /** - * 设置画布背景色 - * - * Set canvas background color - * @param background - 背景色 | background color - * @apiCategory canvas - */ - public setBackground(background: GraphOptions['background']): void { - this.options.background = background; - this.context.canvas?.setBackground(background); - } - - /** - * 获取画布背景色 - * - * Get canvas background color - * @returns 背景色 | background color - * @apiCategory canvas - */ - public getBackground(): GraphOptions['background'] { - return this.options.background; + if (renderer) this.context.canvas.setRenderer(renderer); } /** @@ -280,11 +255,6 @@ export class Graph extends EventEmitter { */ public setTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void { this.options.theme = isFunction(theme) ? theme(this.getTheme()) : theme; - - const { background } = getExtension('theme', this.options.theme) || {}; - if (background && !this.options.background) { - this.setBackground(background); - } } /** @@ -1034,14 +1004,13 @@ export class Graph extends EventEmitter { } private async initCanvas() { - if (this.context.canvas) return await this.context.canvas.init(); + if (this.context.canvas) return await this.context.canvas.ready; const { container = 'container', width, height, renderer, background } = this.options; if (container instanceof Canvas) { this.context.canvas = container; - container.setBackground(background); - await container.init(); + await container.ready; } else { const $container = isString(container) ? document.getElementById(container!) : container; const containerSize = sizeOf($container!); @@ -1057,7 +1026,7 @@ export class Graph extends EventEmitter { }); this.context.canvas = canvas; - await canvas.init(); + await canvas.ready; this.emit(GraphEvent.AFTER_CANVAS_INIT, { canvas }); } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index eccc335f93..4340350e19 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,2 @@ packages: - - 'packages/**' \ No newline at end of file + - 'packages/*' \ No newline at end of file