@buley/hexgrid-3d 1.0.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 (46) hide show
  1. package/.eslintrc.json +28 -0
  2. package/LICENSE +39 -0
  3. package/README.md +291 -0
  4. package/examples/basic-usage.tsx +52 -0
  5. package/package.json +65 -0
  6. package/public/hexgrid-worker.js +1763 -0
  7. package/rust/Cargo.toml +41 -0
  8. package/rust/src/lib.rs +740 -0
  9. package/rust/src/math.rs +574 -0
  10. package/rust/src/spatial.rs +245 -0
  11. package/rust/src/statistics.rs +496 -0
  12. package/src/HexGridEnhanced.ts +16 -0
  13. package/src/Snapshot.ts +1402 -0
  14. package/src/adapters.ts +65 -0
  15. package/src/algorithms/AdvancedStatistics.ts +328 -0
  16. package/src/algorithms/BayesianStatistics.ts +317 -0
  17. package/src/algorithms/FlowField.ts +126 -0
  18. package/src/algorithms/FluidSimulation.ts +99 -0
  19. package/src/algorithms/GraphAlgorithms.ts +184 -0
  20. package/src/algorithms/OutlierDetection.ts +391 -0
  21. package/src/algorithms/ParticleSystem.ts +85 -0
  22. package/src/algorithms/index.ts +13 -0
  23. package/src/compat.ts +96 -0
  24. package/src/components/HexGrid.tsx +31 -0
  25. package/src/components/NarrationOverlay.tsx +221 -0
  26. package/src/components/index.ts +2 -0
  27. package/src/features.ts +125 -0
  28. package/src/index.ts +30 -0
  29. package/src/math/HexCoordinates.ts +15 -0
  30. package/src/math/Matrix4.ts +35 -0
  31. package/src/math/Quaternion.ts +37 -0
  32. package/src/math/SpatialIndex.ts +114 -0
  33. package/src/math/Vector3.ts +69 -0
  34. package/src/math/index.ts +11 -0
  35. package/src/note-adapter.ts +124 -0
  36. package/src/ontology-adapter.ts +77 -0
  37. package/src/stores/index.ts +1 -0
  38. package/src/stores/uiStore.ts +85 -0
  39. package/src/types/index.ts +3 -0
  40. package/src/types.ts +152 -0
  41. package/src/utils/image-utils.ts +25 -0
  42. package/src/wasm/HexGridWasmWrapper.ts +753 -0
  43. package/src/wasm/index.ts +7 -0
  44. package/src/workers/hexgrid-math.ts +177 -0
  45. package/src/workers/hexgrid-worker.worker.ts +1807 -0
  46. package/tsconfig.json +18 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * TypeScript wrapper for hexgrid-wasm
