@cosmos.gl/graph 2.1.0 → 2.2.0
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/dist/index.d.ts +12 -0
- package/dist/index.js +2791 -2651
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +96 -34
- package/dist/index.min.js.map +1 -1
- package/dist/modules/Points/index.d.ts +6 -0
- package/dist/stories/clusters/lasso-selection/index.d.ts +6 -0
- package/dist/stories/clusters/lasso-selection/lasso.d.ts +20 -0
- package/dist/stories/clusters.stories.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +55 -0
- package/src/modules/Points/find-points-on-lasso-selection.frag +65 -0
- package/src/modules/Points/index.ts +69 -0
- package/src/stories/3. api-reference.mdx +19 -0
- package/src/stories/clusters/lasso-selection/index.ts +53 -0
- package/src/stories/clusters/lasso-selection/lasso.ts +143 -0
- package/src/stories/clusters/lasso-selection/style.css +8 -0
- package/src/stories/clusters.stories.ts +16 -0
|
@@ -21,6 +21,7 @@ export declare class Points extends CoreModule {
|
|
|
21
21
|
private updatePositionCommand;
|
|
22
22
|
private dragPointCommand;
|
|
23
23
|
private findPointsOnAreaSelectionCommand;
|
|
24
|
+
private findPointsOnLassoSelectionCommand;
|
|
24
25
|
private findHoveredPointCommand;
|
|
25
26
|
private clearHoveredFboCommand;
|
|
26
27
|
private clearSampledPointsFboCommand;
|
|
@@ -31,6 +32,9 @@ export declare class Points extends CoreModule {
|
|
|
31
32
|
private greyoutStatusTexture;
|
|
32
33
|
private sizeTexture;
|
|
33
34
|
private trackedIndicesTexture;
|
|
35
|
+
private lassoPathTexture;
|
|
36
|
+
private lassoPathFbo;
|
|
37
|
+
private lassoPathLength;
|
|
34
38
|
private drawPointIndices;
|
|
35
39
|
private hoveredPointIndices;
|
|
36
40
|
private sampledPointIndices;
|
|
@@ -45,6 +49,8 @@ export declare class Points extends CoreModule {
|
|
|
45
49
|
updatePosition(): void;
|
|
46
50
|
drag(): void;
|
|
47
51
|
findPointsOnAreaSelection(): void;
|
|
52
|
+
findPointsOnLassoSelection(): void;
|
|
53
|
+
updateLassoPath(lassoPath: [number, number][]): void;
|
|
48
54
|
findHoveredPoint(): void;
|
|
49
55
|
trackPointsByIndices(indices?: number[] | undefined): void;
|
|
50
56
|
getTrackedPositionsMap(): Map<number, [number, number]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class LassoSelection {
|
|
2
|
+
private canvas;
|
|
3
|
+
private ctx;
|
|
4
|
+
private isDrawing;
|
|
5
|
+
private points;
|
|
6
|
+
private graphDiv;
|
|
7
|
+
private onLassoComplete?;
|
|
8
|
+
private boundStartDrawing;
|
|
9
|
+
private boundDraw;
|
|
10
|
+
private boundStopDrawing;
|
|
11
|
+
private resizeObserver;
|
|
12
|
+
constructor(graphDiv: HTMLElement, onLassoComplete?: (points: [number, number][]) => void);
|
|
13
|
+
enableLassoMode(): void;
|
|
14
|
+
disableLassoMode(): void;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
private resizeCanvas;
|
|
17
|
+
private startDrawing;
|
|
18
|
+
private draw;
|
|
19
|
+
private stopDrawing;
|
|
20
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -663,6 +663,31 @@ export class Graph {
|
|
|
663
663
|
.filter(d => d !== -1)
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Get points indices inside a lasso (polygon) area.
|
|
668
|
+
* @param lassoPath - Array of points `[[x1, y1], [x2, y2], ..., [xn, yn]]` that defines the lasso polygon.
|
|
669
|
+
* The coordinates should be from 0 to the width/height of the canvas.
|
|
670
|
+
* @returns A Float32Array containing the indices of points inside the lasso area.
|
|
671
|
+
*/
|
|
672
|
+
public getPointsInLasso (lassoPath: [number, number][]): Float32Array {
|
|
673
|
+
if (this._isDestroyed || !this.reglInstance || !this.points) return new Float32Array()
|
|
674
|
+
if (lassoPath.length < 3) return new Float32Array() // Need at least 3 points for a polygon
|
|
675
|
+
|
|
676
|
+
const h = this.store.screenSize[1]
|
|
677
|
+
// Convert coordinates to WebGL coordinate system (flip Y)
|
|
678
|
+
const convertedPath = lassoPath.map(([x, y]) => [x, h - y] as [number, number])
|
|
679
|
+
this.points.updateLassoPath(convertedPath)
|
|
680
|
+
this.points.findPointsOnLassoSelection()
|
|
681
|
+
const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
|
|
682
|
+
|
|
683
|
+
return pixels
|
|
684
|
+
.map((pixel, i) => {
|
|
685
|
+
if (i % 4 === 0 && pixel !== 0) return i / 4
|
|
686
|
+
else return -1
|
|
687
|
+
})
|
|
688
|
+
.filter(d => d !== -1)
|
|
689
|
+
}
|
|
690
|
+
|
|
666
691
|
/** Select points inside a rectangular area.
|
|
667
692
|
* @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
|
|
668
693
|
* The `left` and `right` coordinates should be from 0 to the width of the canvas.
|
|
@@ -686,6 +711,36 @@ export class Graph {
|
|
|
686
711
|
this.points.updateGreyoutStatus()
|
|
687
712
|
}
|
|
688
713
|
|
|
714
|
+
/** Select points inside a lasso (polygon) area.
|
|
715
|
+
* @param lassoPath - Array of points `[[x1, y1], [x2, y2], ..., [xn, yn]]` that defines the lasso polygon.
|
|
716
|
+
* The coordinates should be from 0 to the width/height of the canvas.
|
|
717
|
+
* Set to null to clear selection. */
|
|
718
|
+
public selectPointsInLasso (lassoPath: [number, number][] | null): void {
|
|
719
|
+
if (this._isDestroyed || !this.reglInstance || !this.points) return
|
|
720
|
+
if (lassoPath) {
|
|
721
|
+
if (lassoPath.length < 3) {
|
|
722
|
+
console.warn('Lasso path requires at least 3 points to form a polygon.')
|
|
723
|
+
return
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const h = this.store.screenSize[1]
|
|
727
|
+
// Convert coordinates to WebGL coordinate system (flip Y)
|
|
728
|
+
const convertedPath = lassoPath.map(([x, y]) => [x, h - y] as [number, number])
|
|
729
|
+
this.points.updateLassoPath(convertedPath)
|
|
730
|
+
this.points.findPointsOnLassoSelection()
|
|
731
|
+
const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
|
|
732
|
+
this.store.selectedIndices = pixels
|
|
733
|
+
.map((pixel, i) => {
|
|
734
|
+
if (i % 4 === 0 && pixel !== 0) return i / 4
|
|
735
|
+
else return -1
|
|
736
|
+
})
|
|
737
|
+
.filter(d => d !== -1)
|
|
738
|
+
} else {
|
|
739
|
+
this.store.selectedIndices = null
|
|
740
|
+
}
|
|
741
|
+
this.points.updateGreyoutStatus()
|
|
742
|
+
}
|
|
743
|
+
|
|
689
744
|
/**
|
|
690
745
|
* Select a point by index. If you want the adjacent points to get selected too, provide `true` as the second argument.
|
|
691
746
|
* @param index The index of the point in the array of points.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#ifdef GL_ES
|
|
2
|
+
precision highp float;
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
uniform sampler2D positionsTexture;
|
|
6
|
+
uniform sampler2D lassoPathTexture; // Texture containing lasso path points
|
|
7
|
+
uniform int lassoPathLength;
|
|
8
|
+
uniform float spaceSize;
|
|
9
|
+
uniform vec2 screenSize;
|
|
10
|
+
uniform mat3 transformationMatrix;
|
|
11
|
+
|
|
12
|
+
varying vec2 textureCoords;
|
|
13
|
+
|
|
14
|
+
// Get a point from the lasso path texture at a specific index
|
|
15
|
+
vec2 getLassoPoint(sampler2D pathTexture, int index, int pathLength) {
|
|
16
|
+
if (index >= pathLength) return vec2(0.0);
|
|
17
|
+
|
|
18
|
+
// Calculate texture coordinates for the index
|
|
19
|
+
int textureSize = int(ceil(sqrt(float(pathLength))));
|
|
20
|
+
int x = index - (index / textureSize) * textureSize;
|
|
21
|
+
int y = index / textureSize;
|
|
22
|
+
|
|
23
|
+
vec2 texCoord = (vec2(float(x), float(y)) + 0.5) / float(textureSize);
|
|
24
|
+
vec4 pathData = texture2D(pathTexture, texCoord);
|
|
25
|
+
|
|
26
|
+
return pathData.xy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Point-in-polygon algorithm using ray casting
|
|
30
|
+
bool pointInPolygon(vec2 point, sampler2D pathTexture, int pathLength) {
|
|
31
|
+
bool inside = false;
|
|
32
|
+
|
|
33
|
+
for (int i = 0; i < 2048; i++) {
|
|
34
|
+
if (i >= pathLength) break;
|
|
35
|
+
|
|
36
|
+
int j = int(mod(float(i + 1), float(pathLength)));
|
|
37
|
+
|
|
38
|
+
vec2 pi = getLassoPoint(pathTexture, i, pathLength);
|
|
39
|
+
vec2 pj = getLassoPoint(pathTexture, j, pathLength);
|
|
40
|
+
|
|
41
|
+
if (((pi.y > point.y) != (pj.y > point.y)) &&
|
|
42
|
+
(point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)) {
|
|
43
|
+
inside = !inside;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return inside;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void main() {
|
|
51
|
+
vec4 pointPosition = texture2D(positionsTexture, textureCoords);
|
|
52
|
+
vec2 p = 2.0 * pointPosition.rg / spaceSize - 1.0;
|
|
53
|
+
p *= spaceSize / screenSize;
|
|
54
|
+
vec3 final = transformationMatrix * vec3(p, 1);
|
|
55
|
+
|
|
56
|
+
// Convert to screen coordinates for polygon check
|
|
57
|
+
vec2 screenPos = (final.xy + 1.0) * screenSize / 2.0;
|
|
58
|
+
|
|
59
|
+
gl_FragColor = vec4(0.0, 0.0, pointPosition.rg);
|
|
60
|
+
|
|
61
|
+
// Check if point center is inside the lasso polygon
|
|
62
|
+
if (pointInPolygon(screenPos, lassoPathTexture, lassoPathLength)) {
|
|
63
|
+
gl_FragColor.r = 1.0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -6,6 +6,7 @@ import { defaultConfigValues } from '@/graph/variables'
|
|
|
6
6
|
import drawPointsFrag from '@/graph/modules/Points/draw-points.frag'
|
|
7
7
|
import drawPointsVert from '@/graph/modules/Points/draw-points.vert'
|
|
8
8
|
import findPointsOnAreaSelectionFrag from '@/graph/modules/Points/find-points-on-area-selection.frag'
|
|
9
|
+
import findPointsOnLassoSelectionFrag from '@/graph/modules/Points/find-points-on-lasso-selection.frag'
|
|
9
10
|
import drawHighlightedFrag from '@/graph/modules/Points/draw-highlighted.frag'
|
|
10
11
|
import drawHighlightedVert from '@/graph/modules/Points/draw-highlighted.vert'
|
|
11
12
|
import findHoveredPointFrag from '@/graph/modules/Points/find-hovered-point.frag'
|
|
@@ -41,6 +42,7 @@ export class Points extends CoreModule {
|
|
|
41
42
|
private updatePositionCommand: regl.DrawCommand | undefined
|
|
42
43
|
private dragPointCommand: regl.DrawCommand | undefined
|
|
43
44
|
private findPointsOnAreaSelectionCommand: regl.DrawCommand | undefined
|
|
45
|
+
private findPointsOnLassoSelectionCommand: regl.DrawCommand | undefined
|
|
44
46
|
private findHoveredPointCommand: regl.DrawCommand | undefined
|
|
45
47
|
private clearHoveredFboCommand: regl.DrawCommand | undefined
|
|
46
48
|
private clearSampledPointsFboCommand: regl.DrawCommand | undefined
|
|
@@ -51,6 +53,9 @@ export class Points extends CoreModule {
|
|
|
51
53
|
private greyoutStatusTexture: regl.Texture2D | undefined
|
|
52
54
|
private sizeTexture: regl.Texture2D | undefined
|
|
53
55
|
private trackedIndicesTexture: regl.Texture2D | undefined
|
|
56
|
+
private lassoPathTexture: regl.Texture2D | undefined
|
|
57
|
+
private lassoPathFbo: regl.Framebuffer2D | undefined
|
|
58
|
+
private lassoPathLength = 0
|
|
54
59
|
private drawPointIndices: regl.Buffer | undefined
|
|
55
60
|
private hoveredPointIndices: regl.Buffer | undefined
|
|
56
61
|
private sampledPointIndices: regl.Buffer | undefined
|
|
@@ -280,6 +285,27 @@ export class Points extends CoreModule {
|
|
|
280
285
|
})
|
|
281
286
|
}
|
|
282
287
|
|
|
288
|
+
if (!this.findPointsOnLassoSelectionCommand) {
|
|
289
|
+
this.findPointsOnLassoSelectionCommand = reglInstance({
|
|
290
|
+
frag: findPointsOnLassoSelectionFrag,
|
|
291
|
+
vert: updateVert,
|
|
292
|
+
framebuffer: () => this.selectedFbo as regl.Framebuffer2D,
|
|
293
|
+
primitive: 'triangle strip',
|
|
294
|
+
count: 4,
|
|
295
|
+
attributes: {
|
|
296
|
+
vertexCoord: createQuadBuffer(reglInstance),
|
|
297
|
+
},
|
|
298
|
+
uniforms: {
|
|
299
|
+
positionsTexture: () => this.currentPositionFbo,
|
|
300
|
+
spaceSize: () => store.adjustedSpaceSize,
|
|
301
|
+
screenSize: () => store.screenSize,
|
|
302
|
+
transformationMatrix: () => store.transform,
|
|
303
|
+
lassoPathTexture: () => this.lassoPathTexture,
|
|
304
|
+
lassoPathLength: () => this.lassoPathLength,
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
283
309
|
if (!this.clearHoveredFboCommand) {
|
|
284
310
|
this.clearHoveredFboCommand = reglInstance({
|
|
285
311
|
frag: clearFrag,
|
|
@@ -547,6 +573,49 @@ export class Points extends CoreModule {
|
|
|
547
573
|
this.findPointsOnAreaSelectionCommand?.()
|
|
548
574
|
}
|
|
549
575
|
|
|
576
|
+
public findPointsOnLassoSelection (): void {
|
|
577
|
+
this.findPointsOnLassoSelectionCommand?.()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
public updateLassoPath (lassoPath: [number, number][]): void {
|
|
581
|
+
const { reglInstance } = this
|
|
582
|
+
this.lassoPathLength = lassoPath.length
|
|
583
|
+
|
|
584
|
+
if (lassoPath.length === 0) {
|
|
585
|
+
this.lassoPathTexture = undefined
|
|
586
|
+
this.lassoPathFbo = undefined
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Calculate texture size (square texture)
|
|
591
|
+
const textureSize = Math.ceil(Math.sqrt(lassoPath.length))
|
|
592
|
+
const textureData = new Float32Array(textureSize * textureSize * 4)
|
|
593
|
+
|
|
594
|
+
// Fill texture with lasso path points
|
|
595
|
+
for (const [i, point] of lassoPath.entries()) {
|
|
596
|
+
const [x, y] = point
|
|
597
|
+
textureData[i * 4] = x
|
|
598
|
+
textureData[i * 4 + 1] = y
|
|
599
|
+
textureData[i * 4 + 2] = 0 // unused
|
|
600
|
+
textureData[i * 4 + 3] = 0 // unused
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!this.lassoPathTexture) this.lassoPathTexture = reglInstance.texture()
|
|
604
|
+
this.lassoPathTexture({
|
|
605
|
+
data: textureData,
|
|
606
|
+
width: textureSize,
|
|
607
|
+
height: textureSize,
|
|
608
|
+
type: 'float',
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
if (!this.lassoPathFbo) this.lassoPathFbo = reglInstance.framebuffer()
|
|
612
|
+
this.lassoPathFbo({
|
|
613
|
+
color: this.lassoPathTexture,
|
|
614
|
+
depth: false,
|
|
615
|
+
stencil: false,
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
550
619
|
public findHoveredPoint (): void {
|
|
551
620
|
this.clearHoveredFboCommand?.()
|
|
552
621
|
this.findHoveredPointCommand?.()
|
|
@@ -249,6 +249,25 @@ Select points within a rectangular area defined by two corner points `[[left, to
|
|
|
249
249
|
|
|
250
250
|
The `top` and `bottom` values represent the vertical position in pixels, relative to the top edge of the canvas, with `0` being the topmost position and the height of the canvas being the bottommost position.
|
|
251
251
|
|
|
252
|
+
### <a name="get_points_in_lasso" href="#get_points_in_lasso">#</a> graph.<b>getPointsInLasso</b>(<i>lassoPath</i>)
|
|
253
|
+
|
|
254
|
+
Get points as a Float32Array within a lasso (polygon) area defined by an array of coordinate points.
|
|
255
|
+
|
|
256
|
+
* **`lassoPath`** (Array): An array of coordinate points in the format `[[x1, y1], [x2, y2], ..., [xN, yN]]` that defines the lasso polygon. The coordinates should be in pixels relative to the canvas, where:
|
|
257
|
+
- **`x`**: Horizontal position from 0 to the width of the canvas
|
|
258
|
+
- **`y`**: Vertical position from 0 to the height of the canvas
|
|
259
|
+
- The polygon requires at least 3 points to form a valid selection area
|
|
260
|
+
|
|
261
|
+
**Returns:** A Float32Array containing the indices of points inside the lasso area.
|
|
262
|
+
|
|
263
|
+
### <a name="select_points_in_lasso" href="#select_points_in_lasso">#</a> graph.<b>selectPointsInLasso</b>(<i>lassoPath</i>)
|
|
264
|
+
|
|
265
|
+
Select points within a lasso (polygon) area defined by an array of coordinate points. This method combines the functionality of `getPointsInLasso` with point selection, making the identified points visually selected in the graph.
|
|
266
|
+
|
|
267
|
+
* **`lassoPath`** (Array | null): An array of coordinate points in the format `[[x1, y1], [x2, y2], ..., [xN, yN]]` that defines the lasso polygon, or `null` to clear the current selection. The coordinates should be in pixels relative to the canvas, where:
|
|
268
|
+
- **`x`**: Horizontal position from 0 to the width of the canvas
|
|
269
|
+
- **`y`**: Vertical position from 0 to the height of the canvas
|
|
270
|
+
- The polygon requires at least 3 points to form a valid selection area
|
|
252
271
|
|
|
253
272
|
### <a name="select_point_by_index" href="#select_point_by_index">#</a> graph.<b>selectPointByIndex</b>(<i>index</i>, [<i>selectAdjacentPoints</i>])
|
|
254
273
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Graph } from '@cosmos.gl/graph'
|
|
2
|
+
import { createCosmos } from '../../create-cosmos'
|
|
3
|
+
import { generateMeshData } from '../../generate-mesh-data'
|
|
4
|
+
import { LassoSelection } from './lasso'
|
|
5
|
+
|
|
6
|
+
export const lassoSelection = (): {div: HTMLDivElement; graph: Graph; destroy: () => void } => {
|
|
7
|
+
const nClusters = 25
|
|
8
|
+
const { pointPositions, pointColors, pointClusters } = generateMeshData(150, 150, nClusters, 1.0)
|
|
9
|
+
|
|
10
|
+
const { div, graph } = createCosmos({
|
|
11
|
+
pointPositions,
|
|
12
|
+
pointColors,
|
|
13
|
+
pointClusters,
|
|
14
|
+
simulationGravity: 1.5,
|
|
15
|
+
simulationCluster: 0.3,
|
|
16
|
+
simulationRepulsion: 8,
|
|
17
|
+
pointSize: 8,
|
|
18
|
+
backgroundColor: '#1a1a2e',
|
|
19
|
+
pointGreyoutOpacity: 0.2,
|
|
20
|
+
onClick: (index: number | undefined): void => {
|
|
21
|
+
if (index === undefined) {
|
|
22
|
+
graph.unselectPoints()
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
graph.setZoomLevel(0.4)
|
|
28
|
+
|
|
29
|
+
const lassoSelection = new LassoSelection(div, (lassoPoints) => {
|
|
30
|
+
graph.selectPointsInLasso(lassoPoints)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const actionsDiv = document.createElement('div')
|
|
34
|
+
actionsDiv.className = 'actions'
|
|
35
|
+
div.appendChild(actionsDiv)
|
|
36
|
+
|
|
37
|
+
const lassoButton = document.createElement('div')
|
|
38
|
+
lassoButton.className = 'action'
|
|
39
|
+
lassoButton.textContent = 'Enable Lasso Selection'
|
|
40
|
+
lassoButton.addEventListener('click', () => {
|
|
41
|
+
lassoSelection.enableLassoMode()
|
|
42
|
+
})
|
|
43
|
+
actionsDiv.appendChild(lassoButton)
|
|
44
|
+
|
|
45
|
+
const destroy = (): void => {
|
|
46
|
+
lassoSelection.destroy()
|
|
47
|
+
if (actionsDiv.parentNode) {
|
|
48
|
+
actionsDiv.parentNode.removeChild(actionsDiv)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { div, graph, destroy }
|
|
53
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import './style.css'
|
|
2
|
+
|
|
3
|
+
export class LassoSelection {
|
|
4
|
+
private canvas: HTMLCanvasElement
|
|
5
|
+
private ctx: CanvasRenderingContext2D
|
|
6
|
+
private isDrawing = false
|
|
7
|
+
private points: Array<{ x: number; y: number }> = []
|
|
8
|
+
private graphDiv: HTMLElement
|
|
9
|
+
private onLassoComplete?: (points: [number, number][]) => void
|
|
10
|
+
private boundStartDrawing: (e: MouseEvent) => void
|
|
11
|
+
private boundDraw: (e: MouseEvent) => void
|
|
12
|
+
private boundStopDrawing: () => void
|
|
13
|
+
private resizeObserver: ResizeObserver
|
|
14
|
+
|
|
15
|
+
public constructor (graphDiv: HTMLElement, onLassoComplete?: (points: [number, number][]) => void) {
|
|
16
|
+
this.graphDiv = graphDiv
|
|
17
|
+
this.onLassoComplete = onLassoComplete
|
|
18
|
+
|
|
19
|
+
// Bind event handlers
|
|
20
|
+
this.boundStartDrawing = this.startDrawing.bind(this)
|
|
21
|
+
this.boundDraw = this.draw.bind(this)
|
|
22
|
+
this.boundStopDrawing = this.stopDrawing.bind(this)
|
|
23
|
+
|
|
24
|
+
// Create canvas
|
|
25
|
+
this.canvas = document.createElement('canvas')
|
|
26
|
+
this.canvas.className = 'lasso-canvas'
|
|
27
|
+
|
|
28
|
+
const ctx = this.canvas.getContext('2d')
|
|
29
|
+
if (!ctx) throw new Error('Could not get canvas context')
|
|
30
|
+
this.ctx = ctx
|
|
31
|
+
|
|
32
|
+
this.graphDiv.appendChild(this.canvas)
|
|
33
|
+
|
|
34
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
35
|
+
this.resizeCanvas()
|
|
36
|
+
})
|
|
37
|
+
this.resizeObserver.observe(this.graphDiv)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public enableLassoMode (): void {
|
|
41
|
+
this.canvas.style.pointerEvents = 'auto'
|
|
42
|
+
this.canvas.style.cursor = 'crosshair'
|
|
43
|
+
|
|
44
|
+
// Add event listeners
|
|
45
|
+
this.canvas.addEventListener('mousedown', this.boundStartDrawing)
|
|
46
|
+
this.canvas.addEventListener('mousemove', this.boundDraw)
|
|
47
|
+
this.canvas.addEventListener('mouseup', this.boundStopDrawing)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public disableLassoMode (): void {
|
|
51
|
+
this.canvas.style.pointerEvents = 'none'
|
|
52
|
+
this.canvas.style.cursor = 'default'
|
|
53
|
+
|
|
54
|
+
// Remove event listeners
|
|
55
|
+
this.canvas.removeEventListener('mousedown', this.boundStartDrawing)
|
|
56
|
+
this.canvas.removeEventListener('mousemove', this.boundDraw)
|
|
57
|
+
this.canvas.removeEventListener('mouseup', this.boundStopDrawing)
|
|
58
|
+
|
|
59
|
+
// Clear canvas
|
|
60
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public destroy (): void {
|
|
64
|
+
this.disableLassoMode()
|
|
65
|
+
this.resizeObserver.disconnect()
|
|
66
|
+
if (this.canvas.parentNode) {
|
|
67
|
+
this.canvas.parentNode.removeChild(this.canvas)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private resizeCanvas (): void {
|
|
72
|
+
const rect = this.graphDiv.getBoundingClientRect()
|
|
73
|
+
|
|
74
|
+
// Apply pixel ratio for crisp rendering
|
|
75
|
+
const pixelRatio = window.devicePixelRatio || 1
|
|
76
|
+
this.canvas.width = rect.width * pixelRatio
|
|
77
|
+
this.canvas.height = rect.height * pixelRatio
|
|
78
|
+
|
|
79
|
+
// Reset transform and scale the context to match the pixel ratio
|
|
80
|
+
this.ctx.resetTransform()
|
|
81
|
+
this.ctx.scale(pixelRatio, pixelRatio)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private startDrawing (e: MouseEvent): void {
|
|
85
|
+
this.isDrawing = true
|
|
86
|
+
this.points = []
|
|
87
|
+
const rect = this.canvas.getBoundingClientRect()
|
|
88
|
+
this.points.push({ x: e.clientX - rect.left, y: e.clientY - rect.top })
|
|
89
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private draw (e: MouseEvent): void {
|
|
93
|
+
if (!this.isDrawing) return
|
|
94
|
+
|
|
95
|
+
const rect = this.canvas.getBoundingClientRect()
|
|
96
|
+
this.points.push({ x: e.clientX - rect.left, y: e.clientY - rect.top })
|
|
97
|
+
|
|
98
|
+
// Clear the entire canvas accounting for pixel ratio
|
|
99
|
+
const pixelRatio = window.devicePixelRatio || 1
|
|
100
|
+
this.ctx.clearRect(0, 0, this.canvas.width / pixelRatio, this.canvas.height / pixelRatio)
|
|
101
|
+
|
|
102
|
+
this.ctx.beginPath()
|
|
103
|
+
if (this.points.length > 0 && this.points[0]) {
|
|
104
|
+
this.ctx.moveTo(this.points[0].x, this.points[0].y)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
108
|
+
const point = this.points[i]
|
|
109
|
+
if (point) {
|
|
110
|
+
this.ctx.lineTo(point.x, point.y)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.ctx.strokeStyle = '#ffffff'
|
|
115
|
+
this.ctx.lineWidth = 2
|
|
116
|
+
this.ctx.stroke()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private stopDrawing (): void {
|
|
120
|
+
if (!this.isDrawing) return
|
|
121
|
+
this.isDrawing = false
|
|
122
|
+
|
|
123
|
+
if (this.points.length > 2) {
|
|
124
|
+
this.ctx.closePath()
|
|
125
|
+
this.ctx.stroke()
|
|
126
|
+
|
|
127
|
+
const lassoPoints: [number, number][] = this.points.map(p => [p.x, p.y])
|
|
128
|
+
const firstLassoPoint = lassoPoints[0]
|
|
129
|
+
const lastLassoPoint = lassoPoints[lassoPoints.length - 1]
|
|
130
|
+
if (firstLassoPoint && lastLassoPoint && (firstLassoPoint[0] !== lastLassoPoint[0] || firstLassoPoint[1] !== lastLassoPoint[1])) {
|
|
131
|
+
lassoPoints.push(firstLassoPoint)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.onLassoComplete) {
|
|
135
|
+
this.onLassoComplete(lassoPoints)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pixelRatio = window.devicePixelRatio || 1
|
|
140
|
+
this.ctx.clearRect(0, 0, this.canvas.width / pixelRatio, this.canvas.height / pixelRatio)
|
|
141
|
+
this.disableLassoMode()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -4,6 +4,7 @@ import { createStory, Story } from '@/graph/stories/create-story'
|
|
|
4
4
|
import { withLabels } from './clusters/with-labels'
|
|
5
5
|
import { worm } from './clusters/worm'
|
|
6
6
|
import { radial } from './clusters/radial'
|
|
7
|
+
import { lassoSelection } from './clusters/lasso-selection'
|
|
7
8
|
|
|
8
9
|
import createCosmosRaw from './create-cosmos?raw'
|
|
9
10
|
import generateMeshDataRaw from './generate-mesh-data?raw'
|
|
@@ -11,6 +12,9 @@ import withLabelsStoryRaw from './clusters/with-labels?raw'
|
|
|
11
12
|
import createClusterLabelsRaw from './create-cluster-labels?raw'
|
|
12
13
|
import wormStory from './clusters/worm?raw'
|
|
13
14
|
import radialStory from './clusters/radial?raw'
|
|
15
|
+
import lassoSelectionStory from './clusters/lasso-selection?raw'
|
|
16
|
+
import lassoSelectionStyleRaw from './clusters/lasso-selection/style.css?raw'
|
|
17
|
+
import lassoSelectionLassoRaw from './clusters/lasso-selection/lasso.ts?raw'
|
|
14
18
|
|
|
15
19
|
const meta: Meta<CosmosStoryProps> = {
|
|
16
20
|
title: 'Examples/Clusters',
|
|
@@ -57,5 +61,17 @@ export const WithLabels: Story = {
|
|
|
57
61
|
},
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
export const LassoSelection: Story = {
|
|
65
|
+
...createStory(lassoSelection),
|
|
66
|
+
parameters: {
|
|
67
|
+
sourceCode: [
|
|
68
|
+
{ name: 'Story', code: lassoSelectionStory },
|
|
69
|
+
{ name: 'lasso.ts', code: lassoSelectionLassoRaw },
|
|
70
|
+
...sourceCodeAddonParams,
|
|
71
|
+
{ name: 'style.css', code: lassoSelectionStyleRaw },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
// eslint-disable-next-line import/no-default-export
|
|
61
77
|
export default meta
|