Skip to content

Commit fd0f66a

Browse files
Sloth9527hustcc
andauthored
fix: correct drag node position at rotated canvas (#7310)
* fix: correct drag node position at rotated canvas * fix: correct force drag node position at rotated canvas * fix: type check & code review suggestion --------- Co-authored-by: hustcc <i@hust.cc>
1 parent d38787a commit fd0f66a

File tree

9 files changed

+120
-13
lines changed

9 files changed

+120
-13
lines changed

packages/g6/__tests__/bugs/behaviors-drag-rotated-canvas.spec.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import type { Graph } from '@/src';
2-
import { CommonEvent } from '@/src';
2+
import { CommonEvent, NodeEvent } from '@/src';
33
import { bugDragRotatedCanvas } from '@@/demos';
44
import { createDemoGraph, dispatchCanvasEvent } from '@@/utils';
55

6+
const fixed2 = (num: number): number => {
7+
return parseFloat(num.toFixed(2));
8+
};
9+
610
describe('behavior drag rotated canvas', () => {
711
let graph: Graph;
812

@@ -61,4 +65,22 @@ describe('behavior drag rotated canvas', () => {
6165
expect(graph.getRotation()).toBe(270);
6266
expect(graph.getPosition()).toBeCloseTo([x + 20, y - 10]);
6367
});
68+
69+
it.each([
70+
{ name: 'element', id: 'node1', targetType: 'node' },
71+
{ name: 'combo', id: 'comboA', targetType: 'combo' },
72+
])('drag $name when 30 rotated canvas', async ({ id, targetType }) => {
73+
await graph.rotateTo(30);
74+
75+
const [x, y] = graph.getElementPosition(id);
76+
77+
graph.emit(NodeEvent.DRAG_START, { target: { id: id }, targetType });
78+
graph.emit(NodeEvent.DRAG, { dx: 10, dy: 10 });
79+
graph.emit(NodeEvent.DRAG_END, { target: { id: id }, targetType });
80+
81+
expect(graph.getRotation()).toBe(30);
82+
const [nextX, nextY] = graph.getElementPosition(id);
83+
expect(fixed2(nextX)).toBeCloseTo(fixed2(x + 3.66));
84+
expect(fixed2(nextY)).toBeCloseTo(fixed2(y + 13.66));
85+
});
6486
});

packages/g6/__tests__/demos/bug-drag-rotated-canvas.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ export const bugDragRotatedCanvas: TestCase = async (context) => {
44
const graph = new Graph({
55
...context,
66
data: {
7-
nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],
7+
nodes: [
8+
{ id: 'node1', combo: 'comboA' },
9+
{ id: 'node2', combo: 'comboA' },
10+
{ id: 'node3' },
11+
{ id: 'node4' },
12+
{ id: 'node5' },
13+
],
14+
combos: [{ id: 'comboA' }],
815
edges: [
916
{ source: 'node1', target: 'node2' },
1017
{ source: 'node1', target: 'node3' },
@@ -17,7 +24,7 @@ export const bugDragRotatedCanvas: TestCase = async (context) => {
1724
layout: {
1825
type: 'grid',
1926
},
20-
behaviors: ['drag-canvas'],
27+
behaviors: ['drag-canvas', 'drag-element'],
2128
});
2229

2330
await graph.render();
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// ref: https://observablehq.com/@d3/force-directed-lattice
2+
import { Graph } from '@antv/g6';
3+
4+
export const bugDragRotatedElementForce: TestCase = async (context) => {
5+
const graph = new Graph({
6+
...context,
7+
data: getData(),
8+
layout: {
9+
type: 'd3-force',
10+
manyBody: {
11+
strength: -30,
12+
},
13+
link: {
14+
strength: 1,
15+
distance: 20,
16+
iterations: 10,
17+
},
18+
},
19+
node: {
20+
style: {
21+
size: 10,
22+
fill: '#000',
23+
},
24+
},
25+
edge: {
26+
style: {
27+
stroke: '#000',
28+
},
29+
},
30+
behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],
31+
});
32+
33+
await graph.render();
34+
35+
graph.rotateTo(2160 + 60);
36+
37+
return graph;
38+
};
39+
40+
function getData(size = 10) {
41+
const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));
42+
const edges = [];
43+
for (let y = 0; y < size; ++y) {
44+
for (let x = 0; x < size; ++x) {
45+
if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });
46+
if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });
47+
}
48+
}
49+
return { nodes, edges };
50+
}

packages/g6/__tests__/demos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export { behaviorOptimizeViewportTransform } from './behavior-optimize-viewport-
2121
export { behaviorScrollCanvas } from './behavior-scroll-canvas';
2222
export { behaviorZoomCanvas } from './behavior-zoom-canvas';
2323
export { bugDragRotatedCanvas } from './bug-drag-rotated-canvas';
24+
export { bugDragRotatedElementForce } from './bug-drag-rotated-element-force';
2425
export { bugProcessParallelEdgesComboFixed } from './bug-process-parallel-edges-combo-fixed';
2526
export { bugTooltipResize } from './bug-tooltip-resize';
2627
export { canvasCursor } from './canvas-cursor';

