@buley/hexgrid-3d 1.0.0 → 1.1.1
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/build_log.txt +500 -0
- package/build_src_log.txt +8 -0
- package/examples/basic-usage.tsx +19 -19
- package/package.json +1 -1
- package/public/hexgrid-worker.js +2350 -1638
- package/site/.eslintrc.json +3 -0
- package/site/DEPLOYMENT.md +196 -0
- package/site/INDEX.md +127 -0
- package/site/QUICK_START.md +86 -0
- package/site/README.md +85 -0
- package/site/SITE_SUMMARY.md +180 -0
- package/site/next.config.js +12 -0
- package/site/package.json +26 -0
- package/site/src/app/docs/page.tsx +148 -0
- package/site/src/app/examples/page.tsx +133 -0
- package/site/src/app/globals.css +160 -0
- package/site/src/app/layout.tsx +29 -0
- package/site/src/app/page.tsx +163 -0
- package/site/tsconfig.json +29 -0
- package/site/vercel.json +6 -0
- package/src/Snapshot.ts +790 -585
- package/src/adapters/DashAdapter.ts +57 -0
- 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 +567 -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 +140 -52
- package/src/components/index.ts +2 -1
- package/src/features.ts +31 -31
- package/src/index.ts +11 -11
- package/src/lib/narration.ts +17 -0
- package/src/lib/stats-tracker.ts +25 -0
- package/src/lib/theme-colors.ts +12 -0
- package/src/math/HexCoordinates.ts +849 -4
- package/src/math/Matrix4.ts +2 -12
- package/src/math/Vector3.ts +49 -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/shared-utils.d.ts +10 -0
- package/src/types.ts +110 -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
- package/tsconfig.json +21 -14
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3D Flow Field
|
|
3
|
+
*
|
|
4
|
+
* A 3D vector field supporting vortices, attractors, and streamline tracing.
|
|
5
|
+
* Used for creating swirling "fluid soup" motion in the Pensieve.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Vector3 } from '../math/Vector3';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for the flow field
|
|
12
|
+
*/
|
|
13
|
+
export interface FlowField3DConfig {
|
|
14
|
+
/** Field width in cells */
|
|
15
|
+
width: number;
|
|
16
|
+
/** Field height in cells */
|
|
17
|
+
height: number;
|
|
18
|
+
/** Field depth in cells */
|
|
19
|
+
depth: number;
|
|
20
|
+
/** Cell size (world units per cell) */
|
|
21
|
+
resolution: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A vortex source in the flow field
|
|
26
|
+
*/
|
|
27
|
+
export interface Vortex3D {
|
|
28
|
+
/** Center position */
|
|
29
|
+
center: Vector3;
|
|
30
|
+
/** Axis of rotation (normalized) */
|
|
31
|
+
axis: Vector3;
|
|
32
|
+
/** Rotational strength (positive = counterclockwise when looking along axis) */
|
|
33
|
+
strength: number;
|
|
34
|
+
/** Influence radius */
|
|
35
|
+
radius: number;
|
|
36
|
+
/** Falloff type */
|
|
37
|
+
falloff: 'linear' | 'quadratic' | 'gaussian';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An attractor/repeller in the flow field
|
|
42
|
+
*/
|
|
43
|
+
export interface Attractor3D {
|
|
44
|
+
/** Center position */
|
|
45
|
+
center: Vector3;
|
|
46
|
+
/** Strength (positive = attract, negative = repel) */
|
|
47
|
+
strength: number;
|
|
48
|
+
/** Influence radius */
|
|
49
|
+
radius: number;
|
|
50
|
+
/** Falloff type */
|
|
51
|
+
falloff: 'linear' | 'quadratic' | 'inverse-square';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A directional source in the flow field
|
|
56
|
+
*/
|
|
57
|
+
export interface DirectionalSource3D {
|
|
58
|
+
/** Position */
|
|
59
|
+
position: Vector3;
|
|
60
|
+
/** Direction vector (will be normalized) */
|
|
61
|
+
direction: Vector3;
|
|
62
|
+
/** Strength */
|
|
63
|
+
strength: number;
|
|
64
|
+
/** Influence radius */
|
|
65
|
+
radius: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Options for streamline tracing
|
|
70
|
+
*/
|
|
71
|
+
export interface StreamlineOptions {
|
|
72
|
+
/** Maximum number of steps */
|
|
73
|
+
maxSteps?: number;
|
|
74
|
+
/** Step size (world units) */
|
|
75
|
+
stepSize?: number;
|
|
76
|
+
/** Minimum velocity magnitude to continue tracing */
|
|
77
|
+
minVelocity?: number;
|
|
78
|
+
/** Whether to trace backwards as well */
|
|
79
|
+
bidirectional?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Full field sample including velocity, divergence, and curl
|
|
84
|
+
*/
|
|
85
|
+
export interface FieldSample {
|
|
86
|
+
/** Velocity at the sampled point */
|
|
87
|
+
velocity: Vector3;
|
|
88
|
+
/** Divergence (scalar) - measure of "outward flow" */
|
|
89
|
+
divergence: number;
|
|
90
|
+
/** Curl (vector) - measure of rotation */
|
|
91
|
+
curl: Vector3;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 3D Flow Field with vortices, attractors, and sources
|
|
96
|
+
*/
|
|
97
|
+
export class FlowField3D {
|
|
98
|
+
protected width: number;
|
|
99
|
+
protected height: number;
|
|
100
|
+
protected depth: number;
|
|
101
|
+
protected resolution: number;
|
|
102
|
+
protected size: number;
|
|
103
|
+
|
|
104
|
+
// Velocity field storage
|
|
105
|
+
protected fieldX: Float32Array;
|
|
106
|
+
protected fieldY: Float32Array;
|
|
107
|
+
protected fieldZ: Float32Array;
|
|
108
|
+
|
|
109
|
+
// Sources
|
|
110
|
+
protected vortices: Vortex3D[] = [];
|
|
111
|
+
protected attractors: Attractor3D[] = [];
|
|
112
|
+
protected sources: DirectionalSource3D[] = [];
|
|
113
|
+
|
|
114
|
+
// Noise for turbulence
|
|
115
|
+
protected noiseTime: number = 0;
|
|
116
|
+
|
|
117
|
+
constructor(config: FlowField3DConfig) {
|
|
118
|
+
this.width = Math.max(1, Math.round(config.width));
|
|
119
|
+
this.height = Math.max(1, Math.round(config.height));
|
|
120
|
+
this.depth = Math.max(1, Math.round(config.depth));
|
|
121
|
+
this.resolution = config.resolution;
|
|
122
|
+
this.size = this.width * this.height * this.depth;
|
|
123
|
+
|
|
124
|
+
this.fieldX = new Float32Array(this.size);
|
|
125
|
+
this.fieldY = new Float32Array(this.size);
|
|
126
|
+
this.fieldZ = new Float32Array(this.size);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Add a vortex to the field
|
|
131
|
+
*/
|
|
132
|
+
addVortex(
|
|
133
|
+
center: Vector3,
|
|
134
|
+
axis: Vector3,
|
|
135
|
+
strength: number,
|
|
136
|
+
radius: number,
|
|
137
|
+
falloff: Vortex3D['falloff'] = 'quadratic'
|
|
138
|
+
): void {
|
|
139
|
+
// Normalize axis
|
|
140
|
+
const len = Math.sqrt(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z);
|
|
141
|
+
const normalizedAxis = new Vector3(
|
|
142
|
+
axis.x / len,
|
|
143
|
+
axis.y / len,
|
|
144
|
+
axis.z / len
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
this.vortices.push({
|
|
148
|
+
center,
|
|
149
|
+
axis: normalizedAxis,
|
|
150
|
+
strength,
|
|
151
|
+
radius,
|
|
152
|
+
falloff,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add an attractor (positive strength) or repeller (negative strength)
|
|
158
|
+
*/
|
|
159
|
+
addAttractor(
|
|
160
|
+
center: Vector3,
|
|
161
|
+
strength: number,
|
|
162
|
+
radius: number = 10,
|
|
163
|
+
falloff: Attractor3D['falloff'] = 'inverse-square'
|
|
164
|
+
): void {
|
|
165
|
+
this.attractors.push({
|
|
166
|
+
center,
|
|
167
|
+
strength,
|
|
168
|
+
radius,
|
|
169
|
+
falloff,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Add a directional source
|
|
175
|
+
*/
|
|
176
|
+
addSource(
|
|
177
|
+
position: Vector3,
|
|
178
|
+
velocity: Vector3,
|
|
179
|
+
strength: number,
|
|
180
|
+
radius: number = 5
|
|
181
|
+
): void {
|
|
182
|
+
// Normalize direction
|
|
183
|
+
const len = Math.sqrt(
|
|
184
|
+
velocity.x * velocity.x +
|
|
185
|
+
velocity.y * velocity.y +
|
|
186
|
+
velocity.z * velocity.z
|
|
187
|
+
);
|
|
188
|
+
const direction =
|
|
189
|
+
len > 0
|
|
190
|
+
? new Vector3(velocity.x / len, velocity.y / len, velocity.z / len)
|
|
191
|
+
: new Vector3(0, 1, 0);
|
|
192
|
+
|
|
193
|
+
this.sources.push({
|
|
194
|
+
position,
|
|
195
|
+
direction,
|
|
196
|
+
strength,
|
|
197
|
+
radius,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Clear all sources
|
|
203
|
+
*/
|
|
204
|
+
clearSources(): void {
|
|
205
|
+
this.vortices = [];
|
|
206
|
+
this.attractors = [];
|
|
207
|
+
this.sources = [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Sample the velocity at a world position
|
|
212
|
+
*/
|
|
213
|
+
sample(position: Vector3): Vector3 {
|
|
214
|
+
let vx = 0;
|
|
215
|
+
let vy = 0;
|
|
216
|
+
let vz = 0;
|
|
217
|
+
|
|
218
|
+
// Contribution from vortices
|
|
219
|
+
for (const vortex of this.vortices) {
|
|
220
|
+
const contribution = this.sampleVortex(position, vortex);
|
|
221
|
+
vx += contribution.x;
|
|
222
|
+
vy += contribution.y;
|
|
223
|
+
vz += contribution.z;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Contribution from attractors
|
|
227
|
+
for (const attractor of this.attractors) {
|
|
228
|
+
const contribution = this.sampleAttractor(position, attractor);
|
|
229
|
+
vx += contribution.x;
|
|
230
|
+
vy += contribution.y;
|
|
231
|
+
vz += contribution.z;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Contribution from directional sources
|
|
235
|
+
for (const source of this.sources) {
|
|
236
|
+
const contribution = this.sampleSource(position, source);
|
|
237
|
+
vx += contribution.x;
|
|
238
|
+
vy += contribution.y;
|
|
239
|
+
vz += contribution.z;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new Vector3(vx, vy, vz);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Sample the full field including curl and divergence
|
|
247
|
+
*/
|
|
248
|
+
sampleFull(position: Vector3): FieldSample {
|
|
249
|
+
const velocity = this.sample(position);
|
|
250
|
+
|
|
251
|
+
// Compute curl numerically
|
|
252
|
+
const h = this.resolution * 0.5;
|
|
253
|
+
const vxp = this.sample(
|
|
254
|
+
new Vector3(position.x + h, position.y, position.z)
|
|
255
|
+
);
|
|
256
|
+
const vxm = this.sample(
|
|
257
|
+
new Vector3(position.x - h, position.y, position.z)
|
|
258
|
+
);
|
|
259
|
+
const vyp = this.sample(
|
|
260
|
+
new Vector3(position.x, position.y + h, position.z)
|
|
261
|
+
);
|
|
262
|
+
const vym = this.sample(
|
|
263
|
+
new Vector3(position.x, position.y - h, position.z)
|
|
264
|
+
);
|
|
265
|
+
const vzp = this.sample(
|
|
266
|
+
new Vector3(position.x, position.y, position.z + h)
|
|
267
|
+
);
|
|
268
|
+
const vzm = this.sample(
|
|
269
|
+
new Vector3(position.x, position.y, position.z - h)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const curl = new Vector3(
|
|
273
|
+
(vyp.z - vym.z - vzp.y + vzm.y) / (2 * h),
|
|
274
|
+
(vzp.x - vzm.x - vxp.z + vxm.z) / (2 * h),
|
|
275
|
+
(vxp.y - vxm.y - vyp.x + vym.x) / (2 * h)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const divergence =
|
|
279
|
+
(vxp.x - vxm.x + vyp.y - vym.y + vzp.z - vzm.z) / (2 * h);
|
|
280
|
+
|
|
281
|
+
return { velocity, curl, divergence };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Trace a streamline from a starting point
|
|
286
|
+
*/
|
|
287
|
+
traceStreamline(start: Vector3, options: StreamlineOptions = {}): Vector3[] {
|
|
288
|
+
const maxSteps = options.maxSteps ?? 100;
|
|
289
|
+
const stepSize = options.stepSize ?? this.resolution;
|
|
290
|
+
const minVelocity = options.minVelocity ?? 0.001;
|
|
291
|
+
const bidirectional = options.bidirectional ?? false;
|
|
292
|
+
|
|
293
|
+
const points: Vector3[] = [start];
|
|
294
|
+
|
|
295
|
+
// Trace forward
|
|
296
|
+
let pos = new Vector3(start.x, start.y, start.z);
|
|
297
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
298
|
+
const vel = this.sample(pos);
|
|
299
|
+
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z);
|
|
300
|
+
|
|
301
|
+
if (speed < minVelocity) break;
|
|
302
|
+
|
|
303
|
+
// Normalize and step
|
|
304
|
+
pos = new Vector3(
|
|
305
|
+
pos.x + (vel.x / speed) * stepSize,
|
|
306
|
+
pos.y + (vel.y / speed) * stepSize,
|
|
307
|
+
pos.z + (vel.z / speed) * stepSize
|
|
308
|
+
);
|
|
309
|
+
points.push(pos);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Trace backward
|
|
313
|
+
if (bidirectional) {
|
|
314
|
+
pos = new Vector3(start.x, start.y, start.z);
|
|
315
|
+
const backwardPoints: Vector3[] = [];
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
318
|
+
const vel = this.sample(pos);
|
|
319
|
+
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z);
|
|
320
|
+
|
|
321
|
+
if (speed < minVelocity) break;
|
|
322
|
+
|
|
323
|
+
// Normalize and step backward
|
|
324
|
+
pos = new Vector3(
|
|
325
|
+
pos.x - (vel.x / speed) * stepSize,
|
|
326
|
+
pos.y - (vel.y / speed) * stepSize,
|
|
327
|
+
pos.z - (vel.z / speed) * stepSize
|
|
328
|
+
);
|
|
329
|
+
backwardPoints.unshift(pos);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return [...backwardPoints, ...points];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return points;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Update the flow field (for time-varying fields)
|
|
340
|
+
*/
|
|
341
|
+
update(dt: number): void {
|
|
342
|
+
this.noiseTime += dt;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get field dimensions
|
|
347
|
+
*/
|
|
348
|
+
getDimensions(): {
|
|
349
|
+
width: number;
|
|
350
|
+
height: number;
|
|
351
|
+
depth: number;
|
|
352
|
+
resolution: number;
|
|
353
|
+
} {
|
|
354
|
+
return {
|
|
355
|
+
width: this.width,
|
|
356
|
+
height: this.height,
|
|
357
|
+
depth: this.depth,
|
|
358
|
+
resolution: this.resolution,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// =========================================================================
|
|
363
|
+
// PRIVATE METHODS
|
|
364
|
+
// =========================================================================
|
|
365
|
+
|
|
366
|
+
private sampleVortex(pos: Vector3, vortex: Vortex3D): Vector3 {
|
|
367
|
+
// Vector from vortex center to position
|
|
368
|
+
const dx = pos.x - vortex.center.x;
|
|
369
|
+
const dy = pos.y - vortex.center.y;
|
|
370
|
+
const dz = pos.z - vortex.center.z;
|
|
371
|
+
|
|
372
|
+
// Project onto plane perpendicular to axis
|
|
373
|
+
// p_proj = p - (p . axis) * axis
|
|
374
|
+
const dot = dx * vortex.axis.x + dy * vortex.axis.y + dz * vortex.axis.z;
|
|
375
|
+
const px = dx - dot * vortex.axis.x;
|
|
376
|
+
const py = dy - dot * vortex.axis.y;
|
|
377
|
+
const pz = dz - dot * vortex.axis.z;
|
|
378
|
+
|
|
379
|
+
// Distance in plane
|
|
380
|
+
const planeDist = Math.sqrt(px * px + py * py + pz * pz);
|
|
381
|
+
|
|
382
|
+
if (planeDist < 0.001) {
|
|
383
|
+
return new Vector3(0, 0, 0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Calculate falloff
|
|
387
|
+
let falloff: number;
|
|
388
|
+
const normalizedDist = planeDist / vortex.radius;
|
|
389
|
+
|
|
390
|
+
if (normalizedDist > 1) {
|
|
391
|
+
falloff = 0;
|
|
392
|
+
} else {
|
|
393
|
+
switch (vortex.falloff) {
|
|
394
|
+
case 'linear':
|
|
395
|
+
falloff = 1 - normalizedDist;
|
|
396
|
+
break;
|
|
397
|
+
case 'quadratic':
|
|
398
|
+
falloff = (1 - normalizedDist) * (1 - normalizedDist);
|
|
399
|
+
break;
|
|
400
|
+
case 'gaussian':
|
|
401
|
+
falloff = Math.exp(-normalizedDist * normalizedDist * 3);
|
|
402
|
+
break;
|
|
403
|
+
default:
|
|
404
|
+
falloff = 1 - normalizedDist;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Cross product: axis x (pos - center) gives tangent direction
|
|
409
|
+
// This creates circular motion around the axis
|
|
410
|
+
const tx = vortex.axis.y * pz - vortex.axis.z * py;
|
|
411
|
+
const ty = vortex.axis.z * px - vortex.axis.x * pz;
|
|
412
|
+
const tz = vortex.axis.x * py - vortex.axis.y * px;
|
|
413
|
+
|
|
414
|
+
// Normalize and apply strength
|
|
415
|
+
const tLen = Math.sqrt(tx * tx + ty * ty + tz * tz);
|
|
416
|
+
if (tLen < 0.001) {
|
|
417
|
+
return new Vector3(0, 0, 0);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const strength = vortex.strength * falloff;
|
|
421
|
+
return new Vector3(
|
|
422
|
+
(tx / tLen) * strength,
|
|
423
|
+
(ty / tLen) * strength,
|
|
424
|
+
(tz / tLen) * strength
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private sampleAttractor(pos: Vector3, attractor: Attractor3D): Vector3 {
|
|
429
|
+
const dx = attractor.center.x - pos.x;
|
|
430
|
+
const dy = attractor.center.y - pos.y;
|
|
431
|
+
const dz = attractor.center.z - pos.z;
|
|
432
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
433
|
+
|
|
434
|
+
if (dist < 0.001 || dist > attractor.radius) {
|
|
435
|
+
return new Vector3(0, 0, 0);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Calculate falloff
|
|
439
|
+
let strength: number;
|
|
440
|
+
const normalizedDist = dist / attractor.radius;
|
|
441
|
+
|
|
442
|
+
switch (attractor.falloff) {
|
|
443
|
+
case 'linear':
|
|
444
|
+
strength = attractor.strength * (1 - normalizedDist);
|
|
445
|
+
break;
|
|
446
|
+
case 'quadratic':
|
|
447
|
+
strength =
|
|
448
|
+
attractor.strength * (1 - normalizedDist) * (1 - normalizedDist);
|
|
449
|
+
break;
|
|
450
|
+
case 'inverse-square':
|
|
451
|
+
// Clamped inverse square to avoid singularity
|
|
452
|
+
strength = attractor.strength / Math.max(1, dist * dist);
|
|
453
|
+
break;
|
|
454
|
+
default:
|
|
455
|
+
strength = attractor.strength * (1 - normalizedDist);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Direction towards/away from attractor
|
|
459
|
+
const invDist = 1 / dist;
|
|
460
|
+
return new Vector3(
|
|
461
|
+
dx * invDist * strength,
|
|
462
|
+
dy * invDist * strength,
|
|
463
|
+
dz * invDist * strength
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private sampleSource(pos: Vector3, source: DirectionalSource3D): Vector3 {
|
|
468
|
+
const dx = pos.x - source.position.x;
|
|
469
|
+
const dy = pos.y - source.position.y;
|
|
470
|
+
const dz = pos.z - source.position.z;
|
|
471
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
472
|
+
|
|
473
|
+
if (dist > source.radius) {
|
|
474
|
+
return new Vector3(0, 0, 0);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const falloff = 1 - dist / source.radius;
|
|
478
|
+
const strength = source.strength * falloff;
|
|
479
|
+
|
|
480
|
+
return new Vector3(
|
|
481
|
+
source.direction.x * strength,
|
|
482
|
+
source.direction.y * strength,
|
|
483
|
+
source.direction.z * strength
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Pensieve-specific flow field with aesthetic presets
|
|
490
|
+
*/
|
|
491
|
+
export class PensieveFlowField extends FlowField3D {
|
|
492
|
+
constructor(size: number = 100) {
|
|
493
|
+
super({
|
|
494
|
+
width: size,
|
|
495
|
+
height: size,
|
|
496
|
+
depth: size,
|
|
497
|
+
resolution: 1,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Add default swirling vortices for the pensieve "soup" effect
|
|
501
|
+
this.addDefaultVortices();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Add default vortices for pensieve aesthetic
|
|
506
|
+
*/
|
|
507
|
+
private addDefaultVortices(): void {
|
|
508
|
+
const dims = this.getDimensions();
|
|
509
|
+
const center = dims.width / 2;
|
|
510
|
+
|
|
511
|
+
// Central upward spiral
|
|
512
|
+
this.addVortex(
|
|
513
|
+
new Vector3(center, center * 0.3, center),
|
|
514
|
+
new Vector3(0, 1, 0),
|
|
515
|
+
2,
|
|
516
|
+
center * 0.8,
|
|
517
|
+
'gaussian'
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// Secondary horizontal vortices
|
|
521
|
+
this.addVortex(
|
|
522
|
+
new Vector3(center * 0.7, center, center),
|
|
523
|
+
new Vector3(1, 0.3, 0),
|
|
524
|
+
1,
|
|
525
|
+
center * 0.4,
|
|
526
|
+
'quadratic'
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
this.addVortex(
|
|
530
|
+
new Vector3(center * 1.3, center, center),
|
|
531
|
+
new Vector3(-1, 0.3, 0),
|
|
532
|
+
1,
|
|
533
|
+
center * 0.4,
|
|
534
|
+
'quadratic'
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// Gentle central attractor to keep particles from flying away
|
|
538
|
+
this.addAttractor(
|
|
539
|
+
new Vector3(center, center, center),
|
|
540
|
+
0.5,
|
|
541
|
+
center * 1.5,
|
|
542
|
+
'linear'
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Add mouse interaction force
|
|
548
|
+
*/
|
|
549
|
+
addMouseForce(
|
|
550
|
+
position: Vector3,
|
|
551
|
+
direction: Vector3,
|
|
552
|
+
strength: number = 5
|
|
553
|
+
): void {
|
|
554
|
+
// Temporarily add a directional source
|
|
555
|
+
this.addSource(position, direction, strength, 10);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Clear mouse forces (call after interaction ends)
|
|
560
|
+
*/
|
|
561
|
+
clearMouseForces(): void {
|
|
562
|
+
// Remove all directional sources but keep vortices and attractors
|
|
563
|
+
const vorticesCopy = [...this.vortices];
|
|
564
|
+
const attractorsCopy = [...this.attractors];
|
|
565
|
+
this.clearSources();
|
|
566
|
+
for (const v of vorticesCopy) {
|
|
567
|
+
this.addVortex(v.center, v.axis, v.strength, v.radius, v.falloff);
|
|
568
|
+
}
|
|
569
|
+
for (const a of attractorsCopy) {
|
|
570
|
+
this.addAttractor(a.center, a.strength, a.radius, a.falloff);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
@@ -26,7 +26,13 @@ export class StableFluids {
|
|
|
26
26
|
this.density[index] += amount;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
addForce(
|
|
29
|
+
addForce(
|
|
30
|
+
x: number,
|
|
31
|
+
y: number,
|
|
32
|
+
vx: number,
|
|
33
|
+
vy: number,
|
|
34
|
+
_radius: number
|
|
35
|
+
): void {
|
|
30
36
|
const index = this.indexFor(x, y);
|
|
31
37
|
this.velocityX[index] += vx;
|
|
32
38
|
this.velocityY[index] += vy;
|
|
@@ -78,10 +84,20 @@ export class InfectionFluidSimulator {
|
|
|
78
84
|
private fluid: StableFluids;
|
|
79
85
|
|
|
80
86
|
constructor(width: number, height: number) {
|
|
81
|
-
this.fluid = new StableFluids({
|
|
87
|
+
this.fluid = new StableFluids({
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
viscosity: 0,
|
|
91
|
+
diffusion: 0,
|
|
92
|
+
});
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
registerTerritory(
|
|
95
|
+
registerTerritory(
|
|
96
|
+
_ownerId: number,
|
|
97
|
+
x: number,
|
|
98
|
+
y: number,
|
|
99
|
+
intensity: number
|
|
100
|
+
): void {
|
|
85
101
|
this.fluid.addDensity(x, y, intensity, 1);
|
|
86
102
|
}
|
|
87
103
|
|