@cosmos.gl/graph 2.0.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.
Files changed (32) hide show
  1. package/dist/config.d.ts +15 -1
  2. package/dist/index.d.ts +12 -0
  3. package/dist/index.js +2825 -2635
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +165 -54
  6. package/dist/index.min.js.map +1 -1
  7. package/dist/modules/Points/index.d.ts +6 -0
  8. package/dist/stories/clusters/lasso-selection/index.d.ts +6 -0
  9. package/dist/stories/clusters/lasso-selection/lasso.d.ts +20 -0
  10. package/dist/stories/clusters.stories.d.ts +1 -0
  11. package/dist/variables.d.ts +2 -0
  12. package/package.json +1 -1
  13. package/src/config.ts +15 -1
  14. package/src/index.ts +104 -5
  15. package/src/modules/Lines/draw-curve-line.frag +7 -8
  16. package/src/modules/Lines/draw-curve-line.vert +65 -15
  17. package/src/modules/Lines/index.ts +2 -3
  18. package/src/modules/Points/find-points-on-lasso-selection.frag +65 -0
  19. package/src/modules/Points/index.ts +69 -0
  20. package/src/stories/2. configuration.mdx +3 -1
  21. package/src/stories/3. api-reference.mdx +19 -0
  22. package/src/stories/beginners/basic-set-up/index.ts +2 -1
  23. package/src/stories/beginners/point-labels/index.ts +2 -1
  24. package/src/stories/beginners/quick-start.ts +1 -0
  25. package/src/stories/beginners/remove-points/config.ts +2 -1
  26. package/src/stories/clusters/lasso-selection/index.ts +53 -0
  27. package/src/stories/clusters/lasso-selection/lasso.ts +143 -0
  28. package/src/stories/clusters/lasso-selection/style.css +8 -0
  29. package/src/stories/clusters.stories.ts +16 -0
  30. package/src/stories/create-cosmos.ts +2 -1
  31. package/src/stories/generate-mesh-data.ts +1 -1
  32. package/src/variables.ts +3 -1
@@ -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,6 @@
1
+ import { Graph } from '../../..';
2
+ export declare const lassoSelection: () => {
3
+ div: HTMLDivElement;
4
+ graph: Graph;
5
+ destroy: () => void;
6
+ };
@@ -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
+ }
@@ -5,4 +5,5 @@ declare const meta: Meta<CosmosStoryProps>;
5
5
  export declare const Worm: Story;
6
6
  export declare const Radial: Story;
7
7
  export declare const WithLabels: Story;
8
+ export declare const LassoSelection: Story;
8
9
  export default meta;
