feat: add 3d elements, plugin (#5597)

* refactor: adjust exports, use enum replace const enum

* fix: fix issue that unexpected z attr

* feat(utils): add TupleMap and getCacheKey

* feat(elements): add 3d elements

* feat(plugins): add 3d light

* refactor: adjust 3d renderer

* refactor: adjust exports

* test: add demos

* chore: config packages.json, jest config

* test: fix test case

* chore: update g6-extension-3d version

* chore: setup project configs

* test: fix demo type issue

* chore: config tsconfig for type-check
This commit is contained in:
Aaron 2024-03-29 11:57:10 +08:00 committed by GitHub
parent bcd080432f
commit da5297cd8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1091 additions and 57 deletions

View File

@ -43,6 +43,7 @@
"GSHAPE",
"mindmap",
"onframe",
"Phong",
"Polyline",
"ranksep"
],

View File

@ -42,6 +42,7 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^23.2.0",
"lil-gui": "^0.19.2",
"limit-size": "^0.1.4",
"lint-staged": "^15.2.2",
"lodash": "^4.17.21",
@ -54,8 +55,10 @@
"rollup": "^4.12.0",
"rollup-plugin-polyfill-node": "^0.13.0",
"rollup-plugin-visualizer": "^5.12.0",
"stats.js": "^0.17.0",
"ts-jest": "^29.1.2",
"turbo": "^1.12.4",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vite": "^5.1.5"
}
}

View File

@ -0,0 +1,4 @@
export * from './layer-top';
export * from './position';
export * from './shapes';
export * from './solar-system';

View File

@ -0,0 +1,71 @@
import type { G6Spec, GraphData } from '@antv/g6';
import { Graph, register } from '@antv/g6';
import { Light, Line3D, Plane, Sphere, renderer } from '../../src';
export const layerTop = async (context: G6Spec) => {
register('plugin', '3d-light', Light);
register('node', 'sphere', Sphere);
register('node', 'plane', Plane);
register('edge', 'line3d', Line3D);
const result = await fetch('https://assets.antv.antgroup.com/g6/3-layer-top.json');
const { nodes, edges } = await result.json();
const colors = ['rgb(240, 134, 82)', 'rgb(30, 160, 230)', 'rgb(122, 225, 116)'];
const data: GraphData = {};
data.nodes = nodes.map(({ name, pos, layer }: any) => ({
id: name,
data: { layer },
style: {
type: 'sphere',
radius: 10,
color: colors[layer - 1],
materialType: 'phong',
...pos,
},
}));
new Array(3).fill(0).forEach((_, i) => {
data.nodes!.push({
id: `plane-${i + 1}`,
style: {
type: 'plane',
size: 1000,
color: colors[i],
y: -300 + 300 * i + 10,
},
});
});
data.edges = edges.map(({ source, target }: any) => ({
source,
target,
}));
const graph = new Graph({
...context,
renderer,
data,
node: {
style: {},
},
edge: {
style: {
type: 'line3d',
lineWidth: 5,
},
},
plugins: [
{
type: '3d-light',
directional: {
direction: [0, 0, 1],
},
},
],
});
await graph.render();
return graph;
};

View File

@ -0,0 +1,44 @@
import type { G6Spec } from '@antv/g6';
import { Graph, register } from '@antv/g6';
import { Light, Line3D, Sphere, renderer } from '../../src';
export const positionValidate = async (context: G6Spec) => {
register('plugin', '3d-light', Light);
register('node', 'sphere', Sphere);
register('edge', 'line3d', Line3D);
const graph = new Graph({
...context,
renderer,
data: {
nodes: [
{ id: '1', style: { x: 100, y: 100 } },
{ id: '2', style: { x: 200, y: 200 } },
{ id: '3', style: { x: 200, y: 100, z: 150 } },
],
edges: [
{ source: '1', target: '2' },
{ source: '2', target: '3' },
{ source: '1', target: '3' },
],
},
node: {
style: { type: 'sphere', materialType: 'phong' },
},
edge: {
style: {
type: 'line3d',
},
},
plugins: [
{
type: '3d-light',
directional: {
direction: [0, 0, 1],
},
},
],
});
await graph.render();
};

View File

@ -0,0 +1,59 @@
import type { G6Spec } from '@antv/g6';
import { Graph, register } from '@antv/g6';
import { Capsule, Cone, Cube, Cylinder, Light, Plane, Sphere, Torus, renderer } from '../../src';
export const shapes = async (context: G6Spec) => {
register('plugin', '3d-light', Light);
register('node', 'sphere', Sphere);
register('node', 'plane', Plane);
register('node', 'cylinder', Cylinder);
register('node', 'cone', Cone);
register('node', 'cube', Cube);
register('node', 'capsule', Capsule);
register('node', 'torus', Torus);
const graph = new Graph({
...context,
renderer,
data: {
nodes: [
{
id: '1',
style: {
type: 'sphere',
texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',
},
},
{ id: '2', style: { type: 'plane', size: 50 } },
{ id: '3', style: { type: 'cylinder' } },
{ id: '4', style: { type: 'cone' } },
{
id: '5',
style: {
type: 'cube',
texture: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*8TlCRIsKeUkAAAAAAAAAAAAAARQnAQ',
},
},
{ id: '6', style: { type: 'capsule' } },
{ id: '7', style: { type: 'torus' } },
],
},
node: {
style: {
materialType: 'phong',
x: (_, i) => 100 + (i % 5) * 100,
y: (_, i) => 100 + Math.floor(i / 5) * 100,
},
},
plugins: [
{
type: '3d-light',
directional: {
direction: [0, 0, 1],
},
},
],
});
await graph.render();
};

