@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,740 @@
1
+ //! HexGrid WASM - High-Performance Hexagonal Grid Computations
2
+ //!
3
+ //! This crate provides WebAssembly bindings for computationally intensive
4
+ //! hexgrid operations, achieving significant speedups over pure JavaScript.
5
+ //!
6
+ //! # Features
7
+ //! - O(1) neighbor lookups with precomputed tables
8
+ //! - SIMD-accelerated vector operations (when available)
9
+ //! - Efficient spatial indexing
10
+ //! - Optimized infection simulation
11
+ //! - Flow field computation
12
+ //!
13
+ //! # Usage from JavaScript
14
+ //! ```javascript
15
+ //! import init, { HexGridWasm } from './hexgrid_wasm';
16
+ //! await init();
17
+ //! const grid = new HexGridWasm(100, 100);
18
+ //! grid.stepInfection();
19
+ //! ```
20
+
21
+ mod spatial;
22
+ mod math;
23
+ mod statistics;
24
+
25
+ use wasm_bindgen::prelude::*;
26
+ use std::collections::{HashMap, HashSet, VecDeque};
27
+ use rand::prelude::*;
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ // INITIALIZATION
31
+ // ═══════════════════════════════════════════════════════════════════════════
32
+
33
+ /// Initialize panic hook for better error messages
34
+ #[wasm_bindgen(start)]
35
+ pub fn init_panic_hook() {
36
+ #[cfg(feature = "console_error_panic_hook")]
37
+ console_error_panic_hook::set_once();
38
+ }
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+ // CORE TYPES
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+
44
+ /// Axial hex coordinate
45
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46
+ pub struct Axial {
47
+ pub q: i32,
48
+ pub r: i32,
49
+ }
50
+
51
+ impl Axial {
52
+ #[inline]
53
+ pub fn new(q: i32, r: i32) -> Self {
54
+ Self { q, r }
55
+ }
56
+
57
+ /// Get the 6 neighbors in axial coordinates
58
+ #[inline]
59
+ pub fn neighbors(&self) -> [Axial; 6] {
60
+ const DIRECTIONS: [(i32, i32); 6] = [
61
+ (1, 0), (1, -1), (0, -1),
62
+ (-1, 0), (-1, 1), (0, 1),
63
+ ];
64
+
65
+ let mut result = [Axial::new(0, 0); 6];
66
+ for (i, (dq, dr)) in DIRECTIONS.iter().enumerate() {
67
+ result[i] = Axial::new(self.q + dq, self.r + dr);
68
+ }
69
+ result
70
+ }
71
+
72
+ /// Distance to another hex
73
+ #[inline]
74
+ pub fn distance(&self, other: &Axial) -> i32 {
75
+ let dq = self.q - other.q;
76
+ let dr = self.r - other.r;
77
+ let ds = -dq - dr;
78
+ (dq.abs() + dr.abs() + ds.abs()) / 2
79
+ }
80
+
81
+ /// Convert to cube coordinates
82
+ #[inline]
83
+ pub fn to_cube(&self) -> (i32, i32, i32) {
84
+ let x = self.q;
85
+ let z = self.r;
86
+ let y = -x - z;
87
+ (x, y, z)
88
+ }
89
+
90
+ /// Convert to pixel coordinates (pointy-top)
91
+ #[inline]
92
+ pub fn to_pixel(&self, size: f32) -> (f32, f32) {
93
+ let x = size * (3.0_f32.sqrt() * self.q as f32 + 3.0_f32.sqrt() / 2.0 * self.r as f32);
94
+ let y = size * (3.0 / 2.0 * self.r as f32);
95
+ (x, y)
96
+ }
97
+ }
98
+
99
+ /// Cell state in the grid
100
+ #[derive(Clone, Copy, Debug, Default)]
101
+ pub struct CellState {
102
+ pub owner: u8, // 0 = neutral, 1-255 = player ID
103
+ pub population: f32, // Strength/population
104
+ pub infected_by: u8, // Who is currently attacking
105
+ pub infection: f32, // Infection progress (0-1)
106
+ pub resistance: f32, // Resistance to infection
107
+ pub flags: u8, // Bit flags for special states
108
+ }
109
+
110
+ // ═══════════════════════════════════════════════════════════════════════════
111
+ // HEXGRID WASM
112
+ // ═══════════════════════════════════════════════════════════════════════════
113
+
114
+ /// Main WebAssembly interface for hexgrid computations
115
+ #[wasm_bindgen]
116
+ pub struct HexGridWasm {
117
+ width: usize,
118
+ height: usize,
119
+ cells: Vec<CellState>,
120
+ // Neighbor lookup table (precomputed for O(1) access)
121
+ neighbor_indices: Vec<[i32; 6]>,
122
+ // Spatial hash for efficient queries
123
+ spatial_hash: HashMap<(i32, i32), Vec<usize>>,
124
+ spatial_cell_size: f32,
125
+ // Infection queue for BFS
126
+ infection_queue: VecDeque<(usize, u8)>,
127
+ // Statistics
128
+ territory_counts: HashMap<u8, usize>,
129
+ total_population: f32,
130
+ // Random number generator
131
+ rng: rand::rngs::SmallRng,
132
+ }
133
+
134
+ #[wasm_bindgen]
135
+ impl HexGridWasm {
136
+ /// Create a new hexgrid
137
+ #[wasm_bindgen(constructor)]
138
+ pub fn new(width: usize, height: usize) -> Self {
139
+ let size = width * height;
140
+ let mut cells = Vec::with_capacity(size);
141
+ let mut neighbor_indices = Vec::with_capacity(size);
142
+
143
+ // Initialize cells and precompute neighbors
144
+ for y in 0..height {
145
+ for x in 0..width {
146
+ cells.push(CellState::default());
147
+
148
+ // Compute neighbor indices (handles odd-r offset coordinates)
149
+ let offset = if y % 2 == 1 { 1i32 } else { 0i32 };
150
+ let mut neighbors = [-1i32; 6];
151
+
152
+ // Direction offsets for odd-r offset coordinates
153
+ let dirs = if y % 2 == 1 {
154
+ [(1, 0), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1)]
155
+ } else {
156
+ [(1, 0), (1, -1), (0, -1), (-1, 0), (0, 1), (1, 1)]
157
+ };
158
+
159
+ for (i, (dx, dy)) in dirs.iter().enumerate() {
160
+ let nx = x as i32 + dx;
161
+ let ny = y as i32 + dy;
162
+
163
+ if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
164
+ neighbors[i] = (ny as usize * width + nx as usize) as i32;
165
+ }
166
+ }
167
+
168
+ neighbor_indices.push(neighbors);
169
+ }
170
+ }
171
+
172
+ Self {
173
+ width,
174
+ height,
175
+ cells,
176
+ neighbor_indices,
177
+ spatial_hash: HashMap::new(),
178
+ spatial_cell_size: 10.0,
179
+ infection_queue: VecDeque::new(),
180
+ territory_counts: HashMap::new(),
181
+ total_population: 0.0,
182
+ rng: rand::rngs::SmallRng::from_entropy(),
183
+ }
184
+ }
185
+
186
+ /// Get cell owner
187
+ #[wasm_bindgen]
188
+ pub fn get_owner(&self, index: usize) -> u8 {
189
+ self.cells.get(index).map(|c| c.owner).unwrap_or(0)
190
+ }
191
+
192
+ /// Set cell owner
193
+ #[wasm_bindgen]
194
+ pub fn set_owner(&mut self, index: usize, owner: u8) {
195
+ if let Some(cell) = self.cells.get_mut(index) {
196
+ // Update territory counts
197
+ if cell.owner != 0 {
198
+ *self.territory_counts.entry(cell.owner).or_insert(0) -= 1;
199
+ }
200
+ cell.owner = owner;
201
+ if owner != 0 {
202
+ *self.territory_counts.entry(owner).or_insert(0) += 1;
203
+ }
204
+ }
205
+ }
206
+
207
+ /// Set cell population
208
+ #[wasm_bindgen]
209
+ pub fn set_population(&mut self, index: usize, population: f32) {
210
+ if let Some(cell) = self.cells.get_mut(index) {
211
+ self.total_population -= cell.population;
212
+ cell.population = population;
213
+ self.total_population += population;
214
+ }
215
+ }
216
+
217
+ /// Get neighbors of a cell (returns JS array)
218
+ #[wasm_bindgen]
219
+ pub fn get_neighbors(&self, index: usize) -> Vec<i32> {
220
+ self.neighbor_indices.get(index)
221
+ .map(|n| n.to_vec())
222
+ .unwrap_or_default()
223
+ }
224
+
225
+ /// Step the infection simulation
226
+ #[wasm_bindgen]
227
+ pub fn step_infection(&mut self, infection_rate: f32, infection_threshold: f32) -> Vec<u32> {
228
+ let mut changed = Vec::new();
229
+
230
+ // Phase 1: Spread infection from borders
231
+ for i in 0..self.cells.len() {
232
+ let cell = self.cells[i];
233
+ if cell.owner == 0 { continue; }
234
+
235
+ let neighbors = &self.neighbor_indices[i];
236
+ for &neighbor_idx in neighbors.iter() {
237
+ if neighbor_idx < 0 { continue; }
238
+ let ni = neighbor_idx as usize;
239
+ let neighbor = &self.cells[ni];
240
+
241
+ // Can infect if: different owner and has population
242
+ if neighbor.owner != cell.owner && cell.population > 0.0 {
243
+ // Calculate infection power
244
+ let power = cell.population * infection_rate;
245
+ let resistance = (neighbor.resistance + neighbor.population).max(0.1);
246
+ let infection_delta = power / resistance;
247
+
248
+ if let Some(target) = self.cells.get_mut(ni) {
249
+ if target.infected_by == 0 || target.infected_by == cell.owner {
250
+ target.infected_by = cell.owner;
251
+ target.infection += infection_delta;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ // Phase 2: Convert fully infected cells
259
+ for i in 0..self.cells.len() {
260
+ let cell = &self.cells[i];
261
+ if cell.infection >= infection_threshold && cell.infected_by != 0 {
262
+ let new_owner = cell.infected_by;
263
+ let old_owner = cell.owner;
264
+
265
+ if let Some(target) = self.cells.get_mut(i) {
266
+ // Update territory counts
267
+ if old_owner != 0 {
268
+ *self.territory_counts.entry(old_owner).or_insert(0) -= 1;
269
+ }
270
+ if new_owner != 0 {
271
+ *self.territory_counts.entry(new_owner).or_insert(0) += 1;
272
+ }
273
+
274
+ target.owner = new_owner;
275
+ target.infection = 0.0;
276
+ target.infected_by = 0;
277
+ target.population = 1.0;
278
+ }
279
+
280
+ changed.push(i as u32);
281
+ }
282
+ }
283
+
284
+ // Phase 3: Decay infection on cells that aren't being actively infected
285
+ for cell in self.cells.iter_mut() {
286
+ if cell.infected_by == 0 && cell.infection > 0.0 {
287
+ cell.infection *= 0.9;
288
+ if cell.infection < 0.01 {
289
+ cell.infection = 0.0;
290
+ }
291
+ }
292
+ }
293
+
294
+ changed
295
+ }
296
+
297
+ /// Find connected components for a given owner
298
+ #[wasm_bindgen]
299
+ pub fn find_connected_regions(&self, owner: u8) -> Vec<u32> {
300
+ let mut visited = vec![false; self.cells.len()];
301
+ let mut region_ids = vec![0u32; self.cells.len()];
302
+ let mut current_region = 1u32;
303
+
304
+ for start in 0..self.cells.len() {
305
+ if visited[start] || self.cells[start].owner != owner {
306
+ continue;
307
+ }
308
+
309
+ // BFS from this cell
310
+ let mut queue = VecDeque::new();
311
+ queue.push_back(start);
312
+ visited[start] = true;
313
+
314
+ while let Some(idx) = queue.pop_front() {
315
+ region_ids[idx] = current_region;
316
+
317
+ for &neighbor_idx in self.neighbor_indices[idx].iter() {
318
+ if neighbor_idx < 0 { continue; }
319
+ let ni = neighbor_idx as usize;
320
+
321
+ if !visited[ni] && self.cells[ni].owner == owner {
322
+ visited[ni] = true;
323
+ queue.push_back(ni);
324
+ }
325
+ }
326
+ }
327
+
328
+ current_region += 1;
329
+ }
330
+
331
+ region_ids
332
+ }
333
+
334
+ /// Find border cells for a given owner
335
+ #[wasm_bindgen]
336
+ pub fn find_border_cells(&self, owner: u8) -> Vec<u32> {
337
+ let mut borders = Vec::new();
338
+
339
+ for i in 0..self.cells.len() {
340
+ if self.cells[i].owner != owner { continue; }
341
+
342
+ let is_border = self.neighbor_indices[i].iter().any(|&ni| {
343
+ if ni < 0 { return true; } // Edge of grid
344
+ self.cells[ni as usize].owner != owner
345
+ });
346
+
347
+ if is_border {
348
+ borders.push(i as u32);
349
+ }
350
+ }
351
+
352
+ borders
353
+ }
354
+
355
+ /// Compute shortest path between two cells using A*
356
+ #[wasm_bindgen]
357
+ pub fn find_path(&self, start: usize, end: usize, owner_filter: u8) -> Vec<u32> {
358
+ if start >= self.cells.len() || end >= self.cells.len() {
359
+ return Vec::new();
360
+ }
361
+
362
+ // A* implementation
363
+ let mut open_set = std::collections::BinaryHeap::new();
364
+ let mut came_from = HashMap::new();
365
+ let mut g_score = HashMap::new();
366
+ let mut in_open = HashSet::new();
367
+
368
+ g_score.insert(start, 0i32);
369
+ open_set.push(std::cmp::Reverse((self.heuristic(start, end), start)));
370
+ in_open.insert(start);
371
+
372
+ while let Some(std::cmp::Reverse((_, current))) = open_set.pop() {
373
+ in_open.remove(&current);
374
+
375
+ if current == end {
376
+ // Reconstruct path
377
+ let mut path = Vec::new();
378
+ let mut curr = current;
379
+ path.push(curr as u32);
380
+
381
+ while let Some(&prev) = came_from.get(&curr) {
382
+ path.push(prev as u32);
383
+ curr = prev;
384
+ }
385
+
386
+ path.reverse();
387
+ return path;
388
+ }
389
+
390
+ for &neighbor_idx in self.neighbor_indices[current].iter() {
391
+ if neighbor_idx < 0 { continue; }
392
+ let ni = neighbor_idx as usize;
393
+
394
+ // Skip if owner doesn't match (0 = any)
395
+ if owner_filter != 0 && self.cells[ni].owner != owner_filter {
396
+ continue;
397
+ }
398
+
399
+ let tentative_g = g_score.get(&current).copied().unwrap_or(i32::MAX) + 1;
400
+
401
+ if tentative_g < g_score.get(&ni).copied().unwrap_or(i32::MAX) {
402
+ came_from.insert(ni, current);
403
+ g_score.insert(ni, tentative_g);
404
+
405
+ if !in_open.contains(&ni) {
406
+ let f_score = tentative_g + self.heuristic(ni, end);
407
+ open_set.push(std::cmp::Reverse((f_score, ni)));
408
+ in_open.insert(ni);
409
+ }
410
+ }
411
+ }
412
+ }
413
+
414
+ Vec::new() // No path found
415
+ }
416
+
417
+ fn heuristic(&self, from: usize, to: usize) -> i32 {
418
+ // Convert to grid coordinates
419
+ let (x1, y1) = (from % self.width, from / self.width);
420
+ let (x2, y2) = (to % self.width, to / self.width);
421
+
422
+ // Manhattan distance as heuristic
423
+ ((x2 as i32 - x1 as i32).abs() + (y2 as i32 - y1 as i32).abs())
424
+ }
425
+
426
+ /// Get territory counts for all players
427
+ #[wasm_bindgen]
428
+ pub fn get_territory_counts(&self) -> Vec<u32> {
429
+ let mut counts = vec![0u32; 256];
430
+ for (&owner, &count) in self.territory_counts.iter() {
431
+ counts[owner as usize] = count as u32;
432
+ }
433
+ counts
434
+ }
435
+
436
+ /// Compute Gini coefficient
437
+ #[wasm_bindgen]
438
+ pub fn compute_gini(&self) -> f64 {
439
+ let mut populations: Vec<f32> = self.cells.iter()
440
+ .filter(|c| c.owner != 0)
441
+ .map(|c| c.population)
442
+ .collect();
443
+
444
+ if populations.is_empty() {
445
+ return 0.0;
446
+ }
447
+
448
+ populations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
449
+
450
+ let n = populations.len() as f64;
451
+ let mut sum = 0.0;
452
+
453
+ for (i, &pop) in populations.iter().enumerate() {
454
+ sum += (2.0 * (i as f64 + 1.0) - n - 1.0) * pop as f64;
455
+ }
456
+
457
+ let mean = populations.iter().sum::<f32>() as f64 / n;
458
+ if mean == 0.0 {
459
+ return 0.0;
460
+ }
461
+
462
+ sum / (n * n * mean)
463
+ }
464
+
465
+ /// Compute Shannon entropy of territory distribution
466
+ #[wasm_bindgen]
467
+ pub fn compute_entropy(&self) -> f64 {
468
+ let total: f64 = self.territory_counts.values().sum::<usize>() as f64;
469
+ if total == 0.0 {
470
+ return 0.0;
471
+ }
472
+
473
+ let mut entropy = 0.0;
474
+ for &count in self.territory_counts.values() {
475
+ if count > 0 {
476
+ let p = count as f64 / total;
477
+ entropy -= p * p.ln();
478
+ }
479
+ }
480
+
481
+ entropy
482
+ }
483
+
484
+ /// Run K-means clustering on cell positions
485
+ #[wasm_bindgen]
486
+ pub fn kmeans_cluster(&mut self, k: usize, iterations: usize) -> Vec<u32> {
487
+ let mut positions: Vec<(f32, f32)> = Vec::new();
488
+ let mut cell_indices: Vec<usize> = Vec::new();
489
+
490
+ // Collect all owned cells
491
+ for i in 0..self.cells.len() {
492
+ if self.cells[i].owner != 0 {
493
+ let x = (i % self.width) as f32;
494
+ let y = (i / self.width) as f32;
495
+ positions.push((x, y));
496
+ cell_indices.push(i);
497
+ }
498
+ }
499
+
500
+ if positions.is_empty() || k == 0 {
501
+ return vec![0; self.cells.len()];
502
+ }
503
+
504
+ // Initialize centroids using k-means++
505
+ let mut centroids = Vec::with_capacity(k);
506
+ let first_idx = self.rng.gen_range(0..positions.len());
507
+ centroids.push(positions[first_idx]);
508
+
509
+ for _ in 1..k {
510
+ let mut distances: Vec<f32> = positions.iter().map(|p| {
511
+ centroids.iter()
512
+ .map(|c| {
513
+ let dx = p.0 - c.0;
514
+ let dy = p.1 - c.1;
515
+ dx * dx + dy * dy
516
+ })
517
+ .fold(f32::MAX, f32::min)
518
+ }).collect();
519
+
520
+ let total: f32 = distances.iter().sum();
521
+ if total == 0.0 { break; }
522
+
523
+ let mut target = self.rng.gen::<f32>() * total;
524
+ for (i, &d) in distances.iter().enumerate() {
525
+ target -= d;
526
+ if target <= 0.0 {
527
+ centroids.push(positions[i]);
528
+ break;
529
+ }
530
+ }
531
+ }
532
+
533
+ // Run iterations
534
+ let mut assignments = vec![0usize; positions.len()];
535
+
536
+ for _ in 0..iterations {
537
+ // Assign points to nearest centroid
538
+ for (i, p) in positions.iter().enumerate() {
539
+ let mut min_dist = f32::MAX;
540
+ let mut best_cluster = 0;
541
+
542
+ for (j, c) in centroids.iter().enumerate() {
543
+ let dx = p.0 - c.0;
544
+ let dy = p.1 - c.1;
545
+ let dist = dx * dx + dy * dy;
546
+
547
+ if dist < min_dist {
548
+ min_dist = dist;
549
+ best_cluster = j;
550
+ }
551
+ }
552
+
553
+ assignments[i] = best_cluster;
554
+ }
555
+
556
+ // Update centroids
557
+ let mut sums = vec![(0.0f32, 0.0f32); k];
558
+ let mut counts = vec![0usize; k];
559
+
560
+ for (i, &cluster) in assignments.iter().enumerate() {
561
+ sums[cluster].0 += positions[i].0;
562
+ sums[cluster].1 += positions[i].1;
563
+ counts[cluster] += 1;
564
+ }
565
+
566
+ for (j, c) in centroids.iter_mut().enumerate() {
567
+ if counts[j] > 0 {
568
+ c.0 = sums[j].0 / counts[j] as f32;
569
+ c.1 = sums[j].1 / counts[j] as f32;
570
+ }
571
+ }
572
+ }
573
+
574
+ // Return cluster assignments for all cells
575
+ let mut result = vec![0u32; self.cells.len()];
576
+ for (i, &cell_idx) in cell_indices.iter().enumerate() {
577
+ result[cell_idx] = assignments[i] as u32;
578
+ }
579
+
580
+ result
581
+ }
582
+
583
+ /// Get size of grid
584
+ #[wasm_bindgen]
585
+ pub fn size(&self) -> usize {
586
+ self.cells.len()
587
+ }
588
+
589
+ /// Get width
590
+ #[wasm_bindgen]
591
+ pub fn width(&self) -> usize {
592
+ self.width
593
+ }
594
+
595
+ /// Get height
596
+ #[wasm_bindgen]
597
+ pub fn height(&self) -> usize {
598
+ self.height
599
+ }
600
+
601
+ /// Clear all cells
602
+ #[wasm_bindgen]
603
+ pub fn clear(&mut self) {
604
+ for cell in self.cells.iter_mut() {
605
+ *cell = CellState::default();
606
+ }
607
+ self.territory_counts.clear();
608
+ self.total_population = 0.0;
609
+ }
610
+ }
611
+
612
+ // ═══════════════════════════════════════════════════════════════════════════
613
+ // FLOW FIELD WASM
614
+ // ═══════════════════════════════════════════════════════════════════════════
615
+
616
+ /// Flow field computation in WASM
617
+ #[wasm_bindgen]
618
+ pub struct FlowFieldWasm {
619
+ width: usize,
620
+ height: usize,
621
+ velocity_x: Vec<f32>,
622
+ velocity_y: Vec<f32>,
623
+ }
624
+
625
+ #[wasm_bindgen]
626
+ impl FlowFieldWasm {
627
+ #[wasm_bindgen(constructor)]
628
+ pub fn new(width: usize, height: usize) -> Self {
629
+ let size = width * height;
630
+ Self {
631
+ width,
632
+ height,
633
+ velocity_x: vec![0.0; size],
634
+ velocity_y: vec![0.0; size],
635
+ }
636
+ }
637
+
638
+ /// Add a source (outward flow)
639
+ #[wasm_bindgen]
640
+ pub fn add_source(&mut self, x: f32, y: f32, strength: f32) {
641
+ for j in 0..self.height {
642
+ for i in 0..self.width {
643
+ let dx = i as f32 - x;
644
+ let dy = j as f32 - y;
645
+ let dist_sq = dx * dx + dy * dy + 0.0001;
646
+ let dist = dist_sq.sqrt();
647
+
648
+ let idx = j * self.width + i;
649
+ self.velocity_x[idx] += strength * dx / (dist * dist_sq);
650
+ self.velocity_y[idx] += strength * dy / (dist * dist_sq);
651
+ }
652
+ }
653
+ }
654
+
655
+ /// Add a vortex (rotational flow)
656
+ #[wasm_bindgen]
657
+ pub fn add_vortex(&mut self, x: f32, y: f32, strength: f32) {
658
+ for j in 0..self.height {
659
+ for i in 0..self.width {
660
+ let dx = i as f32 - x;
661
+ let dy = j as f32 - y;
662
+ let dist_sq = dx * dx + dy * dy + 0.0001;
663
+
664
+ let idx = j * self.width + i;
665
+ self.velocity_x[idx] += -strength * dy / dist_sq;
666
+ self.velocity_y[idx] += strength * dx / dist_sq;
667
+ }
668
+ }
669
+ }
670
+
671
+ /// Sample velocity at position (bilinear interpolation)
672
+ #[wasm_bindgen]
673
+ pub fn sample(&self, x: f32, y: f32) -> Vec<f32> {
674
+ let x0 = (x.floor() as usize).min(self.width - 2);
675
+ let y0 = (y.floor() as usize).min(self.height - 2);
676
+ let x1 = x0 + 1;
677
+ let y1 = y0 + 1;
678
+
679
+ let tx = x - x0 as f32;
680
+ let ty = y - y0 as f32;
681
+
682
+ let i00 = y0 * self.width + x0;
683
+ let i10 = y0 * self.width + x1;
684
+ let i01 = y1 * self.width + x0;
685
+ let i11 = y1 * self.width + x1;
686
+
687
+ let vx = self.velocity_x[i00] * (1.0 - tx) * (1.0 - ty)
688
+ + self.velocity_x[i10] * tx * (1.0 - ty)
689
+ + self.velocity_x[i01] * (1.0 - tx) * ty
690
+ + self.velocity_x[i11] * tx * ty;
691
+
692
+ let vy = self.velocity_y[i00] * (1.0 - tx) * (1.0 - ty)
693
+ + self.velocity_y[i10] * tx * (1.0 - ty)
694
+ + self.velocity_y[i01] * (1.0 - tx) * ty
695
+ + self.velocity_y[i11] * tx * ty;
696
+
697
+ vec![vx, vy]
698
+ }
699
+
700
+ /// Compute divergence field
701
+ #[wasm_bindgen]
702
+ pub fn compute_divergence(&self) -> Vec<f32> {
703
+ let mut div = vec![0.0; self.width * self.height];
704
+
705
+ for j in 1..self.height - 1 {
706
+ for i in 1..self.width - 1 {
707
+ let idx = j * self.width + i;
708
+ let dudx = (self.velocity_x[idx + 1] - self.velocity_x[idx - 1]) * 0.5;
709
+ let dvdy = (self.velocity_y[idx + self.width] - self.velocity_y[idx - self.width]) * 0.5;
710
+ div[idx] = dudx + dvdy;
711
+ }
712
+ }
713
+
714
+ div
715
+ }
716
+
717
+ /// Compute curl field
718
+ #[wasm_bindgen]
719
+ pub fn compute_curl(&self) -> Vec<f32> {
720
+ let mut curl = vec![0.0; self.width * self.height];
721
+
722
+ for j in 1..self.height - 1 {
723
+ for i in 1..self.width - 1 {
724
+ let idx = j * self.width + i;
725
+ let dvdx = (self.velocity_y[idx + 1] - self.velocity_y[idx - 1]) * 0.5;
726
+ let dudy = (self.velocity_x[idx + self.width] - self.velocity_x[idx - self.width]) * 0.5;
727
+ curl[idx] = dvdx - dudy;
728
+ }
729
+ }
730
+
731
+ curl
732
+ }
733
+
734
+ /// Clear field
735
+ #[wasm_bindgen]
736
+ pub fn clear(&mut self) {
737
+ self.velocity_x.fill(0.0);
738
+ self.velocity_y.fill(0.0);
739
+ }
740
+ }