@cosmos.gl/graph 2.6.2-rc.0 → 2.7.0-beta.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 (186) hide show
  1. package/.eslintrc +147 -0
  2. package/.github/SECURITY.md +13 -0
  3. package/.github/dco.yml +4 -0
  4. package/.github/workflows/github_pages.yml +54 -0
  5. package/.storybook/main.ts +26 -0
  6. package/.storybook/manager-head.html +1 -0
  7. package/.storybook/manager.ts +14 -0
  8. package/.storybook/preview.ts +29 -0
  9. package/.storybook/style.css +3 -0
  10. package/CHARTER.md +69 -0
  11. package/CODE_OF_CONDUCT.md +178 -0
  12. package/CONTRIBUTING.md +22 -0
  13. package/GOVERNANCE.md +21 -0
  14. package/cosmos-2-0-migration-notes.md +98 -0
  15. package/cosmos_awesome.md +96 -0
  16. package/dist/config.d.ts +5 -9
  17. package/dist/graph/utils/error-message.d.ts +1 -1
  18. package/dist/helper.d.ts +39 -2
  19. package/dist/index-FUIgayhu.js +19827 -0
  20. package/dist/index-FUIgayhu.js.map +1 -0
  21. package/dist/index.d.ts +17 -64
  22. package/dist/index.js +14 -14654
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.min.js +1062 -475
  25. package/dist/index.min.js.map +1 -1
  26. package/dist/modules/Clusters/index.d.ts +11 -3
  27. package/dist/modules/ForceCenter/index.d.ts +10 -3
  28. package/dist/modules/ForceGravity/index.d.ts +5 -1
  29. package/dist/modules/ForceLink/index.d.ts +8 -5
  30. package/dist/modules/ForceManyBody/index.d.ts +16 -7
  31. package/dist/modules/ForceMouse/index.d.ts +5 -1
  32. package/dist/modules/GraphData/index.d.ts +0 -1
  33. package/dist/modules/Lines/index.d.ts +11 -5
  34. package/dist/modules/Points/index.d.ts +31 -13
  35. package/dist/modules/Store/index.d.ts +93 -0
  36. package/dist/modules/core-module.d.ts +3 -3
  37. package/dist/stories/beginners/basic-set-up/data-gen.d.ts +4 -0
  38. package/dist/stories/beginners/basic-set-up/index.d.ts +6 -0
  39. package/dist/stories/beginners/link-hovering/data-generator.d.ts +19 -0
  40. package/dist/stories/beginners/link-hovering/index.d.ts +6 -0
  41. package/dist/stories/beginners/point-labels/data.d.ts +13 -0
  42. package/dist/stories/beginners/point-labels/index.d.ts +10 -0
  43. package/dist/stories/beginners/point-labels/labels.d.ts +8 -0
  44. package/dist/stories/beginners/quick-start.d.ts +6 -0
  45. package/dist/stories/beginners/remove-points/config.d.ts +2 -0
  46. package/dist/stories/beginners/remove-points/data-gen.d.ts +4 -0
  47. package/dist/stories/beginners/remove-points/index.d.ts +6 -0
  48. package/dist/stories/beginners.stories.d.ts +10 -0
  49. package/dist/stories/clusters/polygon-selection/index.d.ts +6 -0
  50. package/dist/stories/clusters/polygon-selection/polygon.d.ts +20 -0
  51. package/dist/stories/clusters/radial.d.ts +6 -0
  52. package/dist/stories/clusters/with-labels.d.ts +6 -0
  53. package/dist/stories/clusters/worm.d.ts +6 -0
  54. package/dist/stories/clusters.stories.d.ts +9 -0
  55. package/dist/stories/create-cluster-labels.d.ts +4 -0
  56. package/dist/stories/create-cosmos.d.ts +17 -0
  57. package/dist/stories/create-story.d.ts +16 -0
  58. package/dist/stories/experiments/full-mesh.d.ts +6 -0
  59. package/dist/stories/experiments/mesh-with-holes.d.ts +6 -0
  60. package/dist/stories/experiments.stories.d.ts +7 -0
  61. package/dist/stories/generate-mesh-data.d.ts +12 -0
  62. package/dist/stories/geospatial/moscow-metro-stations/index.d.ts +16 -0
  63. package/dist/stories/geospatial/moscow-metro-stations/moscow-metro-coords.d.ts +1 -0
  64. package/dist/stories/geospatial/moscow-metro-stations/point-colors.d.ts +1 -0
  65. package/dist/stories/geospatial.stories.d.ts +6 -0
  66. package/dist/stories/shapes/all-shapes/index.d.ts +6 -0
  67. package/dist/stories/shapes/image-example/index.d.ts +6 -0
  68. package/dist/stories/shapes.stories.d.ts +7 -0
  69. package/dist/stories/test-luma-migration.d.ts +6 -0
  70. package/dist/stories/test.stories.d.ts +6 -0
  71. package/dist/webgl-device-B9ewDj5L.js +3923 -0
  72. package/dist/webgl-device-B9ewDj5L.js.map +1 -0
  73. package/logo.svg +3 -0
  74. package/package.json +5 -7
  75. package/rollup.config.js +70 -0
  76. package/src/config.ts +728 -0
  77. package/src/declaration.d.ts +12 -0
  78. package/src/graph/utils/error-message.ts +23 -0
  79. package/src/helper.ts +113 -0
  80. package/src/index.ts +1769 -0
  81. package/src/modules/Clusters/calculate-centermass.frag +12 -0
  82. package/src/modules/Clusters/calculate-centermass.vert +38 -0
  83. package/src/modules/Clusters/force-cluster.frag +55 -0
  84. package/src/modules/Clusters/index.ts +578 -0
  85. package/src/modules/Drag/index.ts +33 -0
  86. package/src/modules/FPSMonitor/css.ts +53 -0
  87. package/src/modules/FPSMonitor/index.ts +28 -0
  88. package/src/modules/ForceCenter/calculate-centermass.frag +9 -0
  89. package/src/modules/ForceCenter/calculate-centermass.vert +26 -0
  90. package/src/modules/ForceCenter/force-center.frag +37 -0
  91. package/src/modules/ForceCenter/index.ts +284 -0
  92. package/src/modules/ForceGravity/force-gravity.frag +40 -0
  93. package/src/modules/ForceGravity/index.ts +107 -0
  94. package/src/modules/ForceLink/force-spring.ts +89 -0
  95. package/src/modules/ForceLink/index.ts +293 -0
  96. package/src/modules/ForceManyBody/calculate-level.frag +9 -0
  97. package/src/modules/ForceManyBody/calculate-level.vert +37 -0
  98. package/src/modules/ForceManyBody/force-centermass.frag +61 -0
  99. package/src/modules/ForceManyBody/force-level.frag +138 -0
  100. package/src/modules/ForceManyBody/index.ts +525 -0
  101. package/src/modules/ForceManyBody/quadtree-frag-shader.ts +89 -0
  102. package/src/modules/ForceManyBodyQuadtree/calculate-level.frag +9 -0
  103. package/src/modules/ForceManyBodyQuadtree/calculate-level.vert +25 -0
  104. package/src/modules/ForceManyBodyQuadtree/index.ts +157 -0
  105. package/src/modules/ForceManyBodyQuadtree/quadtree-frag-shader.ts +93 -0
  106. package/src/modules/ForceMouse/force-mouse.frag +35 -0
  107. package/src/modules/ForceMouse/index.ts +102 -0
  108. package/src/modules/GraphData/index.ts +383 -0
  109. package/src/modules/Lines/draw-curve-line.frag +59 -0
  110. package/src/modules/Lines/draw-curve-line.vert +248 -0
  111. package/src/modules/Lines/geometry.ts +18 -0
  112. package/src/modules/Lines/hovered-line-index.frag +43 -0
  113. package/src/modules/Lines/hovered-line-index.vert +13 -0
  114. package/src/modules/Lines/index.ts +661 -0
  115. package/src/modules/Points/atlas-utils.ts +137 -0
  116. package/src/modules/Points/drag-point.frag +34 -0
  117. package/src/modules/Points/draw-highlighted.frag +44 -0
  118. package/src/modules/Points/draw-highlighted.vert +145 -0
  119. package/src/modules/Points/draw-points.frag +259 -0
  120. package/src/modules/Points/draw-points.vert +203 -0
  121. package/src/modules/Points/fill-sampled-points.frag +12 -0
  122. package/src/modules/Points/fill-sampled-points.vert +51 -0
  123. package/src/modules/Points/find-hovered-point.frag +15 -0
  124. package/src/modules/Points/find-hovered-point.vert +90 -0
  125. package/src/modules/Points/find-points-on-area-selection.frag +88 -0
  126. package/src/modules/Points/find-points-on-polygon-selection.frag +89 -0
  127. package/src/modules/Points/index.ts +2292 -0
  128. package/src/modules/Points/track-positions.frag +30 -0
  129. package/src/modules/Points/update-position.frag +39 -0
  130. package/src/modules/Shared/buffer.ts +39 -0
  131. package/src/modules/Shared/clear.frag +10 -0
  132. package/src/modules/Shared/quad.vert +13 -0
  133. package/src/modules/Store/index.ts +283 -0
  134. package/src/modules/Zoom/index.ts +148 -0
  135. package/src/modules/core-module.ts +28 -0
  136. package/src/stories/1. welcome.mdx +75 -0
  137. package/src/stories/2. configuration.mdx +111 -0
  138. package/src/stories/3. api-reference.mdx +591 -0
  139. package/src/stories/beginners/basic-set-up/data-gen.ts +33 -0
  140. package/src/stories/beginners/basic-set-up/index.ts +167 -0
  141. package/src/stories/beginners/basic-set-up/style.css +35 -0
  142. package/src/stories/beginners/link-hovering/data-generator.ts +198 -0
  143. package/src/stories/beginners/link-hovering/index.ts +65 -0
  144. package/src/stories/beginners/link-hovering/style.css +73 -0
  145. package/src/stories/beginners/point-labels/data.ts +73 -0
  146. package/src/stories/beginners/point-labels/index.ts +69 -0
  147. package/src/stories/beginners/point-labels/labels.ts +46 -0
  148. package/src/stories/beginners/point-labels/style.css +16 -0
  149. package/src/stories/beginners/quick-start.ts +54 -0
  150. package/src/stories/beginners/remove-points/config.ts +25 -0
  151. package/src/stories/beginners/remove-points/data-gen.ts +30 -0
  152. package/src/stories/beginners/remove-points/index.ts +96 -0
  153. package/src/stories/beginners/remove-points/style.css +31 -0
  154. package/src/stories/beginners.stories.ts +130 -0
  155. package/src/stories/clusters/polygon-selection/index.ts +52 -0
  156. package/src/stories/clusters/polygon-selection/polygon.ts +143 -0
  157. package/src/stories/clusters/polygon-selection/style.css +8 -0
  158. package/src/stories/clusters/radial.ts +24 -0
  159. package/src/stories/clusters/with-labels.ts +54 -0
  160. package/src/stories/clusters/worm.ts +40 -0
  161. package/src/stories/clusters.stories.ts +77 -0
  162. package/src/stories/create-cluster-labels.ts +50 -0
  163. package/src/stories/create-cosmos.ts +72 -0
  164. package/src/stories/create-story.ts +51 -0
  165. package/src/stories/experiments/full-mesh.ts +13 -0
  166. package/src/stories/experiments/mesh-with-holes.ts +13 -0
  167. package/src/stories/experiments.stories.ts +43 -0
  168. package/src/stories/generate-mesh-data.ts +125 -0
  169. package/src/stories/geospatial/moscow-metro-stations/index.ts +66 -0
  170. package/src/stories/geospatial/moscow-metro-stations/moscow-metro-coords.ts +1 -0
  171. package/src/stories/geospatial/moscow-metro-stations/point-colors.ts +46 -0
  172. package/src/stories/geospatial/moscow-metro-stations/style.css +30 -0
  173. package/src/stories/geospatial.stories.ts +30 -0
  174. package/src/stories/shapes/all-shapes/index.ts +73 -0
  175. package/src/stories/shapes/image-example/icons/box.png +0 -0
  176. package/src/stories/shapes/image-example/icons/lego.png +0 -0
  177. package/src/stories/shapes/image-example/icons/s.png +0 -0
  178. package/src/stories/shapes/image-example/icons/swift.png +0 -0
  179. package/src/stories/shapes/image-example/icons/toolbox.png +0 -0
  180. package/src/stories/shapes/image-example/index.ts +246 -0
  181. package/src/stories/shapes.stories.ts +37 -0
  182. package/src/stories/test-luma-migration.ts +195 -0
  183. package/src/stories/test.stories.ts +25 -0
  184. package/src/variables.ts +68 -0
  185. package/tsconfig.json +41 -0
  186. package/vite.config.ts +52 -0
