mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 10:18:14 +08:00
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
This commit is contained in:
parent
5b993b007d
commit
0e911c2ad9
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ export abstract class BaseNode3D<S extends BaseNode3DStyleProps> 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;
|
||||
}
|
||||
|
@ -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<IndentedNodeStyleProps>): 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 });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -43,10 +43,6 @@ describe('Graph', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('setBackground/getBackground', () => {
|
||||
expect(graph.getBackground()).toEqual('#ffffff');
|
||||
});
|
||||
|
||||
it('getSize', () => {
|
||||
expect(graph.getSize()).toEqual([500, 500]);
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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<CanvasLayer, Canvas>,
|
||||
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 <svg>
|
||||
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,
|
||||
);
|
||||
|
@ -376,11 +376,10 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
|
||||
} 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);
|
||||
|
@ -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<T extends BaseShapeStyleProps> extends BaseShape<T> {
|
||||
export abstract class BaseElement<T extends BaseElementStyleProps> extends BaseShape<T> {
|
||||
protected get context(): RuntimeContext {
|
||||
return this.attributes.context!;
|
||||
}
|
||||
|
||||
protected get parsedAttributes() {
|
||||
return this.attributes as Required<T>;
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStylePr
|
||||
}
|
||||
|
||||
protected getComboZIndex(attributes: Required<S>): 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<S extends BaseComboStyleProps = BaseComboStylePr
|
||||
Object.assign(this.style, comboStyle);
|
||||
// Sync combo position to model
|
||||
const { x, y } = comboStyle;
|
||||
attributes.context!.model.syncComboDatum({ id: this.id, style: { x, y } });
|
||||
this.context.model.syncComboDatum({ id: this.id, style: { x, y } });
|
||||
}
|
||||
|
||||
public render(attributes: Required<S>, container: Group = this) {
|
||||
|
@ -50,7 +50,7 @@ export class HTML extends BaseNode<HTMLStyleProps> {
|
||||
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<HTMLStyleProps> {
|
||||
}
|
||||
|
||||
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<HTMLStyleProps> {
|
||||
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 {
|
||||
|
12
packages/g6/src/global.d.ts
vendored
Normal file
12
packages/g6/src/global.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
import '@antv/g';
|
||||
|
||||
declare module '@antv/g' {
|
||||
interface BaseStyleProps {
|
||||
/**
|
||||
* <zh/> 图形所在的图层,默认为 'main'。
|
||||
*
|
||||
* <en/> The layer where the shape is located, default is 'main'.
|
||||
*/
|
||||
$layer?: string;
|
||||
}
|
||||
}
|
@ -58,7 +58,10 @@ export class Fullscreen extends BasePlugin<FullscreenOptions> {
|
||||
|
||||
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<FullscreenOptions> {
|
||||
|
||||
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<FullscreenOptions> {
|
||||
super.update(options);
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.exit();
|
||||
this.style.remove();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor'> {
|
||||
extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor' | 'background'> {
|
||||
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
|
||||
const layersName: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
|
||||
|
||||
export class Canvas extends GCanvas {
|
||||
private extends: {
|
||||
config: CanvasConfig;
|
||||
renderer: CanvasOptions['renderer'];
|
||||
renderers: Record<CanvasLayer, CanvasRenderer>;
|
||||
layers: Record<CanvasLayer, GCanvas>;
|
||||
};
|
||||
|
||||
public getLayer(layer: CanvasLayer) {
|
||||
return this.extends.layers[layer];
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 获取所有图层
|
||||
*
|
||||
* <en/> Get all layers
|
||||
* @returns <zh/> 图层 <en/> Layer
|
||||
*/
|
||||
export class Canvas {
|
||||
protected config: CanvasConfig;
|
||||
public getLayers() {
|
||||
return this.extends.layers;
|
||||
}
|
||||
|
||||
public background!: GCanvas;
|
||||
public main!: GCanvas;
|
||||
public label!: GCanvas;
|
||||
public transient!: GCanvas;
|
||||
/**
|
||||
* <zh/> 获取渲染器
|
||||
*
|
||||
* <en/> Get renderer
|
||||
* @param layer - <zh/> 图层 <en/> Layer
|
||||
* @returns <zh/> 渲染器 <en/> Renderer
|
||||
*/
|
||||
public getRenderer(layer: CanvasLayer) {
|
||||
return this.extends.renderers[layer];
|
||||
}
|
||||
|
||||
public get canvas() {
|
||||
return {
|
||||
main: this.main,
|
||||
label: this.label,
|
||||
transient: this.transient,
|
||||
background: this.background,
|
||||
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<CanvasLayer, GCanvas>;
|
||||
|
||||
configCanvasDom(layers);
|
||||
|
||||
this.extends = {
|
||||
config,
|
||||
renderer,
|
||||
renderers,
|
||||
layers,
|
||||
};
|
||||
}
|
||||
|
||||
public get document() {
|
||||
return this.main.document;
|
||||
}
|
||||
|
||||
public renderers!: Record<CanvasLayer, IRenderer>;
|
||||
|
||||
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<CanvasLayer, IRenderer>;
|
||||
canvas: Record<CanvasLayer, GCanvas>;
|
||||
},
|
||||
);
|
||||
|
||||
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<T extends DisplayObject>(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<T extends IChildNode>(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<DataURLOptions> = {}) {
|
||||
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<string>((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;
|
||||
if (canvas === this) super.destroy(cleanUp, skipTriggerEvent);
|
||||
else canvas.destroy(cleanUp, skipTriggerEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> G Canvas destroy 未处理动画对象,导致内存泄漏
|
||||
* <zh/> 创建渲染器
|
||||
*
|
||||
* <en/> G Canvas destroy does not handle animation objects, causing memory leaks
|
||||
* @param canvas GCanvas
|
||||
* @remarks
|
||||
* <zh/> 这些操作都应该在 G 中完成,这里只是一个临时的解决方案
|
||||
* 此操作大概能在测试环节降低 10~20% 的内存占用(从 2800MB 降低到 2200MB)
|
||||
*
|
||||
* <en/> 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)
|
||||
* <en/> Create renderers
|
||||
* @param renderer - <zh/> 渲染器创建器 <en/> Renderer creator
|
||||
* @returns <zh/> 渲染器 <en/> 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<CanvasLayer, CanvasRenderer>;
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 配置画布 DOM
|
||||
*
|
||||
* <en/> Configure canvas DOM
|
||||
* @param layers - <zh/> 画布 <en/> Canvas
|
||||
*/
|
||||
function configCanvasDom(layers: Record<CanvasLayer, GCanvas>) {
|
||||
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';
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 设置画布背景色
|
||||
*
|
||||
* <en/> Set canvas background color
|
||||
* @param background - <zh/> 背景色 | <en/> background color
|
||||
* @apiCategory canvas
|
||||
*/
|
||||
public setBackground(background: GraphOptions['background']): void {
|
||||
this.options.background = background;
|
||||
this.context.canvas?.setBackground(background);
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 获取画布背景色
|
||||
*
|
||||
* <en/> Get canvas background color
|
||||
* @returns <zh/> 背景色 | <en/> 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 });
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
packages:
|
||||
- 'packages/**'
|
||||
- 'packages/*'
|
Loading…
Reference in New Issue
Block a user