diff --git a/.vscode/settings.json b/.vscode/settings.json index 93f4205f48a..48319a01c4b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,5 +63,6 @@ ], "javascript.preferences.importModuleSpecifier": "relative", "typescript.preferences.importModuleSpecifier": "relative", - "svg.preview.background": "transparent" + "svg.preview.background": "transparent", + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/packages/g6/__tests__/demos/behavior-scroll-canvas.ts b/packages/g6/__tests__/demos/behavior-scroll-canvas.ts index c7ba7ab9c35..7dc2636d30f 100644 --- a/packages/g6/__tests__/demos/behavior-scroll-canvas.ts +++ b/packages/g6/__tests__/demos/behavior-scroll-canvas.ts @@ -39,5 +39,11 @@ export const behaviorScrollCanvas: TestCase = async (context) => { await graph.render(); + (window as any).__graph = graph; + (window as any).__g_instances__ = []; + + const canvas = graph.getCanvas(); + + (window as any).__g_instances__.push(canvas); return graph; }; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index 025c7f86d42..18633138764 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -121,7 +121,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 { pluginSnapline, 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-grid-line.ts b/packages/g6/__tests__/demos/plugin-grid-line.ts index 69bf20d5747..13fe84abcb5 100644 --- a/packages/g6/__tests__/demos/plugin-grid-line.ts +++ b/packages/g6/__tests__/demos/plugin-grid-line.ts @@ -10,7 +10,7 @@ export const pluginGridLine: TestCase = async (context) => { behaviors: ['drag-canvas'], plugins: [{ type: 'grid-line', follow: false }], }); - + (window as any).__graph = graph; await graph.render(); pluginGridLine.form = (panel) => { diff --git a/packages/g6/__tests__/demos/plugin-snapline.ts b/packages/g6/__tests__/demos/plugin-snapline.ts index 05b5df1827b..f95ac62ef6b 100644 --- a/packages/g6/__tests__/demos/plugin-snapline.ts +++ b/packages/g6/__tests__/demos/plugin-snapline.ts @@ -1,74 +1,65 @@ -import { Graph, Node } from '@antv/g6'; +import { Graph } from '@/src'; 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 } }, + { id: 'node-1', style: { x: 100, y: 100 } }, + { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 100 } }, + { id: 'node-3', style: { x: 100, y: 200 } }, + { id: 'node-4', combo: 'combo-1', style: { x: 200, y: 200 } }, ], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-2', target: 'node-4' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-3', target: 'node-4' }, + ], + combos: [{ id: 'combo-1' }], }, - 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, - }, + node: { style: { size: 20 } }, + edge: { + style: { endArrow: true }, }, - behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'], + behaviors: [ + { + type: 'drag-element', + // shadow: true + }, + // { + // type: 'scroll-canvas', + // // shadow: true + // }, + { + type: 'zoom-canvas', + // shadow: true + }, + ], 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, - autoSnap: false, - }; - pluginSnapline.form = (panel) => { + const config = { + enable: true, + hideEdge: 'none', + shadow: false, + }; + const handleChange = () => { + graph.setBehaviors([{ type: 'drag-element', ...config }]); + }; 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), - }); - }), - panel - .add(config, 'offset', [0, 20, Infinity]) - .name('Offset') - .onChange((offset: string) => { - graph.updatePlugin({ - key: 'snapline', - offset, - }); - }), - panel - .add(config, 'autoSnap') - .name('Auto Snap') - .onChange((autoSnap: boolean) => { - graph.updatePlugin({ - key: 'snapline', - autoSnap, - }); - }), + panel.add(config, 'enable').onChange(handleChange), + panel.add(config, 'hideEdge', ['none', 'in', 'out', 'both']).onChange(handleChange), + panel.add(config, 'shadow').onChange(handleChange), ]; }; + return graph; }; diff --git a/packages/g6/src/behaviors/drag-element.ts b/packages/g6/src/behaviors/drag-element.ts index c63b76daa29..cc011d4c57d 100644 --- a/packages/g6/src/behaviors/drag-element.ts +++ b/packages/g6/src/behaviors/drag-element.ts @@ -236,6 +236,7 @@ export class DragElement extends BaseBehavior { * @internal */ protected onDragStart(event: IElementDragEvent) { + // console.log('onDragStart', event, event.target, event.target.id) this.enable = this.validate(event); if (!this.enable) return; diff --git a/packages/g6/src/plugins/index.ts b/packages/g6/src/plugins/index.ts index c9f4cb2b336..ce374f0cb73 100644 --- a/packages/g6/src/plugins/index.ts +++ b/packages/g6/src/plugins/index.ts @@ -11,7 +11,7 @@ export { History } from './history'; export { Hull } from './hull'; export { Legend } from './legend'; export { Minimap } from './minimap'; -export { Snapline } from './snapline'; +export { SnapLine, Snapline } from './snapline'; export { Timebar } from './timebar'; export { Toolbar } from './toolbar'; export { Tooltip } from './tooltip'; diff --git a/packages/g6/src/plugins/snapline.ts b/packages/g6/src/plugins/snapline.ts new file mode 100644 index 00000000000..236acf4c7de --- /dev/null +++ b/packages/g6/src/plugins/snapline.ts @@ -0,0 +1,327 @@ +import { AABB, Line, Tuple3Number } from '@antv/g'; +import { clone } from '@antv/util'; +import { CommonEvent } from '../constants'; +import type { RuntimeContext } from '../runtime/types'; +import { ID, IElementDragEvent, Point } from '../types'; +import { divide } from '../utils/vector'; +import type { BasePluginOptions } from './base-plugin'; +import { BasePlugin } from './base-plugin'; + +export interface SnapLineOptions extends BasePluginOptions {} + +const directionArr = Object.freeze(['x', 'y', 'z'] as const); +type Direction = (typeof directionArr)[number]; + +const alignArr = Object.freeze(['center', 'max', 'min'] as const); +type AlignType = (typeof alignArr)[number]; + +type LineMeta = { + id: string; + align: [AlignType, AlignType]; + diff: { x: number; y: number; z: number }; + aPoint: Tuple3Number; + bPoint: Tuple3Number; + linePoint: { x1: number; y1: number; x2: number; y2: number }; + direction: Direction; +}; + +/** + * 定位辅助线 + * + * Snap line + */ +export class SnapLine extends BasePlugin { + static defaultOptions: Partial = {}; + + constructor(context: RuntimeContext, options: SnapLineOptions) { + super(context, Object.assign({}, SnapLine.defaultOptions, options)); + + this.bindEvents(); + } + + /** + * 更新网格线配置 + * + * Update the configuration of the grid line + * @param options - 配置项 | options + */ + public update(options: Partial) { + super.update(options); + } + + enableElements = ['node', 'combo']; + private bindEvents() { + const { graph } = this.context; + + this.enableElements.forEach((type) => { + graph.on(`${type}:${CommonEvent.DRAG_START}`, this.onDragStart); + graph.on(`${type}:${CommonEvent.DRAG}`, this.onDrag); + graph.on(`${type}:${CommonEvent.DRAG_END}`, this.onDragEnd); + }); + } + + private shadowDragBounds: AABB | undefined; + private onDragStart = (event: IElementDragEvent) => { + // console.log('onDragStart event', event); + const { target } = event; + this.shadowDragBounds = clone(this.context.element?.getElement(target.id)?.getBounds()); + }; + + private onDrag = (event: IElementDragEvent) => { + // console.log('onDrag event', event); + + const { target } = event; + const delta = this.getDelta(event); + this.moveShadow(delta); + const { graph } = this.context; + const nodes = graph.getNodeData(); + const combos = graph.getComboData(); + + // console.log('event.offset', event.offset) + // console.log('event.dx, event.dy', event.dx, event.dy) + // console.log('delta', delta) + // console.log('event.target', event.nativeEvent); + // const { x: eventX, y: eventY } = event.offset + // const [eventX, eventY] = this.shadowDragBounds; + const offset: Tuple3Number = [5, 5, 5]; + + const dragEl = this.context.element?.getElement(target.id); + + const shadowDragBounds = this.shadowDragBounds; + if (!dragEl || !shadowDragBounds) return; + + const lineMetaMap = new Map(); + + nodes.forEach((node) => { + if (node.id === dragEl.id) return; + const relatedEl = this.context.element?.getElement(node.id); + + if (!relatedEl) return; + + const relatedElBounds = relatedEl.getBounds(); + + // console.log('dragElBounds', dragElBounds); + // console.log('relatedElBounds', relatedElBounds); + + const linePositionByX = this.getLinePosition({ + direction: 'x', + aBounds: shadowDragBounds, + bBounds: relatedElBounds, + offset, + }); + const linePositionByY: any[] = []; + // const linePositionByY = this.getLinePosition({ + // direction: 'y', + // aBounds: shadowDragBounds, + // bBounds: relatedElBounds, + // offset + // }); + // console.log(node.id, linePositionByX); + const linePositionByZ = this.getLinePosition({ + direction: 'z', + aBounds: shadowDragBounds, + bBounds: relatedElBounds, + offset, + }); + + [...linePositionByX, ...linePositionByY, ...linePositionByZ].forEach((lineMeta) => { + const { id, diff, direction, align } = lineMeta; + const oldLineMeta = lineMetaMap.get(id); + const diffOfDir = Math.abs(diff[direction]); + if (!oldLineMeta) { + lineMetaMap.set(id, lineMeta); + } else { + // console.log(node.id, diffOfDir, Math.abs(oldLineMeta.diff[direction])); + const oldDiffOfDir = Math.abs(oldLineMeta.diff[direction]); + if (oldDiffOfDir > diffOfDir) { + lineMetaMap.set(id, lineMeta); + // console.log(node.id, diffOfDir, Math.abs(oldLineMeta.diff[direction])); + } else if (oldDiffOfDir === diffOfDir) { + const diffOfOtherDirs = Object.entries(diff).reduce((result, cur) => { + return cur[0] === direction ? result : result + Math.abs(cur[1] as number); + }, 0); + const oldDiffOfOtherDirs = Object.entries(oldLineMeta.diff).reduce((result, cur) => { + return cur[0] === direction ? result : result + Math.abs(cur[1]); + }, 0); + // console.log('diffOfOtherDirs', diffOfOtherDirs, 'oldDiffOfOtherDirs', oldDiffOfOtherDirs); + if (oldDiffOfOtherDirs > diffOfOtherDirs) { + lineMetaMap.set(id, lineMeta); + } + } + } + }); + }); + + for (const [id, meta] of lineMetaMap) { + this.addLine(meta); + } + + // console.log('lineMetaMap', lineMetaMap.keys()); + + // console.log('this.lineMap.size', this.lineMap.keys()); + for (const [id, line] of this.lineMap) { + if (!lineMetaMap.get(id)) { + this.lineMap.delete(id); + // todo + // this.context.graph.getCanvas().main.removeChild(line); + } + } + }; + + alignModes: { [k in Direction]: [AlignType, AlignType][] } = { + x: [ + ['center', 'center'], + ['min', 'max'], + ['max', 'min'], + ], + y: [ + ['center', 'center'], + ['min', 'max'], + ['max', 'min'], + ], + z: [], + }; + + private getLinePosition({ + direction, + aBounds, + bBounds, + offset, + }: { + direction: Direction; + aBounds: AABB; + bBounds: AABB; + offset: Tuple3Number; + }) { + const dataIndex = directionArr.findIndex((dir) => dir === direction); + const offsetData = offset[dataIndex] ?? 0; + + const result: Array = []; + const alignMode = this.alignModes[direction]; + for (let i = 0; i < alignMode.length; i++) { + const align = alignMode[i]; + const [aAlign, bAlign] = align; + // console.log('aAlign', aAlign, 'bAlign', bAlign); + const aPoint = aBounds[aAlign]; + const bPoint = bBounds[bAlign]; + const aData = aPoint[dataIndex]; + const bData = bPoint[dataIndex]; + const abDiff = aData - bData; + const active = Math.abs(abDiff) <= offsetData; + if (!active) continue; + + const diff = directionArr.reduce( + (result, dir, index) => { + if (dir === direction) { + result[dir] = aBounds[aAlign][dataIndex] - bBounds[bAlign][dataIndex]; + } else { + // todo + // result + } + return result; + }, + {} as LineMeta['diff'], + ); + + let [x1, y1] = aPoint; + const [x2, y2] = bPoint; + + if (direction === 'x') { + x1 = x2; + } else if (direction === 'y') { + y1 = y2; + } + + result.push({ + id: `${direction}-${align[0]}`, + direction, + aPoint, + bPoint, + linePoint: { x1, y1, x2, y2 }, + align, + diff, + }); + } + + return result; + } + + private calcABSpacing() {} + + private moveShadow(offset: Point) { + if (!this.shadowDragBounds) return; + const [dx, dy] = offset; + const { center, min, max } = this.shadowDragBounds; + center[0] += dx; + center[1] += dy; + min[0] += dx; + min[1] += dy; + max[0] += dx; + max[1] += dy; + } + + lineMap = new Map(); + private addLine({ id, linePoint: { x1, y1, x2, y2 } }: LineMeta) { + let line = this.lineMap.get(id); + + if (line) { + line.attr({ x1, y1, x2, y2 }); + } else { + line = this.context.graph.getCanvas().appendChild( + new Line({ + id, + style: { + x1, + y1, + x2, + y2, + fill: 'red', + stroke: 'blue', + }, + }), + ); + this.lineMap.set(id, line); + } + return line; + } + + /** + * Get the delta of the drag + * @param event - drag event object + * @returns delta + * @internal + */ + protected getDelta(event: IElementDragEvent) { + const zoom = this.context.graph.getZoom(); + // console.log('zoom', zoom) + return divide([event.dx, event.dy], zoom); + } + /** + * 移动元素 + * + * Move the element + * @param ids - 元素 id 集合 | element id collection + * @param offset 偏移量 | offset + * @internal + */ + protected async moveElement(ids: ID[], offset: Point) { + const { model, element } = this.context; + const { dropEffect } = this.options; + ids.forEach((id) => { + const elementType = model.getElementType(id); + if (elementType === 'node') model.translateNodeBy(id, offset); + else if (elementType === 'combo') model.translateComboBy(id, offset); + }); + + if (dropEffect === 'move') ids.forEach((id) => model.refreshComboData(id)); + await element!.draw({ animation: false })?.finished; + } + + private onDragEnd = (event: IElementDragEvent) => { + // console.log('onDragEnd event', event); + }; + + public destroy(): void { + super.destroy(); + } +} diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts index 8c0aa31bc85..cc0d3077c81 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -80,6 +80,7 @@ import { Hull, Legend, Minimap, + SnapLine, Snapline, Timebar, Toolbar, @@ -199,6 +200,7 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = { toolbar: Toolbar, tooltip: Tooltip, watermark: Watermark, + snapline: SnapLine, }, transform: { 'update-related-edges': UpdateRelatedEdge,