@@ -0,0 +1,2292 @@
1
+ import { Framebuffer, Buffer, Texture, UniformStore, RenderPass } from '@luma.gl/core'
2
+ import { Model } from '@luma.gl/engine'
3
+ // import { scaleLinear } from 'd3-scale'
4
+ // import { extent } from 'd3-array'
5
+ import { CoreModule } from '@/graph/modules/core-module'
6
+ import { defaultConfigValues } from '@/graph/variables'
7
+ import drawPointsFrag from '@/graph/modules/Points/draw-points.frag?raw'
8
+ import drawPointsVert from '@/graph/modules/Points/draw-points.vert?raw'
9
+ import findPointsOnAreaSelectionFrag from '@/graph/modules/Points/find-points-on-area-selection.frag?raw'
10
+ import findPointsOnPolygonSelectionFrag from '@/graph/modules/Points/find-points-on-polygon-selection.frag?raw'
11
+ import drawHighlightedFrag from '@/graph/modules/Points/draw-highlighted.frag?raw'
12
+ import drawHighlightedVert from '@/graph/modules/Points/draw-highlighted.vert?raw'
13
+ import findHoveredPointFrag from '@/graph/modules/Points/find-hovered-point.frag?raw'
14
+ import findHoveredPointVert from '@/graph/modules/Points/find-hovered-point.vert?raw'
15
+ import fillGridWithSampledPointsFrag from '@/graph/modules/Points/fill-sampled-points.frag?raw'
16
+ import fillGridWithSampledPointsVert from '@/graph/modules/Points/fill-sampled-points.vert?raw'
17
+ import updatePositionFrag from '@/graph/modules/Points/update-position.frag?raw'
18
+ import { createIndexesForBuffer } from '@/graph/modules/Shared/buffer'
19
+ import trackPositionsFrag from '@/graph/modules/Points/track-positions.frag?raw'
20
+ import dragPointFrag from '@/graph/modules/Points/drag-point.frag?raw'
21
+ import updateVert from '@/graph/modules/Shared/quad.vert?raw'
22
+ import clearFrag from '@/graph/modules/Shared/clear.frag?raw'
23
+ import { readPixels } from '@/graph/helper'
24
+ import { createAtlasDataFromImageData } from '@/graph/modules/Points/atlas-utils'
25
+
26
+ export class Points extends CoreModule {
27
+ public currentPositionFbo: Framebuffer | undefined
28
+ public previousPositionFbo: Framebuffer | undefined
29
+ public velocityFbo: Framebuffer | undefined
30
+ public selectedFbo: Framebuffer | undefined
31
+ public hoveredFbo: Framebuffer | undefined
32
+ public greyoutStatusFbo: Framebuffer | undefined
33
+ public scaleX: ((x: number) => number) | undefined
34
+ public scaleY: ((y: number) => number) | undefined
35
+ public shouldSkipRescale: boolean | undefined
36
+ public imageAtlasTexture: Texture | undefined
37
+ public imageCount = 0
38
+ // Add texture properties for position data (public for Clusters module access)
39
+ public currentPositionTexture: Texture | undefined
40
+ public previousPositionTexture: Texture | undefined
41
+ public velocityTexture: Texture | undefined
42
+ // Add texture property for greyout status (public for Lines module access)
43
+ public greyoutStatusTexture: Texture | undefined
44
+ private colorBuffer: Buffer | undefined
45
+ private sizeFbo: Framebuffer | undefined
46
+ private sizeBuffer: Buffer | undefined
47
+ private shapeBuffer: Buffer | undefined
48
+ private imageIndicesBuffer: Buffer | undefined
49
+ private imageSizesBuffer: Buffer | undefined
50
+ private imageAtlasCoordsTexture: Texture | undefined
51
+ private imageAtlasCoordsTextureSize: number | undefined
52
+ private trackedIndicesFbo: Framebuffer | undefined
53
+ private trackedPositionsFbo: Framebuffer | undefined
54
+ private sampledPointsFbo: Framebuffer | undefined
55
+ private trackedPositions: Map<number, [number, number]> | undefined
56
+ private isPositionsUpToDate = false
57
+ private drawCommand: Model | undefined
58
+ private drawHighlightedCommand: Model | undefined
59
+ private updatePositionCommand: Model | undefined
60
+ private dragPointCommand: Model | undefined
61
+ private findPointsOnAreaSelectionCommand: Model | undefined
62
+ private findPointsOnPolygonSelectionCommand: Model | undefined
63
+ private findHoveredPointCommand: Model | undefined
64
+ private clearHoveredFboCommand: Model | undefined
65
+ private clearSampledPointsFboCommand: Model | undefined
66
+ private fillSampledPointsFboCommand: Model | undefined
67
+ private trackPointsCommand: Model | undefined
68
+ // Vertex buffers for quad rendering (Model doesn't destroy them automatically)
69
+ private updatePositionVertexCoordBuffer: Buffer | undefined
70
+ private dragPointVertexCoordBuffer: Buffer | undefined
71
+ private findPointsOnAreaSelectionVertexCoordBuffer: Buffer | undefined
72
+ private findPointsOnPolygonSelectionVertexCoordBuffer: Buffer | undefined
73
+ private clearHoveredFboVertexCoordBuffer: Buffer | undefined
74
+ private clearSampledPointsFboVertexCoordBuffer: Buffer | undefined
75
+ private drawHighlightedVertexCoordBuffer: Buffer | undefined
76
+ private trackPointsVertexCoordBuffer: Buffer | undefined
77
+ private trackedIndices: number[] | undefined
78
+ private selectedTexture: Texture | undefined
79
+ private sizeTexture: Texture | undefined
80
+ private trackedIndicesTexture: Texture | undefined
81
+ private polygonPathTexture: Texture | undefined
82
+ private polygonPathFbo: Framebuffer | undefined
83
+ private polygonPathLength = 0
84
+ private drawPointIndices: Buffer | undefined
85
+ private hoveredPointIndices: Buffer | undefined
86
+ private sampledPointIndices: Buffer | undefined
87
+
88
+ // Uniform stores for scalar uniforms
89
+ private updatePositionUniformStore: UniformStore<{
90
+ updatePositionUniforms: {
91
+ friction: number;
92
+ spaceSize: number;
93
+ };
94
+ }> | undefined
95
+
96
+ private dragPointUniformStore: UniformStore<{
97
+ dragPointUniforms: {
98
+ mousePos: [number, number];
99
+ index: number;
100
+ };
101
+ }> | undefined
102
+
103
+ private drawUniformStore: UniformStore<{
104
+ drawVertexUniforms: {
105
+ ratio: number;
106
+ sizeScale: number;
107
+ pointsTextureSize: number;
108
+ transformationMatrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
109
+ spaceSize: number;
110
+ screenSize: [number, number];
111
+ greyoutColor: [number, number, number, number];
112
+ backgroundColor: [number, number, number, number];
113
+ scalePointsOnZoom: number;
114
+ maxPointSize: number;
115
+ isDarkenGreyout: number;
116
+ skipSelected: number;
117
+ skipUnselected: number;
118
+ hasImages: number;
119
+ imageCount: number;
120
+ imageAtlasCoordsTextureSize: number;
121
+ };
122
+ drawFragmentUniforms: {
123
+ greyoutOpacity: number;
124
+ pointOpacity: number;
125
+ isDarkenGreyout: number;
126
+ backgroundColor: [number, number, number, number];
127
+ };
128
+ }> | undefined
129
+
130
+ private findPointsOnAreaSelectionUniformStore: UniformStore<{
131
+ findPointsOnAreaSelectionUniforms: {
132
+ spaceSize: number;
133
+ screenSize: [number, number];
134
+ sizeScale: number;
135
+ transformationMatrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
136
+ ratio: number;
137
+ selection0: [number, number];
138
+ selection1: [number, number];
139
+ scalePointsOnZoom: number;
140
+ maxPointSize: number;
141
+ };
142
+ }> | undefined
143
+
144
+ private findPointsOnPolygonSelectionUniformStore: UniformStore<{
145
+ findPointsOnPolygonSelectionUniforms: {
146
+ spaceSize: number;
147
+ screenSize: [number, number];
148
+ transformationMatrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
149
+ polygonPathLength: number;
150
+ };
151
+ }> | undefined
152
+
153
+ private findHoveredPointUniformStore: UniformStore<{
154
+ findHoveredPointUniforms: {
155
+ ratio: number;
156
+ sizeScale: number;
157
+ pointsTextureSize: number;
158
+ transformationMatrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
159
+ spaceSize: number;
160
+ screenSize: [number, number];
161
+ scalePointsOnZoom: number;
162
+ mousePosition: [number, number];
163
+ maxPointSize: number;
164
+ };
165
+ }> | undefined
166
+
167
+ private fillSampledPointsUniformStore: UniformStore<{
168
+ fillSampledPointsUniforms: {
169
+ pointsTextureSize: number;
170
+ transformationMatrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
171
+ spaceSize: number;
172
+ screenSize: [number, number];
173
+ };
174
+ }> | undefined
175
+
176
+ private drawHighlightedUniformStore: UniformStore<{
177
+ drawHighlightedUniforms: {
178
+ color: [number, number, number, number];
179
+ width: number;
180
+ pointIndex: number;
181
+ size: number;
182
+ sizeScale: number;
183
+ pointsTextureSize: number;
184
+ transformationMatrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
185
+ spaceSize: number;
186
+ screenSize: [number, number];
187
+ scalePointsOnZoom: number; // f32 in shader, not boolean
188
+ maxPointSize: number;
189
+ universalPointOpacity: number;
190
+ greyoutOpacity: number;
191
+ isDarkenGreyout: number; // f32 in shader, not boolean
192
+ backgroundColor: [number, number, number, number];
193
+ greyoutColor: [number, number, number, number];
194
+ };
195
+ }> | undefined
196
+
197
+ private trackPointsUniformStore: UniformStore<{
198
+ trackPointsUniforms: {
199
+ pointsTextureSize: number;
200
+ };
201
+ }> | undefined
202
+
203
+ public updatePositions (): void {
204
+ const { device, store, data, config: { rescalePositions, enableSimulation } } = this
205
+
206
+ const { pointsTextureSize } = store
207
+ if (!pointsTextureSize || !data.pointPositions || data.pointsNumber === undefined) return
208
+
209
+ // Create initial state array with exact size needed for RGBA32Float texture
210
+ // Ensure it's a new contiguous buffer (not a view) with the exact size
211
+ const textureDataSize = pointsTextureSize * pointsTextureSize * 4
212
+ const initialState = new Float32Array(textureDataSize)
213
+
214
+ const expectedBytes = pointsTextureSize * pointsTextureSize * 4 * 4 // width * height * 4 components * 4 bytes
215
+ const actualBytes = initialState.byteLength
216
+ if (actualBytes !== expectedBytes) {
217
+ console.error('Texture data size mismatch:', {
218
+ pointsTextureSize,
219
+ expectedBytes,
220
+ actualBytes,
221
+ textureDataSize,
222
+ dataLength: initialState.length,
223
+ })
224
+ }
225
+
226
+ let shouldRescale = rescalePositions
227
+ // If rescalePositions isn't specified in config and simulation is disabled, default to true
228
+ if (rescalePositions === undefined && !enableSimulation) shouldRescale = true
229
+ // Skip rescaling if `shouldSkipRescale` flag is set (allowing one-time skip of rescaling)
230
+ // Temporary flag is used to skip rescaling when change point positions or adding new points by function `setPointPositions`
231
+ // This flag overrides any other rescaling settings
232
+ if (this.shouldSkipRescale) shouldRescale = false
233
+
234
+ if (shouldRescale) {
235
+ this.rescaleInitialNodePositions()
236
+ } else if (!this.shouldSkipRescale) {
237
+ // Only reset scale functions if not temporarily skipping rescale
238
+ this.scaleX = undefined
239
+ this.scaleY = undefined
240
+ }
241
+
242
+ // Reset temporary flag
243
+ this.shouldSkipRescale = undefined
244
+
245
+ for (let i = 0; i < data.pointsNumber; ++i) {
246
+ initialState[i * 4 + 0] = data.pointPositions[i * 2 + 0] as number
247
+ initialState[i * 4 + 1] = data.pointPositions[i * 2 + 1] as number
248
+ initialState[i * 4 + 2] = i
249
+ }
250
+
251
+ // Create currentPositionTexture and framebuffer
252
+ if (!this.currentPositionTexture || this.currentPositionTexture.width !== pointsTextureSize || this.currentPositionTexture.height !== pointsTextureSize) {
253
+ if (this.currentPositionTexture) {
254
+ this.currentPositionTexture.destroy()
255
+ }
256
+ if (this.currentPositionFbo) {
257
+ this.currentPositionFbo.destroy()
258
+ }
259
+ this.currentPositionTexture = device.createTexture({
260
+ width: pointsTextureSize,
261
+ height: pointsTextureSize,
262
+ format: 'rgba32float',
263
+ })
264
+ this.currentPositionTexture.copyImageData({
265
+ data: initialState,
266
+ bytesPerRow: pointsTextureSize,
267
+ mipLevel: 0,
268
+ x: 0,
269
+ y: 0,
270
+ })
271
+ this.currentPositionFbo = device.createFramebuffer({
272
+ width: pointsTextureSize,
273
+ height: pointsTextureSize,
274
+ colorAttachments: [this.currentPositionTexture],
275
+ })
276
+ } else {
277
+ this.currentPositionTexture.copyImageData({
278
+ data: initialState,
279
+ bytesPerRow: pointsTextureSize,
280
+ mipLevel: 0,
281
+ x: 0,
282
+ y: 0,
283
+ })
284
+ }
285
+
286
+ // Create previousPositionTexture and framebuffer
287
+ if (!this.previousPositionTexture ||
288
+ this.previousPositionTexture.width !== pointsTextureSize ||
289
+ this.previousPositionTexture.height !== pointsTextureSize) {
290
+ if (this.previousPositionTexture) {
291
+ this.previousPositionTexture.destroy()
292
+ }
293
+ if (this.previousPositionFbo) {
294
+ this.previousPositionFbo.destroy()
295
+ }
296
+ this.previousPositionTexture = device.createTexture({
297
+ width: pointsTextureSize,
298
+ height: pointsTextureSize,
299
+ format: 'rgba32float',
300
+ })
301
+ this.previousPositionTexture.copyImageData({
302
+ data: initialState,
303
+ bytesPerRow: pointsTextureSize,
304
+ mipLevel: 0,
305
+ x: 0,
306
+ y: 0,
307
+ })
308
+ this.previousPositionFbo = device.createFramebuffer({
309
+ width: pointsTextureSize,
310
+ height: pointsTextureSize,
311
+ colorAttachments: [this.previousPositionTexture],
312
+ })
313
+ } else {
314
+ this.previousPositionTexture.copyImageData({
315
+ data: initialState,
316
+ bytesPerRow: pointsTextureSize,
317
+ mipLevel: 0,
318
+ x: 0,
319
+ y: 0,
320
+ })
321
+ }
322
+
323
+ if (this.config.enableSimulation) {
324
+ // Create velocityTexture and framebuffer
325
+ const velocityData = new Float32Array(pointsTextureSize * pointsTextureSize * 4).fill(0)
326
+ if (!this.velocityTexture || this.velocityTexture.width !== pointsTextureSize || this.velocityTexture.height !== pointsTextureSize) {
327
+ if (this.velocityTexture) {
328
+ this.velocityTexture.destroy()
329
+ }
330
+ if (this.velocityFbo) {
331
+ this.velocityFbo.destroy()
332
+ }
333
+ this.velocityTexture = device.createTexture({
334
+ width: pointsTextureSize,
335
+ height: pointsTextureSize,
336
+ format: 'rgba32float',
337
+ })
338
+ this.velocityTexture.copyImageData({
339
+ data: velocityData,
340
+ bytesPerRow: pointsTextureSize,
341
+ mipLevel: 0,
342
+ x: 0,
343
+ y: 0,
344
+ })
345
+ this.velocityFbo = device.createFramebuffer({
346
+ width: pointsTextureSize,
347
+ height: pointsTextureSize,
348
+ colorAttachments: [this.velocityTexture],
349
+ })
350
+ } else {
351
+ this.velocityTexture.copyImageData({
352
+ data: velocityData,
353
+ bytesPerRow: pointsTextureSize,
354
+ mipLevel: 0,
355
+ x: 0,
356
+ y: 0,
357
+ })
358
+ }
359
+ }
360
+
361
+ // Create selectedTexture and framebuffer
362
+ if (!this.selectedTexture || this.selectedTexture.width !== pointsTextureSize || this.selectedTexture.height !== pointsTextureSize) {
363
+ if (this.selectedTexture) {
364
+ this.selectedTexture.destroy()
365
+ }
366
+ if (this.selectedFbo) {
367
+ this.selectedFbo.destroy()
368
+ }
369
+ this.selectedTexture = device.createTexture({
370
+ width: pointsTextureSize,
371
+ height: pointsTextureSize,
372
+ format: 'rgba32float',
373
+ })
374
+ this.selectedTexture.copyImageData({
375
+ data: initialState,
376
+ bytesPerRow: pointsTextureSize,
377
+ mipLevel: 0,
378
+ x: 0,
379
+ y: 0,
380
+ })
381
+ this.selectedFbo = device.createFramebuffer({
382
+ width: pointsTextureSize,
383
+ height: pointsTextureSize,
384
+ colorAttachments: [this.selectedTexture],
385
+ })
386
+ } else {
387
+ this.selectedTexture.copyImageData({
388
+ data: initialState,
389
+ bytesPerRow: pointsTextureSize,
390
+ mipLevel: 0,
391
+ x: 0,
392
+ y: 0,
393
+ })
394
+ }
395
+
396
+ // Create hoveredFbo (2x2 for hover detection)
397
+ if (!this.hoveredFbo) {
398
+ this.hoveredFbo = device.createFramebuffer({
399
+ width: 2,
400
+ height: 2,
401
+ colorAttachments: ['rgba32float'],
402
+ })
403
+ }
404
+
405
+ // Create buffers
406
+ const indexData = createIndexesForBuffer(store.pointsTextureSize)
407
+ const requiredByteLength = indexData.byteLength
408
+
409
+ if (!this.drawPointIndices || this.drawPointIndices.byteLength !== requiredByteLength) {
410
+ this.drawPointIndices?.destroy()
411
+ this.drawPointIndices = device.createBuffer({
412
+ data: indexData,
413
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
414
+ })
415
+ } else {
416
+ this.drawPointIndices.write(indexData)
417
+ }
418
+
419
+ if (!this.hoveredPointIndices || this.hoveredPointIndices.byteLength !== requiredByteLength) {
420
+ this.hoveredPointIndices?.destroy()
421
+ this.hoveredPointIndices = device.createBuffer({
422
+ data: indexData,
423
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
424
+ })
425
+ } else {
426
+ this.hoveredPointIndices.write(indexData)
427
+ }
428
+
429
+ if (!this.sampledPointIndices || this.sampledPointIndices.byteLength !== requiredByteLength) {
430
+ this.sampledPointIndices?.destroy()
431
+ this.sampledPointIndices = device.createBuffer({
432
+ data: indexData,
433
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
434
+ })
435
+ } else {
436
+ this.sampledPointIndices.write(indexData)
437
+ }
438
+
439
+ this.updateGreyoutStatus()
440
+ this.updateSampledPointsGrid()
441
+
442
+ this.trackPointsByIndices()
443
+ }
444
+
445
+ public initPrograms (): void {
446
+ const { device, config, store, data } = this
447
+ // Ensure textures are created before Model initialization
448
+ if (!this.imageAtlasCoordsTexture || !this.imageAtlasTexture) {
449
+ this.createAtlas()
450
+ }
451
+ // Ensure buffers exist before Model creation (Model needs attributes at creation time)
452
+ if (!this.colorBuffer) this.updateColor()
453
+ if (!this.sizeBuffer) this.updateSize()
454
+ if (!this.shapeBuffer) this.updateShape()
455
+ if (!this.imageIndicesBuffer) this.updateImageIndices()
456
+ if (!this.imageSizesBuffer) this.updateImageSizes()
457
+ if (!this.greyoutStatusTexture) this.updateGreyoutStatus()
458
+ if (config.enableSimulation) {
459
+ if (!this.updatePositionCommand) {
460
+ // Create vertex buffer for quad
461
+ if (!this.updatePositionVertexCoordBuffer) {
462
+ this.updatePositionVertexCoordBuffer = device.createBuffer({
463
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
464
+ })
465
+ }
466
+
467
+ // Create UniformStore for updatePosition uniforms
468
+ if (!this.updatePositionUniformStore) {
469
+ this.updatePositionUniformStore = new UniformStore({
470
+ updatePositionUniforms: {
471
+ uniformTypes: {
472
+ // Order MUST match shader declaration order (std140 layout)
473
+ friction: 'f32',
474
+ spaceSize: 'f32',
475
+ },
476
+ defaultUniforms: {
477
+ friction: config.simulationFriction ?? 0,
478
+ spaceSize: store.adjustedSpaceSize ?? 0,
479
+ },
480
+ },
481
+ })
482
+ }
483
+
484
+ this.updatePositionCommand = new Model(device, {
485
+ fs: updatePositionFrag,
486
+ vs: updateVert,
487
+ topology: 'triangle-strip',
488
+ vertexCount: 4,
489
+ attributes: {
490
+ vertexCoord: this.updatePositionVertexCoordBuffer,
491
+ },
492
+ bufferLayout: [
493
+ { name: 'vertexCoord', format: 'float32x2' },
494
+ ],
495
+ defines: {
496
+ USE_UNIFORM_BUFFERS: true,
497
+ },
498
+ bindings: {
499
+ updatePositionUniforms: this.updatePositionUniformStore.getManagedUniformBuffer(device, 'updatePositionUniforms'),
500
+ ...(this.previousPositionTexture && { positionsTexture: this.previousPositionTexture }),
501
+ ...(this.velocityTexture && { velocity: this.velocityTexture }),
502
+ },
503
+ })
504
+ }
505
+ }
506
+
507
+ if (!this.dragPointCommand) {
508
+ // Create vertex buffer for quad
509
+ if (!this.dragPointVertexCoordBuffer) {
510
+ this.dragPointVertexCoordBuffer = device.createBuffer({
511
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
512
+ })
513
+ }
514
+
515
+ // Create UniformStore for dragPoint uniforms
516
+ if (!this.dragPointUniformStore) {
517
+ this.dragPointUniformStore = new UniformStore({
518
+ dragPointUniforms: {
519
+ uniformTypes: {
520
+ // Order MUST match shader declaration order (std140 layout)
521
+ mousePos: 'vec2<f32>',
522
+ index: 'f32',
523
+ },
524
+ defaultUniforms: {
525
+ mousePos: (store.mousePosition as [number, number]) ?? [0, 0],
526
+ index: store.hoveredPoint?.index ?? -1,
527
+ },
528
+ },
529
+ })
530
+ }
531
+
532
+ this.dragPointCommand = new Model(device, {
533
+ fs: dragPointFrag,
534
+ vs: updateVert,
535
+ topology: 'triangle-strip',
536
+ vertexCount: 4,
537
+ attributes: {
538
+ vertexCoord: this.dragPointVertexCoordBuffer,
539
+ },
540
+ bufferLayout: [
541
+ { name: 'vertexCoord', format: 'float32x2' },
542
+ ],
543
+ defines: {
544
+ USE_UNIFORM_BUFFERS: true,
545
+ },
546
+ bindings: {
547
+ dragPointUniforms: this.dragPointUniformStore.getManagedUniformBuffer(device, 'dragPointUniforms'),
548
+ ...(this.previousPositionTexture && { positionsTexture: this.previousPositionTexture }),
549
+ },
550
+ })
551
+ }
552
+
553
+ if (!this.drawCommand) {
554
+ // Create UniformStore for draw uniforms
555
+ if (!this.drawUniformStore) {
556
+ this.drawUniformStore = new UniformStore({
557
+ drawVertexUniforms: {
558
+ uniformTypes: {
559
+ // Order MUST match shader declaration order (std140 layout)
560
+ ratio: 'f32',
561
+ transformationMatrix: 'mat4x4<f32>',
562
+ pointsTextureSize: 'f32',
563
+ sizeScale: 'f32',
564
+ spaceSize: 'f32',
565
+ screenSize: 'vec2<f32>',
566
+ greyoutColor: 'vec4<f32>',
567
+ backgroundColor: 'vec4<f32>',
568
+ scalePointsOnZoom: 'f32',
569
+ maxPointSize: 'f32',
570
+ isDarkenGreyout: 'f32',
571
+ skipSelected: 'f32',
572
+ skipUnselected: 'f32',
573
+ hasImages: 'f32',
574
+ imageCount: 'f32',
575
+ imageAtlasCoordsTextureSize: 'f32',
576
+ },
577
+ defaultUniforms: {
578
+ // Order MUST match uniformTypes and shader declaration
579
+ ratio: config.pixelRatio ?? defaultConfigValues.pixelRatio,
580
+ transformationMatrix: ((): [
581
+ number, number, number, number,
582
+ number, number, number, number,
583
+ number, number, number, number,
584
+ number, number, number, number
585
+ ] => {
586
+ const t = store.transform ?? [1, 0, 0, 0, 1, 0, 0, 0, 1]
587
+ return [
588
+ t[0], t[1], t[2], 0,
589
+ t[3], t[4], t[5], 0,
590
+ t[6], t[7], t[8], 0,
591
+ 0, 0, 0, 1,
592
+ ]
593
+ })(),
594
+ pointsTextureSize: store.pointsTextureSize ?? 0,
595
+ sizeScale: config.pointSizeScale ?? 1,
596
+ spaceSize: store.adjustedSpaceSize ?? 0,
597
+ screenSize: store.screenSize ?? [0, 0],
598
+ greyoutColor: (store.greyoutPointColor ?? [0, 0, 0, 1]) as [number, number, number, number],
599
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
600
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0, // Convert boolean to float
601
+ maxPointSize: store.maxPointSize ?? 100,
602
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0, // Convert boolean to float
603
+ skipSelected: 0, // Default to 0 (false)
604
+ skipUnselected: 0, // Default to 0 (false)
605
+ hasImages: (this.imageCount > 0) ? 1 : 0, // Convert boolean to float
606
+ imageCount: this.imageCount,
607
+ imageAtlasCoordsTextureSize: this.imageAtlasCoordsTextureSize ?? 0,
608
+ },
609
+ },
610
+ drawFragmentUniforms: {
611
+ uniformTypes: {
612
+ greyoutOpacity: 'f32',
613
+ pointOpacity: 'f32',
614
+ isDarkenGreyout: 'f32',
615
+ backgroundColor: 'vec4<f32>',
616
+ },
617
+ defaultUniforms: {
618
+ greyoutOpacity: config.pointGreyoutOpacity ?? -1,
619
+ pointOpacity: config.pointOpacity ?? 1,
620
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0, // Convert boolean to float
621
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
622
+ },
623
+ },
624
+ })
625
+ }
626
+
627
+ this.drawCommand = new Model(device, {
628
+ fs: drawPointsFrag,
629
+ vs: drawPointsVert,
630
+ topology: 'point-list',
631
+ vertexCount: data.pointsNumber ?? 0,
632
+ attributes: {
633
+ ...(this.drawPointIndices && { pointIndices: this.drawPointIndices }),
634
+ ...(this.sizeBuffer && { size: this.sizeBuffer }),
635
+ ...(this.colorBuffer && { color: this.colorBuffer }),
636
+ ...(this.shapeBuffer && { shape: this.shapeBuffer }),
637
+ ...(this.imageIndicesBuffer && { imageIndex: this.imageIndicesBuffer }),
638
+ ...(this.imageSizesBuffer && { imageSize: this.imageSizesBuffer }),
639
+ },
640
+ bufferLayout: [
641
+ { name: 'pointIndices', format: 'float32x2' },
642
+ { name: 'size', format: 'float32' },
643
+ { name: 'color', format: 'float32x4' },
644
+ { name: 'shape', format: 'float32' },
645
+ { name: 'imageIndex', format: 'float32' },
646
+ { name: 'imageSize', format: 'float32' },
647
+ ],
648
+ defines: {
649
+ USE_UNIFORM_BUFFERS: true,
650
+ },
651
+ bindings: {
652
+ drawVertexUniforms: this.drawUniformStore.getManagedUniformBuffer(device, 'drawVertexUniforms'),
653
+ drawFragmentUniforms: this.drawUniformStore.getManagedUniformBuffer(device, 'drawFragmentUniforms'),
654
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
655
+ ...(this.greyoutStatusTexture && { pointGreyoutStatus: this.greyoutStatusTexture }),
656
+ ...(this.imageAtlasTexture && { imageAtlasTexture: this.imageAtlasTexture }),
657
+ ...(this.imageAtlasCoordsTexture && { imageAtlasCoords: this.imageAtlasCoordsTexture }),
658
+ },
659
+ parameters: {
660
+ blend: true,
661
+ blendColorOperation: 'add',
662
+ blendColorSrcFactor: 'src-alpha',
663
+ blendColorDstFactor: 'one-minus-src-alpha',
664
+ blendAlphaOperation: 'add',
665
+ blendAlphaSrcFactor: 'one',
666
+ blendAlphaDstFactor: 'one-minus-src-alpha',
667
+ depthWriteEnabled: false,
668
+ depthCompare: 'always',
669
+ },
670
+ })
671
+ }
672
+
673
+ if (!this.findPointsOnAreaSelectionCommand) {
674
+ // Create vertex buffer for quad
675
+ if (!this.findPointsOnAreaSelectionVertexCoordBuffer) {
676
+ this.findPointsOnAreaSelectionVertexCoordBuffer = device.createBuffer({
677
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
678
+ })
679
+ }
680
+
681
+ // Create UniformStore for findPointsOnAreaSelection uniforms
682
+ if (!this.findPointsOnAreaSelectionUniformStore) {
683
+ this.findPointsOnAreaSelectionUniformStore = new UniformStore({
684
+ findPointsOnAreaSelectionUniforms: {
685
+ uniformTypes: {
686
+ // Order MUST match shader declaration order (std140 layout)
687
+ sizeScale: 'f32',
688
+ spaceSize: 'f32',
689
+ screenSize: 'vec2<f32>',
690
+ ratio: 'f32',
691
+ transformationMatrix: 'mat4x4<f32>',
692
+ selection0: 'vec2<f32>',
693
+ selection1: 'vec2<f32>',
694
+ scalePointsOnZoom: 'f32',
695
+ maxPointSize: 'f32',
696
+ },
697
+ defaultUniforms: {
698
+ sizeScale: config.pointSizeScale ?? 1,
699
+ spaceSize: store.adjustedSpaceSize ?? 0,
700
+ screenSize: store.screenSize ?? [0, 0],
701
+ ratio: config.pixelRatio ?? defaultConfigValues.pixelRatio,
702
+ transformationMatrix: store.transformationMatrix4x4,
703
+ selection0: (store.selectedArea?.[0] ?? [0, 0]) as [number, number],
704
+ selection1: (store.selectedArea?.[1] ?? [0, 0]) as [number, number],
705
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0,
706
+ maxPointSize: store.maxPointSize ?? 100,
707
+ },
708
+ },
709
+ })
710
+ }
711
+
712
+ this.findPointsOnAreaSelectionCommand = new Model(device, {
713
+ fs: findPointsOnAreaSelectionFrag,
714
+ vs: updateVert,
715
+ topology: 'triangle-strip',
716
+ vertexCount: 4,
717
+ attributes: {
718
+ vertexCoord: this.findPointsOnAreaSelectionVertexCoordBuffer,
719
+ },
720
+ bufferLayout: [
721
+ { name: 'vertexCoord', format: 'float32x2' },
722
+ ],
723
+ defines: {
724
+ USE_UNIFORM_BUFFERS: true,
725
+ },
726
+ bindings: {
727
+ findPointsOnAreaSelectionUniforms: this.findPointsOnAreaSelectionUniformStore.getManagedUniformBuffer(device, 'findPointsOnAreaSelectionUniforms'),
728
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
729
+ ...(this.sizeTexture && { pointSize: this.sizeTexture }),
730
+ },
731
+ })
732
+ }
733
+
734
+ if (!this.findPointsOnPolygonSelectionCommand) {
735
+ // Create vertex buffer for quad
736
+ if (!this.findPointsOnPolygonSelectionVertexCoordBuffer) {
737
+ this.findPointsOnPolygonSelectionVertexCoordBuffer = device.createBuffer({
738
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
739
+ })
740
+ }
741
+
742
+ // Create UniformStore for findPointsOnPolygonSelection uniforms
743
+ if (!this.findPointsOnPolygonSelectionUniformStore) {
744
+ this.findPointsOnPolygonSelectionUniformStore = new UniformStore({
745
+ findPointsOnPolygonSelectionUniforms: {
746
+ uniformTypes: {
747
+ // Order MUST match shader declaration order (std140 layout)
748
+ spaceSize: 'f32',
749
+ screenSize: 'vec2<f32>',
750
+ transformationMatrix: 'mat4x4<f32>',
751
+ polygonPathLength: 'f32',
752
+ },
753
+ defaultUniforms: {
754
+ spaceSize: store.adjustedSpaceSize ?? 0,
755
+ screenSize: store.screenSize ?? [0, 0],
756
+ transformationMatrix: store.transformationMatrix4x4,
757
+ polygonPathLength: this.polygonPathLength,
758
+ },
759
+ },
760
+ })
761
+ }
762
+
763
+ this.findPointsOnPolygonSelectionCommand = new Model(device, {
764
+ fs: findPointsOnPolygonSelectionFrag,
765
+ vs: updateVert,
766
+ topology: 'triangle-strip',
767
+ vertexCount: 4,
768
+ attributes: {
769
+ vertexCoord: this.findPointsOnPolygonSelectionVertexCoordBuffer,
770
+ },
771
+ bufferLayout: [
772
+ { name: 'vertexCoord', format: 'float32x2' },
773
+ ],
774
+ defines: {
775
+ USE_UNIFORM_BUFFERS: true,
776
+ },
777
+ bindings: {
778
+ findPointsOnPolygonSelectionUniforms: this.findPointsOnPolygonSelectionUniformStore
779
+ .getManagedUniformBuffer(device, 'findPointsOnPolygonSelectionUniforms'),
780
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
781
+ ...(this.polygonPathTexture && { polygonPathTexture: this.polygonPathTexture }),
782
+ },
783
+ })
784
+ }
785
+
786
+ if (!this.clearHoveredFboCommand) {
787
+ // Create vertex buffer for quad
788
+ if (!this.clearHoveredFboVertexCoordBuffer) {
789
+ this.clearHoveredFboVertexCoordBuffer = device.createBuffer({
790
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
791
+ })
792
+ }
793
+
794
+ this.clearHoveredFboCommand = new Model(device, {
795
+ fs: clearFrag,
796
+ vs: updateVert,
797
+ topology: 'triangle-strip',
798
+ vertexCount: 4,
799
+ attributes: {
800
+ vertexCoord: this.clearHoveredFboVertexCoordBuffer,
801
+ },
802
+ bufferLayout: [
803
+ { name: 'vertexCoord', format: 'float32x2' },
804
+ ],
805
+ })
806
+ }
807
+
808
+ if (!this.findHoveredPointCommand) {
809
+ // Create UniformStore for findHoveredPoint uniforms
810
+ if (!this.findHoveredPointUniformStore) {
811
+ this.findHoveredPointUniformStore = new UniformStore({
812
+ findHoveredPointUniforms: {
813
+ uniformTypes: {
814
+ // Order MUST match shader declaration order (std140 layout)
815
+ pointsTextureSize: 'f32',
816
+ sizeScale: 'f32',
817
+ spaceSize: 'f32',
818
+ screenSize: 'vec2<f32>',
819
+ ratio: 'f32',
820
+ transformationMatrix: 'mat4x4<f32>',
821
+ mousePosition: 'vec2<f32>',
822
+ scalePointsOnZoom: 'f32',
823
+ maxPointSize: 'f32',
824
+ },
825
+ defaultUniforms: {
826
+ pointsTextureSize: store.pointsTextureSize ?? 0,
827
+ sizeScale: config.pointSizeScale ?? 1,
828
+ spaceSize: store.adjustedSpaceSize ?? 0,
829
+ screenSize: store.screenSize ?? [0, 0],
830
+ ratio: config.pixelRatio ?? defaultConfigValues.pixelRatio,
831
+ transformationMatrix: store.transformationMatrix4x4,
832
+ mousePosition: store.screenMousePosition ?? [0, 0],
833
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0,
834
+ maxPointSize: store.maxPointSize ?? 100,
835
+ },
836
+ },
837
+ })
838
+ }
839
+
840
+ this.findHoveredPointCommand = new Model(device, {
841
+ fs: findHoveredPointFrag,
842
+ vs: findHoveredPointVert,
843
+ topology: 'point-list',
844
+ vertexCount: data.pointsNumber ?? 0,
845
+ attributes: {
846
+ ...(this.hoveredPointIndices && { pointIndices: this.hoveredPointIndices }),
847
+ ...(this.sizeBuffer && { size: this.sizeBuffer }),
848
+ },
849
+ bufferLayout: [
850
+ { name: 'pointIndices', format: 'float32x2' },
851
+ { name: 'size', format: 'float32' },
852
+ ],
853
+ defines: {
854
+ USE_UNIFORM_BUFFERS: true,
855
+ },
856
+ bindings: {
857
+ findHoveredPointUniforms: this.findHoveredPointUniformStore.getManagedUniformBuffer(device, 'findHoveredPointUniforms'),
858
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
859
+ },
860
+ parameters: {
861
+ depthWriteEnabled: false,
862
+ depthCompare: 'always',
863
+ blend: false, // Disable blending - we want to overwrite, not blend
864
+ },
865
+ })
866
+ }
867
+
868
+ if (!this.clearSampledPointsFboCommand) {
869
+ // Create vertex buffer for quad
870
+ if (!this.clearSampledPointsFboVertexCoordBuffer) {
871
+ this.clearSampledPointsFboVertexCoordBuffer = device.createBuffer({
872
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
873
+ })
874
+ }
875
+
876
+ this.clearSampledPointsFboCommand = new Model(device, {
877
+ fs: clearFrag,
878
+ vs: updateVert,
879
+ topology: 'triangle-strip',
880
+ vertexCount: 4,
881
+ attributes: {
882
+ vertexCoord: this.clearSampledPointsFboVertexCoordBuffer,
883
+ },
884
+ bufferLayout: [
885
+ { name: 'vertexCoord', format: 'float32x2' },
886
+ ],
887
+ })
888
+ }
889
+
890
+ if (!this.fillSampledPointsFboCommand) {
891
+ // Create UniformStore for fillSampledPoints uniforms
892
+ if (!this.fillSampledPointsUniformStore) {
893
+ this.fillSampledPointsUniformStore = new UniformStore({
894
+ fillSampledPointsUniforms: {
895
+ uniformTypes: {
896
+ // Order MUST match shader declaration order (std140 layout)
897
+ pointsTextureSize: 'f32',
898
+ transformationMatrix: 'mat4x4<f32>',
899
+ spaceSize: 'f32',
900
+ screenSize: 'vec2<f32>',
901
+ },
902
+ defaultUniforms: {
903
+ pointsTextureSize: store.pointsTextureSize ?? 0,
904
+ transformationMatrix: store.transformationMatrix4x4,
905
+ spaceSize: store.adjustedSpaceSize ?? 0,
906
+ screenSize: store.screenSize ?? [0, 0],
907
+ },
908
+ },
909
+ })
910
+ }
911
+
912
+ this.fillSampledPointsFboCommand = new Model(device, {
913
+ fs: fillGridWithSampledPointsFrag,
914
+ vs: fillGridWithSampledPointsVert,
915
+ topology: 'point-list',
916
+ vertexCount: data.pointsNumber ?? 0,
917
+ attributes: {
918
+ ...(this.sampledPointIndices && { pointIndices: this.sampledPointIndices }),
919
+ },
920
+ bufferLayout: [
921
+ { name: 'pointIndices', format: 'float32x2' },
922
+ ],
923
+ defines: {
924
+ USE_UNIFORM_BUFFERS: true,
925
+ },
926
+ bindings: {
927
+ fillSampledPointsUniforms: this.fillSampledPointsUniformStore.getManagedUniformBuffer(device, 'fillSampledPointsUniforms'),
928
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
929
+ },
930
+ parameters: {
931
+ depthWriteEnabled: false,
932
+ depthCompare: 'always',
933
+ },
934
+ })
935
+ }
936
+
937
+ if (!this.drawHighlightedCommand) {
938
+ if (!this.drawHighlightedVertexCoordBuffer) {
939
+ this.drawHighlightedVertexCoordBuffer = device.createBuffer({
940
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
941
+ })
942
+ }
943
+
944
+ if (!this.drawHighlightedUniformStore) {
945
+ this.drawHighlightedUniformStore = new UniformStore({
946
+ drawHighlightedUniforms: {
947
+ uniformTypes: {
948
+ // Order MUST match shader declaration order (std140 layout)
949
+ // Vertex shader uniforms:
950
+ size: 'f32',
951
+ transformationMatrix: 'mat4x4<f32>',
952
+ pointsTextureSize: 'f32',
953
+ sizeScale: 'f32',
954
+ spaceSize: 'f32',
955
+ screenSize: 'vec2<f32>',
956
+ scalePointsOnZoom: 'f32',
957
+ pointIndex: 'f32',
958
+ maxPointSize: 'f32',
959
+ color: 'vec4<f32>',
960
+ universalPointOpacity: 'f32',
961
+ greyoutOpacity: 'f32',
962
+ isDarkenGreyout: 'f32',
963
+ backgroundColor: 'vec4<f32>',
964
+ greyoutColor: 'vec4<f32>',
965
+ // Fragment shader uniforms (width is in same block):
966
+ width: 'f32',
967
+ },
968
+ defaultUniforms: {
969
+ size: 1,
970
+ transformationMatrix: store.transformationMatrix4x4,
971
+ pointsTextureSize: store.pointsTextureSize ?? 0,
972
+ sizeScale: config.pointSizeScale ?? 1,
973
+ spaceSize: store.adjustedSpaceSize ?? 0,
974
+ screenSize: store.screenSize ?? [0, 0],
975
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0,
976
+ pointIndex: -1,
977
+ maxPointSize: store.maxPointSize ?? 100,
978
+ color: [0, 0, 0, 1] as [number, number, number, number],
979
+ universalPointOpacity: config.pointOpacity ?? 1,
980
+ greyoutOpacity: config.pointGreyoutOpacity ?? -1,
981
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0,
982
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
983
+ greyoutColor: (store.greyoutPointColor ?? [0, 0, 0, 1]) as [number, number, number, number],
984
+ width: 0.85,
985
+ },
986
+ },
987
+ })
988
+ }
989
+
990
+ this.drawHighlightedCommand = new Model(device, {
991
+ fs: drawHighlightedFrag,
992
+ vs: drawHighlightedVert,
993
+ topology: 'triangle-strip',
994
+ vertexCount: 4,
995
+ attributes: {
996
+ vertexCoord: this.drawHighlightedVertexCoordBuffer,
997
+ },
998
+ bufferLayout: [
999
+ { name: 'vertexCoord', format: 'float32x2' },
1000
+ ],
1001
+ defines: {
1002
+ USE_UNIFORM_BUFFERS: true,
1003
+ },
1004
+ bindings: {
1005
+ drawHighlightedUniforms: this.drawHighlightedUniformStore.getManagedUniformBuffer(device, 'drawHighlightedUniforms'),
1006
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
1007
+ ...(this.greyoutStatusTexture && { pointGreyoutStatusTexture: this.greyoutStatusTexture }),
1008
+ },
1009
+ parameters: {
1010
+ blend: true,
1011
+ blendColorOperation: 'add',
1012
+ blendColorSrcFactor: 'src-alpha',
1013
+ blendColorDstFactor: 'one-minus-src-alpha',
1014
+ blendAlphaOperation: 'add',
1015
+ blendAlphaSrcFactor: 'one',
1016
+ blendAlphaDstFactor: 'one-minus-src-alpha',
1017
+ depthWriteEnabled: false,
1018
+ depthCompare: 'always',
1019
+ },
1020
+ })
1021
+ }
1022
+
1023
+ if (!this.trackPointsCommand) {
1024
+ // Create vertex buffer for quad
1025
+ if (!this.trackPointsVertexCoordBuffer) {
1026
+ this.trackPointsVertexCoordBuffer = device.createBuffer({
1027
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
1028
+ })
1029
+ }
1030
+
1031
+ // Create UniformStore for trackPoints uniforms
1032
+ if (!this.trackPointsUniformStore) {
1033
+ this.trackPointsUniformStore = new UniformStore({
1034
+ trackPointsUniforms: {
1035
+ uniformTypes: {
1036
+ // Order MUST match shader declaration order (std140 layout)
1037
+ pointsTextureSize: 'f32',
1038
+ },
1039
+ defaultUniforms: {
1040
+ pointsTextureSize: store.pointsTextureSize ?? 0,
1041
+ },
1042
+ },
1043
+ })
1044
+ }
1045
+
1046
+ this.trackPointsCommand = new Model(device, {
1047
+ fs: trackPositionsFrag,
1048
+ vs: updateVert,
1049
+ topology: 'triangle-strip',
1050
+ vertexCount: 4,
1051
+ attributes: {
1052
+ vertexCoord: this.trackPointsVertexCoordBuffer,
1053
+ },
1054
+ bufferLayout: [
1055
+ { name: 'vertexCoord', format: 'float32x2' },
1056
+ ],
1057
+ defines: {
1058
+ USE_UNIFORM_BUFFERS: true,
1059
+ },
1060
+ bindings: {
1061
+ trackPointsUniforms: this.trackPointsUniformStore.getManagedUniformBuffer(device, 'trackPointsUniforms'),
1062
+ ...(this.currentPositionTexture && { positionsTexture: this.currentPositionTexture }),
1063
+ ...(this.trackedIndicesTexture && { trackedIndices: this.trackedIndicesTexture }),
1064
+ },
1065
+ })
1066
+ }
1067
+ }
1068
+
1069
+ public updateColor (): void {
1070
+ const { device, store: { pointsTextureSize }, data } = this
1071
+ if (!pointsTextureSize) return
1072
+
1073
+ const colorData = data.pointColors as Float32Array
1074
+ const requiredByteLength = colorData.byteLength
1075
+
1076
+ if (!this.colorBuffer || this.colorBuffer.byteLength !== requiredByteLength) {
1077
+ this.colorBuffer?.destroy()
1078
+ this.colorBuffer = device.createBuffer({
1079
+ data: colorData,
1080
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
1081
+ })
1082
+ } else {
1083
+ this.colorBuffer.write(colorData)
1084
+ }
1085
+ }
1086
+
1087
+ public updateGreyoutStatus (): void {
1088
+ const { device, store: { selectedIndices, pointsTextureSize } } = this
1089
+ if (!pointsTextureSize) return
1090
+
1091
+ // Greyout status: 0 - false, highlighted or normal point; 1 - true, greyout point
1092
+ const initialState = new Float32Array(pointsTextureSize * pointsTextureSize * 4)
1093
+ .fill(selectedIndices ? 1 : 0)
1094
+
1095
+ if (selectedIndices) {
1096
+ for (const selectedIndex of selectedIndices) {
1097
+ initialState[selectedIndex * 4] = 0
1098
+ }
1099
+ }
1100
+
1101
+ if (!this.greyoutStatusTexture || this.greyoutStatusTexture.width !== pointsTextureSize || this.greyoutStatusTexture.height !== pointsTextureSize) {
1102
+ if (this.greyoutStatusTexture) {
1103
+ this.greyoutStatusTexture.destroy()
1104
+ }
1105
+ if (this.greyoutStatusFbo) {
1106
+ this.greyoutStatusFbo.destroy()
1107
+ }
1108
+ this.greyoutStatusTexture = device.createTexture({
1109
+ width: pointsTextureSize,
1110
+ height: pointsTextureSize,
1111
+ format: 'rgba32float',
1112
+ })
1113
+ this.greyoutStatusTexture.copyImageData({
1114
+ data: initialState,
1115
+ bytesPerRow: pointsTextureSize,
1116
+ mipLevel: 0,
1117
+ x: 0,
1118
+ y: 0,
1119
+ })
1120
+ this.greyoutStatusFbo = device.createFramebuffer({
1121
+ width: pointsTextureSize,
1122
+ height: pointsTextureSize,
1123
+ colorAttachments: [this.greyoutStatusTexture],
1124
+ })
1125
+ } else {
1126
+ this.greyoutStatusTexture.copyImageData({
1127
+ data: initialState,
1128
+ bytesPerRow: pointsTextureSize,
1129
+ mipLevel: 0,
1130
+ x: 0,
1131
+ y: 0,
1132
+ })
1133
+ }
1134
+ }
1135
+
1136
+ public updateSize (): void {
1137
+ const { device, store: { pointsTextureSize }, data } = this
1138
+ if (!pointsTextureSize || data.pointsNumber === undefined || data.pointSizes === undefined) return
1139
+
1140
+ const sizeData = data.pointSizes
1141
+ const requiredByteLength = sizeData.byteLength
1142
+
1143
+ if (!this.sizeBuffer || this.sizeBuffer.byteLength !== requiredByteLength) {
1144
+ this.sizeBuffer?.destroy()
1145
+ this.sizeBuffer = device.createBuffer({
1146
+ data: sizeData,
1147
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
1148
+ })
1149
+ } else {
1150
+ this.sizeBuffer.write(sizeData)
1151
+ }
1152
+
1153
+ const initialState = new Float32Array(pointsTextureSize * pointsTextureSize * 4)
1154
+ for (let i = 0; i < data.pointsNumber; i++) {
1155
+ initialState[i * 4] = data.pointSizes[i] as number
1156
+ }
1157
+
1158
+ if (!this.sizeTexture || this.sizeTexture.width !== pointsTextureSize || this.sizeTexture.height !== pointsTextureSize) {
1159
+ if (this.sizeTexture) {
1160
+ this.sizeTexture.destroy()
1161
+ }
1162
+ if (this.sizeFbo) {
1163
+ this.sizeFbo.destroy()
1164
+ }
1165
+ this.sizeTexture = device.createTexture({
1166
+ width: pointsTextureSize,
1167
+ height: pointsTextureSize,
1168
+ format: 'rgba32float',
1169
+ })
1170
+ this.sizeTexture.copyImageData({
1171
+ data: initialState,
1172
+ bytesPerRow: pointsTextureSize,
1173
+ mipLevel: 0,
1174
+ x: 0,
1175
+ y: 0,
1176
+ })
1177
+ this.sizeFbo = device.createFramebuffer({
1178
+ width: pointsTextureSize,
1179
+ height: pointsTextureSize,
1180
+ colorAttachments: [this.sizeTexture],
1181
+ })
1182
+ } else {
1183
+ this.sizeTexture.copyImageData({
1184
+ data: initialState,
1185
+ bytesPerRow: pointsTextureSize,
1186
+ mipLevel: 0,
1187
+ x: 0,
1188
+ y: 0,
1189
+ })
1190
+ }
1191
+ }
1192
+
1193
+ public updateShape (): void {
1194
+ const { device, data } = this
1195
+ if (data.pointsNumber === undefined || data.pointShapes === undefined) return
1196
+
1197
+ const shapeData = data.pointShapes
1198
+ const requiredByteLength = shapeData.byteLength
1199
+
1200
+ if (!this.shapeBuffer || this.shapeBuffer.byteLength !== requiredByteLength) {
1201
+ this.shapeBuffer?.destroy()
1202
+ this.shapeBuffer = device.createBuffer({
1203
+ data: shapeData,
1204
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
1205
+ })
1206
+ } else {
1207
+ this.shapeBuffer.write(shapeData)
1208
+ }
1209
+ }
1210
+
1211
+ public updateImageIndices (): void {
1212
+ const { device, data } = this
1213
+ if (data.pointsNumber === undefined || data.pointImageIndices === undefined) return
1214
+
1215
+ const imageIndicesData = data.pointImageIndices
1216
+ const requiredByteLength = imageIndicesData.byteLength
1217
+
1218
+ if (!this.imageIndicesBuffer || this.imageIndicesBuffer.byteLength !== requiredByteLength) {
1219
+ this.imageIndicesBuffer?.destroy()
1220
+ this.imageIndicesBuffer = device.createBuffer({
1221
+ data: imageIndicesData,
1222
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
1223
+ })
1224
+ } else {
1225
+ this.imageIndicesBuffer.write(imageIndicesData)
1226
+ }
1227
+ }
1228
+
1229
+ public updateImageSizes (): void {
1230
+ const { device, data } = this
1231
+ if (data.pointsNumber === undefined || data.pointImageSizes === undefined) return
1232
+
1233
+ const imageSizesData = data.pointImageSizes
1234
+ const requiredByteLength = imageSizesData.byteLength
1235
+
1236
+ if (!this.imageSizesBuffer || this.imageSizesBuffer.byteLength !== requiredByteLength) {
1237
+ this.imageSizesBuffer?.destroy()
1238
+ this.imageSizesBuffer = device.createBuffer({
1239
+ data: imageSizesData,
1240
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
1241
+ })
1242
+ } else {
1243
+ this.imageSizesBuffer.write(imageSizesData)
1244
+ }
1245
+ }
1246
+
1247
+ public createAtlas (): void {
1248
+ const { device, data, store } = this
1249
+
1250
+ if (!data.inputImageData?.length) {
1251
+ this.imageCount = 0
1252
+ this.imageAtlasCoordsTextureSize = 0
1253
+ // Create dummy textures so bindings are always available
1254
+ if (!this.imageAtlasCoordsTexture) {
1255
+ this.imageAtlasCoordsTexture = device.createTexture({
1256
+ data: new Float32Array(4).fill(0),
1257
+ width: 1,
1258
+ height: 1,
1259
+ format: 'rgba32float',
1260
+ })
1261
+ }
1262
+ if (!this.imageAtlasTexture) {
1263
+ this.imageAtlasTexture = device.createTexture({
1264
+ data: new Uint8Array(4).fill(0),
1265
+ width: 1,
1266
+ height: 1,
1267
+ format: 'rgba8unorm',
1268
+ })
1269
+ }
1270
+ return
1271
+ }
1272
+
1273
+ const atlasResult = createAtlasDataFromImageData(data.inputImageData, store.webglMaxTextureSize)
1274
+ if (!atlasResult) {
1275
+ console.warn('Failed to create atlas from image data')
1276
+ return
1277
+ }
1278
+
1279
+ this.imageCount = data.inputImageData.length
1280
+ const { atlasData, atlasSize, atlasCoords, atlasCoordsSize } = atlasResult
1281
+ this.imageAtlasCoordsTextureSize = atlasCoordsSize
1282
+
1283
+ // Recreate atlas texture to avoid row-stride/format issues
1284
+ this.imageAtlasTexture?.destroy()
1285
+ this.imageAtlasTexture = device.createTexture({
1286
+ width: atlasSize,
1287
+ height: atlasSize,
1288
+ format: 'rgba8unorm',
1289
+ })
1290
+ this.imageAtlasTexture.copyImageData({
1291
+ data: atlasData,
1292
+ // UNPACK_ROW_LENGTH and UNPACK_IMAGE_HEIGHT expect pixel counts (not bytes)
1293
+ bytesPerRow: atlasSize,
1294
+ rowsPerImage: atlasSize,
1295
+ mipLevel: 0,
1296
+ x: 0,
1297
+ y: 0,
1298
+ })
1299
+
1300
+ // Recreate coords texture
1301
+ this.imageAtlasCoordsTexture?.destroy()
1302
+ this.imageAtlasCoordsTexture = device.createTexture({
1303
+ width: atlasCoordsSize,
1304
+ height: atlasCoordsSize,
1305
+ format: 'rgba32float',
1306
+ })
1307
+ this.imageAtlasCoordsTexture.copyImageData({
1308
+ data: atlasCoords,
1309
+ // UNPACK_ROW_LENGTH and UNPACK_IMAGE_HEIGHT expect pixel counts (not bytes)
1310
+ bytesPerRow: atlasCoordsSize,
1311
+ rowsPerImage: atlasCoordsSize,
1312
+ mipLevel: 0,
1313
+ x: 0,
1314
+ y: 0,
1315
+ })
1316
+ }
1317
+
1318
+ public updateSampledPointsGrid (): void {
1319
+ const { store: { screenSize }, config: { pointSamplingDistance }, device } = this
1320
+ let dist = pointSamplingDistance ?? Math.min(...screenSize) / 2
1321
+ if (dist === 0) dist = defaultConfigValues.pointSamplingDistance
1322
+ const w = Math.ceil(screenSize[0] / dist)
1323
+ const h = Math.ceil(screenSize[1] / dist)
1324
+
1325
+ if (!this.sampledPointsFbo || this.sampledPointsFbo.width !== w || this.sampledPointsFbo.height !== h) {
1326
+ if (this.sampledPointsFbo && !this.sampledPointsFbo.destroyed) {
1327
+ this.sampledPointsFbo.destroy()
1328
+ }
1329
+ this.sampledPointsFbo = device.createFramebuffer({
1330
+ width: w,
1331
+ height: h,
1332
+ colorAttachments: ['rgba32float'],
1333
+ })
1334
+ }
1335
+ }
1336
+
1337
+ public trackPoints (): void {
1338
+ if (!this.trackedIndices?.length || !this.trackPointsCommand || !this.trackPointsUniformStore ||
1339
+ !this.trackedPositionsFbo || this.trackedPositionsFbo.destroyed) return
1340
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1341
+ if (!this.trackedIndicesTexture || this.trackedIndicesTexture.destroyed) return
1342
+
1343
+ this.trackPointsUniformStore.setUniforms({
1344
+ trackPointsUniforms: {
1345
+ pointsTextureSize: this.store.pointsTextureSize ?? 0,
1346
+ },
1347
+ })
1348
+
1349
+ this.trackPointsCommand.setBindings({
1350
+ trackPointsUniforms: this.trackPointsUniformStore.getManagedUniformBuffer(this.device, 'trackPointsUniforms'),
1351
+ positionsTexture: this.currentPositionTexture,
1352
+ trackedIndices: this.trackedIndicesTexture,
1353
+ })
1354
+
1355
+ const renderPass = this.device.beginRenderPass({
1356
+ framebuffer: this.trackedPositionsFbo,
1357
+ })
1358
+ this.trackPointsCommand.draw(renderPass)
1359
+ renderPass.end()
1360
+ }
1361
+
1362
+ public draw (renderPass: RenderPass): void {
1363
+ const { data, config, store } = this
1364
+ if (!this.colorBuffer) this.updateColor()
1365
+ if (!this.sizeBuffer) this.updateSize()
1366
+ if (!this.shapeBuffer) this.updateShape()
1367
+ if (!this.imageIndicesBuffer) this.updateImageIndices()
1368
+ if (!this.imageSizesBuffer) this.updateImageSizes()
1369
+
1370
+ if (!this.drawCommand || !this.drawUniformStore) return
1371
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1372
+ if (!this.greyoutStatusTexture || this.greyoutStatusTexture.destroyed) return
1373
+ if (!this.imageAtlasTexture || !this.imageAtlasCoordsTexture) {
1374
+ this.createAtlas()
1375
+ if (!this.imageAtlasTexture || !this.imageAtlasCoordsTexture) return
1376
+ }
1377
+ if (this.imageAtlasTexture.destroyed || this.imageAtlasCoordsTexture.destroyed) return
1378
+
1379
+ // Check if we have points to draw
1380
+ if (!data.pointsNumber || data.pointsNumber === 0) {
1381
+ return
1382
+ }
1383
+
1384
+ // Verify canvas is sized (screenSize must be non-zero to avoid division by zero in shader)
1385
+ if (!store.screenSize || store.screenSize[0] === 0 || store.screenSize[1] === 0) {
1386
+ return
1387
+ }
1388
+
1389
+ // Update vertex count dynamically
1390
+ this.drawCommand.setVertexCount(data.pointsNumber)
1391
+
1392
+ // Base uniforms that don't change between layers
1393
+ // Convert booleans to floats (1.0 or 0.0) since uniform type is 'f32'
1394
+ const baseVertexUniforms = {
1395
+ ratio: config.pixelRatio ?? defaultConfigValues.pixelRatio,
1396
+ transformationMatrix: store.transformationMatrix4x4,
1397
+ pointsTextureSize: store.pointsTextureSize ?? 0,
1398
+ sizeScale: config.pointSizeScale ?? 1,
1399
+ spaceSize: store.adjustedSpaceSize ?? 0,
1400
+ screenSize: store.screenSize ?? [0, 0],
1401
+ greyoutColor: (store.greyoutPointColor ?? [-1, -1, -1, -1]) as [number, number, number, number],
1402
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
1403
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0, // Convert boolean to float
1404
+ maxPointSize: store.maxPointSize ?? 100,
1405
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0, // Convert boolean to float
1406
+ hasImages: (this.imageCount > 0) ? 1 : 0, // Convert boolean to float
1407
+ imageCount: this.imageCount,
1408
+ imageAtlasCoordsTextureSize: this.imageAtlasCoordsTextureSize ?? 0,
1409
+ }
1410
+
1411
+ const baseFragmentUniforms = {
1412
+ greyoutOpacity: config.pointGreyoutOpacity ?? -1,
1413
+ pointOpacity: config.pointOpacity ?? 1,
1414
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0, // Convert boolean to float
1415
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
1416
+ }
1417
+
1418
+ // Render in layers: unselected points first (behind), then selected points (in front)
1419
+ if (store.selectedIndices && store.selectedIndices.length > 0) {
1420
+ // First draw unselected points (they will appear behind)
1421
+ this.drawUniformStore.setUniforms({
1422
+ drawVertexUniforms: {
1423
+ ...baseVertexUniforms,
1424
+ skipSelected: 1, // Skip selected points (1.0 for true)
1425
+ skipUnselected: 0, // Draw unselected points (0.0 for false)
1426
+ },
1427
+ drawFragmentUniforms: baseFragmentUniforms,
1428
+ })
1429
+
1430
+ this.drawCommand.setBindings({
1431
+ drawVertexUniforms: this.drawUniformStore.getManagedUniformBuffer(this.device, 'drawVertexUniforms'),
1432
+ drawFragmentUniforms: this.drawUniformStore.getManagedUniformBuffer(this.device, 'drawFragmentUniforms'),
1433
+ positionsTexture: this.currentPositionTexture,
1434
+ pointGreyoutStatus: this.greyoutStatusTexture,
1435
+ imageAtlasTexture: this.imageAtlasTexture,
1436
+ imageAtlasCoords: this.imageAtlasCoordsTexture,
1437
+ })
1438
+
1439
+ this.drawCommand.draw(renderPass)
1440
+
1441
+ // Then draw selected points (they will appear in front)
1442
+ this.drawUniformStore.setUniforms({
1443
+ drawVertexUniforms: {
1444
+ ...baseVertexUniforms,
1445
+ skipSelected: 0, // Draw selected points (0.0 for false)
1446
+ skipUnselected: 1, // Skip unselected points (1.0 for true)
1447
+ },
1448
+ drawFragmentUniforms: baseFragmentUniforms,
1449
+ })
1450
+
1451
+ this.drawCommand.setBindings({
1452
+ drawVertexUniforms: this.drawUniformStore.getManagedUniformBuffer(this.device, 'drawVertexUniforms'),
1453
+ drawFragmentUniforms: this.drawUniformStore.getManagedUniformBuffer(this.device, 'drawFragmentUniforms'),
1454
+ positionsTexture: this.currentPositionTexture,
1455
+ pointGreyoutStatus: this.greyoutStatusTexture,
1456
+ imageAtlasTexture: this.imageAtlasTexture,
1457
+ imageAtlasCoords: this.imageAtlasCoordsTexture,
1458
+ })
1459
+
1460
+ this.drawCommand.draw(renderPass)
1461
+ } else {
1462
+ // If no selection, draw all points
1463
+ this.drawUniformStore.setUniforms({
1464
+ drawVertexUniforms: {
1465
+ ...baseVertexUniforms,
1466
+ skipSelected: 0, // Draw all points (0.0 for false)
1467
+ skipUnselected: 0, // Draw all points (0.0 for false)
1468
+ },
1469
+ drawFragmentUniforms: baseFragmentUniforms,
1470
+ })
1471
+
1472
+ this.drawCommand.setBindings({
1473
+ drawVertexUniforms: this.drawUniformStore.getManagedUniformBuffer(this.device, 'drawVertexUniforms'),
1474
+ drawFragmentUniforms: this.drawUniformStore.getManagedUniformBuffer(this.device, 'drawFragmentUniforms'),
1475
+ positionsTexture: this.currentPositionTexture,
1476
+ pointGreyoutStatus: this.greyoutStatusTexture,
1477
+ imageAtlasTexture: this.imageAtlasTexture,
1478
+ imageAtlasCoords: this.imageAtlasCoordsTexture,
1479
+ })
1480
+
1481
+ this.drawCommand.draw(renderPass)
1482
+ }
1483
+
1484
+ // Draw highlighted point rings if enabled
1485
+ if (config.renderHoveredPointRing && store.hoveredPoint && this.drawHighlightedCommand && this.drawHighlightedUniformStore) {
1486
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1487
+ if (!this.greyoutStatusTexture || this.greyoutStatusTexture.destroyed) return
1488
+ const pointSize = data.pointSizes?.[store.hoveredPoint.index] ?? 1
1489
+ this.drawHighlightedUniformStore.setUniforms({
1490
+ drawHighlightedUniforms: {
1491
+ size: pointSize,
1492
+ transformationMatrix: store.transformationMatrix4x4,
1493
+ pointsTextureSize: store.pointsTextureSize ?? 0,
1494
+ sizeScale: config.pointSizeScale ?? 1,
1495
+ spaceSize: store.adjustedSpaceSize ?? 0,
1496
+ screenSize: store.screenSize ?? [0, 0],
1497
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0,
1498
+ pointIndex: store.hoveredPoint.index,
1499
+ maxPointSize: store.maxPointSize ?? 100,
1500
+ color: (store.hoveredPointRingColor as [number, number, number, number]),
1501
+ universalPointOpacity: config.pointOpacity ?? 1,
1502
+ greyoutOpacity: config.pointGreyoutOpacity ?? -1,
1503
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0,
1504
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
1505
+ greyoutColor: (store.greyoutPointColor ?? [0, 0, 0, 1]) as [number, number, number, number],
1506
+ width: 0.85,
1507
+ },
1508
+ })
1509
+ this.drawHighlightedCommand.setBindings({
1510
+ drawHighlightedUniforms: this.drawHighlightedUniformStore.getManagedUniformBuffer(this.device, 'drawHighlightedUniforms'),
1511
+ positionsTexture: this.currentPositionTexture,
1512
+ pointGreyoutStatusTexture: this.greyoutStatusTexture,
1513
+ })
1514
+ this.drawHighlightedCommand.draw(renderPass)
1515
+ }
1516
+
1517
+ if (store.focusedPoint && this.drawHighlightedCommand && this.drawHighlightedUniformStore) {
1518
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1519
+ if (!this.greyoutStatusTexture || this.greyoutStatusTexture.destroyed) return
1520
+ const pointSize = data.pointSizes?.[store.focusedPoint.index] ?? 1
1521
+ this.drawHighlightedUniformStore.setUniforms({
1522
+ drawHighlightedUniforms: {
1523
+ size: pointSize,
1524
+ transformationMatrix: store.transformationMatrix4x4,
1525
+ pointsTextureSize: store.pointsTextureSize ?? 0,
1526
+ sizeScale: config.pointSizeScale ?? 1,
1527
+ spaceSize: store.adjustedSpaceSize ?? 0,
1528
+ screenSize: store.screenSize ?? [0, 0],
1529
+ scalePointsOnZoom: (config.scalePointsOnZoom ?? true) ? 1 : 0,
1530
+ pointIndex: store.focusedPoint.index,
1531
+ maxPointSize: store.maxPointSize ?? 100,
1532
+ color: (store.focusedPointRingColor as [number, number, number, number]),
1533
+ universalPointOpacity: config.pointOpacity ?? 1,
1534
+ greyoutOpacity: config.pointGreyoutOpacity ?? -1,
1535
+ isDarkenGreyout: (store.isDarkenGreyout ?? false) ? 1 : 0,
1536
+ backgroundColor: store.backgroundColor ?? [0, 0, 0, 1],
1537
+ greyoutColor: (store.greyoutPointColor ?? [0, 0, 0, 1]) as [number, number, number, number],
1538
+ width: 0.85,
1539
+ },
1540
+ })
1541
+ this.drawHighlightedCommand.setBindings({
1542
+ drawHighlightedUniforms: this.drawHighlightedUniformStore.getManagedUniformBuffer(this.device, 'drawHighlightedUniforms'),
1543
+ positionsTexture: this.currentPositionTexture,
1544
+ pointGreyoutStatusTexture: this.greyoutStatusTexture,
1545
+ })
1546
+ this.drawHighlightedCommand.draw(renderPass)
1547
+ }
1548
+ }
1549
+
1550
+ public updatePosition (): void {
1551
+ if (!this.updatePositionCommand || !this.updatePositionUniformStore || !this.currentPositionFbo || this.currentPositionFbo.destroyed) return
1552
+ if (!this.previousPositionTexture || this.previousPositionTexture.destroyed) return
1553
+ if (!this.velocityTexture || this.velocityTexture.destroyed) return
1554
+
1555
+ this.updatePositionUniformStore.setUniforms({
1556
+ updatePositionUniforms: {
1557
+ friction: this.config.simulationFriction ?? 0,
1558
+ spaceSize: this.store.adjustedSpaceSize ?? 0,
1559
+ },
1560
+ })
1561
+
1562
+ this.updatePositionCommand.setBindings({
1563
+ updatePositionUniforms: this.updatePositionUniformStore.getManagedUniformBuffer(this.device, 'updatePositionUniforms'),
1564
+ positionsTexture: this.previousPositionTexture,
1565
+ velocity: this.velocityTexture,
1566
+ })
1567
+
1568
+ const renderPass = this.device.beginRenderPass({
1569
+ framebuffer: this.currentPositionFbo,
1570
+ })
1571
+ this.updatePositionCommand.draw(renderPass)
1572
+ renderPass.end()
1573
+
1574
+ this.swapFbo()
1575
+ // Invalidate tracked positions cache since positions have changed
1576
+ this.isPositionsUpToDate = false
1577
+ }
1578
+
1579
+ public drag (): void {
1580
+ if (!this.dragPointCommand || !this.dragPointUniformStore || !this.currentPositionFbo || this.currentPositionFbo.destroyed) return
1581
+ if (!this.previousPositionTexture || this.previousPositionTexture.destroyed) return
1582
+
1583
+ this.dragPointUniformStore.setUniforms({
1584
+ dragPointUniforms: {
1585
+ mousePos: (this.store.mousePosition as [number, number]) ?? [0, 0],
1586
+ index: this.store.hoveredPoint?.index ?? -1,
1587
+ },
1588
+ })
1589
+
1590
+ this.dragPointCommand.setBindings({
1591
+ dragPointUniforms: this.dragPointUniformStore.getManagedUniformBuffer(this.device, 'dragPointUniforms'),
1592
+ positionsTexture: this.previousPositionTexture,
1593
+ })
1594
+
1595
+ const renderPass = this.device.beginRenderPass({
1596
+ framebuffer: this.currentPositionFbo,
1597
+ })
1598
+ this.dragPointCommand.draw(renderPass)
1599
+ renderPass.end()
1600
+
1601
+ this.swapFbo()
1602
+ // Invalidate tracked positions cache since positions have changed
1603
+ this.isPositionsUpToDate = false
1604
+ }
1605
+
1606
+ public findPointsOnAreaSelection (): void {
1607
+ if (!this.findPointsOnAreaSelectionCommand || !this.findPointsOnAreaSelectionUniformStore || !this.selectedFbo || this.selectedFbo.destroyed) return
1608
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1609
+ if (!this.sizeTexture || this.sizeTexture.destroyed) return
1610
+
1611
+ this.findPointsOnAreaSelectionUniformStore.setUniforms({
1612
+ findPointsOnAreaSelectionUniforms: {
1613
+ spaceSize: this.store.adjustedSpaceSize ?? 0,
1614
+ screenSize: this.store.screenSize ?? [0, 0],
1615
+ sizeScale: this.config.pointSizeScale ?? 1,
1616
+ transformationMatrix: this.store.transformationMatrix4x4,
1617
+ ratio: this.config.pixelRatio ?? defaultConfigValues.pixelRatio,
1618
+ selection0: (this.store.selectedArea?.[0] ?? [0, 0]) as [number, number],
1619
+ selection1: (this.store.selectedArea?.[1] ?? [0, 0]) as [number, number],
1620
+ scalePointsOnZoom: (this.config.scalePointsOnZoom ?? true) ? 1 : 0, // Convert boolean to number
1621
+ maxPointSize: this.store.maxPointSize ?? 100,
1622
+ },
1623
+ })
1624
+
1625
+ this.findPointsOnAreaSelectionCommand.setBindings({
1626
+ findPointsOnAreaSelectionUniforms: this.findPointsOnAreaSelectionUniformStore.getManagedUniformBuffer(this.device, 'findPointsOnAreaSelectionUniforms'),
1627
+ positionsTexture: this.currentPositionTexture,
1628
+ pointSize: this.sizeTexture,
1629
+ })
1630
+
1631
+ const renderPass = this.device.beginRenderPass({
1632
+ framebuffer: this.selectedFbo,
1633
+ })
1634
+ this.findPointsOnAreaSelectionCommand.draw(renderPass)
1635
+ renderPass.end()
1636
+ }
1637
+
1638
+ public findPointsOnPolygonSelection (): void {
1639
+ if (!this.findPointsOnPolygonSelectionCommand || !this.findPointsOnPolygonSelectionUniformStore || !this.selectedFbo || this.selectedFbo.destroyed) return
1640
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1641
+ if (!this.polygonPathTexture || this.polygonPathTexture.destroyed) return
1642
+
1643
+ this.findPointsOnPolygonSelectionUniformStore.setUniforms({
1644
+ findPointsOnPolygonSelectionUniforms: {
1645
+ spaceSize: this.store.adjustedSpaceSize ?? 0,
1646
+ screenSize: this.store.screenSize ?? [0, 0],
1647
+ transformationMatrix: this.store.transformationMatrix4x4,
1648
+ polygonPathLength: this.polygonPathLength,
1649
+ },
1650
+ })
1651
+
1652
+ this.findPointsOnPolygonSelectionCommand.setBindings({
1653
+ findPointsOnPolygonSelectionUniforms: this.findPointsOnPolygonSelectionUniformStore
1654
+ .getManagedUniformBuffer(this.device, 'findPointsOnPolygonSelectionUniforms'),
1655
+ positionsTexture: this.currentPositionTexture,
1656
+ polygonPathTexture: this.polygonPathTexture,
1657
+ })
1658
+
1659
+ const renderPass = this.device.beginRenderPass({
1660
+ framebuffer: this.selectedFbo,
1661
+ })
1662
+ this.findPointsOnPolygonSelectionCommand.draw(renderPass)
1663
+ renderPass.end()
1664
+ }
1665
+
1666
+ public updatePolygonPath (polygonPath: [number, number][]): void {
1667
+ const { device } = this
1668
+ this.polygonPathLength = polygonPath.length
1669
+
1670
+ if (polygonPath.length === 0) {
1671
+ if (this.polygonPathTexture && !this.polygonPathTexture.destroyed) {
1672
+ this.polygonPathTexture.destroy()
1673
+ }
1674
+ this.polygonPathTexture = undefined
1675
+ if (this.polygonPathFbo && !this.polygonPathFbo.destroyed) {
1676
+ this.polygonPathFbo.destroy()
1677
+ }
1678
+ this.polygonPathFbo = undefined
1679
+ return
1680
+ }
1681
+
1682
+ // Calculate texture size (square texture)
1683
+ const textureSize = Math.ceil(Math.sqrt(polygonPath.length))
1684
+ const textureData = new Float32Array(textureSize * textureSize * 4)
1685
+
1686
+ // Fill texture with polygon path points
1687
+ for (const [i, point] of polygonPath.entries()) {
1688
+ const [x, y] = point
1689
+ textureData[i * 4] = x
1690
+ textureData[i * 4 + 1] = y
1691
+ textureData[i * 4 + 2] = 0 // unused
1692
+ textureData[i * 4 + 3] = 0 // unused
1693
+ }
1694
+
1695
+ if (!this.polygonPathTexture || this.polygonPathTexture.width !== textureSize || this.polygonPathTexture.height !== textureSize) {
1696
+ if (this.polygonPathFbo && !this.polygonPathFbo.destroyed) {
1697
+ this.polygonPathFbo.destroy()
1698
+ }
1699
+ if (this.polygonPathTexture && !this.polygonPathTexture.destroyed) {
1700
+ this.polygonPathTexture.destroy()
1701
+ }
1702
+ this.polygonPathTexture = device.createTexture({
1703
+ width: textureSize,
1704
+ height: textureSize,
1705
+ format: 'rgba32float',
1706
+ })
1707
+ this.polygonPathTexture.copyImageData({
1708
+ data: textureData,
1709
+ bytesPerRow: textureSize,
1710
+ mipLevel: 0,
1711
+ x: 0,
1712
+ y: 0,
1713
+ })
1714
+ this.polygonPathFbo = device.createFramebuffer({
1715
+ width: textureSize,
1716
+ height: textureSize,
1717
+ colorAttachments: [this.polygonPathTexture],
1718
+ })
1719
+ } else {
1720
+ this.polygonPathTexture.copyImageData({
1721
+ data: textureData,
1722
+ bytesPerRow: textureSize,
1723
+ mipLevel: 0,
1724
+ x: 0,
1725
+ y: 0,
1726
+ })
1727
+ }
1728
+ }
1729
+
1730
+ public findHoveredPoint (): void {
1731
+ if (!this.hoveredFbo || this.hoveredFbo.destroyed) return
1732
+
1733
+ if (this.clearHoveredFboCommand) {
1734
+ const clearPass = this.device.beginRenderPass({
1735
+ framebuffer: this.hoveredFbo,
1736
+ })
1737
+ this.clearHoveredFboCommand.draw(clearPass)
1738
+ clearPass.end()
1739
+ }
1740
+
1741
+ if (!this.findHoveredPointCommand || !this.findHoveredPointUniformStore) return
1742
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return
1743
+
1744
+ this.findHoveredPointCommand.setVertexCount(this.data.pointsNumber ?? 0)
1745
+
1746
+ this.findHoveredPointCommand.setAttributes({
1747
+ ...(this.hoveredPointIndices && { pointIndices: this.hoveredPointIndices }),
1748
+ ...(this.sizeBuffer && { size: this.sizeBuffer }),
1749
+ })
1750
+
1751
+ this.findHoveredPointUniformStore.setUniforms({
1752
+ findHoveredPointUniforms: {
1753
+ ratio: this.config.pixelRatio ?? defaultConfigValues.pixelRatio,
1754
+ sizeScale: this.config.pointSizeScale ?? 1,
1755
+ pointsTextureSize: this.store.pointsTextureSize ?? 0,
1756
+ transformationMatrix: this.store.transformationMatrix4x4,
1757
+ spaceSize: this.store.adjustedSpaceSize ?? 0,
1758
+ screenSize: this.store.screenSize ?? [0, 0],
1759
+ scalePointsOnZoom: (this.config.scalePointsOnZoom ?? true) ? 1 : 0,
1760
+ mousePosition: (this.store.screenMousePosition ?? [0, 0]) as [number, number],
1761
+ maxPointSize: this.store.maxPointSize ?? 100,
1762
+ },
1763
+ })
1764
+
1765
+ this.findHoveredPointCommand.setBindings({
1766
+ findHoveredPointUniforms: this.findHoveredPointUniformStore.getManagedUniformBuffer(this.device, 'findHoveredPointUniforms'),
1767
+ positionsTexture: this.currentPositionTexture,
1768
+ })
1769
+
1770
+ const renderPass = this.device.beginRenderPass({
1771
+ framebuffer: this.hoveredFbo,
1772
+ })
1773
+ this.findHoveredPointCommand.draw(renderPass)
1774
+ renderPass.end()
1775
+ }
1776
+
1777
+ public trackPointsByIndices (indices?: number[] | undefined): void {
1778
+ const { store: { pointsTextureSize }, device } = this
1779
+ this.trackedIndices = indices
1780
+
1781
+ // Clear cache when changing tracked indices
1782
+ this.trackedPositions = undefined
1783
+ this.isPositionsUpToDate = false
1784
+
1785
+ if (!indices?.length || !pointsTextureSize) return
1786
+ const textureSize = Math.ceil(Math.sqrt(indices.length))
1787
+
1788
+ const initialState = new Float32Array(textureSize * textureSize * 4).fill(-1)
1789
+ for (const [i, sortedIndex] of indices.entries()) {
1790
+ if (sortedIndex !== undefined) {
1791
+ initialState[i * 4] = sortedIndex % pointsTextureSize
1792
+ initialState[i * 4 + 1] = Math.floor(sortedIndex / pointsTextureSize)
1793
+ initialState[i * 4 + 2] = 0
1794
+ initialState[i * 4 + 3] = 0
1795
+ }
1796
+ }
1797
+
1798
+ if (!this.trackedIndicesTexture || this.trackedIndicesTexture.width !== textureSize || this.trackedIndicesTexture.height !== textureSize) {
1799
+ if (this.trackedIndicesFbo && !this.trackedIndicesFbo.destroyed) {
1800
+ this.trackedIndicesFbo.destroy()
1801
+ }
1802
+ if (this.trackedIndicesTexture && !this.trackedIndicesTexture.destroyed) {
1803
+ this.trackedIndicesTexture.destroy()
1804
+ }
1805
+ this.trackedIndicesTexture = device.createTexture({
1806
+ width: textureSize,
1807
+ height: textureSize,
1808
+ format: 'rgba32float',
1809
+ })
1810
+ this.trackedIndicesTexture.copyImageData({
1811
+ data: initialState,
1812
+ bytesPerRow: textureSize,
1813
+ mipLevel: 0,
1814
+ x: 0,
1815
+ y: 0,
1816
+ })
1817
+ this.trackedIndicesFbo = device.createFramebuffer({
1818
+ width: textureSize,
1819
+ height: textureSize,
1820
+ colorAttachments: [this.trackedIndicesTexture],
1821
+ })
1822
+ } else {
1823
+ this.trackedIndicesTexture.copyImageData({
1824
+ data: initialState,
1825
+ bytesPerRow: textureSize,
1826
+ mipLevel: 0,
1827
+ x: 0,
1828
+ y: 0,
1829
+ })
1830
+ }
1831
+
1832
+ if (!this.trackedPositionsFbo || this.trackedPositionsFbo.width !== textureSize || this.trackedPositionsFbo.height !== textureSize) {
1833
+ if (this.trackedPositionsFbo && !this.trackedPositionsFbo.destroyed) {
1834
+ this.trackedPositionsFbo.destroy()
1835
+ }
1836
+ this.trackedPositionsFbo = device.createFramebuffer({
1837
+ width: textureSize,
1838
+ height: textureSize,
1839
+ colorAttachments: ['rgba32float'],
1840
+ })
1841
+ }
1842
+
1843
+ this.trackPoints()
1844
+ }
1845
+
1846
+ /**
1847
+ * Get current X and Y coordinates of the tracked points.
1848
+ *
1849
+ * When the simulation is disabled or stopped, this method returns a cached
1850
+ * result to avoid expensive GPU-to-CPU memory transfers (`readPixels`).
1851
+ *
1852
+ * @returns A ReadonlyMap where keys are point indices and values are [x, y] coordinates.
1853
+ */
1854
+ public getTrackedPositionsMap (): ReadonlyMap<number, [number, number]> {
1855
+ if (!this.trackedIndices) return new Map()
1856
+
1857
+ const { config: { enableSimulation }, store: { isSimulationRunning } } = this
1858
+
1859
+ // Use cached positions when simulation is inactive and cache is valid
1860
+ if ((!enableSimulation || !isSimulationRunning) &&
1861
+ this.isPositionsUpToDate &&
1862
+ this.trackedPositions) {
1863
+ return this.trackedPositions
1864
+ }
1865
+
1866
+ if (!this.trackedPositionsFbo || this.trackedPositionsFbo.destroyed) return new Map()
1867
+
1868
+ const pixels = readPixels(this.device, this.trackedPositionsFbo as Framebuffer)
1869
+
1870
+ const tracked = new Map<number, [number, number]>()
1871
+ for (let i = 0; i < pixels.length / 4; i += 1) {
1872
+ const x = pixels[i * 4]
1873
+ const y = pixels[i * 4 + 1]
1874
+ const index = this.trackedIndices[i]
1875
+ if (x !== undefined && y !== undefined && index !== undefined) {
1876
+ tracked.set(index, [x, y])
1877
+ }
1878
+ }
1879
+
1880
+ // If simulation is inactive, cache the result for next time
1881
+ if (!enableSimulation || !isSimulationRunning) {
1882
+ this.trackedPositions = tracked
1883
+ this.isPositionsUpToDate = true
1884
+ }
1885
+
1886
+ return tracked
1887
+ }
1888
+
1889
+ public getSampledPointPositionsMap (): Map<number, [number, number]> {
1890
+ const positions = new Map<number, [number, number]>()
1891
+ if (!this.sampledPointsFbo || this.sampledPointsFbo.destroyed) return positions
1892
+
1893
+ // Clear sampled points FBO
1894
+ if (this.clearSampledPointsFboCommand) {
1895
+ const clearPass = this.device.beginRenderPass({
1896
+ framebuffer: this.sampledPointsFbo,
1897
+ })
1898
+ this.clearSampledPointsFboCommand.draw(clearPass)
1899
+ clearPass.end()
1900
+ }
1901
+
1902
+ // Fill sampled points FBO
1903
+ if (this.fillSampledPointsFboCommand && this.fillSampledPointsUniformStore && this.sampledPointsFbo) {
1904
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return positions
1905
+ // Update vertex count dynamically
1906
+ this.fillSampledPointsFboCommand.setVertexCount(this.data.pointsNumber ?? 0)
1907
+
1908
+ this.fillSampledPointsUniformStore.setUniforms({
1909
+ fillSampledPointsUniforms: {
1910
+ pointsTextureSize: this.store.pointsTextureSize ?? 0,
1911
+ transformationMatrix: this.store.transformationMatrix4x4,
1912
+ spaceSize: this.store.adjustedSpaceSize ?? 0,
1913
+ screenSize: this.store.screenSize ?? [0, 0],
1914
+ },
1915
+ })
1916
+
1917
+ this.fillSampledPointsFboCommand.setBindings({
1918
+ fillSampledPointsUniforms: this.fillSampledPointsUniformStore.getManagedUniformBuffer(this.device, 'fillSampledPointsUniforms'),
1919
+ positionsTexture: this.currentPositionTexture,
1920
+ })
1921
+
1922
+ const fillPass = this.device.beginRenderPass({
1923
+ framebuffer: this.sampledPointsFbo,
1924
+ })
1925
+ this.fillSampledPointsFboCommand.draw(fillPass)
1926
+ fillPass.end()
1927
+ }
1928
+
1929
+ const pixels = readPixels(this.device, this.sampledPointsFbo as Framebuffer)
1930
+ for (let i = 0; i < pixels.length / 4; i++) {
1931
+ const index = pixels[i * 4]
1932
+ const isNotEmpty = !!pixels[i * 4 + 1]
1933
+ const x = pixels[i * 4 + 2]
1934
+ const y = pixels[i * 4 + 3]
1935
+
1936
+ if (isNotEmpty && index !== undefined && x !== undefined && y !== undefined) {
1937
+ positions.set(index, [x, y])
1938
+ }
1939
+ }
1940
+ return positions
1941
+ }
1942
+
1943
+ public getSampledPoints (): { indices: number[]; positions: number[] } {
1944
+ const indices: number[] = []
1945
+ const positions: number[] = []
1946
+ if (!this.sampledPointsFbo || this.sampledPointsFbo.destroyed) return { indices, positions }
1947
+
1948
+ // Clear sampled points FBO
1949
+ if (this.clearSampledPointsFboCommand) {
1950
+ const clearPass = this.device.beginRenderPass({
1951
+ framebuffer: this.sampledPointsFbo,
1952
+ })
1953
+ this.clearSampledPointsFboCommand.draw(clearPass)
1954
+ clearPass.end()
1955
+ }
1956
+
1957
+ // Fill sampled points FBO
1958
+ if (this.fillSampledPointsFboCommand && this.fillSampledPointsUniformStore && this.sampledPointsFbo) {
1959
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed) return { indices, positions }
1960
+ // Update vertex count dynamically
1961
+ this.fillSampledPointsFboCommand.setVertexCount(this.data.pointsNumber ?? 0)
1962
+
1963
+ this.fillSampledPointsUniformStore.setUniforms({
1964
+ fillSampledPointsUniforms: {
1965
+ pointsTextureSize: this.store.pointsTextureSize ?? 0,
1966
+ transformationMatrix: this.store.transformationMatrix4x4,
1967
+ spaceSize: this.store.adjustedSpaceSize ?? 0,
1968
+ screenSize: this.store.screenSize ?? [0, 0],
1969
+ },
1970
+ })
1971
+
1972
+ this.fillSampledPointsFboCommand.setBindings({
1973
+ fillSampledPointsUniforms: this.fillSampledPointsUniformStore.getManagedUniformBuffer(this.device, 'fillSampledPointsUniforms'),
1974
+ positionsTexture: this.currentPositionTexture,
1975
+ })
1976
+
1977
+ const fillPass = this.device.beginRenderPass({
1978
+ framebuffer: this.sampledPointsFbo,
1979
+ })
1980
+ this.fillSampledPointsFboCommand.draw(fillPass)
1981
+ fillPass.end()
1982
+ }
1983
+
1984
+ const pixels = readPixels(this.device, this.sampledPointsFbo as Framebuffer)
1985
+
1986
+ for (let i = 0; i < pixels.length / 4; i++) {
1987
+ const index = pixels[i * 4]
1988
+ const isNotEmpty = !!pixels[i * 4 + 1]
1989
+ const x = pixels[i * 4 + 2]
1990
+ const y = pixels[i * 4 + 3]
1991
+
1992
+ if (isNotEmpty && index !== undefined && x !== undefined && y !== undefined) {
1993
+ indices.push(index)
1994
+ positions.push(x, y)
1995
+ }
1996
+ }
1997
+
1998
+ return { indices, positions }
1999
+ }
2000
+
2001
+ public getTrackedPositionsArray (): number[] {
2002
+ const positions: number[] = []
2003
+ if (!this.trackedIndices) return positions
2004
+ if (!this.trackedPositionsFbo || this.trackedPositionsFbo.destroyed) return positions
2005
+ positions.length = this.trackedIndices.length * 2
2006
+ const pixels = readPixels(this.device, this.trackedPositionsFbo as Framebuffer)
2007
+ for (let i = 0; i < pixels.length / 4; i += 1) {
2008
+ const x = pixels[i * 4]
2009
+ const y = pixels[i * 4 + 1]
2010
+ const index = this.trackedIndices[i]
2011
+ if (x !== undefined && y !== undefined && index !== undefined) {
2012
+ positions[i * 2] = x
2013
+ positions[i * 2 + 1] = y
2014
+ }
2015
+ }
2016
+ return positions
2017
+ }
2018
+
2019
+ public destroy (): void {
2020
+ // Destroy UniformStore instances
2021
+ this.updatePositionUniformStore?.destroy()
2022
+ this.updatePositionUniformStore = undefined
2023
+ this.dragPointUniformStore?.destroy()
2024
+ this.dragPointUniformStore = undefined
2025
+ this.drawUniformStore?.destroy()
2026
+ this.drawUniformStore = undefined
2027
+ this.findPointsOnAreaSelectionUniformStore?.destroy()
2028
+ this.findPointsOnAreaSelectionUniformStore = undefined
2029
+ this.findPointsOnPolygonSelectionUniformStore?.destroy()
2030
+ this.findPointsOnPolygonSelectionUniformStore = undefined
2031
+ this.findHoveredPointUniformStore?.destroy()
2032
+ this.findHoveredPointUniformStore = undefined
2033
+ this.fillSampledPointsUniformStore?.destroy()
2034
+ this.fillSampledPointsUniformStore = undefined
2035
+ this.drawHighlightedUniformStore?.destroy()
2036
+ this.drawHighlightedUniformStore = undefined
2037
+ this.trackPointsUniformStore?.destroy()
2038
+ this.trackPointsUniformStore = undefined
2039
+
2040
+ // Destroy Models
2041
+ this.drawCommand?.destroy()
2042
+ this.drawCommand = undefined
2043
+ this.drawHighlightedCommand?.destroy()
2044
+ this.drawHighlightedCommand = undefined
2045
+ this.updatePositionCommand?.destroy()
2046
+ this.updatePositionCommand = undefined
2047
+ this.dragPointCommand?.destroy()
2048
+ this.dragPointCommand = undefined
2049
+ this.findPointsOnAreaSelectionCommand?.destroy()
2050
+ this.findPointsOnAreaSelectionCommand = undefined
2051
+ this.findPointsOnPolygonSelectionCommand?.destroy()
2052
+ this.findPointsOnPolygonSelectionCommand = undefined
2053
+ this.findHoveredPointCommand?.destroy()
2054
+ this.findHoveredPointCommand = undefined
2055
+ this.clearHoveredFboCommand?.destroy()
2056
+ this.clearHoveredFboCommand = undefined
2057
+ this.clearSampledPointsFboCommand?.destroy()
2058
+ this.clearSampledPointsFboCommand = undefined
2059
+ this.fillSampledPointsFboCommand?.destroy()
2060
+ this.fillSampledPointsFboCommand = undefined
2061
+ this.trackPointsCommand?.destroy()
2062
+ this.trackPointsCommand = undefined
2063
+
2064
+ // Destroy Framebuffers (destroy before textures they reference)
2065
+ if (this.currentPositionFbo && !this.currentPositionFbo.destroyed) {
2066
+ this.currentPositionFbo.destroy()
2067
+ }
2068
+ this.currentPositionFbo = undefined
2069
+ if (this.previousPositionFbo && !this.previousPositionFbo.destroyed) {
2070
+ this.previousPositionFbo.destroy()
2071
+ }
2072
+ this.previousPositionFbo = undefined
2073
+ if (this.velocityFbo && !this.velocityFbo.destroyed) {
2074
+ this.velocityFbo.destroy()
2075
+ }
2076
+ this.velocityFbo = undefined
2077
+ if (this.selectedFbo && !this.selectedFbo.destroyed) {
2078
+ this.selectedFbo.destroy()
2079
+ }
2080
+ this.selectedFbo = undefined
2081
+ if (this.hoveredFbo && !this.hoveredFbo.destroyed) {
2082
+ this.hoveredFbo.destroy()
2083
+ }
2084
+ this.hoveredFbo = undefined
2085
+ if (this.greyoutStatusFbo && !this.greyoutStatusFbo.destroyed) {
2086
+ this.greyoutStatusFbo.destroy()
2087
+ }
2088
+ this.greyoutStatusFbo = undefined
2089
+ if (this.sizeFbo && !this.sizeFbo.destroyed) {
2090
+ this.sizeFbo.destroy()
2091
+ }
2092
+ this.sizeFbo = undefined
2093
+ if (this.trackedIndicesFbo && !this.trackedIndicesFbo.destroyed) {
2094
+ this.trackedIndicesFbo.destroy()
2095
+ }
2096
+ this.trackedIndicesFbo = undefined
2097
+ if (this.trackedPositionsFbo && !this.trackedPositionsFbo.destroyed) {
2098
+ this.trackedPositionsFbo.destroy()
2099
+ }
2100
+ this.trackedPositionsFbo = undefined
2101
+ if (this.sampledPointsFbo && !this.sampledPointsFbo.destroyed) {
2102
+ this.sampledPointsFbo.destroy()
2103
+ }
2104
+ this.sampledPointsFbo = undefined
2105
+ if (this.polygonPathFbo && !this.polygonPathFbo.destroyed) {
2106
+ this.polygonPathFbo.destroy()
2107
+ }
2108
+ this.polygonPathFbo = undefined
2109
+
2110
+ // Destroy Textures
2111
+ if (this.currentPositionTexture && !this.currentPositionTexture.destroyed) {
2112
+ this.currentPositionTexture.destroy()
2113
+ }
2114
+ this.currentPositionTexture = undefined
2115
+ if (this.previousPositionTexture && !this.previousPositionTexture.destroyed) {
2116
+ this.previousPositionTexture.destroy()
2117
+ }
2118
+ this.previousPositionTexture = undefined
2119
+ if (this.velocityTexture && !this.velocityTexture.destroyed) {
2120
+ this.velocityTexture.destroy()
2121
+ }
2122
+ this.velocityTexture = undefined
2123
+ if (this.selectedTexture && !this.selectedTexture.destroyed) {
2124
+ this.selectedTexture.destroy()
2125
+ }
2126
+ this.selectedTexture = undefined
2127
+ if (this.greyoutStatusTexture && !this.greyoutStatusTexture.destroyed) {
2128
+ this.greyoutStatusTexture.destroy()
2129
+ }
2130
+ this.greyoutStatusTexture = undefined
2131
+ if (this.sizeTexture && !this.sizeTexture.destroyed) {
2132
+ this.sizeTexture.destroy()
2133
+ }
2134
+ this.sizeTexture = undefined
2135
+ if (this.trackedIndicesTexture && !this.trackedIndicesTexture.destroyed) {
2136
+ this.trackedIndicesTexture.destroy()
2137
+ }
2138
+ this.trackedIndicesTexture = undefined
2139
+ if (this.polygonPathTexture && !this.polygonPathTexture.destroyed) {
2140
+ this.polygonPathTexture.destroy()
2141
+ }
2142
+ this.polygonPathTexture = undefined
2143
+ if (this.imageAtlasTexture && !this.imageAtlasTexture.destroyed) {
2144
+ this.imageAtlasTexture.destroy()
2145
+ }
2146
+ this.imageAtlasTexture = undefined
2147
+ if (this.imageAtlasCoordsTexture && !this.imageAtlasCoordsTexture.destroyed) {
2148
+ this.imageAtlasCoordsTexture.destroy()
2149
+ }
2150
+ this.imageAtlasCoordsTexture = undefined
2151
+
2152
+ // Destroy Buffers
2153
+ if (this.colorBuffer && !this.colorBuffer.destroyed) {
2154
+ this.colorBuffer.destroy()
2155
+ }
2156
+ this.colorBuffer = undefined
2157
+ if (this.sizeBuffer && !this.sizeBuffer.destroyed) {
2158
+ this.sizeBuffer.destroy()
2159
+ }
2160
+ this.sizeBuffer = undefined
2161
+ if (this.shapeBuffer && !this.shapeBuffer.destroyed) {
2162
+ this.shapeBuffer.destroy()
2163
+ }
2164
+ this.shapeBuffer = undefined
2165
+ if (this.imageIndicesBuffer && !this.imageIndicesBuffer.destroyed) {
2166
+ this.imageIndicesBuffer.destroy()
2167
+ }
2168
+ this.imageIndicesBuffer = undefined
2169
+ if (this.imageSizesBuffer && !this.imageSizesBuffer.destroyed) {
2170
+ this.imageSizesBuffer.destroy()
2171
+ }
2172
+ this.imageSizesBuffer = undefined
2173
+ if (this.drawPointIndices && !this.drawPointIndices.destroyed) {
2174
+ this.drawPointIndices.destroy()
2175
+ }
2176
+ this.drawPointIndices = undefined
2177
+ if (this.hoveredPointIndices && !this.hoveredPointIndices.destroyed) {
2178
+ this.hoveredPointIndices.destroy()
2179
+ }
2180
+ this.hoveredPointIndices = undefined
2181
+ if (this.sampledPointIndices && !this.sampledPointIndices.destroyed) {
2182
+ this.sampledPointIndices.destroy()
2183
+ }
2184
+ this.sampledPointIndices = undefined
2185
+
2186
+ // Destroy attribute buffers (Model doesn't destroy them automatically)
2187
+ if (this.updatePositionVertexCoordBuffer && !this.updatePositionVertexCoordBuffer.destroyed) {
2188
+ this.updatePositionVertexCoordBuffer.destroy()
2189
+ }
2190
+ this.updatePositionVertexCoordBuffer = undefined
2191
+ if (this.dragPointVertexCoordBuffer && !this.dragPointVertexCoordBuffer.destroyed) {
2192
+ this.dragPointVertexCoordBuffer.destroy()
2193
+ }
2194
+ this.dragPointVertexCoordBuffer = undefined
2195
+ if (this.findPointsOnAreaSelectionVertexCoordBuffer && !this.findPointsOnAreaSelectionVertexCoordBuffer.destroyed) {
2196
+ this.findPointsOnAreaSelectionVertexCoordBuffer.destroy()
2197
+ }
2198
+ this.findPointsOnAreaSelectionVertexCoordBuffer = undefined
2199
+ if (this.findPointsOnPolygonSelectionVertexCoordBuffer && !this.findPointsOnPolygonSelectionVertexCoordBuffer.destroyed) {
2200
+ this.findPointsOnPolygonSelectionVertexCoordBuffer.destroy()
2201
+ }
2202
+ this.findPointsOnPolygonSelectionVertexCoordBuffer = undefined
2203
+ if (this.clearHoveredFboVertexCoordBuffer && !this.clearHoveredFboVertexCoordBuffer.destroyed) {
2204
+ this.clearHoveredFboVertexCoordBuffer.destroy()
2205
+ }
2206
+ this.clearHoveredFboVertexCoordBuffer = undefined
2207
+ if (this.clearSampledPointsFboVertexCoordBuffer && !this.clearSampledPointsFboVertexCoordBuffer.destroyed) {
2208
+ this.clearSampledPointsFboVertexCoordBuffer.destroy()
2209
+ }
2210
+ this.clearSampledPointsFboVertexCoordBuffer = undefined
2211
+ if (this.drawHighlightedVertexCoordBuffer && !this.drawHighlightedVertexCoordBuffer.destroyed) {
2212
+ this.drawHighlightedVertexCoordBuffer.destroy()
2213
+ }
2214
+ this.drawHighlightedVertexCoordBuffer = undefined
2215
+ if (this.trackPointsVertexCoordBuffer && !this.trackPointsVertexCoordBuffer.destroyed) {
2216
+ this.trackPointsVertexCoordBuffer.destroy()
2217
+ }
2218
+ this.trackPointsVertexCoordBuffer = undefined
2219
+ }
2220
+
2221
+ private swapFbo (): void {
2222
+ // Swap textures and framebuffers
2223
+ // Safety check: ensure resources exist and aren't destroyed before swapping
2224
+ if (!this.currentPositionTexture || this.currentPositionTexture.destroyed ||
2225
+ !this.previousPositionTexture || this.previousPositionTexture.destroyed ||
2226
+ !this.currentPositionFbo || this.currentPositionFbo.destroyed ||
2227
+ !this.previousPositionFbo || this.previousPositionFbo.destroyed) {
2228
+ return
2229
+ }
2230
+ const tempTexture = this.previousPositionTexture
2231
+ const tempFbo = this.previousPositionFbo
2232
+ this.previousPositionTexture = this.currentPositionTexture
2233
+ this.previousPositionFbo = this.currentPositionFbo
2234
+ this.currentPositionTexture = tempTexture
2235
+ this.currentPositionFbo = tempFbo
2236
+ }
2237
+
2238
+ private rescaleInitialNodePositions (): void {
2239
+ const { config: { spaceSize } } = this
2240
+ if (!this.data.pointPositions || !spaceSize) return
2241
+
2242
+ const points = this.data.pointPositions
2243
+ const pointsNumber = points.length / 2
2244
+ let minX = Infinity
2245
+ let maxX = -Infinity
2246
+ let minY = Infinity
2247
+ let maxY = -Infinity
2248
+ for (let i = 0; i < points.length; i += 2) {
2249
+ const x = points[i] as number
2250
+ const y = points[i + 1] as number
2251
+ minX = Math.min(minX, x)
2252
+ maxX = Math.max(maxX, x)
2253
+ minY = Math.min(minY, y)
2254
+ maxY = Math.max(maxY, y)
2255
+ }
2256
+ const w = maxX - minX
2257
+ const h = maxY - minY
2258
+ const range = Math.max(w, h)
2259
+
2260
+ // Do not rescale if the range is greater than the space size (no need to)
2261
+ if (range > spaceSize) {
2262
+ this.scaleX = undefined
2263
+ this.scaleY = undefined
2264
+ return
2265
+ }
2266
+
2267
+ // Density threshold - points per pixel ratio (0.001 = 0.1%)
2268
+ const densityThreshold = spaceSize * spaceSize * 0.001
2269
+ // Calculate effective space size based on point density
2270
+ const effectiveSpaceSize = pointsNumber > densityThreshold
2271
+ // For dense datasets: scale up based on point count, minimum 120% of space
2272
+ ? spaceSize * Math.max(1.2, Math.sqrt(pointsNumber) / spaceSize)
2273
+ // For sparse datasets: use 10% of space to cluster points closer
2274
+ : spaceSize * 0.1
2275
+
2276
+ // Calculate uniform scale factor to fit data within effective space
2277
+ const scaleFactor = effectiveSpaceSize / range
2278
+ // Center the data horizontally by adding padding on x-axis
2279
+ const offsetX = ((range - w) / 2) * scaleFactor
2280
+ // Center the data vertically by adding padding on y-axis
2281
+ const offsetY = ((range - h) / 2) * scaleFactor
2282
+
2283
+ this.scaleX = (x: number): number => (x - minX) * scaleFactor + offsetX
2284
+ this.scaleY = (y: number): number => (y - minY) * scaleFactor + offsetY
2285
+
2286
+ // Apply scaling to point positions
2287
+ for (let i = 0; i < pointsNumber; i++) {
2288
+ this.data.pointPositions[i * 2] = this.scaleX(points[i * 2] as number)
2289
+ this.data.pointPositions[i * 2 + 1] = this.scaleY(points[i * 2 + 1] as number)
2290
+ }
2291
+ }
2292
+ }