View File

@ -0,0 +1,112 @@
import type { DisplayObject } from '@antv/g';
import type { G6Spec, Vector3 } from '@antv/g6';
import { Graph, register } from '@antv/g6';
import { Light, Sphere, renderer } from '../../src';
export const solarSystem = async (context: G6Spec) => {
register('plugin', '3d-light', Light);
register('node', 'sphere', Sphere);
const graph = new Graph({
...context,
renderer,
background: 'black',
data: {
nodes: [
{
id: 'sum',
style: {
x: 300,
y: 300,
radius: 100,
texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-mZfQr8LtPUAAAAAAAAAAAAADmJ7AQ/original',
},
},
{
id: 'mars',
style: {
x: 430,
y: 300,
z: 0,
radius: 20,
texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mniGTZktpecAAAAAAAAAAAAADmJ7AQ/original',
},
},
{
id: 'earth',
style: {
x: 500,
y: 300,
z: 0,
radius: 30,
texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',
},
},
{
id: 'jupiter',
style: {
x: 600,
y: 300,
z: 0,
radius: 50,
texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*t_mQSZYAT70AAAAAAAAAAAAADmJ7AQ/original',
},
},
],
},
node: {
style: {
type: 'sphere',
materialShininess: 0,
labelText: (d) => d.id,
},
},
plugins: [
{
type: '3d-light',
directional: {
direction: [0, 0, 1],
},
},
],
});
await graph.render();
// @ts-expect-error graph is private
const element = graph.context.element!;
const sum = element.getElement('sum')!;
const mars = element.getElement('mars')!;
const earth = element.getElement('earth')!;
const jupiter = element.getElement('jupiter')!;
const setRotation = (element: DisplayObject, speed: number) => {
setInterval(() => {
element.rotate(0, -speed, 0);
}, 30);
};
setRotation(sum, 0.1);
setRotation(mars, 0.8);
setRotation(earth, 1);
setRotation(jupiter, 0.5);
const setRevolution = (element: DisplayObject, center: Vector3, speed: number) => {
setInterval(() => {
const [x, y, z] = element.getPosition();
const [cx, cy, cz] = center;
const angle = (speed * Math.PI) / 180;
const newX = (x - cx) * Math.cos(angle) + (z - cz) * Math.sin(angle) + cx;
const newZ = -(x - cx) * Math.sin(angle) + (z - cz) * Math.cos(angle) + cz;
element.setPosition(newX, y, newZ);
}, 30);
};
setRevolution(mars, [300, 300, 0], 1.5);
setRevolution(earth, [300, 300, 0], 1);
setRevolution(jupiter, [300, 300, 0], 0.5);
return graph;
};

View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@antv/g6-extension-3d</title>
<style>
#container {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,43 @@
import GUI from 'lil-gui';
import * as demos from './demos';
const demoNames = Object.keys(demos);
const options = {
demo: '',
};
const panel = new GUI({ autoPlace: true });
const __STORAGE__ = '__G6_EXTENSION_3D_DEMO__';
const load = () => {
const data = localStorage.getItem(__STORAGE__);
if (data) panel.load(JSON.parse(data));
};
const save = () => {
localStorage.setItem(__STORAGE__, JSON.stringify(panel.save()));
};
panel
.add(options, 'demo', demoNames)
.name('Demo')
.onChange((name: string) => {
render(name);
save();
});
load();
function initContainer() {
const container = document.getElementById('container')!;
container.innerHTML = '';
return container;
}
function initContext() {
const container = initContainer();
return { container };
}
function render(name: string) {
const context = initContext();
const demo = demos[name as keyof typeof demos];
demo(context);
}

View File

@ -0,0 +1,31 @@
import { getCacheKey } from '../../../src/utils/cache';
describe('cache', () => {
it('getCacheKey plain', () => {
const key = Symbol.for('latitudeBands:16 longitudeBands:16 radius:10');
expect(
getCacheKey({
radius: 10,
latitudeBands: 16,
longitudeBands: 16,
}),
).toBe(key);
expect(
getCacheKey({
longitudeBands: 16,
latitudeBands: 16,
radius: 10,
}),
).toBe(key);
});
it('getCacheKey object', () => {
const object = { a: { b: 1 } };
const key1 = getCacheKey(object);
const key2 = getCacheKey(object);
expect(key1).not.toBe(key2);
});
});

View File

@ -0,0 +1,21 @@
import { TupleMap } from '../../../src/utils/map';
describe('map', () => {
it('TupleMap', () => {
const map = new TupleMap<Date, string, number>();
const key1 = new Date();
const key2 = 'key2';
map.set(key1, key2, 1);
expect(map.has(key1, key2)).toBe(true);
expect(map.get(key1, key2)).toBe(1);
map.set(key1, key2, 2);
expect(map.get(key1, key2)).toBe(2);
const key3 = 'key3';
expect(map.has(key1, key3)).toBe(false);
expect(map.get(key1, key3)).toBe(undefined);
});
});

