diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f5a6faad33..93f4205f48a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,7 @@ "pointset", "Polyline", "ranksep", + "Snapline", "Timebar" ], "javascript.preferences.importModuleSpecifier": "relative", diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index 491fd30123a..e1a66f57533 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -118,6 +118,7 @@ export { pluginHistory } from './plugin-history'; export { pluginHull } from './plugin-hull'; export { pluginLegend } from './plugin-legend'; export { pluginMinimap } from './plugin-minimap'; +export { pluginSnapline } from './plugin-snapline'; export { pluginTimebar } from './plugin-timebar'; export { pluginToolbarBuildIn } from './plugin-toolbar-build-in'; export { pluginToolbarIconfont } from './plugin-toolbar-iconfont'; diff --git a/packages/g6/__tests__/demos/plugin-snapline.ts b/packages/g6/__tests__/demos/plugin-snapline.ts new file mode 100644 index 00000000000..e31af8feb6b --- /dev/null +++ b/packages/g6/__tests__/demos/plugin-snapline.ts @@ -0,0 +1,66 @@ +import { Graph, Node } from '@antv/g6'; + +export const pluginSnapline: TestCase = async (context) => { + const graph = new Graph({ + ...context, + data: { + nodes: [ + { id: 'node1', style: { x: 100, y: 100 } }, + { id: 'node2', style: { x: 300, y: 300 } }, + { id: 'node3', style: { x: 120, y: 200 } }, + ], + }, + node: { + type: (datum) => (datum.id === 'node3' ? 'circle' : 'rect'), + style: { + size: (datum) => (datum.id === 'node3' ? 40 : [60, 30]), + fill: 'transparent', + lineWidth: 2, + labelText: (datum) => datum.id, + }, + }, + behaviors: ['drag-element', 'drag-canvas'], + plugins: [ + { + type: 'snapline', + key: 'snapline', + verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 }, + horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 }, + autoSnap: false, + }, + ], + }); + + await graph.render(); + + const config = { + filter: false, + offset: 20, + }; + + pluginSnapline.form = (panel) => { + return [ + panel + .add(config, 'filter') + .name('Add Filter(exclude circle)') + .onChange((filter: boolean) => { + graph.updatePlugin({ + key: 'snapline', + filter: (node: Node) => (filter ? node.id !== 'node3' : true), + }); + graph.render(); + }), + panel + .add(config, 'offset', [0, 20, Infinity]) + .name('Offset') + .onChange((offset: string) => { + graph.updatePlugin({ + key: 'snapline', + offset, + }); + graph.render(); + }), + ]; + }; + return graph; +}; diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/auto-snap.svg b/packages/g6/__tests__/snapshots/plugins/snapline/auto-snap.svg new file mode 100644 index 00000000000..8bbbca5f8a4 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/auto-snap.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/default.svg b/packages/g6/__tests__/snapshots/plugins/snapline/default.svg new file mode 100644 index 00000000000..7741cd50f9d --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/default.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-0.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-0.svg new file mode 100644 index 00000000000..ccc0f301e1b --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-0.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-1.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-1.svg new file mode 100644 index 00000000000..b963fc020d2 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-1.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-10.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-10.svg new file mode 100644 index 00000000000..5e7eb06e1cc --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-10.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-2.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-2.svg new file mode 100644 index 00000000000..66a857601c0 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-2.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-3.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-3.svg new file mode 100644 index 00000000000..a0b0e71e418 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-3.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-4.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-4.svg new file mode 100644 index 00000000000..7f8c4a7fe8e --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-4.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-5.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-5.svg new file mode 100644 index 00000000000..64272a52346 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-5.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-6.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-6.svg new file mode 100644 index 00000000000..71a0cc8f5cc --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-6.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-7.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-7.svg new file mode 100644 index 00000000000..b306d6d0263 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-7.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-8.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-8.svg new file mode 100644 index 00000000000..dead91d7db2 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-8.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-9.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-9.svg new file mode 100644 index 00000000000..3cb84584330 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-horizontal-9.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-0.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-0.svg new file mode 100644 index 00000000000..e111ba9d64f --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-0.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-1.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-1.svg new file mode 100644 index 00000000000..13ddea3d98f --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-1.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-10.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-10.svg new file mode 100644 index 00000000000..ab7a9731a28 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-10.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-11.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-11.svg new file mode 100644 index 00000000000..57bc90cc489 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-11.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-2.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-2.svg new file mode 100644 index 00000000000..8f5b29c3cbb --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-2.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-3.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-3.svg new file mode 100644 index 00000000000..33b2db10a0e --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-3.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-4.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-4.svg new file mode 100644 index 00000000000..bf8dabf4744 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-4.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-5.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-5.svg new file mode 100644 index 00000000000..36d5de3efe3 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-5.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-6.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-6.svg new file mode 100644 index 00000000000..5f7038ba3c8 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-6.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-7.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-7.svg new file mode 100644 index 00000000000..1d5066c8a61 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-7.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-8.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-8.svg new file mode 100644 index 00000000000..0e206345973 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-8.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-9.svg b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-9.svg new file mode 100644 index 00000000000..d1adad9e97f --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/drag-node3-vertical-9.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/filter-node2.svg b/packages/g6/__tests__/snapshots/plugins/snapline/filter-node2.svg new file mode 100644 index 00000000000..aebc4f32a04 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/filter-node2.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/offset-infinity.svg b/packages/g6/__tests__/snapshots/plugins/snapline/offset-infinity.svg new file mode 100644 index 00000000000..8bbbca5f8a4 --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/offset-infinity.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/plugins/snapline.spec.ts b/packages/g6/__tests__/unit/plugins/snapline.spec.ts new file mode 100644 index 00000000000..4f988aa22d3 --- /dev/null +++ b/packages/g6/__tests__/unit/plugins/snapline.spec.ts @@ -0,0 +1,83 @@ +import { Node, NodeEvent, type Graph } from '@/src'; +import { pluginSnapline } from '@@/demos'; +import { createDemoGraph } from '../../utils'; + +describe('plugin snapline', () => { + let graph: Graph; + + beforeAll(async () => { + graph = await createDemoGraph(pluginSnapline); + }); + + it('snapline', async () => { + await expect(graph).toMatchSnapshot(__filename); + + // @ts-expect-error access private property + const node = graph.context.element?.getElement('node3'); + + let i = 0; + + const moveNodeAndCreateSnapshot = async (x: number, y: number, prefix: string, reset = false) => { + graph.updateNodeData([{ id: 'node3', style: { x, y } }]); + graph.render(); + graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' }); + graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 }); + if (reset) i = 0; + await expect(graph).toMatchSnapshot(__filename, `drag-node3-${prefix}-${i}`); + graph.emit(NodeEvent.DRAG_END, { target: node }); + i++; + }; + + await moveNodeAndCreateSnapshot(50, 300, 'vertical'); + await moveNodeAndCreateSnapshot(90, 300, 'vertical'); + await moveNodeAndCreateSnapshot(100, 300, 'vertical'); + await moveNodeAndCreateSnapshot(110, 300, 'vertical'); + await moveNodeAndCreateSnapshot(150, 300, 'vertical'); + await moveNodeAndCreateSnapshot(200, 300, 'vertical'); + await moveNodeAndCreateSnapshot(250, 300, 'vertical'); + await moveNodeAndCreateSnapshot(290, 300, 'vertical'); + await moveNodeAndCreateSnapshot(300, 300, 'vertical'); + await moveNodeAndCreateSnapshot(310, 300, 'vertical'); + await moveNodeAndCreateSnapshot(350, 300, 'vertical'); + await moveNodeAndCreateSnapshot(400, 300, 'vertical'); + + await moveNodeAndCreateSnapshot(200, 65, 'horizontal', true); + await moveNodeAndCreateSnapshot(200, 95, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 100, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 105, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 135, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 150, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 265, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 295, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 300, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 305, 'horizontal'); + await moveNodeAndCreateSnapshot(200, 335, 'horizontal'); + + graph.updatePlugin({ key: 'snapline', offset: Infinity }); + graph.updateNodeData([{ id: 'node3', style: { x: 100, y: 300 } }]); + graph.render(); + graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' }); + graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 }); + await expect(graph).toMatchSnapshot(__filename, `offset-infinity`); + graph.emit(NodeEvent.DRAG_END, { target: node }); + + graph.updatePlugin({ key: 'snapline', filter: (node: Node) => node.id !== 'node2' }); + graph.render(); + graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' }); + graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 }); + await expect(graph).toMatchSnapshot(__filename, `filter-node2`); + graph.emit(NodeEvent.DRAG_END, { target: node }); + + graph.updatePlugin({ key: 'snapline', filter: () => true, autoSnap: true }); + graph.updateNodeData([{ id: 'node3', style: { x: 96, y: 304 } }]); + graph.render(); + graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' }); + graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 }); + await expect(graph).toMatchSnapshot(__filename, `auto-snap`); + graph.emit(NodeEvent.DRAG_END, { target: node }); + }); + + afterAll(() => { + graph.destroy(); + }); +}); diff --git a/packages/g6/__tests__/unit/runtime/viewport.spec.ts b/packages/g6/__tests__/unit/runtime/viewport.spec.ts index c3ab42c77f5..6d744f8f2fd 100644 --- a/packages/g6/__tests__/unit/runtime/viewport.spec.ts +++ b/packages/g6/__tests__/unit/runtime/viewport.spec.ts @@ -109,6 +109,18 @@ describe('ViewportController', () => { // @ts-expect-error expect(graph.context.viewport.getBBoxInViewport(bbox).halfExtents).toBeCloseTo([100, 100, 0]); }); + + it('isInViewport', async () => { + await graph.translateTo([100, 100]); + // @ts-expect-error + expect(graph.context.viewport?.isInViewport([0, 0])).toBe(false); + // @ts-expect-error + expect(graph.context.viewport?.isInViewport([100, 100])).toBe(true); + const bbox = new AABB(); + bbox.setMinMax([0, 0, 0], [100, 100, 0]); + // @ts-expect-error + expect(graph.context.viewport?.isInViewport(bbox)).toBe(true); + }); }); describe('Viewport Fit without Animation', () => { diff --git a/packages/g6/src/plugins/index.ts b/packages/g6/src/plugins/index.ts index 7b90388106f..2035982c097 100644 --- a/packages/g6/src/plugins/index.ts +++ b/packages/g6/src/plugins/index.ts @@ -9,6 +9,7 @@ export { History } from './history'; export { Hull } from './hull'; export { Legend } from './legend'; export { Minimap } from './minimap'; +export { Snapline } from './snapline'; export { Timebar } from './timebar'; export { Toolbar } from './toolbar'; export { Tooltip } from './tooltip'; @@ -25,6 +26,7 @@ export type { HistoryOptions } from './history'; export type { HullOptions } from './hull'; export type { LegendOptions } from './legend'; export type { MinimapOptions } from './minimap'; +export type { SnaplineOptions } from './snapline'; export type { TimebarOptions } from './timebar'; export type { ToolbarOptions } from './toolbar'; export type { TooltipOptions } from './tooltip'; diff --git a/packages/g6/src/plugins/snapline/index.ts b/packages/g6/src/plugins/snapline/index.ts new file mode 100644 index 00000000000..7ff3ef40a7c --- /dev/null +++ b/packages/g6/src/plugins/snapline/index.ts @@ -0,0 +1,359 @@ +import { AABB, BaseStyleProps, DisplayObject, Line, LineStyleProps } from '@antv/g'; +import { isEqual } from '@antv/util'; +import { NodeEvent } from '../../constants'; +import type { RuntimeContext } from '../../runtime/types'; +import type { ID, IDragEvent, Node } from '../../types'; +import { isVisible } from '../../utils/element'; +import type { BasePluginOptions } from '../base-plugin'; +import { BasePlugin } from '../base-plugin'; + +export interface SnaplineOptions extends BasePluginOptions { + /** + * 对齐精度,即移动节点时与目标位置的距离小于 tolerance 时触发显示对齐线 + * + * The alignment accuracy, that is, when the distance between the moved node and the target position is less than tolerance, the alignment line is displayed + * @defaultValue 5 + */ + tolerance?: number; + /** + * 对齐线头尾的延伸距离。取值范围:[0, Infinity] + * + * The extension distance of the snapline. The value range is [0, Infinity] + * @defaultValue 20 + */ + offset?: number; + /** + * 是否启用自动吸附 + * + * Whether to enable automatic adsorption + * @defaultValue true + */ + autoSnap?: boolean; + /** + * 指定元素上的哪个图形作为参照图形 + * + * Specifies which shape on the element to use as the reference shape + * @defaultValue `'key'` + * @remarks + * + * - 'key' 使用元素的主图形作为参照图形 + * - 也可以传入一个函数,接收元素对象,返回一个图形 + * + * + * - `'key'` uses the key shape of the element as the reference shape + * - You can also pass in a function that receives the element and returns a shape + */ + shape?: string | ((node: Node) => DisplayObject); + /** + * 垂直对齐线样式 + * + * Vertical snapline style + * @defaultValue `{ stroke: '#1783FF' }` + */ + verticalLineStyle?: BaseStyleProps; + /** + * 水平对齐线样式 + * + * Horizontal snapline style + * @defaultValue `{ stroke: '#1783FF' }` + */ + horizontalLineStyle?: BaseStyleProps; + /** + * 过滤器,用于过滤不需要作为参考的节点 + * + * Filter, used to filter nodes that do not need to be used as references + * @defaultValue `() => true` + */ + filter?: (node: Node) => boolean; +} + +const defaultLineStyle: LineStyleProps = { x1: 0, y1: 0, x2: 0, y2: 0, visibility: 'hidden' }; + +type Metadata = { + verticalX: number | null; + verticalMinY: number | null; + verticalMaxY: number | null; + horizontalY: number | null; + horizontalMinX: number | null; + horizontalMaxX: number | null; +}; + +export class Snapline extends BasePlugin { + static defaultOptions: Partial = { + tolerance: 5, + offset: 20, + autoSnap: true, + shape: 'key', + verticalLineStyle: { stroke: '#1783FF' }, + horizontalLineStyle: { stroke: '#1783FF' }, + filter: () => true, + }; + + private horizontalLine!: Line; + private verticalLine!: Line; + + constructor(context: RuntimeContext, options: SnaplineOptions) { + super(context, Object.assign({}, Snapline.defaultOptions, options)); + this.bindEvents(); + } + + private initSnapline = () => { + const canvas = this.context.canvas.getLayer('transient'); + + if (!this.horizontalLine) { + this.horizontalLine = canvas.appendChild( + new Line({ style: { ...defaultLineStyle, ...this.options.horizontalLineStyle } }), + ); + } + + if (!this.verticalLine) { + this.verticalLine = canvas.appendChild( + new Line({ style: { ...defaultLineStyle, ...this.options.verticalLineStyle } }), + ); + } + }; + + private getNodes(): Node[] { + const { filter } = this.options; + const allNodes = this.context.element?.getNodes() || []; + + // 不考虑超出画布视口范围、不可见的节点 + // Nodes that are out of the canvas viewport range, invisible are not considered + const nodes = allNodes.filter((node) => { + return isVisible(node) && this.context.viewport?.isInViewport(node.getRenderBounds()); + }); + + if (!filter) return nodes; + + return nodes.filter((node) => filter(node)); + } + + private hideSnapline() { + this.horizontalLine.style.visibility = 'hidden'; + this.verticalLine.style.visibility = 'hidden'; + } + + private updateSnapline(metadata: Metadata) { + const { verticalX, verticalMinY, verticalMaxY, horizontalY, horizontalMinX, horizontalMaxX } = metadata; + const [canvasWidth, canvasHeight] = this.context.canvas.getSize(); + const { offset } = this.options; + + if (horizontalY !== null) { + this.horizontalLine.style.x1 = offset === Infinity ? 0 : horizontalMinX! - offset; + this.horizontalLine.style.y1 = horizontalY; + this.horizontalLine.style.x2 = offset === Infinity ? canvasWidth : horizontalMaxX! + offset; + this.horizontalLine.style.y2 = horizontalY; + this.horizontalLine.style.visibility = 'visible'; + } else { + this.horizontalLine.style.visibility = 'hidden'; + } + + if (verticalX !== null) { + this.verticalLine.style.x1 = verticalX; + this.verticalLine.style.y1 = offset === Infinity ? 0 : verticalMinY! - offset; + this.verticalLine.style.x2 = verticalX; + this.verticalLine.style.y2 = offset === Infinity ? canvasHeight : verticalMaxY! + offset; + this.verticalLine.style.visibility = 'visible'; + } else { + this.verticalLine.style.visibility = 'hidden'; + } + } + + private isHorizontalSticking = false; + private isVerticalSticking = false; + private enableStick = true; + + private autoSnapToLine = async (nodeId: ID, bbox: AABB, metadata: Metadata) => { + const { verticalX, horizontalY } = metadata; + const { tolerance } = this.options; + const { + min: [nodeMinX, nodeMinY], + max: [nodeMaxX, nodeMaxY], + center: [nodeCenterX, nodeCenterY], + } = bbox; + + let dx = 0; + let dy = 0; + if (verticalX !== null) { + if (distance(nodeMaxX, verticalX) < tolerance) dx = verticalX - nodeMaxX; + if (distance(nodeMinX, verticalX) < tolerance) dx = verticalX - nodeMinX; + if (distance(nodeCenterX, verticalX) < tolerance) dx = verticalX - nodeCenterX; + + if (dx !== 0) this.isVerticalSticking = true; + } + if (horizontalY !== null) { + if (distance(nodeMaxY, horizontalY) < tolerance) dy = horizontalY - nodeMaxY; + if (distance(nodeMinY, horizontalY) < tolerance) dy = horizontalY - nodeMinY; + if (distance(nodeCenterY, horizontalY) < tolerance) dy = horizontalY - nodeCenterY; + + if (dy !== 0) this.isHorizontalSticking = true; + } + if (dx !== 0 || dy !== 0) { + // Stick to the line + await this.context.graph.translateElementBy({ [nodeId]: [dx, dy] }, false); + } + }; + + private enableSnap = (event: IDragEvent) => { + const { target } = event; + + const threshold = 0.5; + + if (this.isHorizontalSticking || this.isVerticalSticking) { + if ( + this.isHorizontalSticking && + this.isVerticalSticking && + Math.abs(event.dx) <= threshold && + Math.abs(event.dy) <= threshold + ) { + this.context.graph.translateElementBy({ [target.id]: [-event.dx, -event.dy] }, false); + return false; + } else if (this.isHorizontalSticking && Math.abs(event.dy) <= threshold) { + this.context.graph.translateElementBy({ [target.id]: [0, -event.dy] }, false); + return false; + } else if (this.isVerticalSticking && Math.abs(event.dx) <= threshold) { + this.context.graph.translateElementBy({ [target.id]: [-event.dx, 0] }, false); + return false; + } else { + this.isHorizontalSticking = false; + this.isVerticalSticking = false; + this.enableStick = false; + setTimeout(() => { + this.enableStick = true; + }, 200); + } + } + + return this.enableStick; + }; + + private calcSnaplineMetadata = (target: Node, nodeBBox: AABB): Metadata => { + const { tolerance, shape } = this.options; + + const { + min: [nodeMinX, nodeMinY], + max: [nodeMaxX, nodeMaxY], + center: [nodeCenterX, nodeCenterY], + } = nodeBBox; + + let verticalX: number | null = null; + let verticalMinY: number | null = null; + let verticalMaxY: number | null = null; + let horizontalY: number | null = null; + let horizontalMinX: number | null = null; + let horizontalMaxX: number | null = null; + + this.getNodes().some((snapNode: Node) => { + if (isEqual(target.id, snapNode.id)) return false; + + const snapBBox = getShape(snapNode, shape).getRenderBounds(); + const { + min: [snapMinX, snapMinY], + max: [snapMaxX, snapMaxY], + center: [snapCenterX, snapCenterY], + } = snapBBox; + + if (verticalX === null) { + if (distance(snapCenterX, nodeCenterX) < tolerance) { + verticalX = snapCenterX; + } else if (distance(snapMinX, nodeMinX) < tolerance) { + verticalX = snapMinX; + } else if (distance(snapMinX, nodeMaxX) < tolerance) { + verticalX = snapMinX; + } else if (distance(snapMaxX, nodeMaxX) < tolerance) { + verticalX = snapMaxX; + } else if (distance(snapMaxX, nodeMinX) < tolerance) { + verticalX = snapMaxX; + } + + if (verticalX !== null) { + verticalMinY = Math.min(snapMinY, nodeMinY); + verticalMaxY = Math.max(snapMaxY, nodeMaxY); + } + } + + if (horizontalY === null) { + if (distance(snapCenterY, nodeCenterY) < tolerance) { + horizontalY = snapCenterY; + } else if (distance(snapMinY, nodeMinY) < tolerance) { + horizontalY = snapMinY; + } else if (distance(snapMinY, nodeMaxY) < tolerance) { + horizontalY = snapMinY; + } else if (distance(snapMaxY, nodeMaxY) < tolerance) { + horizontalY = snapMaxY; + } else if (distance(snapMaxY, nodeMinY) < tolerance) { + horizontalY = snapMaxY; + } + + if (horizontalY !== null) { + horizontalMinX = Math.min(snapMinX, nodeMinX); + horizontalMaxX = Math.max(snapMaxX, nodeMaxX); + } + } + + return verticalX !== null && horizontalY !== null; + }); + return { verticalX, verticalMinY, verticalMaxY, horizontalY, horizontalMinX, horizontalMaxX }; + }; + + protected onDragStart = () => { + this.initSnapline(); + }; + + protected onDrag = async (event: IDragEvent) => { + const { target } = event; + + if (this.options.autoSnap) { + const enable = this.enableSnap(event); + if (!enable) return; + } + + const nodeBBox = getShape(target, this.options.shape).getRenderBounds(); + const metadata = this.calcSnaplineMetadata(target, nodeBBox); + + this.hideSnapline(); + + if (metadata.verticalX !== null || metadata.horizontalY !== null) { + this.updateSnapline(metadata); + } + + if (this.options.autoSnap) { + await this.autoSnapToLine(target.id, nodeBBox, metadata); + } + }; + + protected onDragEnd = () => { + this.hideSnapline(); + }; + + private async bindEvents() { + const { graph } = this.context; + graph.on(NodeEvent.DRAG_START, this.onDragStart); + graph.on(NodeEvent.DRAG, this.onDrag); + graph.on(NodeEvent.DRAG_END, this.onDragEnd); + } + + private unbindEvents() { + const { graph } = this.context; + graph.off(NodeEvent.DRAG_START, this.onDragStart); + graph.off(NodeEvent.DRAG, this.onDrag); + graph.off(NodeEvent.DRAG_END, this.onDragEnd); + } + + private destroyElements() { + this.horizontalLine.destroy(); + this.verticalLine.destroy(); + } + + public destroy() { + this.destroyElements(); + this.unbindEvents(); + super.destroy(); + } +} + +const distance = (a: number, b: number) => Math.abs(a - b); + +const getShape = (node: Node, shapeFilter: string | ((node: Node) => DisplayObject)) => { + return typeof shapeFilter === 'function' ? shapeFilter(node) : node.getShape(shapeFilter); +}; diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts index 4aaac181140..b093c53c803 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -65,6 +65,7 @@ import { Hull, Legend, Minimap, + Snapline, Timebar, Toolbar, Tooltip, @@ -175,11 +176,12 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = { history: History, hull: Hull, legend: Legend, + minimap: Minimap, + snapline: Snapline, timebar: Timebar, toolbar: Toolbar, tooltip: Tooltip, watermark: Watermark, - minimap: Minimap, }, transform: { 'update-related-edges': UpdateRelatedEdge, diff --git a/packages/g6/src/runtime/viewport.ts b/packages/g6/src/runtime/viewport.ts index 908373b231e..5a354944c08 100644 --- a/packages/g6/src/runtime/viewport.ts +++ b/packages/g6/src/runtime/viewport.ts @@ -1,10 +1,11 @@ -import { AABB } from '@antv/g'; +import { AABB, ICamera } from '@antv/g'; import { clamp, isNumber, pick } from '@antv/util'; import { AnimationType, GraphEvent } from '../constants'; import type { FitViewOptions, ID, Point, TransformOptions, Vector2, ViewportAnimationEffectTiming } from '../types'; import { getAnimationOptions } from '../utils/animation'; -import { getBBoxSize, getCombinedBBox } from '../utils/bbox'; +import { getBBoxSize, getCombinedBBox, isPointInBBox } from '../utils/bbox'; import { AnimateEvent, ViewportEvent, emit } from '../utils/event'; +import { isPoint } from '../utils/is'; import { parsePadding } from '../utils/padding'; import { add, divide, subtract } from '../utils/vector'; import type { RuntimeContext } from './types'; @@ -30,7 +31,20 @@ export class ViewportController { } private get camera() { - return this.context.canvas.getCamera(); + const { canvas } = this.context; + return new Proxy(canvas.getCamera(), { + get: (target, prop: keyof ICamera) => { + const transientCamera = canvas.getLayer('transient').getCamera(); + const value = target[prop]; + if (typeof value === 'function') { + return (...args: any[]) => { + const result = (value as (...args: any[]) => any).apply(target, args); + (transientCamera[prop] as (...args: any[]) => any).apply(transientCamera, args); + return result; + }; + } + }, + }); } private landmarkCounter = 0; @@ -255,6 +269,26 @@ export class ViewportController { return bboxInViewport; } + /** + * 判断点或包围盒是否在视口中 + * + * Determine whether the point or bounding box is in the viewport + * @param target - 点或包围盒 | Point or bounding box + * @returns - 是否在视口中 | Whether it is in the viewport + */ + public isInViewport(target: Point | AABB) { + const { graph } = this.context; + const size = this.getCanvasSize(); + + const [x1, y1] = graph.getCanvasByViewport([0, 0]); + const [x2, y2] = graph.getCanvasByViewport(size); + + const viewportBBox = new AABB(); + viewportBBox.setMinMax([x1, y1, 0], [x2, y2, 0]); + + return isPoint(target) ? isPointInBBox(target, viewportBBox) : viewportBBox.intersects(target); + } + public cancelAnimation() { // @ts-expect-error landmarks is private if (this.camera.landmarks?.length) {