@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.
- package/examples/basic-usage.tsx +19 -19
- package/package.json +1 -1
- package/public/hexgrid-worker.js +2350 -1638
- package/src/Snapshot.ts +790 -585
- package/src/adapters.ts +16 -18
- package/src/algorithms/AdvancedStatistics.ts +58 -24
- package/src/algorithms/BayesianStatistics.ts +43 -12
- package/src/algorithms/FlowField.ts +30 -6
- package/src/algorithms/FlowField3D.ts +573 -0
- package/src/algorithms/FluidSimulation.ts +19 -3
- package/src/algorithms/FluidSimulation3D.ts +664 -0
- package/src/algorithms/GraphAlgorithms.ts +19 -12
- package/src/algorithms/OutlierDetection.ts +72 -38
- package/src/algorithms/ParticleSystem.ts +12 -2
- package/src/algorithms/ParticleSystem3D.ts +546 -0
- package/src/algorithms/index.ts +14 -8
- package/src/compat.ts +10 -10
- package/src/components/HexGrid.tsx +10 -23
- package/src/components/NarrationOverlay.tsx +139 -51
- package/src/components/index.ts +2 -1
- package/src/features.ts +31 -31
- package/src/index.ts +11 -11
- package/src/math/HexCoordinates.ts +1 -1
- package/src/math/Matrix4.ts +2 -12
- package/src/math/Vector3.ts +5 -1
- package/src/math/index.ts +6 -6
- package/src/note-adapter.ts +50 -42
- package/src/ontology-adapter.ts +30 -23
- package/src/stores/uiStore.ts +34 -34
- package/src/types.ts +109 -98
- package/src/utils/image-utils.ts +9 -6
- package/src/wasm/HexGridWasmWrapper.ts +436 -388
- package/src/wasm/index.ts +2 -2
- package/src/workers/hexgrid-math.ts +40 -35
- package/src/workers/hexgrid-worker.worker.ts +1992 -1018
|
@@ -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(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
109
|
-
await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
115
|
-
|
|
116
|
-
|
|
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(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 =
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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(
|
|
251
|
+
stepInfection(
|
|
252
|
+
infectionRate: number = 0.1,
|
|
253
|
+
infectionThreshold: number = 1.0
|
|
254
|
+
): number[] {
|
|
225
255
|
if (this.wasmInstance) {
|
|
226
|
-
return Array.from(
|
|
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 (
|
|
245
|
-
|
|
246
|
-
|
|
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)
|
|
371
|
-
|
|
372
|
-
|
|
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() ??
|
|
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(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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 =
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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 =
|
|
710
|
-
|
|
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 =
|
|
730
|
-
|
|
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
|
}
|