View File

@ -0,0 +1,23 @@
import { createMaterial } from '../../../src/utils/material';
describe('material', () => {
it('createMaterial', () => {
const plugin: any = {
loadTexture: () => new Object(),
getDevice: () => ({}),
};
const materialWithoutTexture = createMaterial(plugin, { type: 'basic' });
const materialWithTexture = createMaterial(plugin, { type: 'basic' }, 'texture');
const image: any = new Object();
const materialWithImage = createMaterial(plugin, { type: 'basic' }, image);
expect(materialWithoutTexture).toBe(createMaterial(plugin, { type: 'basic' }));
expect(materialWithTexture).not.toBe(materialWithoutTexture);
expect(materialWithImage).not.toBe(materialWithTexture);
expect(materialWithTexture).toBe(createMaterial(plugin, { type: 'basic' }, 'texture'));
expect(materialWithImage).toBe(createMaterial(plugin, { type: 'basic' }, image));
});
});

View File

@ -0,0 +1,18 @@
import { createTexture } from '../../../src/utils/texture';
describe('texture', () => {
it('createTexture', () => {
const img1 = 'texture1';
const img2 = 'texture2';
const plugin: any = {
loadTexture: () => new Object(),
};
const texture1 = createTexture(plugin, img1);
const texture2 = createTexture(plugin, img2);
expect(texture1).toBe(createTexture(plugin, img1));
expect(texture2).not.toBe(texture1);
});
});

View File

@ -0,0 +1,21 @@
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
diagnostics: {
exclude: ['**'],
},
tsconfig: {
allowJs: true,
target: 'esnext',
esModuleInterop: true,
},
},
],
},
testRegex: '(/__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$',
collectCoverageFrom: ['src/**/*.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
};

View File