@@ -43,6 +43,7 @@ export declare const defaultConfigValues: {
43
43
  showFPSMonitor: boolean;
44
44
  pixelRatio: number;
45
45
  scalePointsOnZoom: boolean;
46
+ scaleLinksOnZoom: boolean;
46
47
  enableZoom: boolean;
47
48
  enableSimulationDuringZoom: boolean;
48
49
  enableDrag: boolean;
@@ -53,6 +54,7 @@ export declare const defaultConfigValues: {
53
54
  pointSamplingDistance: number;
54
55
  attribution: string;
55
56
  rescalePositions: undefined;
57
+ enableRightClickRepulsion: boolean;
56
58
  };
57
59
  export declare const hoveredPointRingOpacity = 0.7;
58
60
  export declare const focusedPointRingOpacity = 0.95;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmos.gl/graph",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "GPU-based force graph layout and rendering",
5
5
  "jsdelivr": "dist/index.min.js",
6
6
  "main": "dist/index.js",
package/src/config.ts CHANGED
@@ -143,6 +143,11 @@ export interface GraphConfigInterface {
143
143
  * Default value: `1`
144
144
  */
145
145
  linkWidthScale?: number;
146
+ /**
147
+ * Increase or decrease the size of the links when zooming in or out.
148
+ * Default value: `false`
149
+ */
150
+ scaleLinksOnZoom?: boolean;
146
151
  /**
147
152
  * If set to true, links are rendered as curved lines.
148
153
  * Otherwise as straight lines.
@@ -252,6 +257,13 @@ export interface GraphConfigInterface {
252
257
  * Default value: `2`
253
258
  */
254
259
  simulationRepulsionFromMouse?: number;
260
+ /**
261
+ * Enable or disable the repulsion force from mouse when right-clicking.
262
+ * When set to `true`, holding the right mouse button will activate the mouse repulsion force.
263
+ * When set to `false`, right-clicking will not trigger any repulsion force.
264
+ * Default value: `false`
265
+ */
266
+ enableRightClickRepulsion?: boolean;
255
267
  /**
256
268
  * Friction coefficient.
257
269
  * Values range from 0 (high friction, stops quickly) to 1 (no friction, keeps moving).
@@ -405,7 +417,7 @@ export interface GraphConfigInterface {
405
417
  pixelRatio?: number;
406
418
  /**
407
419
  * Increase or decrease the size of the points when zooming in or out.
408
- * Default value: true
420
+ * Default value: `false`
409
421
  */
410
422
  scalePointsOnZoom?: boolean;
411
423
  /**
@@ -522,6 +534,7 @@ export class GraphConfig implements GraphConfigInterface {
522
534
  public curvedLinkControlPointDistance = defaultConfigValues.curvedLinkControlPointDistance
523
535
  public linkArrows = defaultConfigValues.arrowLinks
524
536
  public linkArrowsSizeScale = defaultConfigValues.arrowSizeScale
537
+ public scaleLinksOnZoom = defaultConfigValues.scaleLinksOnZoom
525
538
  public linkVisibilityDistanceRange = defaultConfigValues.linkVisibilityDistanceRange
526
539
  public linkVisibilityMinTransparency = defaultConfigValues.linkVisibilityMinTransparency
527
540
  public useClassicQuadtree = defaultConfigValues.useClassicQuadtree
@@ -536,6 +549,7 @@ export class GraphConfig implements GraphConfigInterface {
536
549
  public simulationLinkDistance = defaultConfigValues.simulation.linkDistance
537
550
  public simulationLinkDistRandomVariationRange = defaultConfigValues.simulation.linkDistRandomVariationRange
538
551
  public simulationRepulsionFromMouse = defaultConfigValues.simulation.repulsionFromMouse
552
+ public enableRightClickRepulsion = defaultConfigValues.enableRightClickRepulsion
539
553
  public simulationFriction = defaultConfigValues.simulation.friction
540
554
  public simulationCluster = defaultConfigValues.simulation.cluster
541
555
 
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.
@@ -886,6 +941,39 @@ export class Graph {
886
941
  if (this._isDestroyed || !this.reglInstance) return
887
942
  window.clearTimeout(this._fitViewOnInitTimeoutID)
888
943
  this.stopFrames()
944
+
945
+ // Remove all event listeners
946
+ if (this.canvasD3Selection) {
947
+ this.canvasD3Selection
948
+ .on('mouseenter.cosmos', null)
949
+ .on('mousemove.cosmos', null)
950
+ .on('mouseleave.cosmos', null)
951
+ .on('click', null)
952
+ .on('mousemove', null)
953
+ .on('contextmenu', null)
954
+ .on('.drag', null)
955
+ .on('.zoom', null)
956
+ }
957
+
958
+ select(document)
959
+ .on('keydown.cosmos', null)
960
+ .on('keyup.cosmos', null)
961
+
962
+ if (this.zoomInstance?.behavior) {
963
+ this.zoomInstance.behavior
964
+ .on('start.detect', null)
965
+ .on('zoom.detect', null)
966
+ .on('end.detect', null)
967
+ }
968
+
969
+ if (this.dragInstance?.behavior) {
970
+ this.dragInstance.behavior
971
+ .on('start.detect', null)
972
+ .on('drag.detect', null)
973
+ .on('end.detect', null)
974
+ }
975
+
976
+ this.fpsMonitor?.destroy()
889
977
  this.reglInstance.destroy()
890
978
  // Clears the canvas after particle system is destroyed
891
979
  this.reglInstance.clear({
@@ -893,9 +981,21 @@ export class Graph {
893
981
  depth: 1,
894
982
  stencil: 0,
895
983
  })
896
- select(this.canvas).style('cursor', null)
897
- this.fpsMonitor?.destroy()
984
+
985
+ if (this.canvas && this.canvas.parentNode) {
986
+ this.canvas.parentNode.removeChild(this.canvas)
987
+ }
988
+
989
+ if (this.attributionDivElement && this.attributionDivElement.parentNode) {
990
+ this.attributionDivElement.parentNode.removeChild(this.attributionDivElement)
991
+ }
992
+
898
993
  document.getElementById('gl-bench-style')?.remove()
994
+
995
+ this.canvasD3Selection = undefined
996
+ this.reglInstance = undefined
997
+ this.attributionDivElement = undefined
998
+
899
999
  this._isDestroyed = true
900
1000
  }
901
1001
 
@@ -991,8 +1091,7 @@ export class Graph {
991
1091
  if (!this.dragInstance.isActive) this.findHoveredPoint()
992
1092
 
993
1093
  if (enableSimulation) {
994
- if (this.isRightClickMouse) {
995
- if (!isSimulationRunning) this.start(0.1)
1094
+ if (this.isRightClickMouse && this.config.enableRightClickRepulsion) {
996
1095
  this.forceMouse?.run()
997
1096
  this.points?.updatePosition()
998
1097
  }
@@ -1023,7 +1122,7 @@ export class Graph {
1023
1122
  }
1024
1123
 
1025
1124
  this.store.alpha += this.store.addAlpha(this.config.simulationDecay ?? defaultConfigValues.simulation.decay)
1026
- if (this.isRightClickMouse) this.store.alpha = Math.max(this.store.alpha, 0.1)
1125
+ if (this.isRightClickMouse && this.config.enableRightClickRepulsion) this.store.alpha = Math.max(this.store.alpha, 0.1)
1027
1126
  this.store.simulationProgress = Math.sqrt(Math.min(1, ALPHA_MIN / this.store.alpha))
1028
1127
  this.config.onSimulationTick?.(
1029
1128
  this.store.alpha,
@@ -3,9 +3,8 @@ precision highp float;
3
3
  varying vec4 rgbaColor;
4
4
  varying vec2 pos;
5
5
  varying float arrowLength;
6
- varying float linkWidthArrowWidthRatio;
7
- varying float smoothWidthRatio;
8
6
  varying float useArrow;
7
+ varying float smoothing;
9
8
 
10
9
  float map(float value, float min1, float max1, float min2, float max2) {
11
10
  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
@@ -14,22 +13,22 @@ float map(float value, float min1, float max1, float min2, float max2) {
14
13
  void main() {
15
14
  float opacity = 1.0;
16
15
  vec3 color = rgbaColor.rgb;
17
- float smoothDelta = smoothWidthRatio / 2.0;
16
+
18
17
  if (useArrow > 0.5) {
19
18
  float end_arrow = 0.5 + arrowLength / 2.0;
20
19
  float start_arrow = end_arrow - arrowLength;
21
- float arrowWidthDelta = linkWidthArrowWidthRatio / 2.0;
22
- float linkOpacity = rgbaColor.a * smoothstep(0.5 - arrowWidthDelta, 0.5 - arrowWidthDelta - smoothDelta, abs(pos.y));
20
+ float arrowWidthDelta = 0.25;
21
+ float linkOpacity = rgbaColor.a * smoothstep(0.5 - arrowWidthDelta, 0.5 - arrowWidthDelta - smoothing / 2.0, abs(pos.y));
23
22
  float arrowOpacity = 1.0;
24
23
  if (pos.x > start_arrow && pos.x < start_arrow + arrowLength) {
25
24
  float xmapped = map(pos.x, start_arrow, end_arrow, 0.0, 1.0);
26
- arrowOpacity = rgbaColor.a * smoothstep(xmapped - smoothDelta, xmapped, map(abs(pos.y), 0.5, 0.0, 0.0, 1.0));
25
+ arrowOpacity = rgbaColor.a * smoothstep(xmapped - smoothing, xmapped, map(abs(pos.y), 0.5, 0.0, 0.0, 1.0));
27
26
  if (linkOpacity != arrowOpacity) {
28
- linkOpacity += arrowOpacity;
27
+ linkOpacity = max(linkOpacity, arrowOpacity);
29
28
  }
30
29
  }
31
30
  opacity = linkOpacity;
32
- } else opacity = rgbaColor.a * smoothstep(0.5, 0.5 - smoothDelta, abs(pos.y));
31
+ } else opacity = rgbaColor.a * smoothstep(0.5, 0.5 - smoothing, abs(pos.y));
33
32
 
34
33
  gl_FragColor = vec4(color, opacity);
35
34
  }
@@ -1,8 +1,10 @@
1
1
  precision highp float;
2
+
2
3
  attribute vec2 position, pointA, pointB;
3
4
  attribute vec4 color;
4
5
  attribute float width;
5
6
  attribute float arrow;
7
+
6
8
  uniform sampler2D positionsTexture;
7
9
  uniform sampler2D pointGreyoutStatus;
8
10
  uniform mat3 transformationMatrix;
@@ -11,20 +13,20 @@ uniform float widthScale;
11
13
  uniform float arrowSizeScale;
12
14
  uniform float spaceSize;
13
15
  uniform vec2 screenSize;
14
- uniform float ratio;
15
16
  uniform vec2 linkVisibilityDistanceRange;
16
17
  uniform float linkVisibilityMinTransparency;
17
18
  uniform float greyoutOpacity;
18
19
  uniform float curvedWeight;
19
20
  uniform float curvedLinkControlPointDistance;
20
21
  uniform float curvedLinkSegments;
22
+ uniform bool scaleLinksOnZoom;
23
+ uniform float maxPointSize;
21
24
 
22
25
  varying vec4 rgbaColor;
23
26
  varying vec2 pos;
24
27
  varying float arrowLength;
25
- varying float linkWidthArrowWidthRatio;
26
- varying float smoothWidthRatio;
27
28
  varying float useArrow;
29
+ varying float smoothing;
28
30
 
29
31
  float map(float value, float min1, float max1, float min2, float max2) {
30
32
  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
@@ -36,19 +38,48 @@ vec2 conicParametricCurve(vec2 A, vec2 B, vec2 ControlPoint, float t, float w) {
36
38
  return divident / divisor;
37
39
  }
38
40
 
41
+ float calculateLinkWidth(float width) {
42
+ float linkWidth;
43
+ if (scaleLinksOnZoom) {
44
+ // Use original width if links should scale with zoom
45
+ linkWidth = width;
46
+ } else {
47
+ // Adjust width based on zoom level to maintain visual size
48
+ linkWidth = width / transformationMatrix[0][0];
49
+ // Apply a non-linear scaling to avoid extreme widths
50
+ linkWidth *= min(5.0, max(1.0, transformationMatrix[0][0] * 0.01));
51
+ }
52
+ // Limit link width based on whether it has an arrow
53
+ if (useArrow > 0.5) {
54
+ return min(linkWidth, (maxPointSize * 2.0) / transformationMatrix[0][0]);
55
+ } else {
56
+ return min(linkWidth, maxPointSize / transformationMatrix[0][0]);
57
+ }
58
+ }
59
+
60
+ float calculateArrowWidth(float arrowWidth) {
61
+ if (scaleLinksOnZoom) {
62
+ return arrowWidth;
63
+ } else {
64
+ return arrowWidth / transformationMatrix[0][0];
65
+ }
66
+ }
67
+
39
68
  void main() {
40
69
  pos = position;
41
70
 
42
71
  vec2 pointTexturePosA = (pointA + 0.5) / pointsTextureSize;
43
72
  vec2 pointTexturePosB = (pointB + 0.5) / pointsTextureSize;
44
- // Greyed out status of points
73
+
45
74
  vec4 greyoutStatusA = texture2D(pointGreyoutStatus, pointTexturePosA);
46
75
  vec4 greyoutStatusB = texture2D(pointGreyoutStatus, pointTexturePosB);
47
- // Position
76
+
48
77
  vec4 pointPositionA = texture2D(positionsTexture, pointTexturePosA);
49
78
  vec4 pointPositionB = texture2D(positionsTexture, pointTexturePosB);
50
79
  vec2 a = pointPositionA.xy;
51
80
  vec2 b = pointPositionB.xy;
81
+
82
+ // Calculate direction vector and its perpendicular
52
83
  vec2 xBasis = b - a;
53
84
  vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x));
54
85
 
@@ -57,50 +88,69 @@ void main() {
57
88
  float h = curvedLinkControlPointDistance;
58
89
  vec2 controlPoint = (a + b) / 2.0 + yBasis * linkDist * h;
59
90
 
91
+ // Convert link distance to screen pixels
60
92
  float linkDistPx = linkDist * transformationMatrix[0][0];
61
93
 
94
+ // Calculate line width using the width scale
62
95
  float linkWidth = width * widthScale;
63
96
  float k = 2.0;
97
+ // Arrow width is proportionally larger than the line width
64
98
  float arrowWidth = max(5.0, linkWidth * k);
65
99
  arrowWidth *= arrowSizeScale;
66
100
 
67
- float arrowWidthPx = arrowWidth / transformationMatrix[0][0];
101
+ // Calculate arrow width in pixels
102
+ float arrowWidthPx = calculateArrowWidth(arrowWidth);
103
+
104
+ // Calculate arrow length proportional to its width
105
+ // 0.866 is approximately sqrt(3)/2 - related to equilateral triangle geometry
106
+ // Cap the length to avoid overly long arrows on short links
68
107
  arrowLength = min(0.3, (0.866 * arrowWidthPx * 2.0) / linkDist);
69
108
 
70
- float smoothWidth = 2.0;
71
- float arrowExtraWidth = arrowWidth - linkWidth;
72
- linkWidth += smoothWidth / 2.0;
73
109
  useArrow = arrow;
74
110
  if (useArrow > 0.5) {
75
- linkWidth += arrowExtraWidth;
111
+ linkWidth *= 2.0;
76
112
  }
77
- smoothWidthRatio = smoothWidth / linkWidth;
78
- linkWidthArrowWidthRatio = arrowExtraWidth / linkWidth;
79
113
 
80
- float linkWidthPx = linkWidth / transformationMatrix[0][0];
114
+ // Calculate final link width in pixels with smoothing
115
+ float linkWidthPx = calculateLinkWidth(linkWidth);
116
+ float smoothingPx = 0.5 / transformationMatrix[0][0];
117
+ smoothing = smoothingPx / linkWidthPx;
118
+ linkWidthPx += smoothingPx;
81
119
 
82
- // Color
120
+ // Calculate final color with opacity based on link distance
83
121
  vec3 rgbColor = color.rgb;
122
+ // Adjust opacity based on link distance
84
123
  float opacity = color.a * max(linkVisibilityMinTransparency, map(linkDistPx, linkVisibilityDistanceRange.g, linkVisibilityDistanceRange.r, 0.0, 1.0));
85
124
 
125
+ // Apply greyed out opacity if either endpoint is greyed out
86
126
  if (greyoutStatusA.r > 0.0 || greyoutStatusB.r > 0.0) {
87
127
  opacity *= greyoutOpacity;
88
128
  }
89
129
 
130
+ // Pass final color to fragment shader
90
131
  rgbaColor = vec4(rgbColor, opacity);
91
132
 
133
+ // Calculate position on the curved path
92
134
  float t = position.x;
93
135
  float w = curvedWeight;
136
+
94
137
  float tPrev = t - 1.0 / curvedLinkSegments;
95
138
  float tNext = t + 1.0 / curvedLinkSegments;
139
+
96
140
  vec2 pointCurr = conicParametricCurve(a, b, controlPoint, t, w);
141
+
97
142
  vec2 pointPrev = conicParametricCurve(a, b, controlPoint, max(0.0, tPrev), w);
98
143
  vec2 pointNext = conicParametricCurve(a, b, controlPoint, min(tNext, 1.0), w);
144
+
99
145
  vec2 xBasisCurved = pointNext - pointPrev;
100
146
  vec2 yBasisCurved = normalize(vec2(-xBasisCurved.y, xBasisCurved.x));
147
+
101
148
  pointCurr += yBasisCurved * linkWidthPx * position.y;
149
+
150
+ // Transform to clip space coordinates
102
151
  vec2 p = 2.0 * pointCurr / spaceSize - 1.0;
103
152
  p *= spaceSize / screenSize;
104
- vec3 final = transformationMatrix * vec3(p, 1);
153
+ vec3 final = transformationMatrix * vec3(p, 1);
154
+
105
155
  gl_Position = vec4(final.rg, 0, 1);
106
156
  }
@@ -63,16 +63,15 @@ export class Lines extends CoreModule {
63
63
  pointGreyoutStatus: () => this.points?.greyoutStatusFbo,
64
64
  transformationMatrix: () => store.transform,
65
65
  pointsTextureSize: () => store.pointsTextureSize,
66
- pointSizeScale: () => config.pointSizeScale,
67
66
  widthScale: () => config.linkWidthScale,
68
67
  arrowSizeScale: () => config.linkArrowsSizeScale,
69
68
  spaceSize: () => store.adjustedSpaceSize,
70
69
  screenSize: () => store.screenSize,
71
- ratio: () => config.pixelRatio,
72
70
  linkVisibilityDistanceRange: () => config.linkVisibilityDistanceRange,
73
71
  linkVisibilityMinTransparency: () => config.linkVisibilityMinTransparency,
74
72
  greyoutOpacity: () => config.linkGreyoutOpacity,
75
- scalePointsOnZoom: () => config.scalePointsOnZoom,
73
+ scaleLinksOnZoom: () => config.scaleLinksOnZoom,
74
+ maxPointSize: () => store.maxPointSize,
76
75
  curvedWeight: () => config.curvedLinkWeight,
77
76
  curvedLinkControlPointDistance: () => config.curvedLinkControlPointDistance,
78
77
  curvedLinkSegments: () => config.curvedLinks ? config.curvedLinkSegments ?? defaultConfigValues.curvedLinkSegments : 1,
@@ -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?.()