packages/g6/__tests__/unit/utils/vector.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
normalize,
1313
perpendicular,
1414
rad,
15+
rotate,
1516
scale,
1617
subtract,
1718
toVector2,
@@ -111,4 +112,11 @@ describe('Vector Functions', () => {
111112
expect(rad([1, 0])).toEqual(0);
112113
expect(rad([0, 1])).toEqual(Math.PI / 2);
113114
});
115+
116+
it('rotate', () => {
117+
expect(rotate([10, 10], 30)).toBeCloseTo([3.66, 13.66]);
118+
expect(rotate([10, 20], 90)).toBeCloseTo([-20, 10]);
119+
expect(rotate([10, 20], 180)).toBeCloseTo([-10, -20]);
120+
expect(rotate([10, 20], 270)).toBeCloseTo([20, -10]);
121+
});
114122
});

packages/g6/src/behaviors/drag-canvas.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { parsePadding } from '../utils/padding';
88
import { PinchHandler } from '../utils/pinch';
99
import type { ShortcutKey } from '../utils/shortcut';
1010
import { Shortcut } from '../utils/shortcut';
11-
import { multiply, subtract } from '../utils/vector';
11+
import { multiply, rotate, subtract } from '../utils/vector';
1212
import type { BaseBehaviorOptions } from './base-behavior';
1313
import { BaseBehavior } from './base-behavior';
1414

@@ -190,11 +190,7 @@ export class DragCanvas extends BaseBehavior<DragCanvasOptions> {
190190

191191
private clampByRotation([dx, dy]: Vector2): Vector2 {
192192
const rotation = this.context.graph.getRotation();
193-
if (rotation % 360 === 0) return [dx, dy];
194-
const rad = (rotation * Math.PI) / 180;
195-
const cos = Math.cos(rad);
196-
const sin = Math.sin(rad);
197-
return [dx * cos - dy * sin, dx * sin + dy * cos];
193+
return rotate([dx, dy], rotation);
198194
}
199195

200196
private clampByDirection([dx, dy]: Vector2): Vector2 {

packages/g6/src/behaviors/drag-element-force.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export class DragElementForce extends DragElement {
6666
const layout = this.forceLayoutInstance;
6767
this.context.graph.getNodeData(ids).forEach((element, index) => {
6868
const { x = 0, y = 0 } = element.style || {};
69-
if (layout) invokeLayoutMethod(layout, 'setFixedPosition', ids[index], [...add([+x, +y], offset)]);
69+
if (layout)
70+
invokeLayoutMethod(layout, 'setFixedPosition', ids[index], [...add([+x, +y], this.clampByRotation(offset))]);
7071
});
7172
}
7273

packages/g6/src/behaviors/drag-element.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { Rect } from '@antv/g';
33
import { isFunction } from '@antv/util';
44
import { COMBO_KEY, CanvasEvent, ComboEvent, CommonEvent } from '../constants';
55
import type { RuntimeContext } from '../runtime/types';
6-
import type { EdgeDirection, ID, IElementDragEvent, IPointerEvent, Point, Prefix, State } from '../types';
6+
import type { EdgeDirection, ID, IElementDragEvent, IPointerEvent, Point, Prefix, State, Vector2 } from '../types';
77
import { getBBoxSize, getCombinedBBox } from '../utils/bbox';
88
import { isToBeDestroyed } from '../utils/element';
99
import { idOf } from '../utils/id';
1010
import { subStyleProps } from '../utils/prefix';
11-
import { divide, subtract } from '../utils/vector';
11+
import { divide, rotate, subtract } from '../utils/vector';
1212
import type { BaseBehaviorOptions } from './base-behavior';
1313
import { BaseBehavior } from './base-behavior';
1414

@@ -354,6 +354,11 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
354354
return !!enable;
355355
}
356356

357+
protected clampByRotation([dx, dy]: Point): Vector2 {
358+
const rotation = this.context.graph.getRotation();
359+
return rotate([dx, dy], rotation);
360+
}
361+
357362
/**
358363
* <zh/> 移动元素
359364
*
@@ -367,7 +372,7 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
367372
const { dropEffect } = this.options;
368373

369374
if (dropEffect === 'move') ids.forEach((id) => model.refreshComboData(id));
370-
graph.translateElementBy(Object.fromEntries(ids.map((id) => [id, offset])), false);
375+
graph.translateElementBy(Object.fromEntries(ids.map((id) => [id, this.clampByRotation(offset)])), false);
371376
}
372377

373378
private moveShadow(offset: Point) {

packages/g6/src/utils/vector.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,20 @@ export function rad(a: Vector2 | Vector3): number {
219219
if (!x && !y) return 0;
220220
return Math.atan2(y, x);
221221
}
222+
223+
/**
224+
* <zh/> 旋转向量(角度制)
225+
*
226+
* <en/> Rotational vector (Angle system)
227+
* @param a - <zh/> 向量 | <en/> The vector
228+
* @param angle - <zh/> 旋转角度 | <en/> The rotation angle
229+
* @returns <zh/> 向量 | <en/> The vector
230+
*/
231+
export function rotate(a: Vector2, angle: number): Vector2 {
232+
const [dx, dy] = a;
233+
if (angle % 360 === 0) return [dx, dy];
234+
const rad = (angle * Math.PI) / 180;
235+
const cos = Math.cos(rad);
236+
const sin = Math.sin(rad);
237+
return [dx * cos - dy * sin, dx * sin + dy * cos];
238+
}

0 commit comments

Comments
 (0)