@cosmos.gl/graph 2.6.2 → 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 -18
  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 -14658
  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,12 @@
1
+ #version 300 es
2
+ #ifdef GL_ES
3
+ precision highp float;
4
+ #endif
5
+
6
+ in vec4 rgba;
7
+
8
+ out vec4 fragColor;
9
+
10
+ void main() {
11
+ fragColor = rgba;
12
+ }
@@ -0,0 +1,38 @@
1
+ #version 300 es
2
+ #ifdef GL_ES
3
+ precision highp float;
4
+ #endif
5
+
6
+ uniform sampler2D positionsTexture;
7
+ uniform sampler2D clusterTexture;
8
+
9
+ #ifdef USE_UNIFORM_BUFFERS
10
+ layout(std140) uniform calculateCentermassUniforms {
11
+ float pointsTextureSize;
12
+ float clustersTextureSize;
13
+ } calculateCentermass;
14
+
15
+ #define pointsTextureSize calculateCentermass.pointsTextureSize
16
+ #define clustersTextureSize calculateCentermass.clustersTextureSize
17
+ #else
18
+ uniform float pointsTextureSize;
19
+ uniform float clustersTextureSize;
20
+ #endif
21
+
22
+ in vec2 pointIndices;
23
+
24
+ out vec4 rgba;
25
+
26
+ void main() {
27
+ vec4 pointPosition = texture(positionsTexture, pointIndices / pointsTextureSize);
28
+ rgba = vec4(pointPosition.xy, 1.0, 0.0);
29
+
30
+ vec4 pointClusterIndices = texture(clusterTexture, pointIndices / pointsTextureSize);
31
+ vec2 xy = vec2(0.0);
32
+ if (pointClusterIndices.x >= 0.0 && pointClusterIndices.y >= 0.0) {
33
+ xy = 2.0 * (pointClusterIndices.xy + 0.5) / clustersTextureSize - 1.0;
34
+ }
35
+
36
+ gl_Position = vec4(xy, 0.0, 1.0);
37
+ gl_PointSize = 1.0;
38
+ }
@@ -0,0 +1,55 @@
1
+ #version 300 es
2
+ #ifdef GL_ES
3
+ precision highp float;
4
+ #endif
5
+
6
+ uniform sampler2D positionsTexture;
7
+ uniform sampler2D centermassTexture;
8
+ uniform sampler2D clusterTexture;
9
+ uniform sampler2D clusterPositionsTexture;
10
+ uniform sampler2D clusterForceCoefficient;
11
+
12
+ #ifdef USE_UNIFORM_BUFFERS
13
+ layout(std140) uniform applyForcesUniforms {
14
+ float alpha;
15
+ float clustersTextureSize;
16
+ float clusterCoefficient;
17
+ } applyForces;
18
+
19
+ #define alpha applyForces.alpha
20
+ #define clustersTextureSize applyForces.clustersTextureSize
21
+ #define clusterCoefficient applyForces.clusterCoefficient
22
+ #else
23
+ uniform float alpha;
24
+ uniform float clustersTextureSize;
25
+ uniform float clusterCoefficient;
26
+ #endif
27
+
28
+ in vec2 textureCoords;
29
+
30
+ out vec4 fragColor;
31
+
32
+
33
+ void main() {
34
+ vec4 pointPosition = texture(positionsTexture, textureCoords);
35
+ vec4 velocity = vec4(0.0);
36
+ vec4 pointClusterIndices = texture(clusterTexture, textureCoords);
37
+ // no cluster, so no forces
38
+ if (pointClusterIndices.x >= 0.0 && pointClusterIndices.y >= 0.0) {
39
+ // positioning points to custom cluster position or either to the center of mass
40
+ vec2 clusterPositions = texture(clusterPositionsTexture, pointClusterIndices.xy / clustersTextureSize).xy;
41
+ if (clusterPositions.x < 0.0 || clusterPositions.y < 0.0) {
42
+ vec4 centermassValues = texture(centermassTexture, pointClusterIndices.xy / clustersTextureSize);
43
+ clusterPositions = centermassValues.xy / centermassValues.b;
44
+ }
45
+ vec4 clusterCustomCoeff = texture(clusterForceCoefficient, textureCoords);
46
+ vec2 distVector = clusterPositions.xy - pointPosition.xy;
47
+ float dist = length(distVector);
48
+ if (dist > 0.0) {
49
+ float addV = alpha * dist * clusterCoefficient * clusterCustomCoeff.r;
50
+ velocity.rg += addV * normalize(distVector);
51
+ }
52
+ }
53
+
54
+ fragColor = velocity;
55
+ }
@@ -0,0 +1,578 @@
1
+ import { Framebuffer, Buffer, Texture, UniformStore, RenderPass } from '@luma.gl/core'
2
+ import { Model } from '@luma.gl/engine'
3
+ import { CoreModule } from '@/graph/modules/core-module'
4
+ import calculateCentermassFrag from '@/graph/modules/Clusters/calculate-centermass.frag?raw'
5
+ import calculateCentermassVert from '@/graph/modules/Clusters/calculate-centermass.vert?raw'
6
+ import forceFrag from '@/graph/modules/Clusters/force-cluster.frag?raw'
7
+ import { createIndexesForBuffer } from '@/graph/modules/Shared/buffer'
8
+ import clearFrag from '@/graph/modules/Shared/clear.frag?raw'
9
+ import updateVert from '@/graph/modules/Shared/quad.vert?raw'
10
+
11
+ export class Clusters extends CoreModule {
12
+ public centermassFbo: Framebuffer | undefined
13
+ public clusterCount: number | undefined
14
+
15
+ private clusterFbo: Framebuffer | undefined
16
+ private clusterPositionsFbo: Framebuffer | undefined
17
+ private clusterForceCoefficientFbo: Framebuffer | undefined
18
+ private clearCentermassCommand: Model | undefined
19
+ private calculateCentermassCommand: Model | undefined
20
+ private applyForcesCommand: Model | undefined
21
+ private clusterTexture: Texture | undefined
22
+ private clusterPositionsTexture: Texture | undefined
23
+ private clusterForceCoefficientTexture: Texture | undefined
24
+ private centermassTexture: Texture | undefined
25
+ private pointIndices: Buffer | undefined
26
+ private clustersTextureSize: number | undefined
27
+
28
+ // Attribute buffers that need manual cleanup (Model doesn't destroy them)
29
+ private clearCentermassVertexCoordBuffer: Buffer | undefined
30
+ private applyForcesVertexCoordBuffer: Buffer | undefined
31
+
32
+ // Track previous sizes to detect changes
33
+ private previousPointsTextureSize: number | undefined
34
+ private previousClustersTextureSize: number | undefined
35
+ private previousClusterCount: number | undefined
36
+
37
+ // Uniform stores for scalar uniforms
38
+ private calculateCentermassUniformStore: UniformStore<{
39
+ calculateCentermassUniforms: {
40
+ pointsTextureSize: number;
41
+ clustersTextureSize: number;
42
+ };
43
+ }> | undefined
44
+
45
+ private applyForcesUniformStore: UniformStore<{
46
+ applyForcesUniforms: {
47
+ alpha: number;
48
+ clustersTextureSize: number;
49
+ clusterCoefficient: number;
50
+ };
51
+ }> | undefined
52
+
53
+ public create (): void {
54
+ const { device, store, data } = this
55
+ const { pointsTextureSize } = store
56
+ if (data.pointsNumber === undefined || (!data.pointClusters && !data.clusterPositions)) return
57
+
58
+ // Find the highest cluster index in the array and add 1 (since cluster indices start at 0).
59
+ this.clusterCount = (data.pointClusters ?? []).reduce<number>((max, clusterIndex) => {
60
+ if (clusterIndex === undefined || clusterIndex < 0) return max
61
+ return Math.max(max, clusterIndex)
62
+ }, 0) + 1
63
+
64
+ this.clustersTextureSize = Math.ceil(Math.sqrt(this.clusterCount))
65
+
66
+ // Check if sizes have changed - if so, we need to recreate textures/framebuffers
67
+ const sizesChanged =
68
+ this.previousPointsTextureSize !== pointsTextureSize ||
69
+ this.previousClustersTextureSize !== this.clustersTextureSize ||
70
+ this.previousClusterCount !== this.clusterCount
71
+
72
+ const pointsTextureDataSize = pointsTextureSize * pointsTextureSize * 4
73
+ const clustersTextureDataSize = this.clustersTextureSize * this.clustersTextureSize * 4
74
+
75
+ const clusterState = new Float32Array(pointsTextureDataSize)
76
+ const clusterPositions = new Float32Array(clustersTextureDataSize).fill(-1)
77
+ const clusterForceCoefficient = new Float32Array(pointsTextureDataSize).fill(1)
78
+ if (data.clusterPositions) {
79
+ for (let cluster = 0; cluster < this.clusterCount; ++cluster) {
80
+ clusterPositions[cluster * 4 + 0] = data.clusterPositions[cluster * 2 + 0] ?? -1
81
+ clusterPositions[cluster * 4 + 1] = data.clusterPositions[cluster * 2 + 1] ?? -1
82
+ }
83
+ }
84
+
85
+ for (let i = 0; i < data.pointsNumber; ++i) {
86
+ const clusterIndex = data.pointClusters?.[i]
87
+ if (clusterIndex === undefined) {
88
+ // no cluster, so no forces
89
+ clusterState[i * 4 + 0] = -1
90
+ clusterState[i * 4 + 1] = -1
91
+ } else {
92
+ clusterState[i * 4 + 0] = clusterIndex % this.clustersTextureSize
93
+ clusterState[i * 4 + 1] = Math.floor(clusterIndex / this.clustersTextureSize)
94
+ }
95
+
96
+ if (data.clusterStrength) clusterForceCoefficient[i * 4 + 0] = data.clusterStrength[i] ?? 1
97
+ }
98
+
99
+ // Handle clusterTexture - recreate if size changed, update data if size same
100
+ if (!this.clusterTexture || sizesChanged) {
101
+ // Destroy framebuffer FIRST (before texture)
102
+ if (this.clusterFbo && !this.clusterFbo.destroyed) {
103
+ this.clusterFbo.destroy()
104
+ }
105
+ // Then destroy texture
106
+ if (this.clusterTexture && !this.clusterTexture.destroyed) {
107
+ this.clusterTexture.destroy()
108
+ }
109
+ // Create new texture
110
+ this.clusterTexture = device.createTexture({
111
+ width: pointsTextureSize,
112
+ height: pointsTextureSize,
113
+ format: 'rgba32float',
114
+ usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST,
115
+ })
116
+ this.clusterTexture.copyImageData({
117
+ data: clusterState,
118
+ bytesPerRow: pointsTextureSize,
119
+ mipLevel: 0,
120
+ x: 0,
121
+ y: 0,
122
+ })
123
+ // Create new framebuffer with explicit dimensions
124
+ this.clusterFbo = device.createFramebuffer({
125
+ width: pointsTextureSize,
126
+ height: pointsTextureSize,
127
+ colorAttachments: [this.clusterTexture],
128
+ })
129
+ } else {
130
+ // Size hasn't changed, just update the data
131
+ this.clusterTexture.copyImageData({
132
+ data: clusterState,
133
+ bytesPerRow: pointsTextureSize,
134
+ mipLevel: 0,
135
+ x: 0,
136
+ y: 0,
137
+ })
138
+ }
139
+
140
+ // Handle clusterPositionsTexture
141
+ if (!this.clusterPositionsTexture || sizesChanged) {
142
+ // Destroy framebuffer FIRST
143
+ if (this.clusterPositionsFbo && !this.clusterPositionsFbo.destroyed) {
144
+ this.clusterPositionsFbo.destroy()
145
+ }
146
+ // Then destroy texture
147
+ if (this.clusterPositionsTexture && !this.clusterPositionsTexture.destroyed) {
148
+ this.clusterPositionsTexture.destroy()
149
+ }
150
+ // Create new texture
151
+ this.clusterPositionsTexture = device.createTexture({
152
+ width: this.clustersTextureSize,
153
+ height: this.clustersTextureSize,
154
+ format: 'rgba32float',
155
+ usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST,
156
+ })
157
+ this.clusterPositionsTexture.copyImageData({
158
+ data: clusterPositions,
159
+ bytesPerRow: this.clustersTextureSize,
160
+ mipLevel: 0,
161
+ x: 0,
162
+ y: 0,
163
+ })
164
+ // Create new framebuffer with explicit dimensions
165
+ this.clusterPositionsFbo = device.createFramebuffer({
166
+ width: this.clustersTextureSize,
167
+ height: this.clustersTextureSize,
168
+ colorAttachments: [this.clusterPositionsTexture],
169
+ })
170
+ } else {
171
+ // Update data
172
+ this.clusterPositionsTexture.copyImageData({
173
+ data: clusterPositions,
174
+ bytesPerRow: this.clustersTextureSize,
175
+ mipLevel: 0,
176
+ x: 0,
177
+ y: 0,
178
+ })
179
+ }
180
+
181
+ // Handle clusterForceCoefficientTexture
182
+ if (!this.clusterForceCoefficientTexture || sizesChanged) {
183
+ // Destroy framebuffer FIRST
184
+ if (this.clusterForceCoefficientFbo && !this.clusterForceCoefficientFbo.destroyed) {
185
+ this.clusterForceCoefficientFbo.destroy()
186
+ }
187
+ // Then destroy texture
188
+ if (this.clusterForceCoefficientTexture && !this.clusterForceCoefficientTexture.destroyed) {
189
+ this.clusterForceCoefficientTexture.destroy()
190
+ }
191
+ // Create new texture
192
+ this.clusterForceCoefficientTexture = device.createTexture({
193
+ width: pointsTextureSize,
194
+ height: pointsTextureSize,
195
+ format: 'rgba32float',
196
+ usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST,
197
+ })
198
+ this.clusterForceCoefficientTexture.copyImageData({
199
+ data: clusterForceCoefficient,
200
+ bytesPerRow: pointsTextureSize,
201
+ mipLevel: 0,
202
+ x: 0,
203
+ y: 0,
204
+ })
205
+ // Create new framebuffer with explicit dimensions
206
+ this.clusterForceCoefficientFbo = device.createFramebuffer({
207
+ width: pointsTextureSize,
208
+ height: pointsTextureSize,
209
+ colorAttachments: [this.clusterForceCoefficientTexture],
210
+ })
211
+ } else {
212
+ // Update data
213
+ this.clusterForceCoefficientTexture.copyImageData({
214
+ data: clusterForceCoefficient,
215
+ bytesPerRow: pointsTextureSize,
216
+ mipLevel: 0,
217
+ x: 0,
218
+ y: 0,
219
+ })
220
+ }
221
+
222
+ // Handle centermassTexture - only size depends on clustersTextureSize
223
+ if (!this.centermassTexture || this.previousClustersTextureSize !== this.clustersTextureSize) {
224
+ // Destroy framebuffer FIRST
225
+ if (this.centermassFbo && !this.centermassFbo.destroyed) {
226
+ this.centermassFbo.destroy()
227
+ }
228
+ // Then destroy texture
229
+ if (this.centermassTexture && !this.centermassTexture.destroyed) {
230
+ this.centermassTexture.destroy()
231
+ }
232
+ // Create new texture
233
+ this.centermassTexture = device.createTexture({
234
+ width: this.clustersTextureSize,
235
+ height: this.clustersTextureSize,
236
+ format: 'rgba32float',
237
+ usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST,
238
+ })
239
+ this.centermassTexture.copyImageData({
240
+ data: new Float32Array(clustersTextureDataSize).fill(0),
241
+ bytesPerRow: this.clustersTextureSize,
242
+ mipLevel: 0,
243
+ x: 0,
244
+ y: 0,
245
+ })
246
+ // Create new framebuffer with explicit dimensions
247
+ this.centermassFbo = device.createFramebuffer({
248
+ width: this.clustersTextureSize,
249
+ height: this.clustersTextureSize,
250
+ colorAttachments: [this.centermassTexture],
251
+ })
252
+ } else {
253
+ // Clear the centermass texture (fill with zeros)
254
+ this.centermassTexture.copyImageData({
255
+ data: new Float32Array(clustersTextureDataSize).fill(0),
256
+ bytesPerRow: this.clustersTextureSize,
257
+ mipLevel: 0,
258
+ x: 0,
259
+ y: 0,
260
+ })
261
+ }
262
+
263
+ // Update pointIndices buffer if pointsTextureSize changed
264
+ if (!this.pointIndices || this.previousPointsTextureSize !== pointsTextureSize) {
265
+ if (this.pointIndices && !this.pointIndices.destroyed) {
266
+ this.pointIndices.destroy()
267
+ }
268
+ const indexData = createIndexesForBuffer(store.pointsTextureSize)
269
+ this.pointIndices = device.createBuffer({
270
+ data: indexData,
271
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
272
+ })
273
+ }
274
+
275
+ // Update tracked sizes
276
+ this.previousPointsTextureSize = pointsTextureSize
277
+ this.previousClustersTextureSize = this.clustersTextureSize
278
+ this.previousClusterCount = this.clusterCount
279
+ }
280
+
281
+ public initPrograms (): void {
282
+ const { device, store, data, points } = this
283
+ // Use same check as create() and run() for consistency
284
+ if (!data.pointClusters && !data.clusterPositions) return
285
+
286
+ if (!this.clearCentermassCommand) {
287
+ // Create and track vertexCoord buffer
288
+ if (!this.clearCentermassVertexCoordBuffer) {
289
+ this.clearCentermassVertexCoordBuffer = device.createBuffer({
290
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
291
+ })
292
+ }
293
+
294
+ this.clearCentermassCommand = new Model(device, {
295
+ fs: clearFrag,
296
+ vs: updateVert,
297
+ topology: 'triangle-strip',
298
+ vertexCount: 4,
299
+ attributes: {
300
+ vertexCoord: this.clearCentermassVertexCoordBuffer,
301
+ },
302
+ bufferLayout: [
303
+ { name: 'vertexCoord', format: 'float32x2' }, // 2 floats per vertex
304
+ ],
305
+ })
306
+ }
307
+ if (!this.calculateCentermassCommand) {
308
+ // Ensure pointIndices buffer exists
309
+ if (!this.pointIndices) {
310
+ const indexData = createIndexesForBuffer(store.pointsTextureSize)
311
+ this.pointIndices = device.createBuffer({
312
+ data: indexData,
313
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
314
+ })
315
+ }
316
+
317
+ // Create UniformStore for calculateCentermass uniforms
318
+ if (!this.calculateCentermassUniformStore) {
319
+ this.calculateCentermassUniformStore = new UniformStore({
320
+ calculateCentermassUniforms: {
321
+ uniformTypes: {
322
+ pointsTextureSize: 'f32',
323
+ clustersTextureSize: 'f32',
324
+ },
325
+ defaultUniforms: {
326
+ pointsTextureSize: store.pointsTextureSize,
327
+ clustersTextureSize: (this.clustersTextureSize ?? 0),
328
+ },
329
+ },
330
+ })
331
+ }
332
+
333
+ this.calculateCentermassCommand = new Model(device, {
334
+ fs: calculateCentermassFrag,
335
+ vs: calculateCentermassVert,
336
+ topology: 'point-list',
337
+ vertexCount: data.pointsNumber ?? 0,
338
+ attributes: {
339
+ pointIndices: this.pointIndices,
340
+ },
341
+ bufferLayout: [
342
+ { name: 'pointIndices', format: 'float32x2' }, // 2 floats per vertex
343
+ ],
344
+ defines: {
345
+ USE_UNIFORM_BUFFERS: true, // Enable uniform buffers
346
+ },
347
+ bindings: {
348
+ // Uniform buffer via UniformStore (WebGPU-compatible)
349
+ calculateCentermassUniforms: this.calculateCentermassUniformStore.getManagedUniformBuffer(device, 'calculateCentermassUniforms'),
350
+ ...(this.clusterTexture && { clusterTexture: this.clusterTexture }),
351
+ ...(points?.previousPositionTexture && { positionsTexture: points.previousPositionTexture }),
352
+ },
353
+ parameters: {
354
+ blend: true,
355
+ blendColorOperation: 'add',
356
+ blendColorSrcFactor: 'one',
357
+ blendColorDstFactor: 'one',
358
+ blendAlphaOperation: 'add',
359
+ blendAlphaSrcFactor: 'one',
360
+ blendAlphaDstFactor: 'one',
361
+ depthWriteEnabled: false,
362
+ depthCompare: 'always',
363
+ },
364
+ })
365
+ }
366
+ if (!this.applyForcesCommand) {
367
+ // Create UniformStore for applyForces uniforms
368
+ if (!this.applyForcesUniformStore) {
369
+ this.applyForcesUniformStore = new UniformStore({
370
+ applyForcesUniforms: {
371
+ uniformTypes: {
372
+ alpha: 'f32',
373
+ clustersTextureSize: 'f32',
374
+ clusterCoefficient: 'f32',
375
+ },
376
+ defaultUniforms: {
377
+ alpha: store.alpha,
378
+ clustersTextureSize: (this.clustersTextureSize ?? 0),
379
+ clusterCoefficient: (this.config.simulationCluster ?? 0),
380
+ },
381
+ },
382
+ })
383
+ }
384
+
385
+ // Create and track vertexCoord buffer
386
+ if (!this.applyForcesVertexCoordBuffer) {
387
+ this.applyForcesVertexCoordBuffer = device.createBuffer({
388
+ data: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
389
+ })
390
+ }
391
+
392
+ this.applyForcesCommand = new Model(device, {
393
+ fs: forceFrag,
394
+ vs: updateVert,
395
+ topology: 'triangle-strip',
396
+ vertexCount: 4,
397
+ attributes: {
398
+ vertexCoord: this.applyForcesVertexCoordBuffer,
399
+ },
400
+ bufferLayout: [
401
+ { name: 'vertexCoord', format: 'float32x2' }, // 2 floats per vertex
402
+ ],
403
+ defines: {
404
+ USE_UNIFORM_BUFFERS: true, // Enable uniform buffers
405
+ },
406
+ bindings: {
407
+ // Uniform buffer via UniformStore (WebGPU-compatible)
408
+ applyForcesUniforms: this.applyForcesUniformStore.getManagedUniformBuffer(device, 'applyForcesUniforms'),
409
+ ...(this.clusterTexture && { clusterTexture: this.clusterTexture }),
410
+ ...(this.centermassTexture && { centermassTexture: this.centermassTexture }),
411
+ ...(this.clusterPositionsTexture && { clusterPositionsTexture: this.clusterPositionsTexture }),
412
+ ...(this.clusterForceCoefficientTexture && { clusterForceCoefficient: this.clusterForceCoefficientTexture }),
413
+ ...(points?.previousPositionTexture && { positionsTexture: points.previousPositionTexture }),
414
+ },
415
+ })
416
+ }
417
+ }
418
+
419
+ public calculateCentermass (): void {
420
+ // Add safety check
421
+ if (!this.calculateCentermassCommand || !this.calculateCentermassUniformStore) {
422
+ return
423
+ }
424
+
425
+ if (!this.centermassFbo || this.centermassFbo.destroyed) return
426
+ if (!this.clusterTexture || this.clusterTexture.destroyed) return
427
+ if (!this.points?.previousPositionTexture || this.points.previousPositionTexture.destroyed) return
428
+
429
+ // Update vertex count dynamically (using same fallback logic as initialization)
430
+ this.calculateCentermassCommand.setVertexCount(this.data.pointsNumber ?? 0)
431
+
432
+ // Update UniformStore with current values
433
+ this.calculateCentermassUniformStore.setUniforms({
434
+ calculateCentermassUniforms: {
435
+ pointsTextureSize: this.store.pointsTextureSize,
436
+ clustersTextureSize: (this.clustersTextureSize ?? 0),
437
+ },
438
+ })
439
+
440
+ // Update bindings dynamically
441
+ this.calculateCentermassCommand.setBindings({
442
+ calculateCentermassUniforms: this.calculateCentermassUniformStore.getManagedUniformBuffer(this.device, 'calculateCentermassUniforms'),
443
+ clusterTexture: this.clusterTexture,
444
+ positionsTexture: this.points.previousPositionTexture,
445
+ })
446
+
447
+ // Create a RenderPass for the centermass framebuffer
448
+ const centermassPass = this.device.beginRenderPass({
449
+ framebuffer: this.centermassFbo,
450
+ })
451
+
452
+ this.clearCentermassCommand?.draw(centermassPass)
453
+ this.calculateCentermassCommand.draw(centermassPass)
454
+
455
+ centermassPass.end()
456
+ }
457
+
458
+ public run (renderPass?: RenderPass): void {
459
+ if (!this.data.pointClusters && !this.data.clusterPositions) return
460
+
461
+ // Calculate centermass (creates its own RenderPass - different framebuffer)
462
+ this.calculateCentermass()
463
+
464
+ // Add safety check
465
+ if (!this.applyForcesCommand || !this.applyForcesUniformStore) {
466
+ return
467
+ }
468
+
469
+ // Add destroyed checks for resources before use
470
+ if (!this.clusterTexture || this.clusterTexture.destroyed) return
471
+ if (!this.centermassTexture || this.centermassTexture.destroyed) return
472
+ if (!this.clusterPositionsTexture || this.clusterPositionsTexture.destroyed) return
473
+ if (!this.clusterForceCoefficientTexture || this.clusterForceCoefficientTexture.destroyed) return
474
+ if (!this.points?.previousPositionTexture || this.points.previousPositionTexture.destroyed) return
475
+ if (!this.points?.velocityFbo || this.points.velocityFbo.destroyed) return
476
+
477
+ // Update UniformStore with current values
478
+ this.applyForcesUniformStore.setUniforms({
479
+ applyForcesUniforms: {
480
+ alpha: this.store.alpha,
481
+ clustersTextureSize: (this.clustersTextureSize ?? 0),
482
+ clusterCoefficient: this.config.simulationCluster ?? 0,
483
+ },
484
+ })
485
+
486
+ // Update bindings dynamically
487
+ this.applyForcesCommand.setBindings({
488
+ applyForcesUniforms: this.applyForcesUniformStore.getManagedUniformBuffer(this.device, 'applyForcesUniforms'),
489
+ clusterTexture: this.clusterTexture,
490
+ centermassTexture: this.centermassTexture,
491
+ clusterPositionsTexture: this.clusterPositionsTexture,
492
+ clusterForceCoefficient: this.clusterForceCoefficientTexture,
493
+ positionsTexture: this.points.previousPositionTexture,
494
+ })
495
+
496
+ // Use provided render pass or create one if not provided (backward compatibility)
497
+ if (renderPass) {
498
+ // Use the provided render pass (created in simulation loop)
499
+ this.applyForcesCommand.draw(renderPass)
500
+ } else {
501
+ // Create a RenderPass for the velocity framebuffer (fallback for backward compatibility)
502
+ const velocityPass = this.device.beginRenderPass({
503
+ framebuffer: this.points.velocityFbo,
504
+ })
505
+
506
+ this.applyForcesCommand.draw(velocityPass)
507
+
508
+ velocityPass.end()
509
+ }
510
+ }
511
+
512
+ public destroy (): void {
513
+ // Destroy UniformStore
514
+ this.calculateCentermassUniformStore?.destroy()
515
+ this.calculateCentermassUniformStore = undefined
516
+ this.applyForcesUniformStore?.destroy()
517
+ this.applyForcesUniformStore = undefined
518
+
519
+ // Destroy Models
520
+ this.clearCentermassCommand?.destroy()
521
+ this.clearCentermassCommand = undefined
522
+ this.calculateCentermassCommand?.destroy()
523
+ this.calculateCentermassCommand = undefined
524
+ this.applyForcesCommand?.destroy()
525
+ this.applyForcesCommand = undefined
526
+
527
+ // Destroy Framebuffers (destroy before textures they reference)
528
+ if (this.centermassFbo && !this.centermassFbo.destroyed) {
529
+ this.centermassFbo.destroy()
530
+ }
531
+ this.centermassFbo = undefined
532
+ if (this.clusterFbo && !this.clusterFbo.destroyed) {
533
+ this.clusterFbo.destroy()
534
+ }
535
+ this.clusterFbo = undefined
536
+ if (this.clusterPositionsFbo && !this.clusterPositionsFbo.destroyed) {
537
+ this.clusterPositionsFbo.destroy()
538
+ }
539
+ this.clusterPositionsFbo = undefined
540
+ if (this.clusterForceCoefficientFbo && !this.clusterForceCoefficientFbo.destroyed) {
541
+ this.clusterForceCoefficientFbo.destroy()
542
+ }
543
+ this.clusterForceCoefficientFbo = undefined
544
+
545
+ // Destroy Textures
546
+ if (this.clusterTexture && !this.clusterTexture.destroyed) {
547
+ this.clusterTexture.destroy()
548
+ }
549
+ this.clusterTexture = undefined
550
+ if (this.clusterPositionsTexture && !this.clusterPositionsTexture.destroyed) {
551
+ this.clusterPositionsTexture.destroy()
552
+ }
553
+ this.clusterPositionsTexture = undefined
554
+ if (this.clusterForceCoefficientTexture && !this.clusterForceCoefficientTexture.destroyed) {
555
+ this.clusterForceCoefficientTexture.destroy()
556
+ }
557
+ this.clusterForceCoefficientTexture = undefined
558
+ if (this.centermassTexture && !this.centermassTexture.destroyed) {
559
+ this.centermassTexture.destroy()
560
+ }
561
+ this.centermassTexture = undefined
562
+
563
+ // Destroy Buffers
564
+ if (this.pointIndices && !this.pointIndices.destroyed) {
565
+ this.pointIndices.destroy()
566
+ }
567
+ this.pointIndices = undefined
568
+ // Destroy attribute buffers (Model doesn't destroy them automatically)
569
+ if (this.clearCentermassVertexCoordBuffer && !this.clearCentermassVertexCoordBuffer.destroyed) {
570
+ this.clearCentermassVertexCoordBuffer.destroy()
571
+ }
572
+ this.clearCentermassVertexCoordBuffer = undefined
573
+ if (this.applyForcesVertexCoordBuffer && !this.applyForcesVertexCoordBuffer.destroyed) {
574
+ this.applyForcesVertexCoordBuffer.destroy()
575
+ }
576
+ this.applyForcesVertexCoordBuffer = undefined
577
+ }
578
+ }