3
+ *
4
+ * Provides a seamless interface to the Rust/WASM module with
5
+ * automatic fallback to pure TypeScript implementations.
6
+ *
7
+ * @module wasm/HexGridWasmWrapper
8
+ */
9
+
10
+ // Types matching the Rust structs
11
+ export interface WasmCellState {
12
+ owner: number
13
+ population: number
14
+ infectedBy: number
15
+ infection: number
16
+ resistance: number
17
+ flags: number
18
+ }
19
+
20
+ export interface WasmFluidSource {
21
+ x: number
22
+ y: number
23
+ radius: number
24
+ density: number
25
+ velocityX: number
26
+ velocityY: number
27
+ color?: [number, number, number]
28
+ }
29
+
30
+ // WASM module interface (generated by wasm-bindgen)
31
+ interface HexGridWasmModule {
32
+ HexGridWasm: {
33
+ new (width: number, height: number): HexGridWasmInstance
34
+ }
35
+ FlowFieldWasm: {
36
+ new (width: number, height: number): FlowFieldWasmInstance
37
+ }
38
+ }
39
+
40
+ interface HexGridWasmInstance {
41
+ get_owner(index: number): number
42
+ set_owner(index: number, owner: number): void
43
+ set_population(index: number, population: number): void
44
+ get_neighbors(index: number): Int32Array
45
+ step_infection(infectionRate: number, infectionThreshold: number): Uint32Array
46
+ find_connected_regions(owner: number): Uint32Array
47
+ find_border_cells(owner: number): Uint32Array
48
+ find_path(start: number, end: number, ownerFilter: number): Uint32Array
49
+ get_territory_counts(): Uint32Array
50
+ compute_gini(): number
51
+ compute_entropy(): number
52
+ kmeans_cluster(k: number, iterations: number): Uint32Array
53
+ size(): number
54
+ width(): number
55
+ height(): number
56
+ clear(): void
57
+ free(): void
58
+ }
59
+
60
+ interface FlowFieldWasmInstance {
61
+ add_source(x: number, y: number, strength: number): void
62
+ add_vortex(x: number, y: number, strength: number): void
63
+ sample(x: number, y: number): Float32Array
64
+ compute_divergence(): Float32Array
65
+ compute_curl(): Float32Array
66
+ clear(): void
67
+ free(): void
68
+ }
69
+
70
+ /**
71
+ * Wrapper class that provides TypeScript interface to WASM
72
+ * with automatic fallback
73
+ */
74
+ export class HexGridWasmWrapper {
75
+ private static module: HexGridWasmModule | null = null
76
+ private static loading: Promise<HexGridWasmModule | null> | null = null
77
+ private static loadFailed = false
78
+
79
+ private wasmInstance: HexGridWasmInstance | null = null
80
+ private fallbackData: {
81
+ width: number
82
+ height: number
83
+ owners: Uint8Array
84
+ populations: Float32Array
85
+ neighborCache: Int32Array[]
86
+ } | null = null
87
+
88
+ private constructor(
89
+ private readonly width: number,
90
+ private readonly height: number
91
+ ) {}
92
+
93
+ /**
94
+ * Load the WASM module
95
+ */
96
+ static async loadModule(): Promise<boolean> {
97
+ if (this.loadFailed) return false
98
+ if (this.module) return true
99
+
100
+ if (this.loading) {
101
+ const result = await this.loading
102
+ return result !== null
103
+ }
104
+
105
+ this.loading = (async () => {
106
+ try {
107
+ // Try to dynamically import the WASM module
108
+ const wasmModule = await import('../rust/pkg/hexgrid_wasm')
109
+ await wasmModule.default()
110
+ this.module = wasmModule as unknown as HexGridWasmModule
111
+ console.log('[HexGrid] WASM module loaded successfully')
112
+ return this.module
113
+ } catch (error) {
114
+ console.warn('[HexGrid] WASM module not available, using fallback:', error)
115
+ this.loadFailed = true
116
+ return null
117
+ }
118
+ })()
119
+
120
+ const result = await this.loading
121
+ return result !== null
122
+ }
123
+
124
+ /**
125
+ * Create a new HexGrid wrapper
126
+ */
127
+ static async create(width: number, height: number): Promise<HexGridWasmWrapper> {
128
+ const wrapper = new HexGridWasmWrapper(width, height)
129
+
130
+ const hasWasm = await this.loadModule()
131
+
132
+ if (hasWasm && this.module) {
133
+ wrapper.wasmInstance = new this.module.HexGridWasm(width, height)
134
+ } else {
135
+ // Initialize fallback data
136
+ const size = width * height
137
+ wrapper.fallbackData = {
138
+ width,
139
+ height,
140
+ owners: new Uint8Array(size),
141
+ populations: new Float32Array(size),
142
+ neighborCache: []
143
+ }
144
+
145
+ // Precompute neighbors
146
+ for (let y = 0; y < height; y++) {
147
+ for (let x = 0; x < width; x++) {
148
+ const neighbors = new Int32Array(6).fill(-1)
149
+ const offset = y % 2 === 1 ? 1 : 0
150
+
151
+ // Odd-r offset coordinates
152
+ const dirs = y % 2 === 1
153
+ ? [[1, 0], [0, -1], [-1, -1], [-1, 0], [-1, 1], [0, 1]]
154
+ : [[1, 0], [1, -1], [0, -1], [-1, 0], [0, 1], [1, 1]]
155
+
156
+ for (let i = 0; i < 6; i++) {
157
+ const nx = x + dirs[i][0]
158
+ const ny = y + dirs[i][1]
159
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
160
+ neighbors[i] = ny * width + nx
161
+ }
162
+ }
163
+
164
+ wrapper.fallbackData.neighborCache.push(neighbors)
165
+ }
166
+ }
167
+ }
168
+
169
+ return wrapper
170
+ }
171
+
172
+ /**
173
+ * Check if WASM is being used
174
+ */
175
+ isUsingWasm(): boolean {
176
+ return this.wasmInstance !== null
177
+ }
178
+
179
+ /**
180
+ * Get cell owner
181
+ */
182
+ getOwner(index: number): number {
183
+ if (this.wasmInstance) {
184
+ return this.wasmInstance.get_owner(index)
185
+ }
186
+ return this.fallbackData?.owners[index] ?? 0
187
+ }
188
+
189
+ /**
190
+ * Set cell owner
191
+ */
192
+ setOwner(index: number, owner: number): void {
193
+ if (this.wasmInstance) {
194
+ this.wasmInstance.set_owner(index, owner)
195
+ } else if (this.fallbackData) {
196
+ this.fallbackData.owners[index] = owner
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Set cell population
202
+ */
203
+ setPopulation(index: number, population: number): void {
204
+ if (this.wasmInstance) {
205
+ this.wasmInstance.set_population(index, population)
206
+ } else if (this.fallbackData) {
207
+ this.fallbackData.populations[index] = population
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Get neighbors of a cell
213
+ */
214
+ getNeighbors(index: number): Int32Array {
215
+ if (this.wasmInstance) {
216
+ return this.wasmInstance.get_neighbors(index)
217
+ }
218
+ return this.fallbackData?.neighborCache[index] ?? new Int32Array(6).fill(-1)
219
+ }
220
+
221
+ /**
222
+ * Step the infection simulation
223
+ */
224
+ stepInfection(infectionRate: number = 0.1, infectionThreshold: number = 1.0): number[] {
225
+ if (this.wasmInstance) {
226
+ return Array.from(this.wasmInstance.step_infection(infectionRate, infectionThreshold))
227
+ }
228
+
229
+ // Fallback implementation
230
+ const changed: number[] = []
231
+ const data = this.fallbackData!
232
+ const size = data.width * data.height
233
+
234
+ // Simple infection spread (basic fallback)
235
+ const newOwners = new Uint8Array(data.owners)
236
+
237
+ for (let i = 0; i < size; i++) {
238
+ if (data.owners[i] === 0) continue
239
+
240
+ const neighbors = data.neighborCache[i]
241
+ for (const ni of neighbors) {
242
+ if (ni < 0) continue
243
+
244
+ if (data.owners[ni] !== data.owners[i] && Math.random() < infectionRate) {
245
+ newOwners[ni] = data.owners[i]
246
+ changed.push(ni)
247
+ }
248
+ }
249
+ }
250
+
251
+ data.owners = newOwners
252
+ return changed
253
+ }
254
+
255
+ /**
256
+ * Find connected regions for an owner
257
+ */
258
+ findConnectedRegions(owner: number): number[] {
259
+ if (this.wasmInstance) {
260
+ return Array.from(this.wasmInstance.find_connected_regions(owner))
261
+ }
262
+
263
+ // Fallback: BFS implementation
264
+ const data = this.fallbackData!
265
+ const size = data.width * data.height
266
+ const regionIds = new Array(size).fill(0)
267
+ const visited = new Array(size).fill(false)
268
+ let currentRegion = 1
269
+
270
+ for (let start = 0; start < size; start++) {
271
+ if (visited[start] || data.owners[start] !== owner) continue
272
+
273
+ const queue = [start]
274
+ visited[start] = true
275
+
276
+ while (queue.length > 0) {
277
+ const idx = queue.shift()!
278
+ regionIds[idx] = currentRegion
279
+
280
+ for (const ni of data.neighborCache[idx]) {
281
+ if (ni < 0 || visited[ni] || data.owners[ni] !== owner) continue
282
+ visited[ni] = true
283
+ queue.push(ni)
284
+ }
285
+ }
286
+
287
+ currentRegion++
288
+ }
289
+
290
+ return regionIds
291
+ }
292
+
293
+ /**
294
+ * Find border cells for an owner
295
+ */
296
+ findBorderCells(owner: number): number[] {
297
+ if (this.wasmInstance) {
298
+ return Array.from(this.wasmInstance.find_border_cells(owner))
299
+ }
300
+
301
+ const data = this.fallbackData!
302
+ const borders: number[] = []
303
+
304
+ for (let i = 0; i < data.owners.length; i++) {
305
+ if (data.owners[i] !== owner) continue
306
+
307
+ const isBorder = data.neighborCache[i].some(ni => {
308
+ if (ni < 0) return true
309
+ return data.owners[ni] !== owner
310
+ })
311
+
312
+ if (isBorder) borders.push(i)
313
+ }
314
+
315
+ return borders
316
+ }
317
+
318
+ /**
319
+ * Find path between two cells
320
+ */
321
+ findPath(start: number, end: number, ownerFilter: number = 0): number[] {
322
+ if (this.wasmInstance) {
323
+ return Array.from(this.wasmInstance.find_path(start, end, ownerFilter))
324
+ }
325
+
326
+ // Fallback: A* implementation
327
+ const data = this.fallbackData!
328
+ const gScore = new Map<number, number>()
329
+ const fScore = new Map<number, number>()
330
+ const cameFrom = new Map<number, number>()
331
+ const openSet = new Set<number>([start])
332
+
333
+ const heuristic = (a: number, b: number) => {
334
+ const ax = a % data.width
335
+ const ay = Math.floor(a / data.width)
336
+ const bx = b % data.width
337
+ const by = Math.floor(b / data.width)
338
+ return Math.abs(bx - ax) + Math.abs(by - ay)
339
+ }
340
+
341
+ gScore.set(start, 0)
342
+ fScore.set(start, heuristic(start, end))
343
+
344
+ while (openSet.size > 0) {
345
+ // Find node with lowest fScore
346
+ let current = -1
347
+ let lowestF = Infinity
348
+ for (const node of openSet) {
349
+ const f = fScore.get(node) ?? Infinity
350
+ if (f < lowestF) {
351
+ lowestF = f
352
+ current = node
353
+ }
354
+ }
355
+
356
+ if (current === end) {
357
+ // Reconstruct path
358
+ const path: number[] = [current]
359
+ while (cameFrom.has(current)) {
360
+ current = cameFrom.get(current)!
361
+ path.unshift(current)
362
+ }
363
+ return path
364
+ }
365
+
366
+ openSet.delete(current)
367
+
368
+ for (const neighbor of data.neighborCache[current]) {
369
+ if (neighbor < 0) continue
370
+ if (ownerFilter !== 0 && data.owners[neighbor] !== ownerFilter) continue
371
+
372
+ const tentativeG = (gScore.get(current) ?? Infinity) + 1
373
+
374
+ if (tentativeG < (gScore.get(neighbor) ?? Infinity)) {
375
+ cameFrom.set(neighbor, current)
376
+ gScore.set(neighbor, tentativeG)
377
+ fScore.set(neighbor, tentativeG + heuristic(neighbor, end))
378
+ openSet.add(neighbor)
379
+ }
380
+ }
381
+ }
382
+
383
+ return [] // No path found
384
+ }
385
+
386
+ /**
387
+ * Get territory counts
388
+ */
389
+ getTerritoryCounts(): Map<number, number> {
390
+ if (this.wasmInstance) {
391
+ const counts = this.wasmInstance.get_territory_counts()
392
+ const result = new Map<number, number>()
393
+ for (let i = 0; i < counts.length; i++) {
394
+ if (counts[i] > 0) {
395
+ result.set(i, counts[i])
396
+ }
397
+ }
398
+ return result
399
+ }
400
+
401
+ const data = this.fallbackData!
402
+ const counts = new Map<number, number>()
403
+ for (const owner of data.owners) {
404
+ if (owner !== 0) {
405
+ counts.set(owner, (counts.get(owner) ?? 0) + 1)
406
+ }
407
+ }
408
+ return counts
409
+ }
410
+
411
+ /**
412
+ * Compute Gini coefficient
413
+ */
414
+ computeGini(): number {
415
+ if (this.wasmInstance) {
416
+ return this.wasmInstance.compute_gini()
417
+ }
418
+
419
+ const populations = Array.from(this.fallbackData?.populations ?? [])
420
+ .filter(p => p > 0)
421
+ .sort((a, b) => a - b)
422
+
423
+ if (populations.length === 0) return 0
424
+
425
+ const n = populations.length
426
+ let sum = 0
427
+
428
+ for (let i = 0; i < n; i++) {
429
+ sum += (2 * (i + 1) - n - 1) * populations[i]
430
+ }
431
+
432
+ const mean = populations.reduce((a, b) => a + b, 0) / n
433
+ if (mean === 0) return 0
434
+
435
+ return sum / (n * n * mean)
436
+ }
437
+
438
+ /**
439
+ * Compute Shannon entropy
440
+ */
441
+ computeEntropy(): number {
442
+ if (this.wasmInstance) {
443
+ return this.wasmInstance.compute_entropy()
444
+ }
445
+
446
+ const counts = this.getTerritoryCounts()
447
+ const total = Array.from(counts.values()).reduce((a, b) => a + b, 0)
448
+ if (total === 0) return 0
449
+
450
+ let entropy = 0
451
+ for (const count of counts.values()) {
452
+ if (count > 0) {
453
+ const p = count / total
454
+ entropy -= p * Math.log(p)
455
+ }
456
+ }
457
+
458
+ return entropy
459
+ }
460
+
461
+ /**
462
+ * K-means clustering
463
+ */
464
+ kmeansCluster(k: number, iterations: number = 20): number[] {
465
+ if (this.wasmInstance) {
466
+ return Array.from(this.wasmInstance.kmeans_cluster(k, iterations))
467
+ }
468
+
469
+ // Fallback implementation
470
+ const data = this.fallbackData!
471
+ const positions: Array<{ x: number; y: number; idx: number }> = []
472
+
473
+ for (let i = 0; i < data.owners.length; i++) {
474
+ if (data.owners[i] !== 0) {
475
+ positions.push({
476
+ x: i % data.width,
477
+ y: Math.floor(i / data.width),
478
+ idx: i
479
+ })
480
+ }
481
+ }
482
+
483
+ if (positions.length === 0 || k === 0) {
484
+ return new Array(data.owners.length).fill(0)
485
+ }
486
+
487
+ // Initialize centroids randomly
488
+ const centroids: Array<{ x: number; y: number }> = []
489
+ const shuffled = [...positions].sort(() => Math.random() - 0.5)
490
+ for (let i = 0; i < Math.min(k, shuffled.length); i++) {
491
+ centroids.push({ x: shuffled[i].x, y: shuffled[i].y })
492
+ }
493
+
494
+ const assignments = new Array(positions.length).fill(0)
495
+
496
+ // Run iterations
497
+ for (let iter = 0; iter < iterations; iter++) {
498
+ // Assign points to nearest centroid
499
+ for (let i = 0; i < positions.length; i++) {
500
+ let minDist = Infinity
501
+ let bestCluster = 0
502
+
503
+ for (let j = 0; j < centroids.length; j++) {
504
+ const dx = positions[i].x - centroids[j].x
505
+ const dy = positions[i].y - centroids[j].y
506
+ const dist = dx * dx + dy * dy
507
+
508
+ if (dist < minDist) {
509
+ minDist = dist
510
+ bestCluster = j
511
+ }
512
+ }
513
+
514
+ assignments[i] = bestCluster
515
+ }
516
+
517
+ // Update centroids
518
+ const sums = centroids.map(() => ({ x: 0, y: 0, count: 0 }))
519
+
520
+ for (let i = 0; i < positions.length; i++) {
521
+ const cluster = assignments[i]
522
+ sums[cluster].x += positions[i].x
523
+ sums[cluster].y += positions[i].y
524
+ sums[cluster].count++
525
+ }
526
+
527
+ for (let j = 0; j < centroids.length; j++) {
528
+ if (sums[j].count > 0) {
529
+ centroids[j].x = sums[j].x / sums[j].count
530
+ centroids[j].y = sums[j].y / sums[j].count
531
+ }
532
+ }
533
+ }
534
+
535
+ // Return cluster assignments for all cells
536
+ const result = new Array(data.owners.length).fill(0)
537
+ for (let i = 0; i < positions.length; i++) {
538
+ result[positions[i].idx] = assignments[i]
539
+ }
540
+
541
+ return result
542
+ }
543
+
544
+ /**
545
+ * Get size
546
+ */
547
+ size(): number {
548
+ return this.wasmInstance?.size() ?? (this.width * this.height)
549
+ }
550
+
551
+ /**
552
+ * Clear all cells
553
+ */
554
+ clear(): void {
555
+ if (this.wasmInstance) {
556
+ this.wasmInstance.clear()
557
+ } else if (this.fallbackData) {
558
+ this.fallbackData.owners.fill(0)
559
+ this.fallbackData.populations.fill(0)
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Dispose resources
565
+ */
566
+ dispose(): void {
567
+ if (this.wasmInstance) {
568
+ this.wasmInstance.free()
569
+ this.wasmInstance = null
570
+ }
571
+ this.fallbackData = null
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Flow Field WASM Wrapper
577
+ */
578
+ export class FlowFieldWasmWrapper {
579
+ private wasmInstance: FlowFieldWasmInstance | null = null
580
+ private fallbackData: {
581
+ width: number
582
+ height: number
583
+ velocityX: Float32Array
584
+ velocityY: Float32Array
585
+ } | null = null
586
+
587
+ private constructor(
588
+ private readonly width: number,
589
+ private readonly height: number
590
+ ) {}
591
+
592
+ static async create(width: number, height: number): Promise<FlowFieldWasmWrapper> {
593
+ const wrapper = new FlowFieldWasmWrapper(width, height)
594
+
595
+ const hasWasm = await HexGridWasmWrapper.loadModule()
596
+
597
+ if (hasWasm) {
598
+ try {
599
+ const module = await import('../rust/pkg/hexgrid_wasm')
600
+ wrapper.wasmInstance = new module.FlowFieldWasm(width, height)
601
+ } catch {
602
+ // Fall through to fallback
603
+ }
604
+ }
605
+
606
+ if (!wrapper.wasmInstance) {
607
+ const size = width * height
608
+ wrapper.fallbackData = {
609
+ width,
610
+ height,
611
+ velocityX: new Float32Array(size),
612
+ velocityY: new Float32Array(size)
613
+ }
614
+ }
615
+
616
+ return wrapper
617
+ }
618
+
619
+ isUsingWasm(): boolean {
620
+ return this.wasmInstance !== null
621
+ }
622
+
623
+ addSource(x: number, y: number, strength: number): void {
624
+ if (this.wasmInstance) {
625
+ this.wasmInstance.add_source(x, y, strength)
626
+ return
627
+ }
628
+
629
+ const data = this.fallbackData!
630
+ for (let j = 0; j < data.height; j++) {
631
+ for (let i = 0; i < data.width; i++) {
632
+ const dx = i - x
633
+ const dy = j - y
634
+ const distSq = dx * dx + dy * dy + 0.0001
635
+ const dist = Math.sqrt(distSq)
636
+
637
+ const idx = j * data.width + i
638
+ data.velocityX[idx] += strength * dx / (dist * distSq)
639
+ data.velocityY[idx] += strength * dy / (dist * distSq)
640
+ }
641
+ }
642
+ }
643
+
644
+ addVortex(x: number, y: number, strength: number): void {
645
+ if (this.wasmInstance) {
646
+ this.wasmInstance.add_vortex(x, y, strength)
647
+ return
648
+ }
649
+
650
+ const data = this.fallbackData!
651
+ for (let j = 0; j < data.height; j++) {
652
+ for (let i = 0; i < data.width; i++) {
653
+ const dx = i - x
654
+ const dy = j - y
655
+ const distSq = dx * dx + dy * dy + 0.0001
656
+
657
+ const idx = j * data.width + i
658
+ data.velocityX[idx] += -strength * dy / distSq
659
+ data.velocityY[idx] += strength * dx / distSq
660
+ }
661
+ }
662
+ }
663
+
664
+ sample(x: number, y: number): [number, number] {
665
+ if (this.wasmInstance) {
666
+ const result = this.wasmInstance.sample(x, y)
667
+ return [result[0], result[1]]
668
+ }
669
+
670
+ const data = this.fallbackData!
671
+ const x0 = Math.min(Math.floor(x), data.width - 2)
672
+ const y0 = Math.min(Math.floor(y), data.height - 2)
673
+ const x1 = x0 + 1
674
+ const y1 = y0 + 1
675
+
676
+ const tx = x - x0
677
+ const ty = y - y0
678
+
679
+ const i00 = y0 * data.width + x0
680
+ const i10 = y0 * data.width + x1
681
+ const i01 = y1 * data.width + x0
682
+ const i11 = y1 * data.width + x1
683
+
684
+ const vx = data.velocityX[i00] * (1 - tx) * (1 - ty)
685
+ + data.velocityX[i10] * tx * (1 - ty)
686
+ + data.velocityX[i01] * (1 - tx) * ty
687
+ + data.velocityX[i11] * tx * ty
688
+
689
+ const vy = data.velocityY[i00] * (1 - tx) * (1 - ty)
690
+ + data.velocityY[i10] * tx * (1 - ty)
691
+ + data.velocityY[i01] * (1 - tx) * ty
692
+ + data.velocityY[i11] * tx * ty
693
+
694
+ return [vx, vy]
695
+ }
696
+
697
+ computeDivergence(): Float32Array {
698
+ if (this.wasmInstance) {
699
+ return this.wasmInstance.compute_divergence()
700
+ }
701
+
702
+ const data = this.fallbackData!
703
+ const div = new Float32Array(data.width * data.height)
704
+
705
+ for (let j = 1; j < data.height - 1; j++) {
706
+ for (let i = 1; i < data.width - 1; i++) {
707
+ const idx = j * data.width + i
708
+ const dudx = (data.velocityX[idx + 1] - data.velocityX[idx - 1]) * 0.5
709
+ const dvdy = (data.velocityY[idx + data.width] - data.velocityY[idx - data.width]) * 0.5
710
+ div[idx] = dudx + dvdy
711
+ }
712
+ }
713
+
714
+ return div
715
+ }
716
+
717
+ computeCurl(): Float32Array {
718
+ if (this.wasmInstance) {
719
+ return this.wasmInstance.compute_curl()
720
+ }
721
+
722
+ const data = this.fallbackData!
723
+ const curl = new Float32Array(data.width * data.height)
724
+
725
+ for (let j = 1; j < data.height - 1; j++) {
726
+ for (let i = 1; i < data.width - 1; i++) {
727
+ const idx = j * data.width + i
728
+ const dvdx = (data.velocityY[idx + 1] - data.velocityY[idx - 1]) * 0.5
729
+ const dudy = (data.velocityX[idx + data.width] - data.velocityX[idx - data.width]) * 0.5
730
+ curl[idx] = dvdx - dudy
731
+ }
732
+ }
733
+
734
+ return curl
735
+ }
736
+
737
+ clear(): void {
738
+ if (this.wasmInstance) {
739
+ this.wasmInstance.clear()
740
+ } else if (this.fallbackData) {
741
+ this.fallbackData.velocityX.fill(0)
742
+ this.fallbackData.velocityY.fill(0)
743
+ }
744
+ }
745
+
746
+ dispose(): void {
747
+ if (this.wasmInstance) {
748
+ this.wasmInstance.free()
749
+ this.wasmInstance = null
750
+ }
751
+ this.fallbackData = null
752
+ }
753
+ }