mirror of
https://gitee.com/antv/g6.git
synced 2024-11-30 02:38:20 +08:00
refactor: support dynamic switch renderer (#6062)
* feat(runtime): graph emit renderer change event * refactor: refactor canvas and support switch renderer * fix(3d): remove light on destroy plugin * refactor(3d): clear cache when renderer change * test: add switch renderer demo --------- Co-authored-by: antv <antv@antfin.com>
This commit is contained in:
parent
6775bbc70c
commit
ed6e940876
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -11,9 +11,10 @@
|
|||||||
"afterelementtranslate",
|
"afterelementtranslate",
|
||||||
"afterelementupdate",
|
"afterelementupdate",
|
||||||
"afterlayout",
|
"afterlayout",
|
||||||
"afterstagelayout",
|
|
||||||
"afterrender",
|
"afterrender",
|
||||||
|
"afterrendererchange",
|
||||||
"aftersizechange",
|
"aftersizechange",
|
||||||
|
"afterstagelayout",
|
||||||
"aftertransform",
|
"aftertransform",
|
||||||
"afterviewportanimate",
|
"afterviewportanimate",
|
||||||
"antv",
|
"antv",
|
||||||
@ -30,9 +31,10 @@
|
|||||||
"beforeelementtranslate",
|
"beforeelementtranslate",
|
||||||
"beforeelementupdate",
|
"beforeelementupdate",
|
||||||
"beforelayout",
|
"beforelayout",
|
||||||
"beforestagelayout",
|
|
||||||
"beforerender",
|
"beforerender",
|
||||||
|
"beforerendererchange",
|
||||||
"beforesizechange",
|
"beforesizechange",
|
||||||
|
"beforestagelayout",
|
||||||
"beforetransform",
|
"beforetransform",
|
||||||
"beforeviewportanimate",
|
"beforeviewportanimate",
|
||||||
"bubblesets",
|
"bubblesets",
|
||||||
|
@ -8,3 +8,4 @@ export { massiveElements } from './massive-elements';
|
|||||||
export * from './position';
|
export * from './position';
|
||||||
export * from './shapes';
|
export * from './shapes';
|
||||||
export * from './solar-system';
|
export * from './solar-system';
|
||||||
|
export { switchRenderer } from './switch-renderer';
|
||||||
|
60
packages/g6-extension-3d/__tests__/demos/switch-renderer.ts
Normal file
60
packages/g6-extension-3d/__tests__/demos/switch-renderer.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||||
|
import type { NodeData } from '@antv/g6';
|
||||||
|
import { ExtensionCategory, Graph, register } from '@antv/g6';
|
||||||
|
import { Light, Sphere, renderer } from '../../src';
|
||||||
|
|
||||||
|
export const switchRenderer: TestCase = async (context) => {
|
||||||
|
register(ExtensionCategory.PLUGIN, '3d-light', Light);
|
||||||
|
register(ExtensionCategory.NODE, 'sphere', Sphere);
|
||||||
|
|
||||||
|
const nodes: NodeData[] = [{ id: '1' }, { id: '2' }];
|
||||||
|
|
||||||
|
const graph = new Graph({
|
||||||
|
...context,
|
||||||
|
data: {
|
||||||
|
nodes,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'grid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.render();
|
||||||
|
|
||||||
|
switchRenderer.form = (panel) => {
|
||||||
|
panel.add({ renderer: '2d' }, 'renderer', ['2d', '3d']).onChange((name: string) => {
|
||||||
|
if (name === '2d') {
|
||||||
|
graph.setOptions({
|
||||||
|
renderer: () => new CanvasRenderer(),
|
||||||
|
node: {
|
||||||
|
type: 'circle',
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
graph.setOptions({
|
||||||
|
renderer,
|
||||||
|
node: {
|
||||||
|
type: 'sphere',
|
||||||
|
style: {
|
||||||
|
materialType: 'phong',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
type: '3d-light',
|
||||||
|
directional: {
|
||||||
|
direction: [0, 0, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.draw();
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
};
|
@ -72,6 +72,8 @@ export class Light extends BasePlugin<LightOptions> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
|
this.ambient?.remove();
|
||||||
|
this.directional?.remove();
|
||||||
this.unbindEvents();
|
this.unbindEvents();
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import type { Device } from '@antv/g-device-api';
|
import type { Device } from '@antv/g-device-api';
|
||||||
import type { ProceduralGeometry } from '@antv/g-plugin-3d';
|
import type { ProceduralGeometry } from '@antv/g-plugin-3d';
|
||||||
|
|
||||||
|
let DEVICE: Device;
|
||||||
|
|
||||||
const GEOMETRY_CACHE = new Map<string, unknown>();
|
const GEOMETRY_CACHE = new Map<string, unknown>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,6 +21,12 @@ export function createGeometry<T extends ProceduralGeometry<any>>(
|
|||||||
Ctor: new (...args: any[]) => T,
|
Ctor: new (...args: any[]) => T,
|
||||||
style: Record<string, unknown>,
|
style: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
|
if (!DEVICE) DEVICE = device;
|
||||||
|
else if (DEVICE !== device) {
|
||||||
|
DEVICE = device;
|
||||||
|
GEOMETRY_CACHE.clear();
|
||||||
|
}
|
||||||
|
|
||||||
const cacheKey =
|
const cacheKey =
|
||||||
type +
|
type +
|
||||||
'|' +
|
'|' +
|
||||||
|
@ -15,4 +15,8 @@ export class TupleMap<K1, K2, V> {
|
|||||||
}
|
}
|
||||||
this.map.get(key1)!.set(key2, value);
|
this.map.get(key1)!.set(key2, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.map.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import { getCacheKey } from './cache';
|
|||||||
import { TupleMap } from './map';
|
import { TupleMap } from './map';
|
||||||
import { createTexture } from './texture';
|
import { createTexture } from './texture';
|
||||||
|
|
||||||
|
let PLUGIN: Plugin;
|
||||||
|
|
||||||
const MATERIAL_CACHE = new TupleMap<symbol, string | TexImageSource | undefined, GMaterial>();
|
const MATERIAL_CACHE = new TupleMap<symbol, string | TexImageSource | undefined, GMaterial>();
|
||||||
|
|
||||||
const MATERIAL_MAP = {
|
const MATERIAL_MAP = {
|
||||||
@ -25,6 +27,12 @@ const MATERIAL_MAP = {
|
|||||||
* @returns <zh/> 材质对象 <en/> material object
|
* @returns <zh/> 材质对象 <en/> material object
|
||||||
*/
|
*/
|
||||||
export function createMaterial(plugin: Plugin, options: Material, texture?: string | TexImageSource): GMaterial {
|
export function createMaterial(plugin: Plugin, options: Material, texture?: string | TexImageSource): GMaterial {
|
||||||
|
if (!PLUGIN) PLUGIN = plugin;
|
||||||
|
else if (PLUGIN !== plugin) {
|
||||||
|
PLUGIN = plugin;
|
||||||
|
MATERIAL_CACHE.clear();
|
||||||
|
}
|
||||||
|
|
||||||
const key = getCacheKey(options);
|
const key = getCacheKey(options);
|
||||||
|
|
||||||
if (MATERIAL_CACHE.has(key, texture)) {
|
if (MATERIAL_CACHE.has(key, texture)) {
|
||||||
|
33
packages/g6/__tests__/demos/canvas-switch-renderer.ts
Normal file
33
packages/g6/__tests__/demos/canvas-switch-renderer.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Graph } from '@/src';
|
||||||
|
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||||
|
import { Renderer as SVGRenderer } from '@antv/g-svg';
|
||||||
|
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
|
||||||
|
|
||||||
|
export const canvasSwitchRenderer: TestCase = async (context) => {
|
||||||
|
const graph = new Graph({
|
||||||
|
...context,
|
||||||
|
data: {
|
||||||
|
nodes: Array.from({ length: 10 }).map((_, i) => ({ id: `node-${i}` })),
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'grid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.render();
|
||||||
|
|
||||||
|
canvasSwitchRenderer.form = (panel) => {
|
||||||
|
panel.add({ renderer: 'canvas' }, 'renderer', ['canvas', 'svg', 'webgl']).onChange((name: string) => {
|
||||||
|
graph.setOptions({
|
||||||
|
renderer: () => {
|
||||||
|
if (name === 'svg') return new SVGRenderer();
|
||||||
|
if (name === 'webgl') return new WebGLRenderer();
|
||||||
|
return new CanvasRenderer();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
};
|
@ -19,6 +19,7 @@ export { behaviorLassoSelect } from './behavior-lasso-select';
|
|||||||
export { behaviorOptimizeViewportTransform } from './behavior-optimize-viewport-transform';
|
export { behaviorOptimizeViewportTransform } from './behavior-optimize-viewport-transform';
|
||||||
export { behaviorScrollCanvas } from './behavior-scroll-canvas';
|
export { behaviorScrollCanvas } from './behavior-scroll-canvas';
|
||||||
export { behaviorZoomCanvas } from './behavior-zoom-canvas';
|
export { behaviorZoomCanvas } from './behavior-zoom-canvas';
|
||||||
|
export { canvasSwitchRenderer } from './canvas-switch-renderer';
|
||||||
export { caseIndentedTree } from './case-indented-tree';
|
export { caseIndentedTree } from './case-indented-tree';
|
||||||
export { caseOrgChart } from './case-org-chart';
|
export { caseOrgChart } from './case-org-chart';
|
||||||
export { elementCombo } from './combo';
|
export { elementCombo } from './combo';
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { parsePoint } from '@/src/utils/point';
|
|
||||||
import { createGraphCanvas } from '@@/utils';
|
import { createGraphCanvas } from '@@/utils';
|
||||||
|
|
||||||
describe('Canvas', () => {
|
describe('Canvas', () => {
|
||||||
@ -19,32 +18,32 @@ describe('Canvas', () => {
|
|||||||
|
|
||||||
it('coordinate transform', () => {
|
it('coordinate transform', () => {
|
||||||
// TODO g canvas client 坐标转换疑似异常
|
// TODO g canvas client 坐标转换疑似异常
|
||||||
expect(parsePoint(svg.viewport2Client({ x: 0, y: 0 }))).toBeCloseTo([0, 0, 0]);
|
expect(svg.getClientByCanvas([0, 0])).toBeCloseTo([0, 0, 0]);
|
||||||
expect(parsePoint(svg.viewport2Canvas({ x: 0, y: 0 }))).toBeCloseTo([0, 0, 0]);
|
expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([0, 0, 0]);
|
||||||
|
|
||||||
expect(parsePoint(svg.client2Viewport({ x: 0, y: 0 }))).toBeCloseTo([0, 0, 0]);
|
expect(svg.getViewportByClient([0, 0])).toBeCloseTo([0, 0, 0]);
|
||||||
expect(parsePoint(svg.canvas2Viewport({ x: 0, y: 0 }))).toBeCloseTo([0, 0, 0]);
|
expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([0, 0, 0]);
|
||||||
|
|
||||||
const camera = svg.getCamera();
|
const camera = svg.getCamera();
|
||||||
camera.pan(100, 100);
|
camera.pan(100, 100);
|
||||||
expect([...camera.getPosition()]).toBeCloseTo([350, 350, 500]);
|
expect([...camera.getPosition()]).toBeCloseTo([350, 350, 500]);
|
||||||
expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);
|
expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);
|
||||||
expect(parsePoint(svg.viewport2Canvas({ x: 0, y: 0 }))).toBeCloseTo([100, 100, 0]);
|
expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([100, 100, 0]);
|
||||||
expect(parsePoint(svg.canvas2Viewport({ x: 0, y: 0 }))).toBeCloseTo([-100, -100, 0]);
|
expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([-100, -100, 0]);
|
||||||
|
|
||||||
// camera pan 采用相对移动
|
// camera pan 采用相对移动
|
||||||
camera.pan(-200, -200);
|
camera.pan(-200, -200);
|
||||||
// focal point wont change
|
// focal point wont change
|
||||||
// expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);
|
// expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);
|
||||||
expect([...camera.getPosition()]).toBeCloseTo([150, 150, 500]);
|
expect([...camera.getPosition()]).toBeCloseTo([150, 150, 500]);
|
||||||
expect(parsePoint(svg.viewport2Canvas({ x: 0, y: 0 }))).toBeCloseTo([-100, -100, 0]);
|
expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);
|
||||||
expect(parsePoint(svg.canvas2Viewport({ x: 0, y: 0 }))).toBeCloseTo([100, 100, 0]);
|
expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([100, 100, 0]);
|
||||||
|
|
||||||
// move to origin
|
// move to origin
|
||||||
camera.pan(100, 100);
|
camera.pan(100, 100);
|
||||||
|
|
||||||
camera.pan(-100, -100);
|
camera.pan(-100, -100);
|
||||||
expect(parsePoint(svg.viewport2Canvas({ x: 0, y: 0 }))).toBeCloseTo([-100, -100, 0]);
|
expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);
|
||||||
|
|
||||||
camera.pan(100, 100);
|
camera.pan(100, 100);
|
||||||
});
|
});
|
||||||
@ -69,8 +68,8 @@ describe('Canvas', () => {
|
|||||||
camera.gotoLandmark(landmark1, { onfinish: resolve });
|
camera.gotoLandmark(landmark1, { onfinish: resolve });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parsePoint(svg.viewport2Canvas({ x: 0, y: 0 }))).toBeCloseTo([100, 100, 0]);
|
expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([100, 100, 0]);
|
||||||
expect(parsePoint(svg.canvas2Viewport({ x: 0, y: 0 }))).toBeCloseTo([-100, -100, 0]);
|
expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([-100, -100, 0]);
|
||||||
|
|
||||||
const landmark2 = camera.createLandmark('landmark2', {
|
const landmark2 = camera.createLandmark('landmark2', {
|
||||||
// 视点坐标 / viewport coordinates
|
// 视点坐标 / viewport coordinates
|
||||||
@ -83,8 +82,8 @@ describe('Canvas', () => {
|
|||||||
camera.gotoLandmark(landmark2, { onfinish: resolve });
|
camera.gotoLandmark(landmark2, { onfinish: resolve });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parsePoint(svg.viewport2Canvas({ x: 0, y: 0 }))).toBeCloseTo([-100, -100, 0]);
|
expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);
|
||||||
expect(parsePoint(svg.canvas2Viewport({ x: 0, y: 0 }))).toBeCloseTo([100, 100, 0]);
|
expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([100, 100, 0]);
|
||||||
|
|
||||||
expect([...camera.getFocalPoint()]).toBeCloseTo([150, 150, 0]);
|
expect([...camera.getFocalPoint()]).toBeCloseTo([150, 150, 0]);
|
||||||
expect([...camera.getPosition()]).toBeCloseTo([150, 150, 500]);
|
expect([...camera.getPosition()]).toBeCloseTo([150, 150, 500]);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { GraphEvent } from '@/src';
|
import { GraphEvent } from '@/src';
|
||||||
import { createGraph } from '@@/utils';
|
import { createGraph } from '@@/utils';
|
||||||
|
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||||
|
|
||||||
describe('event', () => {
|
describe('event', () => {
|
||||||
it('canvas ready', async () => {
|
it('canvas ready', async () => {
|
||||||
@ -153,4 +154,33 @@ describe('event', () => {
|
|||||||
|
|
||||||
graph.destroy();
|
graph.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renderer change event', async () => {
|
||||||
|
const graph = createGraph({
|
||||||
|
data: {
|
||||||
|
nodes: [{ id: 'node-1' }, { id: 'node-2' }],
|
||||||
|
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const beforeRendererChange = jest.fn();
|
||||||
|
const afterRendererChange = jest.fn();
|
||||||
|
|
||||||
|
graph.on(GraphEvent.BEFORE_RENDERER_CHANGE, beforeRendererChange);
|
||||||
|
graph.on(GraphEvent.AFTER_RENDERER_CHANGE, afterRendererChange);
|
||||||
|
|
||||||
|
await graph.render();
|
||||||
|
|
||||||
|
expect(beforeRendererChange).toHaveBeenCalledTimes(0);
|
||||||
|
expect(afterRendererChange).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
const renderer = () => new CanvasRenderer();
|
||||||
|
|
||||||
|
graph.setOptions({
|
||||||
|
renderer,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(beforeRendererChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(afterRendererChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@ import type { Node, Point } from '@/src/types';
|
|||||||
import { resetEntityCounter } from '@antv/g';
|
import { resetEntityCounter } from '@antv/g';
|
||||||
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||||
import { Renderer as SVGRenderer } from '@antv/g-svg';
|
import { Renderer as SVGRenderer } from '@antv/g-svg';
|
||||||
|
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
|
||||||
import { OffscreenCanvasContext } from './offscreen-canvas-context';
|
import { OffscreenCanvasContext } from './offscreen-canvas-context';
|
||||||
|
|
||||||
function getRenderer(renderer: string) {
|
function getRenderer(renderer: string) {
|
||||||
@ -13,6 +14,7 @@ function getRenderer(renderer: string) {
|
|||||||
case 'svg':
|
case 'svg':
|
||||||
return new SVGRenderer();
|
return new SVGRenderer();
|
||||||
case 'webgl':
|
case 'webgl':
|
||||||
|
return new WebGLRenderer();
|
||||||
case 'canvas':
|
case 'canvas':
|
||||||
return new CanvasRenderer();
|
return new CanvasRenderer();
|
||||||
default:
|
default:
|
||||||
|
@ -71,6 +71,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antv/g-svg": "^2.0.8",
|
"@antv/g-svg": "^2.0.8",
|
||||||
|
"@antv/g-webgl": "^2.0.11",
|
||||||
"@antv/layout-gpu": "^1.1.6",
|
"@antv/layout-gpu": "^1.1.6",
|
||||||
"@antv/layout-wasm": "^1.4.1",
|
"@antv/layout-wasm": "^1.4.1",
|
||||||
"@types/hull.js": "^1.0.4",
|
"@types/hull.js": "^1.0.4",
|
||||||
|
@ -167,4 +167,16 @@ export enum GraphEvent {
|
|||||||
* <en/> After destruction
|
* <en/> After destruction
|
||||||
*/
|
*/
|
||||||
AFTER_DESTROY = 'afterdestroy',
|
AFTER_DESTROY = 'afterdestroy',
|
||||||
|
/**
|
||||||
|
* <zh/> 渲染器变更之前
|
||||||
|
*
|
||||||
|
* <en/> Before the renderer changes
|
||||||
|
*/
|
||||||
|
BEFORE_RENDERER_CHANGE = 'beforerendererchange',
|
||||||
|
/**
|
||||||
|
* <zh/> 渲染器变更之后
|
||||||
|
*
|
||||||
|
* <en/> After the renderer changes
|
||||||
|
*/
|
||||||
|
AFTER_RENDERER_CHANGE = 'afterrendererchange',
|
||||||
}
|
}
|
||||||
|
@ -215,7 +215,7 @@ export class HTML extends BaseNode<HTMLStyleProps> {
|
|||||||
const { x, y } = this.getViewportXY(normalizedEvent);
|
const { x, y } = this.getViewportXY(normalizedEvent);
|
||||||
event.viewport.x = x;
|
event.viewport.x = x;
|
||||||
event.viewport.y = y;
|
event.viewport.y = y;
|
||||||
const { x: canvasX, y: canvasY } = this.attributes.context!.canvas.viewport2Canvas(event.viewport);
|
const [canvasX, canvasY] = this.context.canvas.getCanvasByViewport([x, y]);
|
||||||
event.canvas.x = canvasX;
|
event.canvas.x = canvasX;
|
||||||
event.canvas.y = canvasY;
|
event.canvas.y = canvasY;
|
||||||
event.global.copyFrom(event.canvas);
|
event.global.copyFrom(event.canvas);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import type { DisplayObject, CanvasConfig as GCanvasConfig, IChildNode } from '@antv/g';
|
import type { Cursor, DisplayObject, CanvasConfig as GCanvasConfig, IChildNode } from '@antv/g';
|
||||||
import { CanvasEvent, Canvas as GCanvas } from '@antv/g';
|
import { CanvasEvent, Canvas as GCanvas } from '@antv/g';
|
||||||
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||||
import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';
|
import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';
|
||||||
import { createDOM } from '@antv/util';
|
import { createDOM } from '@antv/util';
|
||||||
import type { CanvasOptions } from '../spec/canvas';
|
import type { CanvasOptions } from '../spec/canvas';
|
||||||
import type { CanvasLayer } from '../types';
|
import type { CanvasLayer, Point } from '../types';
|
||||||
import { getBBoxSize, getCombinedBBox } from '../utils/bbox';
|
import { getBBoxSize, getCombinedBBox } from '../utils/bbox';
|
||||||
|
import { parsePoint, toPointObject } from '../utils/point';
|
||||||
|
|
||||||
export interface CanvasConfig
|
export interface CanvasConfig
|
||||||
extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor' | 'background'> {
|
extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor' | 'background'> {
|
||||||
@ -40,7 +41,7 @@ export interface DataURLOptions {
|
|||||||
|
|
||||||
const layersName: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
|
const layersName: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
|
||||||
|
|
||||||
export class Canvas extends GCanvas {
|
export class Canvas {
|
||||||
private extends: {
|
private extends: {
|
||||||
config: CanvasConfig;
|
config: CanvasConfig;
|
||||||
renderer: CanvasOptions['renderer'];
|
renderer: CanvasOptions['renderer'];
|
||||||
@ -48,7 +49,13 @@ export class Canvas extends GCanvas {
|
|||||||
layers: Record<CanvasLayer, GCanvas>;
|
layers: Record<CanvasLayer, GCanvas>;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getLayer(layer: CanvasLayer) {
|
private config: CanvasConfig;
|
||||||
|
|
||||||
|
public getConfig() {
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLayer(layer: CanvasLayer = 'main') {
|
||||||
return this.extends.layers[layer];
|
return this.extends.layers[layer];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,17 +80,46 @@ export class Canvas extends GCanvas {
|
|||||||
return this.extends.renderers[layer];
|
return this.extends.renderers[layer];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 获取相机
|
||||||
|
*
|
||||||
|
* <en/> Get camera
|
||||||
|
* @param layer - <zh/> 图层 <en/> Layer
|
||||||
|
* @returns <zh/> 相机 <en/> Camera
|
||||||
|
*/
|
||||||
|
public getCamera(layer: CanvasLayer = 'main') {
|
||||||
|
return this.getLayer(layer).getCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRoot(layer: CanvasLayer = 'main') {
|
||||||
|
return this.getLayer(layer).getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getContextService(layer: CanvasLayer = 'main') {
|
||||||
|
return this.getLayer(layer).getContextService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCursor(cursor: Cursor): void {
|
||||||
|
this.config.cursor = cursor;
|
||||||
|
this.getLayer().setCursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get document() {
|
||||||
|
return this.getLayer().document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get context() {
|
||||||
|
return this.getLayer().context;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(config: CanvasConfig) {
|
constructor(config: CanvasConfig) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
const { renderer, background, cursor, ...restConfig } = config;
|
const { renderer, background, cursor, ...restConfig } = config;
|
||||||
const renderers = createRenderers(renderer);
|
const renderers = createRenderers(renderer);
|
||||||
|
|
||||||
// init main canvas
|
|
||||||
super({ ...restConfig, supportsMutipleCanvasesInOneContainer: true, cursor, renderer: renderers.main });
|
|
||||||
|
|
||||||
const layers = Object.fromEntries(
|
const layers = Object.fromEntries(
|
||||||
layersName.map((layer) => {
|
layersName.map((layer) => {
|
||||||
if (layer === 'main') return [layer, this];
|
|
||||||
|
|
||||||
const canvas = new GCanvas({
|
const canvas = new GCanvas({
|
||||||
...restConfig,
|
...restConfig,
|
||||||
supportsMutipleCanvasesInOneContainer: true,
|
supportsMutipleCanvasesInOneContainer: true,
|
||||||
@ -106,20 +142,12 @@ export class Canvas extends GCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get ready() {
|
public get ready() {
|
||||||
return Promise.all([
|
return Promise.all(Object.entries(this.getLayers()).map(([, canvas]) => canvas.ready));
|
||||||
super.ready,
|
|
||||||
...Object.entries(this.getLayers()).map(([layer, canvas]) =>
|
|
||||||
layer === 'main' ? Promise.resolve() : canvas.ready,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(width: number, height: number) {
|
public resize(width: number, height: number) {
|
||||||
Object.assign(this.extends.config, { width, height });
|
Object.assign(this.extends.config, { width, height });
|
||||||
Object.values(this.getLayers()).forEach((canvas) => {
|
Object.values(this.getLayers()).forEach((canvas) => canvas.resize(width, height));
|
||||||
if (canvas === this) super.resize(width, height);
|
|
||||||
else canvas.resize(width, height);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBounds() {
|
public getBounds() {
|
||||||
@ -142,19 +170,41 @@ export class Canvas extends GCanvas {
|
|||||||
|
|
||||||
public appendChild<T extends IChildNode>(child: T, index?: number): T {
|
public appendChild<T extends IChildNode>(child: T, index?: number): T {
|
||||||
const layer = ((child as unknown as DisplayObject).style?.$layer || 'main') as CanvasLayer;
|
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);
|
return this.getLayer(layer).appendChild(child, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRenderer(renderer: any) {
|
public setRenderer(renderer: CanvasOptions['renderer']) {
|
||||||
if (renderer === this.extends.renderer) return;
|
if (renderer === this.extends.renderer) return;
|
||||||
const renderers = createRenderers(renderer);
|
const renderers = createRenderers(renderer);
|
||||||
this.extends.renderers = renderers;
|
this.extends.renderers = renderers;
|
||||||
|
Object.entries(renderers).forEach(([layer, instance]) => this.getLayer(layer as CanvasLayer).setRenderer(instance));
|
||||||
|
configCanvasDom(this.getLayers());
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(renderers).forEach(([layer, instance]) => {
|
public getCanvasByViewport(point: Point): Point {
|
||||||
if (layer === 'main') super.setRenderer(instance);
|
return parsePoint(this.getLayer().viewport2Canvas(toPointObject(point)));
|
||||||
else this.getLayer(layer as CanvasLayer).setRenderer(instance);
|
}
|
||||||
});
|
|
||||||
|
public getViewportByCanvas(point: Point): Point {
|
||||||
|
return parsePoint(this.getLayer().canvas2Viewport(toPointObject(point)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportByClient(point: Point): Point {
|
||||||
|
return parsePoint(this.getLayer().client2Viewport(toPointObject(point)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClientByViewport(point: Point): Point {
|
||||||
|
return parsePoint(this.getLayer().viewport2Client(toPointObject(point)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClientByCanvas(point: Point): Point {
|
||||||
|
return this.getClientByViewport(this.getViewportByCanvas(point));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCanvasByClient(point: Point): Point {
|
||||||
|
const main = this.getLayer();
|
||||||
|
const viewportPoint = main.client2Viewport(toPointObject(point));
|
||||||
|
return parsePoint(main.viewport2Canvas(viewportPoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toDataURL(options: Partial<DataURLOptions> = {}) {
|
public async toDataURL(options: Partial<DataURLOptions> = {}) {
|
||||||
@ -190,10 +240,10 @@ export class Canvas extends GCanvas {
|
|||||||
// Handle label canvas
|
// Handle label canvas
|
||||||
const label = this.getLayer('label').getRoot().cloneNode(true);
|
const label = this.getLayer('label').getRoot().cloneNode(true);
|
||||||
const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 });
|
const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 });
|
||||||
const currentCanvasPosition = this.viewport2Canvas({ x: 0, y: 0 });
|
const currentCanvasPosition = this.getCanvasByViewport([0, 0]);
|
||||||
label.translate([
|
label.translate([
|
||||||
currentCanvasPosition.x - originCanvasPosition.x,
|
currentCanvasPosition[0] - originCanvasPosition.x,
|
||||||
currentCanvasPosition.y - originCanvasPosition.y,
|
currentCanvasPosition[1] - originCanvasPosition.y,
|
||||||
]);
|
]);
|
||||||
label.scale(1 / this.getCamera().getZoom());
|
label.scale(1 / this.getCamera().getZoom());
|
||||||
offscreenCanvas.appendChild(label);
|
offscreenCanvas.appendChild(label);
|
||||||
@ -226,12 +276,11 @@ export class Canvas extends GCanvas {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(cleanUp?: boolean, skipTriggerEvent?: boolean) {
|
public destroy() {
|
||||||
Object.values(this.getLayers()).forEach((canvas) => {
|
Object.values(this.getLayers()).forEach((canvas) => {
|
||||||
const camera = canvas.getCamera();
|
const camera = canvas.getCamera();
|
||||||
camera.cancelLandmarkAnimation();
|
camera.cancelLandmarkAnimation();
|
||||||
if (canvas === this) super.destroy(cleanUp, skipTriggerEvent);
|
canvas.destroy();
|
||||||
else canvas.destroy(cleanUp, skipTriggerEvent);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -451,6 +451,9 @@ export class ElementController {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
before: () => {
|
before: () => {
|
||||||
|
// 通过 elementMap[id] 访问最新的 element,防止 type 不同导致的 element 丢失
|
||||||
|
// Access the latest element through elementMap[id] to prevent the loss of element caused by different types
|
||||||
|
const element = this.elementMap[id];
|
||||||
if (stage !== 'collapse') updateStyle(element, style);
|
if (stage !== 'collapse') updateStyle(element, style);
|
||||||
|
|
||||||
if (stage === 'visibility') {
|
if (stage === 'visibility') {
|
||||||
@ -462,6 +465,7 @@ export class ElementController {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
after: () => {
|
after: () => {
|
||||||
|
const element = this.elementMap[id];
|
||||||
if (stage === 'collapse') updateStyle(element, style);
|
if (stage === 'collapse') updateStyle(element, style);
|
||||||
if (exactStage === 'hide') updateStyle(element, { visibility: getCachedStyle(element, 'visibility') });
|
if (exactStage === 'hide') updateStyle(element, { visibility: getCachedStyle(element, 'visibility') });
|
||||||
this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_UPDATE, elementType, datum), context);
|
this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_UPDATE, elementType, datum), context);
|
||||||
|
@ -43,7 +43,6 @@ import { isCollapsed } from '../utils/collapsibility';
|
|||||||
import { sizeOf } from '../utils/dom';
|
import { sizeOf } from '../utils/dom';
|
||||||
import { GraphLifeCycleEvent, emit } from '../utils/event';
|
import { GraphLifeCycleEvent, emit } from '../utils/event';
|
||||||
import { idOf } from '../utils/id';
|
import { idOf } from '../utils/id';
|
||||||
import { parsePoint, toPointObject } from '../utils/point';
|
|
||||||
import { format } from '../utils/print';
|
import { format } from '../utils/print';
|
||||||
import { subtract } from '../utils/vector';
|
import { subtract } from '../utils/vector';
|
||||||
import { Animation } from './animation';
|
import { Animation } from './animation';
|
||||||
@ -120,6 +119,15 @@ export class Graph extends EventEmitter {
|
|||||||
public setOptions(options: GraphOptions): void {
|
public setOptions(options: GraphOptions): void {
|
||||||
const { behaviors, combo, data, edge, height, layout, node, plugins, theme, transforms, width, renderer } = options;
|
const { behaviors, combo, data, edge, height, layout, node, plugins, theme, transforms, width, renderer } = options;
|
||||||
|
|
||||||
|
if (renderer) {
|
||||||
|
const canvas = this.context.canvas;
|
||||||
|
if (canvas) {
|
||||||
|
this.emit(GraphEvent.BEFORE_RENDERER_CHANGE, { renderer: this.options.renderer });
|
||||||
|
canvas.setRenderer(renderer);
|
||||||
|
this.emit(GraphEvent.AFTER_RENDERER_CHANGE, { renderer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this.options, options);
|
Object.assign(this.options, options);
|
||||||
|
|
||||||
if (behaviors) this.setBehaviors(behaviors);
|
if (behaviors) this.setBehaviors(behaviors);
|
||||||
@ -133,7 +141,6 @@ export class Graph extends EventEmitter {
|
|||||||
if (transforms) this.setTransforms(transforms);
|
if (transforms) this.setTransforms(transforms);
|
||||||
if (isNumber(width) || isNumber(height))
|
if (isNumber(width) || isNumber(height))
|
||||||
this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0);
|
this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0);
|
||||||
if (renderer) this.context.canvas?.setRenderer(renderer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1785,7 +1792,7 @@ export class Graph extends EventEmitter {
|
|||||||
* @apiCategory viewport
|
* @apiCategory viewport
|
||||||
*/
|
*/
|
||||||
public getCanvasByViewport(point: Point): Point {
|
public getCanvasByViewport(point: Point): Point {
|
||||||
return parsePoint(this.context.canvas!.viewport2Canvas(toPointObject(point)));
|
return this.context.canvas.getCanvasByViewport(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1797,7 +1804,7 @@ export class Graph extends EventEmitter {
|
|||||||
* @apiCategory viewport
|
* @apiCategory viewport
|
||||||
*/
|
*/
|
||||||
public getViewportByCanvas(point: Point): Point {
|
public getViewportByCanvas(point: Point): Point {
|
||||||
return parsePoint(this.context.canvas.canvas2Viewport(toPointObject(point)));
|
return this.context.canvas.getViewportByCanvas(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1809,8 +1816,7 @@ export class Graph extends EventEmitter {
|
|||||||
* @apiCategory viewport
|
* @apiCategory viewport
|
||||||
*/
|
*/
|
||||||
public getClientByCanvas(point: Point): Point {
|
public getClientByCanvas(point: Point): Point {
|
||||||
const viewportPoint = this.context.canvas.canvas2Viewport(toPointObject(point));
|
return this.context.canvas.getClientByCanvas(point);
|
||||||
return parsePoint(this.context.canvas.viewport2Canvas(viewportPoint));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1822,8 +1828,7 @@ export class Graph extends EventEmitter {
|
|||||||
* @apiCategory viewport
|
* @apiCategory viewport
|
||||||
*/
|
*/
|
||||||
public getCanvasByClient(point: Point): Point {
|
public getCanvasByClient(point: Point): Point {
|
||||||
const viewportPoint = this.context.canvas.client2Viewport(toPointObject(point));
|
return this.context.canvas.getCanvasByClient(point);
|
||||||
return parsePoint(this.context.canvas.viewport2Canvas(viewportPoint));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user