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