@ -1,6 +1,6 @@
{
"name": "g6-extension-3d",
"version": "1.0.0",
"name": "@antv/g6-extension-3d",
"version": "0.1.0",
"description": "",
"keywords": [
"antv",
@ -9,7 +9,7 @@
"3d"
],
"license": "MIT",
"author": "",
"author": "Aarebecca",
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
@ -21,15 +21,15 @@
],
"scripts": {
"build": "run-p build:*",
"build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib",
"build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm",
"build:esm:watch": "rimraf ./esm && tsc --module ESNext --outDir esm --watch",
"build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json",
"build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json",
"build:umd": "rimraf ./dist && rollup -c",
"ci": "run-s lint type-check build test",
"dev": "vite",
"lint": "eslint ./src __tests__ --quiet && prettier ./src __tests__ --check",
"prepublishOnly": "npm run ci",
"test": "jest",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit -p tsconfig.test.json"
},
"dependencies": {
"@antv/g": "^5.18.25",
@ -37,7 +37,10 @@
"@antv/g-device-api": "^1.6.4",
"@antv/g-plugin-3d": "^1.9.34",
"@antv/g-plugin-control": "^1.9.22",
"@antv/g-webgl": "^1.9.37"
"@antv/g-plugin-device-renderer": "^2.0.0",
"@antv/g-plugin-dragndrop": "^1.8.22",
"@antv/g-webgl": "^1.9.37",
"@antv/util": "^3.3.7"
},
"devDependencies": {
"@antv/g6": "workspace:*"

View File

@ -7,7 +7,7 @@ import { visualizer } from 'rollup-plugin-visualizer';
const isBundleVis = !!process.env.BUNDLE_VIS;
export default[
export default [
{
input: 'src/index.ts',
output: {
@ -16,6 +16,15 @@ export default[
format: 'umd',
sourcemap: false,
},
plugins: [nodePolyfills(), resolve(), commonjs(), typescript(), terser(), ...(isBundleVis ? [visualizer()] : [])],
plugins: [
nodePolyfills(),
resolve(),
commonjs(),
typescript({
tsconfig: 'tsconfig.build.json',
}),
terser(),
...(isBundleVis ? [visualizer()] : []),
],
},
];

View File

@ -1,21 +1,68 @@
import type { BaseStyleProps } from '@antv/g';
import { Device } from '@antv/g-device-api';
import type { BufferGeometry, Material } from '@antv/g-plugin-3d';
import type { BaseStyleProps, DisplayObjectConfig, Group } from '@antv/g';
import type { ProceduralGeometry as GGeometry, Material as GMaterial } from '@antv/g-plugin-3d';
import { Mesh } from '@antv/g-plugin-3d';
import type { IMaterial, Plugin } from '@antv/g-plugin-device-renderer';
import type { BaseNodeStyleProps } from '@antv/g6';
import { BaseNode } from '@antv/g6';
import { BaseNode, Utils } from '@antv/g6';
import { PrefixObject } from '@antv/g6/lib/types';
import { deepMix } from '@antv/util';
import { Material } from '../types';
import { createMaterial } from '../utils/material';
export interface BaseNode3DStyleProps extends BaseNodeStyleProps<MeshStyleProps> {
z?: number;
device: Device;
material: Material;
export type BaseNode3DStyleProps = BaseNodeStyleProps<MeshStyleProps> & {
texture?: string | TexImageSource;
materialType?: Material['type'];
} & PrefixObject<IMaterial, 'material'>;
export abstract class BaseNode3D<S extends BaseNode3DStyleProps> extends BaseNode<S> {
static defaultStyleProps: Partial<BaseNode3DStyleProps> = {
materialType: 'basic',
};
public type = 'node-3d';
protected get plugin() {
const renderer = this.attributes.context!.canvas.renderers['main'];
const plugin = renderer.getPlugin('device-renderer');
return plugin as unknown as Plugin;
}
protected get device() {
return this.plugin.getDevice();
}
constructor(options: DisplayObjectConfig<S>) {
super(deepMix({}, { style: BaseNode3D.defaultStyleProps }, options));
}
public render(attributes: Required<S>, container: Group) {
super.render(attributes, container);
}
protected getKeyStyle(attributes: Required<S>): MeshStyleProps {
const style = Utils.omitStyleProps(super.getKeyStyle(attributes), 'material');
const geometry = this.getGeometry(attributes);
const material = this.getMaterial(attributes);
return { x: 0, y: 0, z: 0, ...style, geometry, material };
}
protected drawKeyShape(attributes: Required<S>, container: Group = this) {
return this.upsert('key', Mesh, this.getKeyStyle(attributes), container);
}
protected abstract getGeometry(attributes: Required<S>): GGeometry<any> | undefined;
protected getMaterial(attributes: Required<S>): GMaterial<any> | undefined {
const { texture } = attributes;
const materialStyle = Utils.subStyleProps<Material>(attributes, 'material');
return createMaterial(this.plugin, materialStyle, texture);
}
}
export abstract class BaseNode3D extends BaseNode<BaseNode3DStyleProps> {}
export interface MeshStyleProps extends BaseStyleProps {
x?: number | string;
y?: number | string;
z?: number | string;
geometry: BufferGeometry;
material: Material;
geometry?: GGeometry<any>;
material?: GMaterial<any>;
}

View File

@ -0,0 +1,27 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { CapsuleGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';
import { CapsuleGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export type CapsuleStyleProps = BaseNode3DStyleProps & CapsuleGeometryProps;
export class Capsule extends BaseNode3D<CapsuleStyleProps> {
static defaultStyleProps: Partial<CapsuleStyleProps> = {
// radius, height
size: [24, 48],
heightSegments: 1,
sides: 20,
};
constructor(options: DisplayObjectConfig<CapsuleStyleProps>) {
super(deepMix({}, { style: Capsule.defaultStyleProps }, options));
}
protected getGeometry(attributes: Required<CapsuleStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const { radius = size[0] / 2, height = size[1], heightSegments, sides } = attributes;
return new CapsuleGeometry(this.device, { radius, height, heightSegments, sides });
}
}

View File

@ -0,0 +1,33 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { ConeGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';
import { ConeGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export type ConeStyleProps = BaseNode3DStyleProps & ConeGeometryProps;
export class Cone extends BaseNode3D<ConeStyleProps> {
static defaultStyleProps: Partial<ConeStyleProps> = {
// baseRadius, peakRadius, height
size: [24, 0, 48],
heightSegments: 5,
capSegments: 20,
};
constructor(options: DisplayObjectConfig<ConeStyleProps>) {
super(deepMix({}, { style: Cone.defaultStyleProps }, options));
}
protected getGeometry(attributes: Required<ConeStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const {
baseRadius = size[0] / 2,
peakRadius = size[1] / 2,
height = size[2],
heightSegments,
capSegments,
} = attributes;
return new ConeGeometry(this.device, { baseRadius, peakRadius, height, heightSegments, capSegments });
}
}

View File

@ -0,0 +1,33 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { CubeGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';
import { CubeGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export type CubeStyleProps = BaseNode3DStyleProps & CubeGeometryProps;
export class Cube extends BaseNode3D<CubeStyleProps> {
static defaultStyleProps: Partial<CubeStyleProps> = {
widthSegments: 1,
heightSegments: 1,
depthSegments: 1,
};
constructor(options: DisplayObjectConfig<CubeStyleProps>) {
super(deepMix({}, { style: Cube.defaultStyleProps }, options));
}
protected getGeometry(attributes: Required<CubeStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const {
width = size[0],
height = size[1],
depth = size[2],
widthSegments,
heightSegments,
depthSegments,
} = attributes;
return new CubeGeometry(this.device, { width, height, depth, widthSegments, heightSegments, depthSegments });
}
}

View File

@ -0,0 +1,27 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { CylinderGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';
import { CylinderGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export type CylinderStyleProps = BaseNode3DStyleProps & CylinderGeometryProps;
export class Cylinder extends BaseNode3D<CylinderStyleProps> {
static defaultStyleProps: Partial<CylinderStyleProps> = {
// radius, height
size: [24, 48],
heightSegments: 5,
capSegments: 20,
};
constructor(options: DisplayObjectConfig<CylinderStyleProps>) {
super(deepMix({}, { style: Cylinder.defaultStyleProps }, options));
}
protected getGeometry(attributes: Required<CylinderStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const { radius = size[0] / 2, height = size[1], heightSegments, capSegments } = attributes;
return new CylinderGeometry(this.device, { radius, height, heightSegments, capSegments });
}
}

View File

@ -1,5 +1,19 @@
export { BaseNode3D } from './base-node-3d';
export { Capsule } from './capsule';
export { Cone } from './cone';
export { Cube } from './cube';
export { Cylinder } from './cylinder';
export { Line3D } from './line-3d';
export { Plane } from './plane';
export { Sphere } from './sphere';
export { Torus } from './torus';
export type { BaseNode3DStyleProps } from './base-node-3d';
export type { CapsuleStyleProps } from './capsule';
export type { ConeStyleProps } from './cone';
export type { CubeStyleProps } from './cube';
export type { CylinderStyleProps } from './cylinder';
export type { Line3DStyleProps } from './line-3d';
export type { PlaneStyleProps } from './plane';
export type { SphereStyleProps } from './sphere';
export type { TorusStyleProps } from './torus';

View File

@ -0,0 +1,26 @@
import type { Group } from '@antv/g';
import { Line } from '@antv/g';
import type { BaseEdgeStyleProps } from '@antv/g6';
import { BaseEdge } from '@antv/g6';
export interface Line3DStyleProps extends BaseEdgeStyleProps {}
export class Line3D extends BaseEdge {
protected getKeyPath(): any {
return [];
}
protected getKeyStyle(attributes: Required<Line3DStyleProps>): any {
const { sourceNode, targetNode } = attributes;
const [x1, y1, z1] = sourceNode.getPosition();
const [x2, y2, z2] = targetNode.getPosition();
// omit path
const { path, ...style } = super.getKeyStyle(attributes);
return { x1, y1, z1, x2, y2, z2, ...style };
}
protected drawKeyShape(attributes = this.parsedAttributes, container: Group = this): any {
return this.upsert('key', Line, this.getKeyStyle(attributes), container);
}
}

View File

@ -0,0 +1,24 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { ProceduralGeometry as GGeometry, PlaneGeometryProps } from '@antv/g-plugin-3d';
import { CullMode, PlaneGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export type PlaneStyleProps = BaseNode3DStyleProps & PlaneGeometryProps;
export class Plane extends BaseNode3D<PlaneStyleProps> {
static defaultStyleProps: Partial<PlaneStyleProps> = {
materialCullMode: CullMode.NONE,
};
constructor(options: DisplayObjectConfig<PlaneStyleProps>) {
super(deepMix({}, { style: Plane.defaultStyleProps }, options));
}
protected getGeometry(attributes: Required<PlaneStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const { width = size[0], depth = size[1], widthSegments, depthSegments } = attributes;
return new PlaneGeometry(this.device, { width, depth, widthSegments, depthSegments });
}
}

View File

@ -1,21 +1,27 @@
import type { Group } from '@antv/g';
import { Mesh } from '@antv/g-plugin-3d';
import type { BaseNode3DStyleProps, MeshStyleProps } from './base-node-3d';
import type { DisplayObjectConfig } from '@antv/g';
import type { ProceduralGeometry as GGeometry, SphereGeometryProps } from '@antv/g-plugin-3d';
import { SphereGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export interface SphereStyleProps extends BaseNode3DStyleProps {}
export type SphereStyleProps = BaseNode3DStyleProps & SphereGeometryProps;
type ParsedSphereStyleProps = Required<SphereStyleProps>;
export class Sphere extends BaseNode3D<SphereStyleProps> {
static defaultStyleProps: Partial<SphereStyleProps> = {
// radius
size: 24,
latitudeBands: 16,
longitudeBands: 16,
};
export class Sphere extends BaseNode3D {
protected drawKeyShape(attributes = this.parsedAttributes, container: Group = this) {
return this.upsert('key', Mesh, this.getKeyStyle(attributes), container);
constructor(options: DisplayObjectConfig<SphereStyleProps>) {
super(deepMix({}, { style: Sphere.defaultStyleProps }, options));
}
protected getKeyStyle(attributes: ParsedSphereStyleProps): MeshStyleProps {
return {
...super.getKeyStyle(attributes),
z: attributes.z || 0,
};
protected getGeometry(attributes: Required<SphereStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const { radius = size[0] / 2, latitudeBands, longitudeBands } = attributes;
return new SphereGeometry(this.device, { radius, latitudeBands, longitudeBands });
}
}

View File

@ -0,0 +1,27 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { ProceduralGeometry as GGeometry, TorusGeometryProps } from '@antv/g-plugin-3d';
import { TorusGeometry } from '@antv/g-plugin-3d';
import { deepMix } from '@antv/util';
import type { BaseNode3DStyleProps } from './base-node-3d';
import { BaseNode3D } from './base-node-3d';
export type TorusStyleProps = BaseNode3DStyleProps & TorusGeometryProps;
export class Torus extends BaseNode3D<TorusStyleProps> {
static defaultStyleProps: Partial<TorusStyleProps> = {
// tubeRadius, ringRadius
size: [8, 48],
segments: 30,
sides: 20,
};
constructor(options: DisplayObjectConfig<TorusStyleProps>) {
super(deepMix({}, { style: Torus.defaultStyleProps }, options));
}
protected getGeometry(attributes: Required<TorusStyleProps>): GGeometry<any> | undefined {
const size = this.getSize();
const { tubeRadius = size[0] / 2, ringRadius = size[1] / 2, segments, sides } = attributes;
return new TorusGeometry(this.device, { tubeRadius, ringRadius, segments, sides });
}
}

View File

@ -0,0 +1,16 @@
export { BaseNode3D, Capsule, Cone, Cube, Cylinder, Line3D, Plane, Sphere, Torus } from './elements';
export { Light } from './plugins';
export { renderer } from './renderer';
export type {
BaseNode3DStyleProps,
CapsuleStyleProps,
ConeStyleProps,
CubeStyleProps,
CylinderStyleProps,
Line3DStyleProps,
PlaneStyleProps,
SphereStyleProps,
TorusStyleProps,
} from './elements';
export type { LightOptions } from './plugins';

View File

@ -1,4 +1 @@
export { BaseNode3D, Sphere } from './elements';
export { renderer } from './renderer';
export type { BaseNode3DStyleProps, SphereStyleProps } from './elements';
export * from './exports';

View File

@ -0,0 +1,3 @@
export { Light } from './light';
export type { LightOptions } from './light';

View File

@ -0,0 +1,78 @@
import type { AmbientLightProps, DirectionalLightProps } from '@antv/g-plugin-3d';
import { AmbientLight, DirectionalLight } from '@antv/g-plugin-3d';
import type { BasePluginOptions, RuntimeContext } from '@antv/g6';
import { BasePlugin, GraphEvent } from '@antv/g6';
import { deepMix } from '@antv/util';
export interface LightOptions extends BasePluginOptions {
/**
* <zh/>
*
* <en/> Ambient light
*/
ambient?: AmbientLightProps;
/**
* <zh/>
*
* <en/> Directional light
*/
directional?: DirectionalLightProps;
}
export class Light extends BasePlugin<LightOptions> {
static defaultOptions: Partial<LightOptions> = {
ambient: {
fill: '#fff',
intensity: Math.PI * 2,
},
directional: {
fill: '#fff',
direction: [-1, 0, 1],
intensity: Math.PI * 0.7,
},
};
private ambient?: AmbientLight;
private directional?: DirectionalLight;
constructor(context: RuntimeContext, options: LightOptions) {
super(context, deepMix({}, Light.defaultOptions, options));
this.bindEvents();
}
private bindEvents() {
this.context.graph.on(GraphEvent.BEFORE_DRAW, this.setLight);
}
private unbindEvents() {
this.context.graph.off(GraphEvent.BEFORE_DRAW, this.setLight);
}
private setLight = () => {
const { ambient, directional } = this.options;
this.upsertLight('directional', directional);
this.upsertLight('ambient', ambient);
};
private upsertLight(type: 'ambient', options?: AmbientLightProps): void;
private upsertLight(type: 'directional', options?: DirectionalLightProps): void;
private upsertLight(type: 'ambient' | 'directional', options?: AmbientLightProps | DirectionalLightProps) {
if (options) {
const light = this[type];
if (light) light.attr(options);
else {
const Ctor = type === 'ambient' ? AmbientLight : DirectionalLight;
const light = new Ctor({ style: options });
this[type] = light as any;
this.context.canvas.appendChild(light);
}
} else this[type]?.remove();
}
public destroy() {
this.unbindEvents();
super.destroy();
}
}

View File

@ -1,6 +1,7 @@
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Plugin as Plugin3D } from '@antv/g-plugin-3d';
import { Plugin as PluginControl } from '@antv/g-plugin-control';
import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
import type { CanvasOptions } from '@antv/g6';
@ -11,9 +12,22 @@ export const renderer: CanvasOptions['renderer'] = (layer) => {
const renderer = new WebGLRenderer();
if (['main', 'transient'].includes(layer)) {
renderer.registerPlugin(
new DragNDropPlugin({
isDocumentDraggable: true,
isDocumentDroppable: true,
dragstartDistanceThreshold: 10,
dragstartTimeThreshold: 100,
}),
);
}
if (layer === 'main') {
renderer.registerPlugin(new Plugin3D());
renderer.registerPlugin(new PluginControl());
} else {
renderer.unregisterPlugin(renderer.getPlugin('dom-interaction'));
}
return renderer;

View File

@ -0,0 +1 @@
export * from './material';

View File

@ -0,0 +1,26 @@
import type { IPointMaterial } from '@antv/g-plugin-3d';
import { IMeshBasicMaterial, IMeshLambertMaterial, IMeshPhongMaterial } from '@antv/g-plugin-3d';
export type Material = PointMaterial | BasicMaterial | LambertMaterial | PhongMaterial;
export interface PointMaterial extends Partial<Omit<IPointMaterial, 'map'>> {
type: 'point';
map?: string | TexImageSource;
}
interface BasicMaterial extends Partial<Omit<IMeshBasicMaterial, 'map' | 'aoMap'>> {
type: 'basic';
map?: string | TexImageSource;
}
interface LambertMaterial extends Partial<Omit<IMeshLambertMaterial, 'map' | 'aoMap'>> {
type: 'lambert';
map?: string | TexImageSource;
// aoMap?: string | Texture;
}
interface PhongMaterial extends Partial<Omit<IMeshPhongMaterial, 'map' | 'aoMap'>> {
type: 'phong';
map?: string | TexImageSource;
// aoMap?: string | Texture;
}

View File

@ -0,0 +1,23 @@
/**
* <zh/>
*
* <en/> Generate cache key of geometry configuration
* @param props - <zh/> <en/> geometry configuration
* @returns <zh/> <en/> cache key
*/
export function getCacheKey(props: Record<string, any>): symbol {
const entries = Object.entries(props);
if (entries.some(([, value]) => typeof value === 'object')) {
return Symbol();
}
const str = entries
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([key, value]) => {
return `${key}:${value}`;
})
.join(' ');
return Symbol.for(str);
}

View File

@ -0,0 +1,18 @@
export class TupleMap<K1, K2, V> {
private map = new Map<K1, Map<K2, V>>();
has(key1: K1, key2: K2) {
return this.map.has(key1) && this.map.get(key1)!.has(key2);
}
get(key1: K1, key2: K2) {
return this.map.get(key1)?.get(key2);
}
set(key1: K1, key2: K2, value: V) {
if (!this.map.has(key1)) {
this.map.set(key1, new Map());
}
this.map.get(key1)!.set(key2, value);
}
}

View File

@ -0,0 +1,42 @@
import type { Material as GMaterial } from '@antv/g-plugin-3d';
import { MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, PointMaterial } from '@antv/g-plugin-3d';
import type { Plugin } from '@antv/g-plugin-device-renderer';
import type { Material } from '../types';
import { getCacheKey } from './cache';
import { TupleMap } from './map';
import { createTexture } from './texture';
const MATERIAL_CACHE = new TupleMap<symbol, string | TexImageSource | undefined, GMaterial>();
const MATERIAL_MAP = {
basic: MeshBasicMaterial,
point: PointMaterial,
lambert: MeshLambertMaterial,
phong: MeshPhongMaterial,
};
/**
* <zh/>
*
* <en/> Create material based on configuration, support cache
* @param plugin - <zh/> <en/> plugin
* @param options - <zh/> <en/> material configuration
* @param texture - <zh/> <en/> texture
* @returns <zh/> <en/> material object
*/
export function createMaterial(plugin: Plugin, options: Material, texture?: string | TexImageSource): GMaterial {
const key = getCacheKey(options);
if (MATERIAL_CACHE.has(key, texture)) {
return MATERIAL_CACHE.get(key, texture)!;
}
const device = plugin.getDevice();
const { type, map = texture, ...opts } = options;
const Ctor = MATERIAL_MAP[type];
// @ts-expect-error ignore
const material = new Ctor(device, { map: createTexture(plugin, map), ...opts });
MATERIAL_CACHE.set(key, texture, material);
return material;
}

View File

@ -0,0 +1,23 @@
import type { Texture } from '@antv/g-device-api';
import type { Plugin } from '@antv/g-plugin-device-renderer';
const TEXTURE_CACHE = new Map<string | TexImageSource, Texture>();
/**
* <zh/>
*
* <en/> Create texture, support cache
* @param plugin - <zh/> <en/> plugin
* @param src - <zh/> <en/> texture path or image object
* @returns <zh/> <en/> texture object
*/
export function createTexture(plugin: Plugin, src?: string | TexImageSource): Texture | undefined {
if (!src) return;
if (TEXTURE_CACHE.has(src)) {
return TEXTURE_CACHE.get(src);
}
const texture = plugin.loadTexture(src);
TEXTURE_CACHE.set(src, texture);
return texture;
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {}
},
"include": ["src/**/*"],
"extends": "./tsconfig.json"
}

View File

@ -1,7 +1,11 @@
{
"compilerOptions": {
"outDir": "lib"
"strict": true,
"outDir": "lib",
"paths": {
"@antv/g6": ["../g6/src/index.ts"]
}
},
"extends": "../../tsconfig.json",
"include": ["src/**/*"]
"include": ["src/**/*", "__tests__/**/*"]
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {}
},
"include": ["src/**/*", "__tests__/**/*"],
"extends": "./tsconfig.json"
}

View File

@ -0,0 +1,16 @@
import path from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
root: './__tests__',
server: {
port: 8081,
open: '/',
},
plugins: [{ name: 'isolation' }],
resolve: {
alias: {
'@antv/g6': path.resolve(__dirname, '../g6/src'),
},
},
});

View File

@ -33,8 +33,8 @@
"scripts": {
"build": "run-p build:*",
"build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json",
"build:dev:watch": "npm run build:esm -- --watch",
"build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json",
"build:esm:watch": "rimraf ./esm && tsc --module ESNext --outDir esm --watch -p tsconfig.build.json",
"build:umd": "rimraf ./dist && rollup -c && npm run size",
"bundle-vis": "cross-env BUNDLE_VIS=1 npm run build:umd",
"ci": "run-s lint type-check build test",
@ -75,9 +75,6 @@
"@types/xmlserializer": "^0.6.6",
"jest-canvas-mock": "^2.5.1",
"jest-random-mock": "^1.0.0",
"lil-gui": "^0.19.2",
"stats.js": "^0.17.0",
"vite": "^5.1.5",
"xmlserializer": "^0.6.1"
},
"publishConfig": {

View File

@ -7,7 +7,7 @@ import { visualizer } from 'rollup-plugin-visualizer';
const isBundleVis = !!process.env.BUNDLE_VIS;
export default[
export default [
{
input: 'src/index.ts',
output: {
@ -16,6 +16,15 @@ export default[
format: 'umd',
sourcemap: false,
},
plugins: [nodePolyfills(), resolve(), commonjs(), typescript(), terser(), ...(isBundleVis ? [visualizer()] : [])],
plugins: [
nodePolyfills(),
resolve(),
commonjs(),
typescript({
tsconfig: 'tsconfig.build.json',
}),
terser(),
...(isBundleVis ? [visualizer()] : []),
],
},
];

View File

@ -1,4 +1,4 @@
export const enum AnimationType {
export enum AnimationType {
DRAW = 'draw',
TRANSFORM = 'transform',
// LAYOUT = 'layout', // 布局没有统一的动画对象,因此不抛出动画事件 | There is no unified animation object for layout, so no animation event is thrown

View File

@ -1,4 +1,4 @@
export const enum CanvasEvent {
export enum CanvasEvent {
/** <zh/> 点击时触发 | <en/> Triggered when click */
CLICK = 'click',
/** <zh/> 双击时触发 | <en/> Triggered when double click */

View File

@ -1,4 +1,4 @@
export const enum CommonEvent {
export enum CommonEvent {
/** <zh/> 点击时触发 | <en/> Triggered when click */
CLICK = 'click',
/** <zh/> 双击时触发 | <en/> Triggered when double click */

View File

@ -1,4 +1,4 @@
export const enum ContainerEvent {
export enum ContainerEvent {
/** <zh/> 按下键盘时触发 | <en/> Triggered when the keyboard is pressed */
KEY_DOWN = 'keydown',
/** <zh/> 抬起键盘时触发 | <en/> Triggered when the keyboard is lifted */

View File

@ -1,4 +1,4 @@
export const enum EdgeEvent {
export enum EdgeEvent {
/** <zh/> 点击时触发 | <en/> Triggered when click */
CLICK = 'click',
/** <zh/> 双击时触发 | <en/> Triggered when double click */

View File

@ -1,4 +1,4 @@
export const enum GraphEvent {
export enum GraphEvent {
/** <zh/> 视口尺寸变更之前 | <en/> Before the viewport size changes */
BEFORE_SIZE_CHANGE = 'beforesizechange',
/** <zh/> 视口尺寸变更之后 | <en/> After the viewport size changes */

View File

@ -1,4 +1,4 @@
export const enum NodeEvent {
export enum NodeEvent {
/** <zh/> 点击时触发 | <en/> Triggered when click */
CLICK = 'click',
/** <zh/> 双击时触发 | <en/> Triggered when double click */

View File

@ -107,8 +107,8 @@ export abstract class BaseShape<StyleProps extends BaseShapeStyleProps> extends
*/
public getGraphicStyle<T extends Record<string, any>>(
attributes: T,
): Omit<T, 'x' | 'y' | 'transform' | 'transformOrigin' | 'className' | 'context' | 'zIndex'> {
const { x, y, className, transform, transformOrigin, context, zIndex, ...style } = attributes;
): Omit<T, 'x' | 'y' | 'z' | 'transform' | 'transformOrigin' | 'className' | 'context' | 'zIndex'> {
const { x, y, z, className, transform, transformOrigin, context, zIndex, ...style } = attributes;
return style;
}

View File

@ -1,5 +1,6 @@
import { dark, light } from './themes';
import { idOf } from './utils/id';
import { omitStyleProps, subStyleProps } from './utils/prefix';
import { treeToGraphData } from './utils/tree';
export { BaseBehavior } from './behaviors';
@ -7,6 +8,8 @@ export { CanvasEvent, ComboEvent, CommonEvent, ContainerEvent, EdgeEvent, GraphE
export { BaseCombo } from './elements/combos';
export { BaseEdge } from './elements/edges';
export { BaseNode } from './elements/nodes';
export { BaseShape } from './elements/shapes';
export { BasePlugin } from './plugins';
export { getExtension, getExtensions, register } from './registry';
export { Graph } from './runtime/graph';
@ -14,6 +17,9 @@ export type { BaseBehaviorOptions } from './behaviors';
export type { BaseComboStyleProps } from './elements/combos';
export type { BaseEdgeStyleProps } from './elements/edges';
export type { BaseNodeStyleProps } from './elements/nodes';
export type { BaseShapeStyleProps } from './elements/shapes';
export type { BasePluginOptions } from './plugins';
export type { RuntimeContext } from './runtime/types';
export type {
BehaviorOptions,
CanvasOptions,
@ -35,6 +41,8 @@ export type { Point, Vector2, Vector3 } from './types';
const Utils = {
idOf,
treeToGraphData,
subStyleProps,
omitStyleProps,
};
const Theme = {