diff --git a/packages/g6/__tests__/demos/plugin-snapline.ts b/packages/g6/__tests__/demos/plugin-snapline.ts index e31af8feb6b..05b5df1827b 100644 --- a/packages/g6/__tests__/demos/plugin-snapline.ts +++ b/packages/g6/__tests__/demos/plugin-snapline.ts @@ -19,7 +19,7 @@ export const pluginSnapline: TestCase = async (context) => { labelText: (datum) => datum.id, }, }, - behaviors: ['drag-element', 'drag-canvas'], + behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'], plugins: [ { type: 'snapline', @@ -36,6 +36,7 @@ export const pluginSnapline: TestCase = async (context) => { const config = { filter: false, offset: 20, + autoSnap: false, }; pluginSnapline.form = (panel) => { @@ -48,7 +49,6 @@ export const pluginSnapline: TestCase = async (context) => { key: 'snapline', filter: (node: Node) => (filter ? node.id !== 'node3' : true), }); - graph.render(); }), panel .add(config, 'offset', [0, 20, Infinity]) @@ -58,7 +58,15 @@ export const pluginSnapline: TestCase = async (context) => { key: 'snapline', offset, }); - graph.render(); + }), + panel + .add(config, 'autoSnap') + .name('Auto Snap') + .onChange((autoSnap: boolean) => { + graph.updatePlugin({ + key: 'snapline', + autoSnap, + }); }), ]; }; diff --git a/packages/g6/__tests__/snapshots/behaviors/brush-select/brush-selecting-zoom.svg b/packages/g6/__tests__/snapshots/behaviors/brush-select/brush-selecting-zoom.svg new file mode 100644 index 00000000000..fea0e1ad314 --- /dev/null +++ b/packages/g6/__tests__/snapshots/behaviors/brush-select/brush-selecting-zoom.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + node4 + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/plugins/snapline/zoom-5.svg b/packages/g6/__tests__/snapshots/plugins/snapline/zoom-5.svg new file mode 100644 index 00000000000..5dbd4d68edd --- /dev/null +++ b/packages/g6/__tests__/snapshots/plugins/snapline/zoom-5.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + node1 + + + + + + + + + + + + node2 + + + + + + + + + + + + node3 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/behaviors/brush-select.spec.ts b/packages/g6/__tests__/unit/behaviors/brush-select.spec.ts index 1c07a530a21..67722af67b7 100644 --- a/packages/g6/__tests__/unit/behaviors/brush-select.spec.ts +++ b/packages/g6/__tests__/unit/behaviors/brush-select.spec.ts @@ -128,6 +128,14 @@ describe('behavior brush select', () => { graph.emit(CanvasEvent.CLICK); await expect(graph).toMatchSnapshot(__filename, 'brush-clear-mode-intersect'); + + // zoom to test line width + graph.zoomTo(5); + graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); + graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 250, y: 400 } }); + + await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-zoom'); + graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 250, y: 400 } }); }); afterAll(() => { diff --git a/packages/g6/__tests__/unit/plugins/snapline.spec.ts b/packages/g6/__tests__/unit/plugins/snapline.spec.ts index 4f988aa22d3..a39f542330b 100644 --- a/packages/g6/__tests__/unit/plugins/snapline.spec.ts +++ b/packages/g6/__tests__/unit/plugins/snapline.spec.ts @@ -1,4 +1,5 @@ -import { Node, NodeEvent, type Graph } from '@/src'; +import type { Graph, Node } from '@/src'; +import { NodeEvent } from '@/src'; import { pluginSnapline } from '@@/demos'; import { createDemoGraph } from '../../utils'; @@ -75,6 +76,16 @@ describe('plugin snapline', () => { graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 }); await expect(graph).toMatchSnapshot(__filename, `auto-snap`); graph.emit(NodeEvent.DRAG_END, { target: node }); + + // zoom to test lineWidth + graph.zoomTo(5, false, [300, 300]); + graph.updatePlugin({ key: 'snapline', autoSnap: false }); + graph.updateNodeData([{ id: 'node3', style: { x: 260, 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, 'zoom-5'); + graph.emit(NodeEvent.DRAG_END, { target: node }); }); afterAll(() => { diff --git a/packages/g6/__tests__/unit/utils/shortcut.spec.ts b/packages/g6/__tests__/unit/utils/shortcut.spec.ts index d39555cf7ad..db36d3484c8 100644 --- a/packages/g6/__tests__/unit/utils/shortcut.spec.ts +++ b/packages/g6/__tests__/unit/utils/shortcut.spec.ts @@ -80,6 +80,24 @@ describe('shortcut', () => { expect(drag).toHaveBeenCalledTimes(1); expect(drag.mock.calls[0][0].deltaX).toBe(10); expect(drag.mock.calls[0][0].deltaY).toBe(0); + + drag.mockClear(); + + // shift drag + emitter.emit(CommonEvent.KEY_DOWN, { key: 'Shift' }); + emitter.emit(CommonEvent.DRAG, { deltaX: 10, deltaY: 0 }); + emitter.emit(CommonEvent.KEY_UP, { key: 'Shift' }); + expect(drag).toHaveBeenCalledTimes(0); + + shortcut.unbindAll(); + shortcut.bind(['Shift', 'drag'], drag); + + emitter.emit(CommonEvent.KEY_DOWN, { key: 'Shift' }); + emitter.emit(CommonEvent.DRAG, { deltaX: 10, deltaY: 0 }); + emitter.emit(CommonEvent.KEY_UP, { key: 'Shift' }); + expect(drag).toHaveBeenCalledTimes(1); + expect(drag.mock.calls[0][0].deltaX).toBe(10); + expect(drag.mock.calls[0][0].deltaY).toBe(0); }); it('focus', () => { diff --git a/packages/g6/src/behaviors/brush-select.ts b/packages/g6/src/behaviors/brush-select.ts index 998a3ff5940..95dee0fee82 100644 --- a/packages/g6/src/behaviors/brush-select.ts +++ b/packages/g6/src/behaviors/brush-select.ts @@ -144,9 +144,16 @@ export class BrushSelect extends BaseBehavior { */ protected onPointerDown(event: IPointerEvent) { if (!this.validate(event) || !this.isKeydown() || this.startPoint) return; - const { canvas } = this.context; + const { canvas, graph } = this.context; + const style = { ...this.options.style }; - this.rectShape = new Rect({ id: 'g6-brush-select', style: this.options.style }); + // 根据缩放比例调整 lineWidth + // Adjust lineWidth according to the zoom ratio + if (this.options.style.lineWidth) { + style.lineWidth = +this.options.style.lineWidth / graph.getZoom(); + } + + this.rectShape = new Rect({ id: 'g6-brush-select', style }); canvas.appendChild(this.rectShape); this.startPoint = [event.canvas.x, event.canvas.y]; @@ -327,8 +334,7 @@ export class BrushSelect extends BaseBehavior { protected isKeydown(): boolean { const { trigger } = this.options; const keys = (Array.isArray(trigger) ? trigger : [trigger]) as string[]; - if (!trigger || keys.includes('drag')) return true; - return this.shortcut!.match(keys); + return this.shortcut!.match(keys.filter((key) => key !== 'drag')); } /** @@ -348,7 +354,6 @@ export class BrushSelect extends BaseBehavior { private bindEvents() { const { graph } = this.context; - this.unbindEvents(); graph.on(CommonEvent.POINTER_DOWN, this.onPointerDown); graph.on(CommonEvent.POINTER_MOVE, this.onPointerMove); @@ -373,7 +378,9 @@ export class BrushSelect extends BaseBehavior { * @internal */ public update(options: Partial) { + this.unbindEvents(); this.options = deepMix(this.options, options); + this.bindEvents(); } /** diff --git a/packages/g6/src/plugins/snapline/index.ts b/packages/g6/src/plugins/snapline/index.ts index 7ff3ef40a7c..f6469881bd3 100644 --- a/packages/g6/src/plugins/snapline/index.ts +++ b/packages/g6/src/plugins/snapline/index.ts @@ -4,6 +4,7 @@ import { NodeEvent } from '../../constants'; import type { RuntimeContext } from '../../runtime/types'; import type { ID, IDragEvent, Node } from '../../types'; import { isVisible } from '../../utils/element'; +import { divide } from '../../utils/vector'; import type { BasePluginOptions } from '../base-plugin'; import { BasePlugin } from '../base-plugin'; @@ -133,27 +134,38 @@ export class Snapline extends BasePlugin { this.verticalLine.style.visibility = 'hidden'; } + private getLineWidth(direction: 'horizontal' | 'vertical') { + const { lineWidth } = this.options[`${direction}LineStyle`] as LineStyleProps; + return +(lineWidth || defaultLineStyle.lineWidth || 1) / this.context.graph.getZoom(); + } + 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'; + Object.assign(this.horizontalLine.style, { + x1: offset === Infinity ? 0 : horizontalMinX! - offset, + y1: horizontalY, + x2: offset === Infinity ? canvasWidth : horizontalMaxX! + offset, + y2: horizontalY, + visibility: 'visible', + lineWidth: this.getLineWidth('horizontal'), + }); } 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'; + Object.assign(this.verticalLine.style, { + x1: verticalX, + y1: offset === Infinity ? 0 : verticalMinY! - offset, + x2: verticalX, + y2: offset === Infinity ? canvasHeight : verticalMaxY! + offset, + visibility: 'visible', + lineWidth: this.getLineWidth('vertical'), + }); } else { this.verticalLine.style.visibility = 'hidden'; } @@ -194,25 +206,37 @@ export class Snapline extends BasePlugin { } }; + /** + * Get the delta of the drag + * @param event - drag event object + * @returns delta + * @internal + */ + protected getDelta(event: IDragEvent) { + const zoom = this.context.graph.getZoom(); + return divide([event.dx, event.dy], zoom); + } + private enableSnap = (event: IDragEvent) => { const { target } = event; const threshold = 0.5; if (this.isHorizontalSticking || this.isVerticalSticking) { + const [dx, dy] = this.getDelta(event); if ( this.isHorizontalSticking && this.isVerticalSticking && - Math.abs(event.dx) <= threshold && - Math.abs(event.dy) <= threshold + Math.abs(dx) <= threshold && + Math.abs(dy) <= threshold ) { - this.context.graph.translateElementBy({ [target.id]: [-event.dx, -event.dy] }, false); + this.context.graph.translateElementBy({ [target.id]: [-dx, -dy] }, false); return false; - } else if (this.isHorizontalSticking && Math.abs(event.dy) <= threshold) { - this.context.graph.translateElementBy({ [target.id]: [0, -event.dy] }, false); + } else if (this.isHorizontalSticking && Math.abs(dy) <= threshold) { + this.context.graph.translateElementBy({ [target.id]: [0, -dy] }, false); return false; - } else if (this.isVerticalSticking && Math.abs(event.dx) <= threshold) { - this.context.graph.translateElementBy({ [target.id]: [-event.dx, 0] }, false); + } else if (this.isVerticalSticking && Math.abs(dx) <= threshold) { + this.context.graph.translateElementBy({ [target.id]: [-dx, 0] }, false); return false; } else { this.isHorizontalSticking = false; diff --git a/packages/g6/src/runtime/viewport.ts b/packages/g6/src/runtime/viewport.ts index 5a354944c08..cfb053aca62 100644 --- a/packages/g6/src/runtime/viewport.ts +++ b/packages/g6/src/runtime/viewport.ts @@ -34,12 +34,17 @@ export class ViewportController { const { canvas } = this.context; return new Proxy(canvas.getCamera(), { get: (target, prop: keyof ICamera) => { - const transientCamera = canvas.getLayer('transient').getCamera(); + const layers = Object.entries(canvas.getLayers()).filter(([name]) => !['main'].includes(name)); + const cameras = layers.map(([, layer]) => layer.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); + cameras.forEach((camera) => { + (camera[prop] as (...args: any[]) => any).apply(camera, args); + }); + return result; }; }