@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
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3D Stable Fluids Implementation
|
|
3
|
+
*
|
|
4
|
+
* Based on Jos Stam's "Stable Fluids" paper (SIGGRAPH 1999)
|
|
5
|
+
* Extended to 3D for the Pensieve particle visualization.
|
|
6
|
+
*
|
|
7
|
+
* Key operations:
|
|
8
|
+
* 1. Add sources (density, velocity)
|
|
9
|
+
* 2. Diffuse (spread quantities)
|
|
10
|
+
* 3. Advect (move quantities along velocity field)
|
|
11
|
+
* 4. Project (make velocity field divergence-free)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Vector3 } from '../math/Vector3';
|
|
15
|
+
|
|
16
|
+
export interface FluidConfig3D {
|
|
17
|
+
/** Grid width in cells */
|
|
18
|
+
width: number;
|
|
19
|
+
/** Grid height in cells */
|
|
20
|
+
height: number;
|
|
21
|
+
/** Grid depth in cells */
|
|
22
|
+
depth: number;
|
|
23
|
+
/** Viscosity coefficient (0 = inviscid, higher = more viscous) */
|
|
24
|
+
viscosity: number;
|
|
25
|
+
/** Diffusion rate for density (0 = no diffusion) */
|
|
26
|
+
diffusion: number;
|
|
27
|
+
/** Number of iterations for linear solver (higher = more accurate but slower) */
|
|
28
|
+
iterations?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 3D Stable Fluids solver for realistic fluid dynamics
|
|
33
|
+
*/
|
|
34
|
+
export class StableFluids3D {
|
|
35
|
+
private width: number;
|
|
36
|
+
private height: number;
|
|
37
|
+
private depth: number;
|
|
38
|
+
private size: number;
|
|
39
|
+
private viscosity: number;
|
|
40
|
+
private diffusion: number;
|
|
41
|
+
private iterations: number;
|
|
42
|
+
|
|
43
|
+
// Current state
|
|
44
|
+
private density: Float32Array;
|
|
45
|
+
private velocityX: Float32Array;
|
|
46
|
+
private velocityY: Float32Array;
|
|
47
|
+
private velocityZ: Float32Array;
|
|
48
|
+
|
|
49
|
+
// Previous state (for advection)
|
|
50
|
+
private density0: Float32Array;
|
|
51
|
+
private velocityX0: Float32Array;
|
|
52
|
+
private velocityY0: Float32Array;
|
|
53
|
+
private velocityZ0: Float32Array;
|
|
54
|
+
|
|
55
|
+
constructor(config: FluidConfig3D) {
|
|
56
|
+
this.width = Math.max(1, Math.round(config.width));
|
|
57
|
+
this.height = Math.max(1, Math.round(config.height));
|
|
58
|
+
this.depth = Math.max(1, Math.round(config.depth));
|
|
59
|
+
this.size = this.width * this.height * this.depth;
|
|
60
|
+
this.viscosity = config.viscosity;
|
|
61
|
+
this.diffusion = config.diffusion;
|
|
62
|
+
this.iterations = config.iterations ?? 4;
|
|
63
|
+
|
|
64
|
+
// Allocate arrays
|
|
65
|
+
this.density = new Float32Array(this.size);
|
|
66
|
+
this.velocityX = new Float32Array(this.size);
|
|
67
|
+
this.velocityY = new Float32Array(this.size);
|
|
68
|
+
this.velocityZ = new Float32Array(this.size);
|
|
69
|
+
|
|
70
|
+
this.density0 = new Float32Array(this.size);
|
|
71
|
+
this.velocityX0 = new Float32Array(this.size);
|
|
72
|
+
this.velocityY0 = new Float32Array(this.size);
|
|
73
|
+
this.velocityZ0 = new Float32Array(this.size);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add density at a point with radius falloff
|
|
78
|
+
*/
|
|
79
|
+
addDensity(
|
|
80
|
+
x: number,
|
|
81
|
+
y: number,
|
|
82
|
+
z: number,
|
|
83
|
+
amount: number,
|
|
84
|
+
radius: number
|
|
85
|
+
): void {
|
|
86
|
+
const r2 = radius * radius;
|
|
87
|
+
const ix0 = Math.max(0, Math.floor(x - radius));
|
|
88
|
+
const ix1 = Math.min(this.width - 1, Math.ceil(x + radius));
|
|
89
|
+
const iy0 = Math.max(0, Math.floor(y - radius));
|
|
90
|
+
const iy1 = Math.min(this.height - 1, Math.ceil(y + radius));
|
|
91
|
+
const iz0 = Math.max(0, Math.floor(z - radius));
|
|
92
|
+
const iz1 = Math.min(this.depth - 1, Math.ceil(z + radius));
|
|
93
|
+
|
|
94
|
+
for (let iz = iz0; iz <= iz1; iz++) {
|
|
95
|
+
for (let iy = iy0; iy <= iy1; iy++) {
|
|
96
|
+
for (let ix = ix0; ix <= ix1; ix++) {
|
|
97
|
+
const dx = ix - x;
|
|
98
|
+
const dy = iy - y;
|
|
99
|
+
const dz = iz - z;
|
|
100
|
+
const dist2 = dx * dx + dy * dy + dz * dz;
|
|
101
|
+
if (dist2 < r2) {
|
|
102
|
+
const falloff = 1 - dist2 / r2;
|
|
103
|
+
const idx = this.indexFor(ix, iy, iz);
|
|
104
|
+
this.density[idx] += amount * falloff;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add force at a point with radius falloff
|
|
113
|
+
*/
|
|
114
|
+
addForce(pos: Vector3, force: Vector3, radius: number): void {
|
|
115
|
+
const r2 = radius * radius;
|
|
116
|
+
const ix0 = Math.max(0, Math.floor(pos.x - radius));
|
|
117
|
+
const ix1 = Math.min(this.width - 1, Math.ceil(pos.x + radius));
|
|
118
|
+
const iy0 = Math.max(0, Math.floor(pos.y - radius));
|
|
119
|
+
const iy1 = Math.min(this.height - 1, Math.ceil(pos.y + radius));
|
|
120
|
+
const iz0 = Math.max(0, Math.floor(pos.z - radius));
|
|
121
|
+
const iz1 = Math.min(this.depth - 1, Math.ceil(pos.z + radius));
|
|
122
|
+
|
|
123
|
+
for (let iz = iz0; iz <= iz1; iz++) {
|
|
124
|
+
for (let iy = iy0; iy <= iy1; iy++) {
|
|
125
|
+
for (let ix = ix0; ix <= ix1; ix++) {
|
|
126
|
+
const dx = ix - pos.x;
|
|
127
|
+
const dy = iy - pos.y;
|
|
128
|
+
const dz = iz - pos.z;
|
|
129
|
+
const dist2 = dx * dx + dy * dy + dz * dz;
|
|
130
|
+
if (dist2 < r2) {
|
|
131
|
+
const falloff = 1 - dist2 / r2;
|
|
132
|
+
const idx = this.indexFor(ix, iy, iz);
|
|
133
|
+
this.velocityX[idx] += force.x * falloff;
|
|
134
|
+
this.velocityY[idx] += force.y * falloff;
|
|
135
|
+
this.velocityZ[idx] += force.z * falloff;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Step the simulation forward by dt seconds
|
|
144
|
+
*/
|
|
145
|
+
step(dt: number): void {
|
|
146
|
+
// Velocity step
|
|
147
|
+
this.velocityStep(dt);
|
|
148
|
+
|
|
149
|
+
// Density step
|
|
150
|
+
this.densityStep(dt);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get velocity at a point (with trilinear interpolation)
|
|
155
|
+
*/
|
|
156
|
+
getVelocityAt(pos: Vector3): Vector3 {
|
|
157
|
+
return new Vector3(
|
|
158
|
+
this.sampleField(this.velocityX, pos.x, pos.y, pos.z),
|
|
159
|
+
this.sampleField(this.velocityY, pos.x, pos.y, pos.z),
|
|
160
|
+
this.sampleField(this.velocityZ, pos.x, pos.y, pos.z)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get density at a point (with trilinear interpolation)
|
|
166
|
+
*/
|
|
167
|
+
getDensityAt(pos: Vector3): number {
|
|
168
|
+
return this.sampleField(this.density, pos.x, pos.y, pos.z);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the raw velocity fields for direct access
|
|
173
|
+
*/
|
|
174
|
+
getVelocityFields(): { x: Float32Array; y: Float32Array; z: Float32Array } {
|
|
175
|
+
return { x: this.velocityX, y: this.velocityY, z: this.velocityZ };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get the raw density field
|
|
180
|
+
*/
|
|
181
|
+
getDensityField(): Float32Array {
|
|
182
|
+
return this.density;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get grid dimensions
|
|
187
|
+
*/
|
|
188
|
+
getDimensions(): { width: number; height: number; depth: number } {
|
|
189
|
+
return { width: this.width, height: this.height, depth: this.depth };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Clear all fields
|
|
194
|
+
*/
|
|
195
|
+
clear(): void {
|
|
196
|
+
this.density.fill(0);
|
|
197
|
+
this.velocityX.fill(0);
|
|
198
|
+
this.velocityY.fill(0);
|
|
199
|
+
this.velocityZ.fill(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =========================================================================
|
|
203
|
+
// PRIVATE METHODS
|
|
204
|
+
// =========================================================================
|
|
205
|
+
|
|
206
|
+
private velocityStep(dt: number): void {
|
|
207
|
+
// Add sources
|
|
208
|
+
this.addSource(this.velocityX, this.velocityX0, dt);
|
|
209
|
+
this.addSource(this.velocityY, this.velocityY0, dt);
|
|
210
|
+
this.addSource(this.velocityZ, this.velocityZ0, dt);
|
|
211
|
+
|
|
212
|
+
// Diffuse
|
|
213
|
+
this.swap(this.velocityX0, this.velocityX);
|
|
214
|
+
this.swap(this.velocityY0, this.velocityY);
|
|
215
|
+
this.swap(this.velocityZ0, this.velocityZ);
|
|
216
|
+
|
|
217
|
+
this.diffuse(1, this.velocityX, this.velocityX0, this.viscosity, dt);
|
|
218
|
+
this.diffuse(2, this.velocityY, this.velocityY0, this.viscosity, dt);
|
|
219
|
+
this.diffuse(3, this.velocityZ, this.velocityZ0, this.viscosity, dt);
|
|
220
|
+
|
|
221
|
+
// Project to make divergence-free
|
|
222
|
+
this.project(
|
|
223
|
+
this.velocityX,
|
|
224
|
+
this.velocityY,
|
|
225
|
+
this.velocityZ,
|
|
226
|
+
this.velocityX0,
|
|
227
|
+
this.velocityY0
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Advect
|
|
231
|
+
this.swap(this.velocityX0, this.velocityX);
|
|
232
|
+
this.swap(this.velocityY0, this.velocityY);
|
|
233
|
+
this.swap(this.velocityZ0, this.velocityZ);
|
|
234
|
+
|
|
235
|
+
this.advect(
|
|
236
|
+
1,
|
|
237
|
+
this.velocityX,
|
|
238
|
+
this.velocityX0,
|
|
239
|
+
this.velocityX0,
|
|
240
|
+
this.velocityY0,
|
|
241
|
+
this.velocityZ0,
|
|
242
|
+
dt
|
|
243
|
+
);
|
|
244
|
+
this.advect(
|
|
245
|
+
2,
|
|
246
|
+
this.velocityY,
|
|
247
|
+
this.velocityY0,
|
|
248
|
+
this.velocityX0,
|
|
249
|
+
this.velocityY0,
|
|
250
|
+
this.velocityZ0,
|
|
251
|
+
dt
|
|
252
|
+
);
|
|
253
|
+
this.advect(
|
|
254
|
+
3,
|
|
255
|
+
this.velocityZ,
|
|
256
|
+
this.velocityZ0,
|
|
257
|
+
this.velocityX0,
|
|
258
|
+
this.velocityY0,
|
|
259
|
+
this.velocityZ0,
|
|
260
|
+
dt
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Project again
|
|
264
|
+
this.project(
|
|
265
|
+
this.velocityX,
|
|
266
|
+
this.velocityY,
|
|
267
|
+
this.velocityZ,
|
|
268
|
+
this.velocityX0,
|
|
269
|
+
this.velocityY0
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private densityStep(dt: number): void {
|
|
274
|
+
// Add sources
|
|
275
|
+
this.addSource(this.density, this.density0, dt);
|
|
276
|
+
|
|
277
|
+
// Diffuse
|
|
278
|
+
this.swap(this.density0, this.density);
|
|
279
|
+
this.diffuse(0, this.density, this.density0, this.diffusion, dt);
|
|
280
|
+
|
|
281
|
+
// Advect
|
|
282
|
+
this.swap(this.density0, this.density);
|
|
283
|
+
this.advect(
|
|
284
|
+
0,
|
|
285
|
+
this.density,
|
|
286
|
+
this.density0,
|
|
287
|
+
this.velocityX,
|
|
288
|
+
this.velocityY,
|
|
289
|
+
this.velocityZ,
|
|
290
|
+
dt
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private addSource(
|
|
295
|
+
target: Float32Array,
|
|
296
|
+
source: Float32Array,
|
|
297
|
+
dt: number
|
|
298
|
+
): void {
|
|
299
|
+
for (let i = 0; i < this.size; i++) {
|
|
300
|
+
target[i] += dt * source[i];
|
|
301
|
+
}
|
|
302
|
+
source.fill(0);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private diffuse(
|
|
306
|
+
b: number,
|
|
307
|
+
x: Float32Array,
|
|
308
|
+
x0: Float32Array,
|
|
309
|
+
diff: number,
|
|
310
|
+
dt: number
|
|
311
|
+
): void {
|
|
312
|
+
const a = dt * diff * this.width * this.height * this.depth;
|
|
313
|
+
this.linearSolve(b, x, x0, a, 1 + 6 * a);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private advect(
|
|
317
|
+
b: number,
|
|
318
|
+
d: Float32Array,
|
|
319
|
+
d0: Float32Array,
|
|
320
|
+
u: Float32Array,
|
|
321
|
+
v: Float32Array,
|
|
322
|
+
w: Float32Array,
|
|
323
|
+
dt: number
|
|
324
|
+
): void {
|
|
325
|
+
const dtx = dt * (this.width - 2);
|
|
326
|
+
const dty = dt * (this.height - 2);
|
|
327
|
+
const dtz = dt * (this.depth - 2);
|
|
328
|
+
|
|
329
|
+
for (let k = 1; k < this.depth - 1; k++) {
|
|
330
|
+
for (let j = 1; j < this.height - 1; j++) {
|
|
331
|
+
for (let i = 1; i < this.width - 1; i++) {
|
|
332
|
+
const idx = this.indexFor(i, j, k);
|
|
333
|
+
|
|
334
|
+
// Trace back
|
|
335
|
+
let x = i - dtx * u[idx];
|
|
336
|
+
let y = j - dty * v[idx];
|
|
337
|
+
let z = k - dtz * w[idx];
|
|
338
|
+
|
|
339
|
+
// Clamp to grid
|
|
340
|
+
x = Math.max(0.5, Math.min(this.width - 1.5, x));
|
|
341
|
+
y = Math.max(0.5, Math.min(this.height - 1.5, y));
|
|
342
|
+
z = Math.max(0.5, Math.min(this.depth - 1.5, z));
|
|
343
|
+
|
|
344
|
+
// Trilinear interpolation
|
|
345
|
+
d[idx] = this.sampleField(d0, x, y, z);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.setBoundary(b, d);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private project(
|
|
354
|
+
u: Float32Array,
|
|
355
|
+
v: Float32Array,
|
|
356
|
+
w: Float32Array,
|
|
357
|
+
p: Float32Array,
|
|
358
|
+
div: Float32Array
|
|
359
|
+
): void {
|
|
360
|
+
const h = 1.0 / Math.max(this.width, this.height, this.depth);
|
|
361
|
+
|
|
362
|
+
// Calculate divergence
|
|
363
|
+
for (let k = 1; k < this.depth - 1; k++) {
|
|
364
|
+
for (let j = 1; j < this.height - 1; j++) {
|
|
365
|
+
for (let i = 1; i < this.width - 1; i++) {
|
|
366
|
+
const idx = this.indexFor(i, j, k);
|
|
367
|
+
div[idx] =
|
|
368
|
+
-0.5 *
|
|
369
|
+
h *
|
|
370
|
+
(u[this.indexFor(i + 1, j, k)] -
|
|
371
|
+
u[this.indexFor(i - 1, j, k)] +
|
|
372
|
+
v[this.indexFor(i, j + 1, k)] -
|
|
373
|
+
v[this.indexFor(i, j - 1, k)] +
|
|
374
|
+
w[this.indexFor(i, j, k + 1)] -
|
|
375
|
+
w[this.indexFor(i, j, k - 1)]);
|
|
376
|
+
p[idx] = 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.setBoundary(0, div);
|
|
382
|
+
this.setBoundary(0, p);
|
|
383
|
+
|
|
384
|
+
// Solve for pressure
|
|
385
|
+
this.linearSolve(0, p, div, 1, 6);
|
|
386
|
+
|
|
387
|
+
// Subtract pressure gradient from velocity
|
|
388
|
+
for (let k = 1; k < this.depth - 1; k++) {
|
|
389
|
+
for (let j = 1; j < this.height - 1; j++) {
|
|
390
|
+
for (let i = 1; i < this.width - 1; i++) {
|
|
391
|
+
const idx = this.indexFor(i, j, k);
|
|
392
|
+
u[idx] -=
|
|
393
|
+
(0.5 *
|
|
394
|
+
(p[this.indexFor(i + 1, j, k)] - p[this.indexFor(i - 1, j, k)])) /
|
|
395
|
+
h;
|
|
396
|
+
v[idx] -=
|
|
397
|
+
(0.5 *
|
|
398
|
+
(p[this.indexFor(i, j + 1, k)] - p[this.indexFor(i, j - 1, k)])) /
|
|
399
|
+
h;
|
|
400
|
+
w[idx] -=
|
|
401
|
+
(0.5 *
|
|
402
|
+
(p[this.indexFor(i, j, k + 1)] - p[this.indexFor(i, j, k - 1)])) /
|
|
403
|
+
h;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.setBoundary(1, u);
|
|
409
|
+
this.setBoundary(2, v);
|
|
410
|
+
this.setBoundary(3, w);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private linearSolve(
|
|
414
|
+
b: number,
|
|
415
|
+
x: Float32Array,
|
|
416
|
+
x0: Float32Array,
|
|
417
|
+
a: number,
|
|
418
|
+
c: number
|
|
419
|
+
): void {
|
|
420
|
+
const cRecip = 1.0 / c;
|
|
421
|
+
|
|
422
|
+
for (let iter = 0; iter < this.iterations; iter++) {
|
|
423
|
+
for (let k = 1; k < this.depth - 1; k++) {
|
|
424
|
+
for (let j = 1; j < this.height - 1; j++) {
|
|
425
|
+
for (let i = 1; i < this.width - 1; i++) {
|
|
426
|
+
const idx = this.indexFor(i, j, k);
|
|
427
|
+
x[idx] =
|
|
428
|
+
(x0[idx] +
|
|
429
|
+
a *
|
|
430
|
+
(x[this.indexFor(i + 1, j, k)] +
|
|
431
|
+
x[this.indexFor(i - 1, j, k)] +
|
|
432
|
+
x[this.indexFor(i, j + 1, k)] +
|
|
433
|
+
x[this.indexFor(i, j - 1, k)] +
|
|
434
|
+
x[this.indexFor(i, j, k + 1)] +
|
|
435
|
+
x[this.indexFor(i, j, k - 1)])) *
|
|
436
|
+
cRecip;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
this.setBoundary(b, x);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private setBoundary(b: number, x: Float32Array): void {
|
|
445
|
+
// Set boundary conditions (reflective for velocity, continuous for density)
|
|
446
|
+
for (let k = 1; k < this.depth - 1; k++) {
|
|
447
|
+
for (let j = 1; j < this.height - 1; j++) {
|
|
448
|
+
x[this.indexFor(0, j, k)] =
|
|
449
|
+
b === 1 ? -x[this.indexFor(1, j, k)] : x[this.indexFor(1, j, k)];
|
|
450
|
+
x[this.indexFor(this.width - 1, j, k)] =
|
|
451
|
+
b === 1
|
|
452
|
+
? -x[this.indexFor(this.width - 2, j, k)]
|
|
453
|
+
: x[this.indexFor(this.width - 2, j, k)];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (let k = 1; k < this.depth - 1; k++) {
|
|
458
|
+
for (let i = 1; i < this.width - 1; i++) {
|
|
459
|
+
x[this.indexFor(i, 0, k)] =
|
|
460
|
+
b === 2 ? -x[this.indexFor(i, 1, k)] : x[this.indexFor(i, 1, k)];
|
|
461
|
+
x[this.indexFor(i, this.height - 1, k)] =
|
|
462
|
+
b === 2
|
|
463
|
+
? -x[this.indexFor(i, this.height - 2, k)]
|
|
464
|
+
: x[this.indexFor(i, this.height - 2, k)];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (let j = 1; j < this.height - 1; j++) {
|
|
469
|
+
for (let i = 1; i < this.width - 1; i++) {
|
|
470
|
+
x[this.indexFor(i, j, 0)] =
|
|
471
|
+
b === 3 ? -x[this.indexFor(i, j, 1)] : x[this.indexFor(i, j, 1)];
|
|
472
|
+
x[this.indexFor(i, j, this.depth - 1)] =
|
|
473
|
+
b === 3
|
|
474
|
+
? -x[this.indexFor(i, j, this.depth - 2)]
|
|
475
|
+
: x[this.indexFor(i, j, this.depth - 2)];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Corner cases - average of neighbors
|
|
480
|
+
x[this.indexFor(0, 0, 0)] =
|
|
481
|
+
0.33 *
|
|
482
|
+
(x[this.indexFor(1, 0, 0)] +
|
|
483
|
+
x[this.indexFor(0, 1, 0)] +
|
|
484
|
+
x[this.indexFor(0, 0, 1)]);
|
|
485
|
+
x[this.indexFor(0, this.height - 1, 0)] =
|
|
486
|
+
0.33 *
|
|
487
|
+
(x[this.indexFor(1, this.height - 1, 0)] +
|
|
488
|
+
x[this.indexFor(0, this.height - 2, 0)] +
|
|
489
|
+
x[this.indexFor(0, this.height - 1, 1)]);
|
|
490
|
+
x[this.indexFor(0, 0, this.depth - 1)] =
|
|
491
|
+
0.33 *
|
|
492
|
+
(x[this.indexFor(1, 0, this.depth - 1)] +
|
|
493
|
+
x[this.indexFor(0, 1, this.depth - 1)] +
|
|
494
|
+
x[this.indexFor(0, 0, this.depth - 2)]);
|
|
495
|
+
x[this.indexFor(0, this.height - 1, this.depth - 1)] =
|
|
496
|
+
0.33 *
|
|
497
|
+
(x[this.indexFor(1, this.height - 1, this.depth - 1)] +
|
|
498
|
+
x[this.indexFor(0, this.height - 2, this.depth - 1)] +
|
|
499
|
+
x[this.indexFor(0, this.height - 1, this.depth - 2)]);
|
|
500
|
+
x[this.indexFor(this.width - 1, 0, 0)] =
|
|
501
|
+
0.33 *
|
|
502
|
+
(x[this.indexFor(this.width - 2, 0, 0)] +
|
|
503
|
+
x[this.indexFor(this.width - 1, 1, 0)] +
|
|
504
|
+
x[this.indexFor(this.width - 1, 0, 1)]);
|
|
505
|
+
x[this.indexFor(this.width - 1, this.height - 1, 0)] =
|
|
506
|
+
0.33 *
|
|
507
|
+
(x[this.indexFor(this.width - 2, this.height - 1, 0)] +
|
|
508
|
+
x[this.indexFor(this.width - 1, this.height - 2, 0)] +
|
|
509
|
+
x[this.indexFor(this.width - 1, this.height - 1, 1)]);
|
|
510
|
+
x[this.indexFor(this.width - 1, 0, this.depth - 1)] =
|
|
511
|
+
0.33 *
|
|
512
|
+
(x[this.indexFor(this.width - 2, 0, this.depth - 1)] +
|
|
513
|
+
x[this.indexFor(this.width - 1, 1, this.depth - 1)] +
|
|
514
|
+
x[this.indexFor(this.width - 1, 0, this.depth - 2)]);
|
|
515
|
+
x[this.indexFor(this.width - 1, this.height - 1, this.depth - 1)] =
|
|
516
|
+
0.33 *
|
|
517
|
+
(x[this.indexFor(this.width - 2, this.height - 1, this.depth - 1)] +
|
|
518
|
+
x[this.indexFor(this.width - 1, this.height - 2, this.depth - 1)] +
|
|
519
|
+
x[this.indexFor(this.width - 1, this.height - 1, this.depth - 2)]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private sampleField(
|
|
523
|
+
field: Float32Array,
|
|
524
|
+
x: number,
|
|
525
|
+
y: number,
|
|
526
|
+
z: number
|
|
527
|
+
): number {
|
|
528
|
+
// Trilinear interpolation
|
|
529
|
+
const i0 = Math.floor(x);
|
|
530
|
+
const i1 = i0 + 1;
|
|
531
|
+
const j0 = Math.floor(y);
|
|
532
|
+
const j1 = j0 + 1;
|
|
533
|
+
const k0 = Math.floor(z);
|
|
534
|
+
const k1 = k0 + 1;
|
|
535
|
+
|
|
536
|
+
const sx = x - i0;
|
|
537
|
+
const sy = y - j0;
|
|
538
|
+
const sz = z - k0;
|
|
539
|
+
|
|
540
|
+
const c000 = field[this.indexForClamped(i0, j0, k0)];
|
|
541
|
+
const c001 = field[this.indexForClamped(i0, j0, k1)];
|
|
542
|
+
const c010 = field[this.indexForClamped(i0, j1, k0)];
|
|
543
|
+
const c011 = field[this.indexForClamped(i0, j1, k1)];
|
|
544
|
+
const c100 = field[this.indexForClamped(i1, j0, k0)];
|
|
545
|
+
const c101 = field[this.indexForClamped(i1, j0, k1)];
|
|
546
|
+
const c110 = field[this.indexForClamped(i1, j1, k0)];
|
|
547
|
+
const c111 = field[this.indexForClamped(i1, j1, k1)];
|
|
548
|
+
|
|
549
|
+
// Interpolate along x
|
|
550
|
+
const c00 = c000 * (1 - sx) + c100 * sx;
|
|
551
|
+
const c01 = c001 * (1 - sx) + c101 * sx;
|
|
552
|
+
const c10 = c010 * (1 - sx) + c110 * sx;
|
|
553
|
+
const c11 = c011 * (1 - sx) + c111 * sx;
|
|
554
|
+
|
|
555
|
+
// Interpolate along y
|
|
556
|
+
const c0 = c00 * (1 - sy) + c10 * sy;
|
|
557
|
+
const c1 = c01 * (1 - sy) + c11 * sy;
|
|
558
|
+
|
|
559
|
+
// Interpolate along z
|
|
560
|
+
return c0 * (1 - sz) + c1 * sz;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private indexFor(x: number, y: number, z: number): number {
|
|
564
|
+
return z * this.width * this.height + y * this.width + x;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private indexForClamped(x: number, y: number, z: number): number {
|
|
568
|
+
const ix = Math.min(this.width - 1, Math.max(0, x));
|
|
569
|
+
const iy = Math.min(this.height - 1, Math.max(0, y));
|
|
570
|
+
const iz = Math.min(this.depth - 1, Math.max(0, z));
|
|
571
|
+
return this.indexFor(ix, iy, iz);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private swap(a: Float32Array, b: Float32Array): void {
|
|
575
|
+
// Swap contents efficiently
|
|
576
|
+
const temp = new Float32Array(a);
|
|
577
|
+
a.set(b);
|
|
578
|
+
b.set(temp);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Pensieve-specific fluid simulator with aesthetic presets
|
|
584
|
+
*/
|
|
585
|
+
export class PensieveFluidSimulator {
|
|
586
|
+
private fluid: StableFluids3D;
|
|
587
|
+
private time: number = 0;
|
|
588
|
+
|
|
589
|
+
constructor(size: number = 64) {
|
|
590
|
+
this.fluid = new StableFluids3D({
|
|
591
|
+
width: size,
|
|
592
|
+
height: size,
|
|
593
|
+
depth: size,
|
|
594
|
+
viscosity: 0.0001,
|
|
595
|
+
diffusion: 0.00001,
|
|
596
|
+
iterations: 4,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Add a "memory splash" - when a particle enters the pensieve
|
|
602
|
+
*/
|
|
603
|
+
addMemorySplash(position: Vector3, intensity: number): void {
|
|
604
|
+
this.fluid.addDensity(
|
|
605
|
+
position.x,
|
|
606
|
+
position.y,
|
|
607
|
+
position.z,
|
|
608
|
+
intensity * 10,
|
|
609
|
+
5
|
|
610
|
+
);
|
|
611
|
+
// Add a slight upward swirl
|
|
612
|
+
this.fluid.addForce(position, new Vector3(0, intensity * 2, 0), 3);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Add ambient swirling motion
|
|
617
|
+
*/
|
|
618
|
+
addSwirl(dt: number): void {
|
|
619
|
+
this.time += dt;
|
|
620
|
+
const dims = this.fluid.getDimensions();
|
|
621
|
+
const center = new Vector3(dims.width / 2, dims.height / 2, dims.depth / 2);
|
|
622
|
+
|
|
623
|
+
// Rotating force around Y axis
|
|
624
|
+
const angle = this.time * 0.5;
|
|
625
|
+
const radius = dims.width * 0.3;
|
|
626
|
+
const pos = new Vector3(
|
|
627
|
+
center.x + Math.cos(angle) * radius,
|
|
628
|
+
center.y,
|
|
629
|
+
center.z + Math.sin(angle) * radius
|
|
630
|
+
);
|
|
631
|
+
const tangent = new Vector3(-Math.sin(angle), 0, Math.cos(angle));
|
|
632
|
+
this.fluid.addForce(pos, tangent, radius * 0.5);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Step the simulation
|
|
637
|
+
*/
|
|
638
|
+
step(dt: number): void {
|
|
639
|
+
this.addSwirl(dt);
|
|
640
|
+
this.fluid.step(dt);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Get velocity at a point for particle advection
|
|
645
|
+
*/
|
|
646
|
+
getVelocityAt(pos: Vector3): Vector3 {
|
|
647
|
+
return this.fluid.getVelocityAt(pos);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Get the underlying fluid for direct access
|
|
652
|
+
*/
|
|
653
|
+
getFluid(): StableFluids3D {
|
|
654
|
+
return this.fluid;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Clear the simulation
|
|
659
|
+
*/
|
|
660
|
+
clear(): void {
|
|
661
|
+
this.fluid.clear();
|
|
662
|
+
this.time = 0;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
@@ -41,16 +41,17 @@ export function kMeansClustering(points: number[][], k: number): Cluster[] {
|
|
|
41
41
|
return sum / cluster.members.length;
|
|
42
42
|
});
|
|
43
43
|
cluster.centroid = centroid;
|
|
44
|
-
cluster.cohesion =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
cluster.cohesion =
|
|
45
|
+
cluster.members.reduce((sum, idx) => {
|
|
46
|
+
const point = points[idx];
|
|
47
|
+
const distance = Math.sqrt(
|
|
48
|
+
centroid.reduce((acc, value, dim) => {
|
|
49
|
+
const diff = value - (point?.[dim] ?? 0);
|
|
50
|
+
return acc + diff * diff;
|
|
51
|
+
}, 0)
|
|
52
|
+
);
|
|
53
|
+
return sum + distance;
|
|
54
|
+
}, 0) / cluster.members.length;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
return clusters;
|
|
@@ -118,12 +119,18 @@ export function analyzeTerritorBoundaries(
|
|
|
118
119
|
return { frontLength, hotspots };
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
export function kMeansClustering2D(
|
|
122
|
+
export function kMeansClustering2D(
|
|
123
|
+
points: Array<[number, number]>,
|
|
124
|
+
k: number
|
|
125
|
+
): Cluster[] {
|
|
122
126
|
const asPoints = points.map((p) => [p[0], p[1]]);
|
|
123
127
|
return kMeansClustering(asPoints, k);
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
export function findConnectedComponents(graph: {
|
|
130
|
+
export function findConnectedComponents(graph: {
|
|
131
|
+
nodes: number[];
|
|
132
|
+
edges: Map<number, number[]>;
|
|
133
|
+
}): number[][] {
|
|
127
134
|
const visited = new Set<number>();
|
|
128
135
|
const components: number[][] = [];
|
|
129
136
|
|