@neo4j-nvl/interaction-handlers 0.2.45 → 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 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 type { MultiSelectInteractionCallbacks, MultiSelectInteractionOptions } from './interaction-handlers/multi-select-interaction';
10
- import { MultiSelectInteraction } from './interaction-handlers/multi-select-interaction';
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 { MultiSelectInteractionOptions, MultiSelectInteractionCallbacks, ClickInteractionOptions, HoverInteractionOptions, HoverInteractionCallbacks, DragNodeInteractionCallbacks, ClickInteractionCallbacks, PanInteractionCallbacks, DrawInteractionCallbacks, PanInteractionOptions, ZoomInteractionCallbacks };
16
- export { ZoomInteraction, PanInteraction, MultiSelectInteraction, ClickInteraction, HoverInteraction, DragNodeInteraction, DrawInteraction };
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 { MultiSelectInteraction } from './interaction-handlers/multi-select-interaction';
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, MultiSelectInteraction, ClickInteraction, HoverInteraction, DragNodeInteraction, DrawInteraction };
9
+ export { ZoomInteraction, PanInteraction, BoxSelectInteraction, ClickInteraction, HoverInteraction, DragNodeInteraction, DrawInteraction, LassoInteraction };
@@ -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 MultiSelectInteractionOptions = {
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 MultiSelectInteractionCallbacks = {
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
- onMultiSelect?: (({ nodes, rels }: {
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 multiSelectInteraction = new MultiSelectInteraction(nvl)
37
+ * const boxSelectInteraction = new BoxSelectInteraction(nvl)
33
38
  *
34
- * multiSelectInteraction.updateCallback('onMultiSelect', ({ nodes, rels }) => {
39
+ * boxSelectInteraction.updateCallback('onBoxSelect', ({ nodes, rels }) => {
35
40
  * console.log('Selected elements:', nodes, rels)
36
41
  * })
37
42
  */
38
- export declare class MultiSelectInteraction extends BaseInteraction<MultiSelectInteractionCallbacks> {
43
+ export declare class BoxSelectInteraction extends BaseInteraction<BoxSelectInteractionCallbacks> {
39
44
  private mousePosition;
40
45
  private startWorldPosition;
41
46
  private overlayRenderer;
42
- private isMultiSelecting;
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?: MultiSelectInteractionOptions);
54
+ constructor(nvl: NVL, options?: BoxSelectInteractionOptions);
50
55
  private handleMouseDown;
51
56
  private handleDrag;
52
- private endMultiSelect;
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 turnOnMultiSelect;
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 multiSelectInteraction = new MultiSelectInteraction(nvl)
13
+ * const boxSelectInteraction = new BoxSelectInteraction(nvl)
13
14
  *
14
- * multiSelectInteraction.updateCallback('onMultiSelect', ({ nodes, rels }) => {
15
+ * boxSelectInteraction.updateCallback('onBoxSelect', ({ nodes, rels }) => {
15
16
  * console.log('Selected elements:', nodes, rels)
16
17
  * })
17
18
  */
18
- export class MultiSelectInteraction extends BaseInteraction {
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, "isMultiSelecting", {
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.isMultiSelecting = false;
63
+ this.isBoxSelecting = false;
63
64
  return;
64
65
  }
65
- this.turnOnMultiSelect(event);
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.isMultiSelecting) {
74
- this.overlayRenderer.drawBox(this.mousePosition.x, this.mousePosition.y, event.clientX, event.clientY);
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.turnOnMultiSelect(event);
79
+ this.turnOnBoxSelect(event);
78
80
  }
79
81
  }
80
82
  });
81
- Object.defineProperty(this, "endMultiSelect", {
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.isMultiSelecting) {
124
+ if (!this.isBoxSelecting) {
87
125
  return;
88
126
  }
89
- this.isMultiSelecting = false;
127
+ this.isBoxSelecting = false;
90
128
  this.overlayRenderer.clear();
91
- const endWorldPosition = this.nvlInstance.getMousePosition(event);
92
- const { nodes, rels } = this.nvlInstance.getHitsInBox(this.startWorldPosition.x, this.startWorldPosition.y, endWorldPosition.x, endWorldPosition.y, ['node', 'relationship']);
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('onMultiSelect', { nodes, rels }, event);
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.isMultiSelecting = false;
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.endMultiSelect, true);
107
- this.addEventListener('mouseleave', this.endMultiSelect, true);
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.endMultiSelect, true);
116
- this.removeEventListener('mouseleave', this.endMultiSelect, true);
154
+ this.removeEventListener('mouseup', this.endBoxSelect, true);
155
+ this.removeEventListener('mouseleave', this.endBoxSelect, true);
117
156
  this.overlayRenderer.destroy();
118
157
  }
119
- turnOnMultiSelect(event) {
120
- this.mousePosition = { x: event.clientX, y: event.clientY };
121
- this.startWorldPosition = this.nvlInstance.getMousePosition(event);
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.isMultiSelecting = false;
163
+ this.isBoxSelecting = false;
125
164
  }
126
165
  else {
127
- this.isMultiSelecting = true;
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
- // Rename to getMousePosition in world coordinate
88
- const pos = this.nvlInstance.getMousePosition(event);
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 pos = this.nvlInstance.getMousePosition(event);
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
- x: number;
4
- y: number;
5
- }) => boolean;
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 { offsetX, offsetY } = this.nvlInstance.getMouseElementCoordinates(event);
45
- const newPanX = x + (offsetX / zoom - offsetX / newZoomTarget);
46
- const newPanY = y + (offsetY / zoom - offsetY / newZoomTarget);
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 _ctx;
3
- canvas: HTMLCanvasElement;
4
- removeResizeListener: any;
2
+ private ctx;
3
+ private canvas;
4
+ private removeResizeListener;
5
5
  constructor(canvasParent: HTMLElement);
6
- _fixCanvasSize(canvas: HTMLCanvasElement): void;
7
- _zoomAndPan(zoom: number, panX: number, panY: number): void;
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, "_ctx", {
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._ctx = context;
42
+ this.ctx = context;
43
43
  this.canvas = canvas;
44
44
  this.removeResizeListener = onResize(canvasParent, () => {
45
- this._fixCanvasSize(canvas);
45
+ this.fixCanvasSize(canvas);
46
46
  });
47
- this._fixCanvasSize(canvas);
47
+ this.fixCanvasSize(canvas);
48
48
  }
49
- _fixCanvasSize(canvas) {
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
- _zoomAndPan(zoom, panX, panY) {
62
- const { width, height } = this.canvas;
63
- this._ctx.translate((-width / 2) * zoom, (-height / 2) * zoom);
64
- this._ctx.translate(-panX * zoom, -panY * zoom);
65
- this._ctx.scale(zoom, zoom);
66
- this._ctx.translate(width / 2 / zoom, height / 2 / zoom);
67
- this._ctx.translate(width / 2, height / 2);
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
- this._ctx.save();
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
- const rect = this.canvas.getBoundingClientRect();
74
- const localStartX = (startX - rect.left) * devicePixelRatio;
75
- const localStartY = (startY - rect.top) * devicePixelRatio;
76
- const localEndX = (endX - rect.left) * devicePixelRatio;
77
- const localEndY = (endY - rect.top) * devicePixelRatio;
78
- if (!this._ctx) {
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
- this._ctx.beginPath();
82
- this._ctx.rect(localStartX, localStartY, localEndX - localStartX, localEndY - localStartY);
83
- this._ctx.closePath();
84
- this._ctx.strokeStyle = marquee.frameColor;
85
- this._ctx.lineWidth = marquee.frameWidth * devicePixelRatio;
86
- this._ctx.fillStyle = marquee.color;
87
- this._ctx.globalAlpha = marquee.opacity;
88
- this._ctx.stroke();
89
- this._ctx.fill();
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
- this._ctx.save();
122
+ ctx.save();
94
123
  const devicePixelRatio = window.devicePixelRatio || 1;
95
- this._zoomAndPan(zoom, panX, panY);
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(this._ctx, x, y, (radius !== null && radius !== void 0 ? radius : 25) * devicePixelRatio, [{ width, color: 'blue' }]);
99
- this._ctx.restore();
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 rect = this.canvas.getBoundingClientRect();
103
- const devicePixelRatio = window.devicePixelRatio || 1;
104
- if (!this._ctx) {
131
+ const { ctx, canvas } = this;
132
+ if (ctx === null) {
105
133
  return;
106
134
  }
107
- this._ctx.clearRect(0, 0, rect.width * devicePixelRatio, rect.height * devicePixelRatio);
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
- this.canvas.remove();
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.45",
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
  },