@cosmos.gl/graph 2.1.0 → 2.2.1
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 +28 -0
- package/dist/index.js +2813 -2653
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +102 -40
- package/dist/index.min.js.map +1 -1
- package/dist/modules/Points/index.d.ts +6 -0
- package/dist/stories/clusters/polygon-selection/index.d.ts +6 -0
- package/dist/stories/clusters/polygon-selection/polygon.d.ts +20 -0
- package/dist/stories/clusters.stories.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +79 -2
- package/src/modules/Points/find-points-on-polygon-selection.frag +65 -0
- package/src/modules/Points/index.ts +69 -0
- package/src/stories/3. api-reference.mdx +39 -2
- package/src/stories/beginners/basic-set-up/index.ts +1 -1
- package/src/stories/clusters/polygon-selection/index.ts +53 -0
- package/src/stories/clusters/polygon-selection/polygon.ts +143 -0
- package/src/stories/clusters/polygon-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 findPointsOnPolygonSelectionCommand;
|
|
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 polygonPathTexture;
|
|
36
|
+
private polygonPathFbo;
|
|
37
|
+
private polygonPathLength;
|
|
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
|
+
findPointsOnPolygonSelection(): void;
|
|
53
|
+
updatePolygonPath(polygonPath: [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 PolygonSelection {
|
|
2
|
+
private canvas;
|
|
3
|
+
private ctx;
|
|
4
|
+
private isDrawing;
|
|
5
|
+
private points;
|
|
6
|
+
private graphDiv;
|
|
7
|
+
private onPolygonComplete?;
|
|
8
|
+
private boundStartDrawing;
|
|
9
|
+
private boundDraw;
|
|
10
|
+
private boundStopDrawing;
|
|
11
|
+
private resizeObserver;
|
|
12
|
+
constructor(graphDiv: HTMLElement, onPolygonComplete?: (points: [number, number][]) => void);
|
|
13
|
+
enablePolygonMode(): void;
|
|
14
|
+
disablePolygonMode(): 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
|
@@ -648,7 +648,7 @@ export class Graph {
|
|
|
648
648
|
* The `top` and `bottom` coordinates should be from 0 to the height of the canvas.
|
|
649
649
|
* @returns A Float32Array containing the indices of points inside a rectangular area.
|
|
650
650
|
*/
|
|
651
|
-
public
|
|
651
|
+
public getPointsInRect (selection: [[number, number], [number, number]]): Float32Array {
|
|
652
652
|
if (this._isDestroyed || !this.reglInstance || !this.points) return new Float32Array()
|
|
653
653
|
const h = this.store.screenSize[1]
|
|
654
654
|
this.store.selectedArea = [[selection[0][0], (h - selection[1][1])], [selection[1][0], (h - selection[0][1])]]
|
|
@@ -663,11 +663,48 @@ export class Graph {
|
|
|
663
663
|
.filter(d => d !== -1)
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Get points indices inside a rectangular area.
|
|
668
|
+
* @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
|
|
669
|
+
* The `left` and `right` coordinates should be from 0 to the width of the canvas.
|
|
670
|
+
* The `top` and `bottom` coordinates should be from 0 to the height of the canvas.
|
|
671
|
+
* @returns A Float32Array containing the indices of points inside a rectangular area.
|
|
672
|
+
* @deprecated Use `getPointsInRect` instead. This method will be removed in a future version.
|
|
673
|
+
*/
|
|
674
|
+
public getPointsInRange (selection: [[number, number], [number, number]]): Float32Array {
|
|
675
|
+
return this.getPointsInRect(selection)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Get points indices inside a polygon area.
|
|
680
|
+
* @param polygonPath - Array of points `[[x1, y1], [x2, y2], ..., [xn, yn]]` that defines the polygon.
|
|
681
|
+
* The coordinates should be from 0 to the width/height of the canvas.
|
|
682
|
+
* @returns A Float32Array containing the indices of points inside the polygon area.
|
|
683
|
+
*/
|
|
684
|
+
public getPointsInPolygon (polygonPath: [number, number][]): Float32Array {
|
|
685
|
+
if (this._isDestroyed || !this.reglInstance || !this.points) return new Float32Array()
|
|
686
|
+
if (polygonPath.length < 3) return new Float32Array() // Need at least 3 points for a polygon
|
|
687
|
+
|
|
688
|
+
const h = this.store.screenSize[1]
|
|
689
|
+
// Convert coordinates to WebGL coordinate system (flip Y)
|
|
690
|
+
const convertedPath = polygonPath.map(([x, y]) => [x, h - y] as [number, number])
|
|
691
|
+
this.points.updatePolygonPath(convertedPath)
|
|
692
|
+
this.points.findPointsOnPolygonSelection()
|
|
693
|
+
const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
|
|
694
|
+
|
|
695
|
+
return pixels
|
|
696
|
+
.map((pixel, i) => {
|
|
697
|
+
if (i % 4 === 0 && pixel !== 0) return i / 4
|
|
698
|
+
else return -1
|
|
699
|
+
})
|
|
700
|
+
.filter(d => d !== -1)
|
|
701
|
+
}
|
|
702
|
+
|
|
666
703
|
/** Select points inside a rectangular area.
|
|
667
704
|
* @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
|
|
668
705
|
* The `left` and `right` coordinates should be from 0 to the width of the canvas.
|
|
669
706
|
* The `top` and `bottom` coordinates should be from 0 to the height of the canvas. */
|
|
670
|
-
public
|
|
707
|
+
public selectPointsInRect (selection: [[number, number], [number, number]] | null): void {
|
|
671
708
|
if (this._isDestroyed || !this.reglInstance || !this.points) return
|
|
672
709
|
if (selection) {
|
|
673
710
|
const h = this.store.screenSize[1]
|
|
@@ -686,6 +723,46 @@ export class Graph {
|
|
|
686
723
|
this.points.updateGreyoutStatus()
|
|
687
724
|
}
|
|
688
725
|
|
|
726
|
+
/** Select points inside a rectangular area.
|
|
727
|
+
* @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
|
|
728
|
+
* The `left` and `right` coordinates should be from 0 to the width of the canvas.
|
|
729
|
+
* The `top` and `bottom` coordinates should be from 0 to the height of the canvas.
|
|
730
|
+
* @deprecated Use `selectPointsInRect` instead. This method will be removed in a future version.
|
|
731
|
+
*/
|
|
732
|
+
public selectPointsInRange (selection: [[number, number], [number, number]] | null): void {
|
|
733
|
+
return this.selectPointsInRect(selection)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Select points inside a polygon area.
|
|
737
|
+
* @param polygonPath - Array of points `[[x1, y1], [x2, y2], ..., [xn, yn]]` that defines the polygon.
|
|
738
|
+
* The coordinates should be from 0 to the width/height of the canvas.
|
|
739
|
+
* Set to null to clear selection. */
|
|
740
|
+
public selectPointsInPolygon (polygonPath: [number, number][] | null): void {
|
|
741
|
+
if (this._isDestroyed || !this.reglInstance || !this.points) return
|
|
742
|
+
if (polygonPath) {
|
|
743
|
+
if (polygonPath.length < 3) {
|
|
744
|
+
console.warn('Polygon path requires at least 3 points to form a polygon.')
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const h = this.store.screenSize[1]
|
|
749
|
+
// Convert coordinates to WebGL coordinate system (flip Y)
|
|
750
|
+
const convertedPath = polygonPath.map(([x, y]) => [x, h - y] as [number, number])
|
|
751
|
+
this.points.updatePolygonPath(convertedPath)
|
|
752
|
+
this.points.findPointsOnPolygonSelection()
|
|
753
|
+
const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
|
|
754
|
+
this.store.selectedIndices = pixels
|
|
755
|
+
.map((pixel, i) => {
|
|
756
|
+
if (i % 4 === 0 && pixel !== 0) return i / 4
|
|
757
|
+
else return -1
|
|
758
|
+
})
|
|
759
|
+
.filter(d => d !== -1)
|
|
760
|
+
} else {
|
|
761
|
+
this.store.selectedIndices = null
|
|
762
|
+
}
|
|
763
|
+
this.points.updateGreyoutStatus()
|
|
764
|
+
}
|
|
765
|
+
|
|
689
766
|
/**
|
|
690
767
|
* Select a point by index. If you want the adjacent points to get selected too, provide `true` as the second argument.
|
|
691
768
|
* @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 polygonPathTexture; // Texture containing polygon path points
|
|
7
|
+
uniform int polygonPathLength;
|
|
8
|
+
uniform float spaceSize;
|
|
9
|
+
uniform vec2 screenSize;
|
|
10
|
+
uniform mat3 transformationMatrix;
|
|
11
|
+
|
|
12
|
+
varying vec2 textureCoords;
|
|
13
|
+
|
|
14
|
+
// Get a point from the polygon path texture at a specific index
|
|
15
|
+
vec2 getPolygonPoint(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 = getPolygonPoint(pathTexture, i, pathLength);
|
|
39
|
+
vec2 pj = getPolygonPoint(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 polygon
|
|
62
|
+
if (pointInPolygon(screenPos, polygonPathTexture, polygonPathLength)) {
|
|
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 findPointsOnPolygonSelectionFrag from '@/graph/modules/Points/find-points-on-polygon-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 findPointsOnPolygonSelectionCommand: 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 polygonPathTexture: regl.Texture2D | undefined
|
|
57
|
+
private polygonPathFbo: regl.Framebuffer2D | undefined
|
|
58
|
+
private polygonPathLength = 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.findPointsOnPolygonSelectionCommand) {
|
|
289
|
+
this.findPointsOnPolygonSelectionCommand = reglInstance({
|
|
290
|
+
frag: findPointsOnPolygonSelectionFrag,
|
|
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
|
+
polygonPathTexture: () => this.polygonPathTexture,
|
|
304
|
+
polygonPathLength: () => this.polygonPathLength,
|
|
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 findPointsOnPolygonSelection (): void {
|
|
577
|
+
this.findPointsOnPolygonSelectionCommand?.()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
public updatePolygonPath (polygonPath: [number, number][]): void {
|
|
581
|
+
const { reglInstance } = this
|
|
582
|
+
this.polygonPathLength = polygonPath.length
|
|
583
|
+
|
|
584
|
+
if (polygonPath.length === 0) {
|
|
585
|
+
this.polygonPathTexture = undefined
|
|
586
|
+
this.polygonPathFbo = undefined
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Calculate texture size (square texture)
|
|
591
|
+
const textureSize = Math.ceil(Math.sqrt(polygonPath.length))
|
|
592
|
+
const textureData = new Float32Array(textureSize * textureSize * 4)
|
|
593
|
+
|
|
594
|
+
// Fill texture with polygon path points
|
|
595
|
+
for (const [i, point] of polygonPath.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.polygonPathTexture) this.polygonPathTexture = reglInstance.texture()
|
|
604
|
+
this.polygonPathTexture({
|
|
605
|
+
data: textureData,
|
|
606
|
+
width: textureSize,
|
|
607
|
+
height: textureSize,
|
|
608
|
+
type: 'float',
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
if (!this.polygonPathFbo) this.polygonPathFbo = reglInstance.framebuffer()
|
|
612
|
+
this.polygonPathFbo({
|
|
613
|
+
color: this.polygonPathTexture,
|
|
614
|
+
depth: false,
|
|
615
|
+
stencil: false,
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
550
619
|
public findHoveredPoint (): void {
|
|
551
620
|
this.clearHoveredFboCommand?.()
|
|
552
621
|
this.findHoveredPointCommand?.()
|
|
@@ -237,18 +237,55 @@ The `fitViewByPointPositions` method centers and zooms the view to fit the point
|
|
|
237
237
|
* **`duration`** (Number, optional): The duration of the animation in milliseconds. Default is 250 ms.
|
|
238
238
|
* **`padding`** (Number, optional): The padding around the viewport in percentage. This value should be between 0 and 1. Default is 0.1 (10% padding).
|
|
239
239
|
|
|
240
|
-
### <a name="
|
|
240
|
+
### <a name="get_points_in_rect" href="#get_points_in_rect">#</a> graph.<b>getPointsInRect</b>(<i>selection</i>)
|
|
241
241
|
|
|
242
242
|
Get points as a Float32Array within a rectangular area defined by two corner points `[[left, top], [right, bottom]]`. The `left` and `right` values represent the horizontal position in pixels, relative to the left edge of the canvas, with `0` being the leftmost position and the width of the canvas being the rightmost position.
|
|
243
243
|
|
|
244
244
|
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.
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
* **`selection`** (Array): An array containing two coordinate arrays representing the corners of the selection rectangle in the format `[[left, top], [right, bottom]]`.
|
|
247
|
+
|
|
248
|
+
**Returns:** A Float32Array containing the indices of points inside the rectangular area.
|
|
249
|
+
|
|
250
|
+
### <a name="get_points_in_range" href="#get_points_in_range">#</a> graph.<b>getPointsInRange</b>(<i>selection</i>) <b style={{ color: 'orange' }}>[DEPRECATED]</b>
|
|
251
|
+
|
|
252
|
+
**⚠️ Deprecated:** Use `getPointsInRect` instead. This method will be removed in a future version.
|
|
253
|
+
|
|
254
|
+
Get points as a Float32Array within a rectangular area defined by two corner points `[[left, top], [right, bottom]]`. This method has the same functionality as `getPointsInRect`.
|
|
255
|
+
|
|
256
|
+
### <a name="select_points_in_rect" href="#select_points_in_rect">#</a> graph.<b>selectPointsInRect</b>(<i>selection</i>)
|
|
247
257
|
|
|
248
258
|
Select points within a rectangular area defined by two corner points `[[left, top], [right, bottom]]`. The `left` and `right` values represent the horizontal position in pixels, relative to the left edge of the canvas, with `0` being the leftmost position and the width of the canvas being the rightmost position.
|
|
249
259
|
|
|
250
260
|
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
261
|
|
|
262
|
+
* **`selection`** (Array | null): An array containing two coordinate arrays representing the corners of the selection rectangle in the format `[[left, top], [right, bottom]]`, or `null` to clear the current selection.
|
|
263
|
+
|
|
264
|
+
### <a name="select_points_in_range" href="#select_points_in_range">#</a> graph.<b>selectPointsInRange</b>(<i>selection</i>) <b style={{ color: 'orange' }}>[DEPRECATED]</b>
|
|
265
|
+
|
|
266
|
+
**⚠️ Deprecated:** Use `selectPointsInRect` instead. This method will be removed in a future version.
|
|
267
|
+
|
|
268
|
+
Select points within a rectangular area defined by two corner points `[[left, top], [right, bottom]]`. This method has the same functionality as `selectPointsInRect`.
|
|
269
|
+
|
|
270
|
+
### <a name="get_points_in_polygon" href="#get_points_in_polygon">#</a> graph.<b>getPointsInPolygon</b>(<i>polygonPath</i>)
|
|
271
|
+
|
|
272
|
+
Get points as a Float32Array within a polygon area defined by an array of coordinate points.
|
|
273
|
+
|
|
274
|
+
* **`polygonPath`** (Array): An array of coordinate points in the format `[[x1, y1], [x2, y2], ..., [xN, yN]]` that defines the polygon. The coordinates should be in pixels relative to the canvas, where:
|
|
275
|
+
- **`x`**: Horizontal position from 0 to the width of the canvas
|
|
276
|
+
- **`y`**: Vertical position from 0 to the height of the canvas
|
|
277
|
+
- The polygon requires at least 3 points to form a valid selection area
|
|
278
|
+
|
|
279
|
+
**Returns:** A Float32Array containing the indices of points inside the polygon area.
|
|
280
|
+
|
|
281
|
+
### <a name="select_points_in_polygon" href="#select_points_in_polygon">#</a> graph.<b>selectPointsInPolygon</b>(<i>polygonPath</i>)
|
|
282
|
+
|
|
283
|
+
Select points within a polygon area defined by an array of coordinate points. This method combines the functionality of `getPointsInPolygon` with point selection, making the identified points visually selected in the graph.
|
|
284
|
+
|
|
285
|
+
* **`polygonPath`** (Array | null): An array of coordinate points in the format `[[x1, y1], [x2, y2], ..., [xN, yN]]` that defines the polygon, or `null` to clear the current selection. The coordinates should be in pixels relative to the canvas, where:
|
|
286
|
+
- **`x`**: Horizontal position from 0 to the width of the canvas
|
|
287
|
+
- **`y`**: Vertical position from 0 to the height of the canvas
|
|
288
|
+
- The polygon requires at least 3 points to form a valid selection area
|
|
252
289
|
|
|
253
290
|
### <a name="select_point_by_index" href="#select_point_by_index">#</a> graph.<b>selectPointByIndex</b>(<i>index</i>, [<i>selectAdjacentPoints</i>])
|
|
254
291
|
|
|
@@ -119,7 +119,7 @@ export const basicSetUp = (): { graph: Graph; div: HTMLDivElement} => {
|
|
|
119
119
|
const top = getRandomInRange([h / 4, h / 2])
|
|
120
120
|
const bottom = getRandomInRange([top, (h * 3) / 4])
|
|
121
121
|
pause()
|
|
122
|
-
graph.
|
|
122
|
+
graph.selectPointsInRect([
|
|
123
123
|
[left, top],
|
|
124
124
|
[right, bottom],
|
|
125
125
|
])
|
|
@@ -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 { PolygonSelection } from './polygon'
|
|
5
|
+
|
|
6
|
+
export const polygonSelection = (): {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 polygonSelection = new PolygonSelection(div, (polygonPoints) => {
|
|
30
|
+
graph.selectPointsInPolygon(polygonPoints)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const actionsDiv = document.createElement('div')
|
|
34
|
+
actionsDiv.className = 'actions'
|
|
35
|
+
div.appendChild(actionsDiv)
|
|
36
|
+
|
|
37
|
+
const polygonButton = document.createElement('div')
|
|
38
|
+
polygonButton.className = 'action'
|
|
39
|
+
polygonButton.textContent = 'Enable Polygon Selection'
|
|
40
|
+
polygonButton.addEventListener('click', () => {
|
|
41
|
+
polygonSelection.enablePolygonMode()
|
|
42
|
+
})
|
|
43
|
+
actionsDiv.appendChild(polygonButton)
|
|
44
|
+
|
|
45
|
+
const destroy = (): void => {
|
|
46
|
+
polygonSelection.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 PolygonSelection {
|
|
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 onPolygonComplete?: (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, onPolygonComplete?: (points: [number, number][]) => void) {
|
|
16
|
+
this.graphDiv = graphDiv
|
|
17
|
+
this.onPolygonComplete = onPolygonComplete
|
|
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 = 'polygon-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 enablePolygonMode (): 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 disablePolygonMode (): 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.disablePolygonMode()
|
|
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 polygonPoints: [number, number][] = this.points.map(p => [p.x, p.y])
|
|
128
|
+
const firstPolygonPoint = polygonPoints[0]
|
|
129
|
+
const lastPolygonPoint = polygonPoints[polygonPoints.length - 1]
|
|
130
|
+
if (firstPolygonPoint && lastPolygonPoint && (firstPolygonPoint[0] !== lastPolygonPoint[0] || firstPolygonPoint[1] !== lastPolygonPoint[1])) {
|
|
131
|
+
polygonPoints.push(firstPolygonPoint)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.onPolygonComplete) {
|
|
135
|
+
this.onPolygonComplete(polygonPoints)
|
|
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.disablePolygonMode()
|
|
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 { polygonSelection } from './clusters/polygon-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 polygonSelectionStory from './clusters/polygon-selection?raw'
|
|
16
|
+
import polygonSelectionStyleRaw from './clusters/polygon-selection/style.css?raw'
|
|
17
|
+
import polygonSelectionPolygonRaw from './clusters/polygon-selection/polygon.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 PolygonSelection: Story = {
|
|
65
|
+
...createStory(polygonSelection),
|
|
66
|
+
parameters: {
|
|
67
|
+
sourceCode: [
|
|
68
|
+
{ name: 'Story', code: polygonSelectionStory },
|
|
69
|
+
{ name: 'polygon.ts', code: polygonSelectionPolygonRaw },
|
|
70
|
+
...sourceCodeAddonParams,
|
|
71
|
+
{ name: 'style.css', code: polygonSelectionStyleRaw },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
// eslint-disable-next-line import/no-default-export
|
|
61
77
|
export default meta
|