@neo4j-nvl/interaction-handlers 0.2.46 → 0.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +6 -4
- package/lib/index.js +3 -2
- package/lib/interaction-handlers/{multi-select-interaction.d.ts → box-select-interaction.d.ts} +16 -10
- package/lib/interaction-handlers/{multi-select-interaction.js → box-select-interaction.js} +65 -25
- package/lib/interaction-handlers/draw-interaction.js +5 -4
- package/lib/interaction-handlers/lasso-interaction.d.ts +64 -0
- package/lib/interaction-handlers/lasso-interaction.js +181 -0
- package/lib/interaction-handlers/utils.d.ts +5 -4
- package/lib/interaction-handlers/utils.js +29 -0
- package/lib/interaction-handlers/zoom-interaction.js +4 -3
- package/lib/overlay-renderer/overlay-renderer.d.ts +9 -5
- package/lib/overlay-renderer/overlay-renderer.js +68 -37
- package/package.json +2 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { BoxSelectInteractionCallbacks, BoxSelectInteractionOptions } from './interaction-handlers/box-select-interaction';
|
|
2
|
+
import { BoxSelectInteraction } from './interaction-handlers/box-select-interaction';
|
|
1
3
|
import type { ClickInteractionCallbacks, ClickInteractionOptions } from './interaction-handlers/click-interaction';
|
|
2
4
|
import { ClickInteraction } from './interaction-handlers/click-interaction';
|
|
3
5
|
import { DragNodeInteraction } from './interaction-handlers/drag-node-interaction';
|
|
@@ -6,11 +8,11 @@ import { DrawInteraction } from './interaction-handlers/draw-interaction';
|
|
|
6
8
|
import type { DrawInteractionCallbacks } from './interaction-handlers/draw-interaction';
|
|
7
9
|
import type { HoverInteractionCallbacks, HoverInteractionOptions } from './interaction-handlers/hover-interaction';
|
|
8
10
|
import { HoverInteraction } from './interaction-handlers/hover-interaction';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
+
import { LassoInteraction } from './interaction-handlers/lasso-interaction';
|
|
12
|
+
import type { LassoInteractionCallbacks, LassoInteractionOptions } from './interaction-handlers/lasso-interaction';
|
|
11
13
|
import type { PanInteractionCallbacks, PanInteractionOptions } from './interaction-handlers/pan-interaction';
|
|
12
14
|
import { PanInteraction } from './interaction-handlers/pan-interaction';
|
|
13
15
|
import type { ZoomInteractionCallbacks } from './interaction-handlers/zoom-interaction';
|
|
14
16
|
import { ZoomInteraction } from './interaction-handlers/zoom-interaction';
|
|
15
|
-
export type {
|
|
16
|
-
export { ZoomInteraction, PanInteraction,
|
|
17
|
+
export type { BoxSelectInteractionOptions, BoxSelectInteractionCallbacks, ClickInteractionOptions, HoverInteractionOptions, HoverInteractionCallbacks, DragNodeInteractionCallbacks, ClickInteractionCallbacks, PanInteractionCallbacks, DrawInteractionCallbacks, PanInteractionOptions, ZoomInteractionCallbacks, LassoInteractionOptions, LassoInteractionCallbacks };
|
|
18
|
+
export { ZoomInteraction, PanInteraction, BoxSelectInteraction, ClickInteraction, HoverInteraction, DragNodeInteraction, DrawInteraction, LassoInteraction };
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { BoxSelectInteraction } from './interaction-handlers/box-select-interaction';
|
|
1
2
|
import { ClickInteraction } from './interaction-handlers/click-interaction';
|
|
2
3
|
import { DragNodeInteraction } from './interaction-handlers/drag-node-interaction';
|
|
3
4
|
import { DrawInteraction } from './interaction-handlers/draw-interaction';
|
|
4
5
|
import { HoverInteraction } from './interaction-handlers/hover-interaction';
|
|
5
|
-
import {
|
|
6
|
+
import { LassoInteraction } from './interaction-handlers/lasso-interaction';
|
|
6
7
|
import { PanInteraction } from './interaction-handlers/pan-interaction';
|
|
7
8
|
import { ZoomInteraction } from './interaction-handlers/zoom-interaction';
|
|
8
|
-
export { ZoomInteraction, PanInteraction,
|
|
9
|
+
export { ZoomInteraction, PanInteraction, BoxSelectInteraction, ClickInteraction, HoverInteraction, DragNodeInteraction, DrawInteraction, LassoInteraction };
|
package/lib/interaction-handlers/{multi-select-interaction.d.ts → box-select-interaction.d.ts}
RENAMED
|
@@ -3,20 +3,25 @@ import { BaseInteraction } from './base';
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for the multi-select interaction handler to customize its behavior.
|
|
5
5
|
*/
|
|
6
|
-
export type
|
|
6
|
+
export type BoxSelectInteractionOptions = {
|
|
7
7
|
/**
|
|
8
8
|
* If true, the selection will be applied when the mouse is released.
|
|
9
9
|
* @default false
|
|
10
10
|
*/
|
|
11
11
|
selectOnRelease?: boolean;
|
|
12
12
|
};
|
|
13
|
-
export type
|
|
13
|
+
export type BoxSelectInteractionCallbacks = {
|
|
14
|
+
/**
|
|
15
|
+
* Called once when the user presses the mouse on the canvas, starting the box selection.
|
|
16
|
+
* @param event - The original mouse event
|
|
17
|
+
*/
|
|
18
|
+
onBoxStarted?: (event: MouseEvent) => void;
|
|
14
19
|
/**
|
|
15
20
|
* Called after once the user releases the mouse after multi-selecting.
|
|
16
21
|
* @param selectionObject - The selected nodes and relationships
|
|
17
22
|
* @param event - The original mouse event
|
|
18
23
|
*/
|
|
19
|
-
|
|
24
|
+
onBoxSelect?: (({ nodes, rels }: {
|
|
20
25
|
nodes: Node[];
|
|
21
26
|
rels: Relationship[];
|
|
22
27
|
}, event: MouseEvent) => void) | boolean;
|
|
@@ -29,30 +34,31 @@ export type MultiSelectInteractionCallbacks = {
|
|
|
29
34
|
* @example
|
|
30
35
|
* ```js
|
|
31
36
|
* const nvl = new NVL(container, nodes, relationships)
|
|
32
|
-
* const
|
|
37
|
+
* const boxSelectInteraction = new BoxSelectInteraction(nvl)
|
|
33
38
|
*
|
|
34
|
-
*
|
|
39
|
+
* boxSelectInteraction.updateCallback('onBoxSelect', ({ nodes, rels }) => {
|
|
35
40
|
* console.log('Selected elements:', nodes, rels)
|
|
36
41
|
* })
|
|
37
42
|
*/
|
|
38
|
-
export declare class
|
|
43
|
+
export declare class BoxSelectInteraction extends BaseInteraction<BoxSelectInteractionCallbacks> {
|
|
39
44
|
private mousePosition;
|
|
40
45
|
private startWorldPosition;
|
|
41
46
|
private overlayRenderer;
|
|
42
|
-
private
|
|
47
|
+
private isBoxSelecting;
|
|
43
48
|
private readonly options;
|
|
44
49
|
/**
|
|
45
50
|
* Creates a new instance of the multi-select interaction handler.
|
|
46
51
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
47
52
|
* @param options - Options for the multi-select interaction handler to customize its behavior
|
|
48
53
|
*/
|
|
49
|
-
constructor(nvl: NVL, options?:
|
|
54
|
+
constructor(nvl: NVL, options?: BoxSelectInteractionOptions);
|
|
50
55
|
private handleMouseDown;
|
|
51
56
|
private handleDrag;
|
|
52
|
-
private
|
|
57
|
+
private getHitsInBox;
|
|
58
|
+
private endBoxSelect;
|
|
53
59
|
/**
|
|
54
60
|
* Removes all related event listeners and the overlay renderer for the box.
|
|
55
61
|
*/
|
|
56
62
|
destroy(): void;
|
|
57
|
-
private
|
|
63
|
+
private turnOnBoxSelect;
|
|
58
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NODE_EDGE_WIDTH } from '../constants';
|
|
2
2
|
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
3
3
|
import { BaseInteraction } from './base';
|
|
4
|
+
import { getCanvasPosition, getWorldPosition } from './utils';
|
|
4
5
|
/**
|
|
5
6
|
* Interaction handler for multi-selecting nodes and relationships.
|
|
6
7
|
* When dragging, draws a box on the canvas and selects all
|
|
@@ -9,13 +10,13 @@ import { BaseInteraction } from './base';
|
|
|
9
10
|
* @example
|
|
10
11
|
* ```js
|
|
11
12
|
* const nvl = new NVL(container, nodes, relationships)
|
|
12
|
-
* const
|
|
13
|
+
* const boxSelectInteraction = new BoxSelectInteraction(nvl)
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
+
* boxSelectInteraction.updateCallback('onBoxSelect', ({ nodes, rels }) => {
|
|
15
16
|
* console.log('Selected elements:', nodes, rels)
|
|
16
17
|
* })
|
|
17
18
|
*/
|
|
18
|
-
export class
|
|
19
|
+
export class BoxSelectInteraction extends BaseInteraction {
|
|
19
20
|
/**
|
|
20
21
|
* Creates a new instance of the multi-select interaction handler.
|
|
21
22
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
@@ -41,7 +42,7 @@ export class MultiSelectInteraction extends BaseInteraction {
|
|
|
41
42
|
writable: true,
|
|
42
43
|
value: void 0
|
|
43
44
|
});
|
|
44
|
-
Object.defineProperty(this, "
|
|
45
|
+
Object.defineProperty(this, "isBoxSelecting", {
|
|
45
46
|
enumerable: true,
|
|
46
47
|
configurable: true,
|
|
47
48
|
writable: true,
|
|
@@ -59,10 +60,10 @@ export class MultiSelectInteraction extends BaseInteraction {
|
|
|
59
60
|
writable: true,
|
|
60
61
|
value: (event) => {
|
|
61
62
|
if (event.button !== 0) {
|
|
62
|
-
this.
|
|
63
|
+
this.isBoxSelecting = false;
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
|
-
this.
|
|
66
|
+
this.turnOnBoxSelect(event);
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
Object.defineProperty(this, "handleDrag", {
|
|
@@ -70,41 +71,79 @@ export class MultiSelectInteraction extends BaseInteraction {
|
|
|
70
71
|
configurable: true,
|
|
71
72
|
writable: true,
|
|
72
73
|
value: (event) => {
|
|
73
|
-
if (this.
|
|
74
|
-
|
|
74
|
+
if (this.isBoxSelecting) {
|
|
75
|
+
const curPos = getCanvasPosition(this.containerInstance, event);
|
|
76
|
+
this.overlayRenderer.drawBox(this.mousePosition.x, this.mousePosition.y, curPos.x, curPos.y);
|
|
75
77
|
}
|
|
76
78
|
else if (event.buttons === 1) {
|
|
77
|
-
this.
|
|
79
|
+
this.turnOnBoxSelect(event);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
});
|
|
81
|
-
Object.defineProperty(this, "
|
|
83
|
+
Object.defineProperty(this, "getHitsInBox", {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
writable: true,
|
|
87
|
+
value: (startPos, endPos) => {
|
|
88
|
+
const inside = (pos, start, end) => {
|
|
89
|
+
const minX = Math.min(start.x, end.x);
|
|
90
|
+
const maxX = Math.max(start.x, end.x);
|
|
91
|
+
const minY = Math.min(start.y, end.y);
|
|
92
|
+
const maxY = Math.max(start.y, end.y);
|
|
93
|
+
return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY;
|
|
94
|
+
};
|
|
95
|
+
const positions = this.nvlInstance.getNodePositions();
|
|
96
|
+
const hitNodes = new Set();
|
|
97
|
+
for (const pos of positions) {
|
|
98
|
+
if (pos.x === undefined || pos.y === undefined || pos.id === undefined) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (inside(pos, startPos, endPos)) {
|
|
102
|
+
hitNodes.add(pos.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const rels = this.nvlInstance.getRelationships();
|
|
106
|
+
const hitRels = [];
|
|
107
|
+
for (const rel of rels) {
|
|
108
|
+
if (hitNodes.has(rel.from) && hitNodes.has(rel.to)) {
|
|
109
|
+
hitRels.push(rel);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const hitNodeArray = Array.from(hitNodes).map((id) => this.nvlInstance.getNodeById(id));
|
|
113
|
+
return {
|
|
114
|
+
nodes: hitNodeArray,
|
|
115
|
+
rels: hitRels
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
Object.defineProperty(this, "endBoxSelect", {
|
|
82
120
|
enumerable: true,
|
|
83
121
|
configurable: true,
|
|
84
122
|
writable: true,
|
|
85
123
|
value: (event) => {
|
|
86
|
-
if (!this.
|
|
124
|
+
if (!this.isBoxSelecting) {
|
|
87
125
|
return;
|
|
88
126
|
}
|
|
89
|
-
this.
|
|
127
|
+
this.isBoxSelecting = false;
|
|
90
128
|
this.overlayRenderer.clear();
|
|
91
|
-
const
|
|
92
|
-
const
|
|
129
|
+
const endPosition = getCanvasPosition(this.containerInstance, event);
|
|
130
|
+
const endWorldPosition = getWorldPosition(this.nvlInstance, endPosition);
|
|
131
|
+
const { nodes, rels } = this.getHitsInBox(this.startWorldPosition, endWorldPosition);
|
|
93
132
|
if (this.options.selectOnRelease) {
|
|
94
133
|
this.nvlInstance.updateElementsInGraph(nodes.map((node) => ({ id: node.id, selected: true })), rels.map((rel) => ({ id: rel.id, selected: true })));
|
|
95
134
|
}
|
|
96
|
-
this.callCallbackIfRegistered('
|
|
135
|
+
this.callCallbackIfRegistered('onBoxSelect', { nodes, rels }, event);
|
|
97
136
|
}
|
|
98
137
|
});
|
|
99
138
|
this.mousePosition = { x: 0, y: 0 };
|
|
100
139
|
this.startWorldPosition = { x: 0, y: 0 };
|
|
101
140
|
this.overlayRenderer = new OverlayRenderer(this.containerInstance);
|
|
102
|
-
this.
|
|
141
|
+
this.isBoxSelecting = false;
|
|
103
142
|
this.options = options;
|
|
104
143
|
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
105
144
|
this.addEventListener('mousemove', this.handleDrag, true);
|
|
106
|
-
this.addEventListener('mouseup', this.
|
|
107
|
-
this.addEventListener('mouseleave', this.
|
|
145
|
+
this.addEventListener('mouseup', this.endBoxSelect, true);
|
|
146
|
+
this.addEventListener('mouseleave', this.endBoxSelect, true);
|
|
108
147
|
}
|
|
109
148
|
/**
|
|
110
149
|
* Removes all related event listeners and the overlay renderer for the box.
|
|
@@ -112,22 +151,23 @@ export class MultiSelectInteraction extends BaseInteraction {
|
|
|
112
151
|
destroy() {
|
|
113
152
|
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
114
153
|
this.removeEventListener('mousemove', this.handleDrag, true);
|
|
115
|
-
this.removeEventListener('mouseup', this.
|
|
116
|
-
this.removeEventListener('mouseleave', this.
|
|
154
|
+
this.removeEventListener('mouseup', this.endBoxSelect, true);
|
|
155
|
+
this.removeEventListener('mouseleave', this.endBoxSelect, true);
|
|
117
156
|
this.overlayRenderer.destroy();
|
|
118
157
|
}
|
|
119
|
-
|
|
120
|
-
this.mousePosition =
|
|
121
|
-
this.startWorldPosition = this.nvlInstance.
|
|
158
|
+
turnOnBoxSelect(event) {
|
|
159
|
+
this.mousePosition = getCanvasPosition(this.containerInstance, event);
|
|
160
|
+
this.startWorldPosition = getWorldPosition(this.nvlInstance, this.mousePosition);
|
|
122
161
|
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
123
162
|
if (hits.nvlTargets.nodes.length > 0) {
|
|
124
|
-
this.
|
|
163
|
+
this.isBoxSelecting = false;
|
|
125
164
|
}
|
|
126
165
|
else {
|
|
127
|
-
this.
|
|
166
|
+
this.isBoxSelecting = true;
|
|
128
167
|
if (this.options.selectOnRelease) {
|
|
129
168
|
this.nvlInstance.deselectAll();
|
|
130
169
|
}
|
|
131
170
|
}
|
|
171
|
+
this.callCallbackIfRegistered('onBoxStarted', event);
|
|
132
172
|
}
|
|
133
173
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NODE_EDGE_WIDTH } from '../constants';
|
|
2
2
|
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
3
3
|
import { BaseInteraction } from './base';
|
|
4
|
-
import { generateUniqueId } from './utils';
|
|
4
|
+
import { generateUniqueId, getCanvasPosition, getWorldPosition } from './utils';
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
7
7
|
* @experimental
|
|
@@ -84,8 +84,8 @@ export class DrawInteraction extends BaseInteraction {
|
|
|
84
84
|
this.isMoved = true;
|
|
85
85
|
if (this.isDrawing) {
|
|
86
86
|
this.overlayRenderer.clear();
|
|
87
|
-
|
|
88
|
-
const pos = this.nvlInstance
|
|
87
|
+
const canvasPos = getCanvasPosition(this.containerInstance, event);
|
|
88
|
+
const pos = getWorldPosition(this.nvlInstance, canvasPos);
|
|
89
89
|
const hits = this.nvlInstance.getHits(event, ['node']);
|
|
90
90
|
const hitNode = hits.nvlTargets.nodes.filter((n) => n.data.id !== this.newTempTargetNode.id)[0];
|
|
91
91
|
const targetNode = hitNode
|
|
@@ -193,7 +193,8 @@ export class DrawInteraction extends BaseInteraction {
|
|
|
193
193
|
this.isDrawing = true;
|
|
194
194
|
this.isDraggingNode = false;
|
|
195
195
|
this.mouseDownNode = hitNodeEdges[0];
|
|
196
|
-
const
|
|
196
|
+
const canvasPos = getCanvasPosition(this.containerInstance, event);
|
|
197
|
+
const pos = getWorldPosition(this.nvlInstance, canvasPos);
|
|
197
198
|
this.newTempTargetNode = {
|
|
198
199
|
id: generateUniqueId(13),
|
|
199
200
|
size: 25,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { NVL, Node, Relationship } from '@neo4j-nvl/core';
|
|
2
|
+
import { BaseInteraction } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the lasso interaction handler to customize its behavior.
|
|
5
|
+
*/
|
|
6
|
+
export type LassoInteractionOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* If true, the selected items will be automatically selected when the gesture is finished.
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
selectOnRelease?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type LassoInteractionCallbacks = {
|
|
14
|
+
/**
|
|
15
|
+
* Called once when the user presses the mouse on the canvas, starting the lasso.
|
|
16
|
+
* @param event - The original mouse event
|
|
17
|
+
*/
|
|
18
|
+
onLassoStarted?: (event: MouseEvent) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Called once when the user releases the mouse after lasso selecting.
|
|
21
|
+
* @param selectionObject - The selected node and relationship ids
|
|
22
|
+
* @param event - The original mouse event
|
|
23
|
+
*/
|
|
24
|
+
onLassoSelect?: (({ nodes, rels }: {
|
|
25
|
+
nodes: Node[];
|
|
26
|
+
rels: Relationship[];
|
|
27
|
+
}, event: MouseEvent) => void) | boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Interaction handler for selecting nodes and relationships by drawing a lasso around them.
|
|
31
|
+
* When dragging, a line is drawn in the visualisation, and when selecting all nodes inside the drawn
|
|
32
|
+
* area will be selected.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```js
|
|
36
|
+
* const nvl = new NVL(container, nodes, relationships)
|
|
37
|
+
* const lassoInteraction = new LassoInteraction(nvl)
|
|
38
|
+
*
|
|
39
|
+
* lassoInteraction.updateCallback('onLassoSelect', ({ nodes, rels }) => {
|
|
40
|
+
* console.log('Selected elements:', nodes, rels)
|
|
41
|
+
* })
|
|
42
|
+
*/
|
|
43
|
+
export declare class LassoInteraction extends BaseInteraction<LassoInteractionCallbacks> {
|
|
44
|
+
private active;
|
|
45
|
+
private points;
|
|
46
|
+
private overlayRenderer;
|
|
47
|
+
private readonly options;
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new instance of the lasso interaction handler.
|
|
50
|
+
* @param nvl - The NVL instance to attach the interaction handler to
|
|
51
|
+
* @param options - Options for the lasso interaction handler to customize its behavior
|
|
52
|
+
*/
|
|
53
|
+
constructor(nvl: NVL, options?: LassoInteractionOptions);
|
|
54
|
+
private startLasso;
|
|
55
|
+
private handleMouseDown;
|
|
56
|
+
private handleDrag;
|
|
57
|
+
private handleMouseUp;
|
|
58
|
+
private getLassoItems;
|
|
59
|
+
private endLasso;
|
|
60
|
+
/**
|
|
61
|
+
* Removes all related event listeners and the overlay renderer for the box.
|
|
62
|
+
*/
|
|
63
|
+
destroy(): void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import concaveman from 'concaveman';
|
|
2
|
+
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
3
|
+
import { BaseInteraction } from './base';
|
|
4
|
+
import { getCanvasPosition, getWorldPosition } from './utils';
|
|
5
|
+
const pointDist = 10;
|
|
6
|
+
const shapeShowTime = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Interaction handler for selecting nodes and relationships by drawing a lasso around them.
|
|
9
|
+
* When dragging, a line is drawn in the visualisation, and when selecting all nodes inside the drawn
|
|
10
|
+
* area will be selected.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```js
|
|
14
|
+
* const nvl = new NVL(container, nodes, relationships)
|
|
15
|
+
* const lassoInteraction = new LassoInteraction(nvl)
|
|
16
|
+
*
|
|
17
|
+
* lassoInteraction.updateCallback('onLassoSelect', ({ nodes, rels }) => {
|
|
18
|
+
* console.log('Selected elements:', nodes, rels)
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
export class LassoInteraction extends BaseInteraction {
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new instance of the lasso interaction handler.
|
|
24
|
+
* @param nvl - The NVL instance to attach the interaction handler to
|
|
25
|
+
* @param options - Options for the lasso interaction handler to customize its behavior
|
|
26
|
+
*/
|
|
27
|
+
constructor(nvl, options = { selectOnRelease: false }) {
|
|
28
|
+
super(nvl);
|
|
29
|
+
Object.defineProperty(this, "active", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: void 0
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "points", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: void 0
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(this, "overlayRenderer", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: void 0
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(this, "options", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
value: void 0
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(this, "startLasso", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: (event) => {
|
|
58
|
+
this.active = true;
|
|
59
|
+
this.points = [getCanvasPosition(this.containerInstance, event)];
|
|
60
|
+
this.callCallbackIfRegistered('onLassoStarted', event);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
Object.defineProperty(this, "handleMouseDown", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
configurable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
value: (event) => {
|
|
68
|
+
if (event.button === 0 && !this.active) {
|
|
69
|
+
this.startLasso(event);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(this, "handleDrag", {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
writable: true,
|
|
77
|
+
value: (event) => {
|
|
78
|
+
if (this.active) {
|
|
79
|
+
const lastPoint = this.points[this.points.length - 1];
|
|
80
|
+
const pos = getCanvasPosition(this.containerInstance, event);
|
|
81
|
+
const dx = Math.abs(lastPoint.x - pos.x);
|
|
82
|
+
const dy = Math.abs(lastPoint.y - pos.y);
|
|
83
|
+
if (dx > pointDist || dy > pointDist) {
|
|
84
|
+
this.points.push(pos);
|
|
85
|
+
this.overlayRenderer.drawLasso(this.points, true, false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(this, "handleMouseUp", {
|
|
91
|
+
enumerable: true,
|
|
92
|
+
configurable: true,
|
|
93
|
+
writable: true,
|
|
94
|
+
value: (event) => {
|
|
95
|
+
this.points.push(getCanvasPosition(this.containerInstance, event));
|
|
96
|
+
this.endLasso(event);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "getLassoItems", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: (points) => {
|
|
104
|
+
const inside = (x, y, vs) => {
|
|
105
|
+
// ray-casting algorithm based on
|
|
106
|
+
// https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
|
|
107
|
+
let isInside = false;
|
|
108
|
+
for (let i = 0, j = vs.length - 1; i < vs.length; j = i, i += 1) {
|
|
109
|
+
const { x: xi, y: yi } = vs[i];
|
|
110
|
+
const { x: xj, y: yj } = vs[j];
|
|
111
|
+
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
112
|
+
if (intersect) {
|
|
113
|
+
isInside = !isInside;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return isInside;
|
|
117
|
+
};
|
|
118
|
+
const worldPoints = points.map((p) => getWorldPosition(this.nvlInstance, p));
|
|
119
|
+
const nodePositions = this.nvlInstance.getNodePositions();
|
|
120
|
+
const hitNodes = new Set();
|
|
121
|
+
for (const pos of nodePositions) {
|
|
122
|
+
if (pos.x === undefined || pos.y === undefined || pos.id === undefined) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (inside(pos.x, pos.y, worldPoints)) {
|
|
126
|
+
hitNodes.add(pos.id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const rels = this.nvlInstance.getRelationships();
|
|
130
|
+
const hitRels = [];
|
|
131
|
+
for (const rel of rels) {
|
|
132
|
+
if (hitNodes.has(rel.from) && hitNodes.has(rel.to)) {
|
|
133
|
+
hitRels.push(rel);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const hitNodeArray = Array.from(hitNodes).map((id) => this.nvlInstance.getNodeById(id));
|
|
137
|
+
return {
|
|
138
|
+
nodes: hitNodeArray,
|
|
139
|
+
rels: hitRels
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
Object.defineProperty(this, "endLasso", {
|
|
144
|
+
enumerable: true,
|
|
145
|
+
configurable: true,
|
|
146
|
+
writable: true,
|
|
147
|
+
value: (event) => {
|
|
148
|
+
if (!this.active) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.active = false;
|
|
152
|
+
const pointArrays = this.points.map((p) => [p.x, p.y]);
|
|
153
|
+
const hull = concaveman(pointArrays, 3).map((p) => ({ x: p[0], y: p[1] }));
|
|
154
|
+
this.overlayRenderer.drawLasso(hull, false, true);
|
|
155
|
+
setTimeout(() => this.overlayRenderer.clear(), shapeShowTime);
|
|
156
|
+
const hitItems = this.getLassoItems(hull);
|
|
157
|
+
if (this.options.selectOnRelease) {
|
|
158
|
+
this.nvlInstance.updateElementsInGraph(hitItems.nodes.map((node) => ({ id: node.id, selected: true })), hitItems.rels.map((rel) => ({ id: rel.id, selected: true })));
|
|
159
|
+
}
|
|
160
|
+
this.callCallbackIfRegistered('onLassoSelect', hitItems, event);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
this.overlayRenderer = new OverlayRenderer(this.containerInstance);
|
|
164
|
+
this.active = false;
|
|
165
|
+
this.options = options;
|
|
166
|
+
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
167
|
+
this.addEventListener('mousemove', this.handleDrag, true);
|
|
168
|
+
this.addEventListener('mouseup', this.handleMouseUp, true);
|
|
169
|
+
this.addEventListener('mouseleave', this.endLasso, true);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Removes all related event listeners and the overlay renderer for the box.
|
|
173
|
+
*/
|
|
174
|
+
destroy() {
|
|
175
|
+
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
176
|
+
this.removeEventListener('mousemove', this.handleDrag, true);
|
|
177
|
+
this.removeEventListener('mouseup', this.endLasso, true);
|
|
178
|
+
this.removeEventListener('mouseleave', this.endLasso, true);
|
|
179
|
+
this.overlayRenderer.destroy();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { NVL, Point } from '@neo4j-nvl/core';
|
|
1
2
|
export declare const generateUniqueId: (digit: number) => string;
|
|
2
|
-
export declare const isDraggingMovement: (event: MouseEvent, originalPosition:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export declare const isDraggingMovement: (event: MouseEvent, originalPosition: Point) => boolean;
|
|
4
|
+
export declare const getCanvasPosition: (canvas: HTMLElement, mouseEvent: MouseEvent) => Point;
|
|
5
|
+
export declare const getCanvasCenterOffset: (canvas: HTMLElement, mouseEvent: MouseEvent) => Point;
|
|
6
|
+
export declare const getWorldPosition: (nvl: NVL, pos: Point) => Point;
|
|
@@ -9,3 +9,32 @@ export const isDraggingMovement = (event, originalPosition) => {
|
|
|
9
9
|
const distanceSquared = Math.pow(diffX, 2) + Math.pow(diffY, 2);
|
|
10
10
|
return distanceSquared > DRAG_THRESHOLD;
|
|
11
11
|
};
|
|
12
|
+
export const getCanvasPosition = (canvas, mouseEvent) => {
|
|
13
|
+
const rect = canvas.getBoundingClientRect();
|
|
14
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
15
|
+
return {
|
|
16
|
+
x: (mouseEvent.clientX - rect.left) * devicePixelRatio,
|
|
17
|
+
y: (mouseEvent.clientY - rect.top) * devicePixelRatio
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export const getCanvasCenterOffset = (canvas, mouseEvent) => {
|
|
21
|
+
const rect = canvas.getBoundingClientRect();
|
|
22
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
23
|
+
return {
|
|
24
|
+
x: (mouseEvent.clientX - rect.left - rect.width * 0.5) * devicePixelRatio,
|
|
25
|
+
y: (mouseEvent.clientY - rect.top - rect.height * 0.5) * devicePixelRatio
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export const getWorldPosition = (nvl, pos) => {
|
|
29
|
+
const scale = nvl.getScale();
|
|
30
|
+
const pan = nvl.getPan();
|
|
31
|
+
const container = nvl.getContainer();
|
|
32
|
+
const { width, height } = container.getBoundingClientRect();
|
|
33
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
34
|
+
const x = pos.x - width * 0.5 * devicePixelRatio;
|
|
35
|
+
const y = pos.y - height * 0.5 * devicePixelRatio;
|
|
36
|
+
return {
|
|
37
|
+
x: pan.x + x / scale,
|
|
38
|
+
y: pan.y + y / scale
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { throttle } from 'lodash';
|
|
2
2
|
import { BaseInteraction } from './base';
|
|
3
|
+
import { getCanvasCenterOffset } from './utils';
|
|
3
4
|
/**
|
|
4
5
|
* Interaction handler for zooming the canvas.
|
|
5
6
|
* Zooming is achieved by scrolling the mouse wheel on the canvas.
|
|
@@ -41,9 +42,9 @@ export class ZoomInteraction extends BaseInteraction {
|
|
|
41
42
|
const { x, y } = this.nvlInstance.getPan();
|
|
42
43
|
const factor = event.deltaY / 500;
|
|
43
44
|
const newZoomTarget = zoom - factor * Math.min(1, zoom);
|
|
44
|
-
const
|
|
45
|
-
const newPanX = x + (
|
|
46
|
-
const newPanY = y + (
|
|
45
|
+
const offs = getCanvasCenterOffset(this.containerInstance, event);
|
|
46
|
+
const newPanX = x + (offs.x / zoom - offs.x / newZoomTarget);
|
|
47
|
+
const newPanY = y + (offs.y / zoom - offs.y / newZoomTarget);
|
|
47
48
|
this.nvlInstance.setZoomAndPan(newZoomTarget, newPanX, newPanY);
|
|
48
49
|
this.callCallbackIfRegistered('onZoom', newZoomTarget, event);
|
|
49
50
|
}, 25, { leading: true })
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
export declare class OverlayRenderer {
|
|
2
|
-
private
|
|
3
|
-
canvas
|
|
4
|
-
removeResizeListener
|
|
2
|
+
private ctx;
|
|
3
|
+
private canvas;
|
|
4
|
+
private removeResizeListener;
|
|
5
5
|
constructor(canvasParent: HTMLElement);
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
private fixCanvasSize;
|
|
7
|
+
private zoomAndPan;
|
|
8
8
|
drawBox(startX: number, startY: number, endX: number, endY: number): void;
|
|
9
|
+
drawLasso(points: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}[], stroke: boolean, fill: boolean): void;
|
|
9
13
|
drawNodeRing(x: number, y: number, radius: number, zoom: number, panX: number, panY: number): void;
|
|
10
14
|
clear(): void;
|
|
11
15
|
destroy(): void;
|
|
@@ -8,7 +8,7 @@ const marquee = {
|
|
|
8
8
|
};
|
|
9
9
|
export class OverlayRenderer {
|
|
10
10
|
constructor(canvasParent) {
|
|
11
|
-
Object.defineProperty(this, "
|
|
11
|
+
Object.defineProperty(this, "ctx", {
|
|
12
12
|
enumerable: true,
|
|
13
13
|
configurable: true,
|
|
14
14
|
writable: true,
|
|
@@ -39,14 +39,14 @@ export class OverlayRenderer {
|
|
|
39
39
|
canvas.style.msTouchAction = 'none';
|
|
40
40
|
canvasParent.appendChild(canvas);
|
|
41
41
|
const context = canvas.getContext('2d');
|
|
42
|
-
this.
|
|
42
|
+
this.ctx = context;
|
|
43
43
|
this.canvas = canvas;
|
|
44
44
|
this.removeResizeListener = onResize(canvasParent, () => {
|
|
45
|
-
this.
|
|
45
|
+
this.fixCanvasSize(canvas);
|
|
46
46
|
});
|
|
47
|
-
this.
|
|
47
|
+
this.fixCanvasSize(canvas);
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
fixCanvasSize(canvas) {
|
|
50
50
|
const parent = canvas.parentElement;
|
|
51
51
|
const rect = parent.getBoundingClientRect();
|
|
52
52
|
const { width } = rect;
|
|
@@ -58,56 +58,87 @@ export class OverlayRenderer {
|
|
|
58
58
|
canvas.style.height = `${height}px`;
|
|
59
59
|
}
|
|
60
60
|
// Can be extracted as a util
|
|
61
|
-
|
|
62
|
-
const {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
zoomAndPan(zoom, panX, panY) {
|
|
62
|
+
const { ctx, canvas } = this;
|
|
63
|
+
const { width, height } = canvas;
|
|
64
|
+
ctx.translate((-width / 2) * zoom, (-height / 2) * zoom);
|
|
65
|
+
ctx.translate(-panX * zoom, -panY * zoom);
|
|
66
|
+
ctx.scale(zoom, zoom);
|
|
67
|
+
ctx.translate(width / 2 / zoom, height / 2 / zoom);
|
|
68
|
+
ctx.translate(width / 2, height / 2);
|
|
68
69
|
}
|
|
69
70
|
drawBox(startX, startY, endX, endY) {
|
|
71
|
+
const { ctx } = this;
|
|
72
|
+
if (ctx === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
70
75
|
this.clear();
|
|
71
|
-
|
|
76
|
+
ctx.save();
|
|
77
|
+
ctx.beginPath();
|
|
78
|
+
ctx.rect(startX, startY, endX - startX, endY - startY);
|
|
79
|
+
ctx.closePath();
|
|
80
|
+
ctx.strokeStyle = marquee.frameColor;
|
|
72
81
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
ctx.lineWidth = marquee.frameWidth * devicePixelRatio;
|
|
83
|
+
ctx.fillStyle = marquee.color;
|
|
84
|
+
ctx.globalAlpha = marquee.opacity;
|
|
85
|
+
ctx.stroke();
|
|
86
|
+
ctx.fill();
|
|
87
|
+
}
|
|
88
|
+
drawLasso(points, stroke, fill) {
|
|
89
|
+
const { ctx } = this;
|
|
90
|
+
if (ctx === null) {
|
|
79
91
|
return;
|
|
80
92
|
}
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
ctx.save();
|
|
94
|
+
this.clear();
|
|
95
|
+
ctx.beginPath();
|
|
96
|
+
for (let i = 0; i < points.length; i++) {
|
|
97
|
+
const { x, y } = points[i];
|
|
98
|
+
if (i === 0) {
|
|
99
|
+
ctx.moveTo(x, y);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
ctx.lineTo(x, y);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
106
|
+
ctx.strokeStyle = marquee.frameColor;
|
|
107
|
+
ctx.setLineDash([10, 15]);
|
|
108
|
+
ctx.lineWidth = 3 * devicePixelRatio;
|
|
109
|
+
ctx.fillStyle = marquee.color;
|
|
110
|
+
ctx.globalAlpha = 0.5;
|
|
111
|
+
if (stroke) {
|
|
112
|
+
ctx.stroke();
|
|
113
|
+
}
|
|
114
|
+
if (fill) {
|
|
115
|
+
ctx.fill();
|
|
116
|
+
}
|
|
117
|
+
ctx.restore();
|
|
90
118
|
}
|
|
91
119
|
drawNodeRing(x, y, radius, zoom, panX, panY) {
|
|
120
|
+
const { ctx } = this;
|
|
92
121
|
this.clear();
|
|
93
|
-
|
|
122
|
+
ctx.save();
|
|
94
123
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
95
|
-
this.
|
|
124
|
+
this.zoomAndPan(zoom, panX, panY);
|
|
96
125
|
// TODO: make 0.2 configurable from state and same as colour
|
|
97
126
|
const width = (radius !== null && radius !== void 0 ? radius : 25) * devicePixelRatio * 0.2;
|
|
98
|
-
drawCircleBand(
|
|
99
|
-
|
|
127
|
+
drawCircleBand(ctx, x, y, (radius !== null && radius !== void 0 ? radius : 25) * devicePixelRatio, [{ width, color: 'blue' }]);
|
|
128
|
+
ctx.restore();
|
|
100
129
|
}
|
|
101
130
|
clear() {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
if (!this._ctx) {
|
|
131
|
+
const { ctx, canvas } = this;
|
|
132
|
+
if (ctx === null) {
|
|
105
133
|
return;
|
|
106
134
|
}
|
|
107
|
-
|
|
135
|
+
const rect = canvas.getBoundingClientRect();
|
|
136
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
137
|
+
ctx.clearRect(0, 0, rect.width * devicePixelRatio, rect.height * devicePixelRatio);
|
|
108
138
|
}
|
|
109
139
|
destroy() {
|
|
140
|
+
const { canvas } = this;
|
|
110
141
|
this.removeResizeListener();
|
|
111
|
-
|
|
142
|
+
canvas.remove();
|
|
112
143
|
}
|
|
113
144
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/interaction-handlers",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
4
4
|
"license": "SEE LICENSE IN 'Neo4j Early Access Agreement - Visualization Library.pdf'",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"yarn": "^1.10.1"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"concaveman": "^1.2.1",
|
|
28
29
|
"lodash": "^4.17.21",
|
|
29
30
|
"resizelistener": "^1.1.0"
|
|
30
31
|
},
|