@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,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3D Particle System
|
|
3
|
+
*
|
|
4
|
+
* GPU-friendly particle physics for React Three Fiber InstancedMesh rendering.
|
|
5
|
+
* Supports forces, attractors, fluid coupling, and biometric visualization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Vector3 } from '../math/Vector3';
|
|
9
|
+
import type { StableFluids3D } from './FluidSimulation3D';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A single 3D particle
|
|
13
|
+
*/
|
|
14
|
+
export interface Particle3D {
|
|
15
|
+
/** Unique identifier */
|
|
16
|
+
id: string;
|
|
17
|
+
/** 3D position */
|
|
18
|
+
position: Vector3;
|
|
19
|
+
/** 3D velocity */
|
|
20
|
+
velocity: Vector3;
|
|
21
|
+
/** 3D acceleration (accumulated forces) */
|
|
22
|
+
acceleration: Vector3;
|
|
23
|
+
/** RGB color (0-1 range) */
|
|
24
|
+
color: [number, number, number];
|
|
25
|
+
/** Base size */
|
|
26
|
+
size: number;
|
|
27
|
+
/** Current life remaining (seconds) */
|
|
28
|
+
life: number;
|
|
29
|
+
/** Maximum life (seconds) */
|
|
30
|
+
maxLife: number;
|
|
31
|
+
/** Mass for physics calculations */
|
|
32
|
+
mass: number;
|
|
33
|
+
/** Custom data attached to particle */
|
|
34
|
+
userData?: Record<string, unknown>;
|
|
35
|
+
|
|
36
|
+
// Biometric visualization
|
|
37
|
+
/** Heart rate in BPM - affects pulse animation */
|
|
38
|
+
heartRate?: number;
|
|
39
|
+
/** HRV in ms - affects stability/jitter */
|
|
40
|
+
hrvValue?: number;
|
|
41
|
+
/** Current pulse scale (computed each frame) */
|
|
42
|
+
pulseScale?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for emitting particles
|
|
47
|
+
*/
|
|
48
|
+
export interface EmitOptions {
|
|
49
|
+
/** Number of particles to emit */
|
|
50
|
+
count?: number;
|
|
51
|
+
/** RGB color (0-1 range) */
|
|
52
|
+
color?: [number, number, number];
|
|
53
|
+
/** Initial velocity */
|
|
54
|
+
velocity?: Vector3;
|
|
55
|
+
/** Velocity spread (random variation) */
|
|
56
|
+
velocitySpread?: Vector3;
|
|
57
|
+
/** Initial size */
|
|
58
|
+
size?: number;
|
|
59
|
+
/** Size spread (random variation) */
|
|
60
|
+
sizeSpread?: number;
|
|
61
|
+
/** Particle lifetime in seconds */
|
|
62
|
+
life?: number;
|
|
63
|
+
/** Life spread (random variation) */
|
|
64
|
+
lifeSpread?: number;
|
|
65
|
+
/** Particle mass */
|
|
66
|
+
mass?: number;
|
|
67
|
+
/** Custom user data */
|
|
68
|
+
userData?: Record<string, unknown>;
|
|
69
|
+
/** Heart rate for biometric visualization */
|
|
70
|
+
heartRate?: number;
|
|
71
|
+
/** HRV for biometric visualization */
|
|
72
|
+
hrvValue?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Configuration for the particle system
|
|
77
|
+
*/
|
|
78
|
+
export interface ParticleSystem3DConfig {
|
|
79
|
+
/** Maximum number of particles */
|
|
80
|
+
maxParticles: number;
|
|
81
|
+
/** Global gravity vector */
|
|
82
|
+
gravity?: Vector3;
|
|
83
|
+
/** Global drag coefficient (0-1, where 1 = no drag) */
|
|
84
|
+
drag?: number;
|
|
85
|
+
/** Bounds for particle containment (spherical) */
|
|
86
|
+
boundsSphereRadius?: number;
|
|
87
|
+
/** Bounds center */
|
|
88
|
+
boundsCenter?: Vector3;
|
|
89
|
+
/** Bounce factor when hitting bounds (0 = absorb, 1 = perfect bounce) */
|
|
90
|
+
bounceFactor?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Instance data for GPU rendering
|
|
95
|
+
*/
|
|
96
|
+
export interface InstanceData {
|
|
97
|
+
/** Flat array of positions [x,y,z, x,y,z, ...] */
|
|
98
|
+
positions: Float32Array;
|
|
99
|
+
/** Flat array of colors [r,g,b, r,g,b, ...] */
|
|
100
|
+
colors: Float32Array;
|
|
101
|
+
/** Flat array of scales [s, s, s, ...] */
|
|
102
|
+
scales: Float32Array;
|
|
103
|
+
/** Number of active particles */
|
|
104
|
+
count: number;
|
|
105
|
+
/** Particle IDs in order */
|
|
106
|
+
ids: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 3D Particle System with GPU-friendly output
|
|
111
|
+
*/
|
|
112
|
+
export class ParticleSystem3D {
|
|
113
|
+
private particles: Map<string, Particle3D> = new Map();
|
|
114
|
+
private particleOrder: string[] = [];
|
|
115
|
+
private maxParticles: number;
|
|
116
|
+
private gravity: Vector3;
|
|
117
|
+
private drag: number;
|
|
118
|
+
private boundsSphereRadius: number;
|
|
119
|
+
private boundsCenter: Vector3;
|
|
120
|
+
private bounceFactor: number;
|
|
121
|
+
private time: number = 0;
|
|
122
|
+
private idCounter: number = 0;
|
|
123
|
+
|
|
124
|
+
// Pre-allocated buffers for GPU data
|
|
125
|
+
private positionBuffer: Float32Array;
|
|
126
|
+
private colorBuffer: Float32Array;
|
|
127
|
+
private scaleBuffer: Float32Array;
|
|
128
|
+
|
|
129
|
+
constructor(config: ParticleSystem3DConfig) {
|
|
130
|
+
this.maxParticles = config.maxParticles;
|
|
131
|
+
this.gravity = config.gravity ?? new Vector3(0, 0, 0);
|
|
132
|
+
this.drag = config.drag ?? 0.99;
|
|
133
|
+
this.boundsSphereRadius = config.boundsSphereRadius ?? Infinity;
|
|
134
|
+
this.boundsCenter = config.boundsCenter ?? new Vector3(0, 0, 0);
|
|
135
|
+
this.bounceFactor = config.bounceFactor ?? 0.5;
|
|
136
|
+
|
|
137
|
+
// Pre-allocate buffers
|
|
138
|
+
this.positionBuffer = new Float32Array(this.maxParticles * 3);
|
|
139
|
+
this.colorBuffer = new Float32Array(this.maxParticles * 3);
|
|
140
|
+
this.scaleBuffer = new Float32Array(this.maxParticles);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Emit particles at a position
|
|
145
|
+
*/
|
|
146
|
+
emit(position: Vector3, options: EmitOptions = {}): string[] {
|
|
147
|
+
const count = options.count ?? 1;
|
|
148
|
+
const emittedIds: string[] = [];
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < count; i++) {
|
|
151
|
+
if (this.particles.size >= this.maxParticles) {
|
|
152
|
+
// Remove oldest particle
|
|
153
|
+
const oldest = this.particleOrder.shift();
|
|
154
|
+
if (oldest) {
|
|
155
|
+
this.particles.delete(oldest);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const id = `p_${this.idCounter++}`;
|
|
160
|
+
const spread = options.velocitySpread ?? new Vector3(0, 0, 0);
|
|
161
|
+
const sizeSpread = options.sizeSpread ?? 0;
|
|
162
|
+
const lifeSpread = options.lifeSpread ?? 0;
|
|
163
|
+
|
|
164
|
+
const particle: Particle3D = {
|
|
165
|
+
id,
|
|
166
|
+
position: new Vector3(position.x, position.y, position.z),
|
|
167
|
+
velocity: new Vector3(
|
|
168
|
+
(options.velocity?.x ?? 0) + (Math.random() - 0.5) * 2 * spread.x,
|
|
169
|
+
(options.velocity?.y ?? 0) + (Math.random() - 0.5) * 2 * spread.y,
|
|
170
|
+
(options.velocity?.z ?? 0) + (Math.random() - 0.5) * 2 * spread.z
|
|
171
|
+
),
|
|
172
|
+
acceleration: new Vector3(0, 0, 0),
|
|
173
|
+
color: options.color ?? [1, 1, 1],
|
|
174
|
+
size: (options.size ?? 1) + (Math.random() - 0.5) * 2 * sizeSpread,
|
|
175
|
+
life: (options.life ?? 5) + (Math.random() - 0.5) * 2 * lifeSpread,
|
|
176
|
+
maxLife: options.life ?? 5,
|
|
177
|
+
mass: options.mass ?? 1,
|
|
178
|
+
userData: options.userData,
|
|
179
|
+
heartRate: options.heartRate,
|
|
180
|
+
hrvValue: options.hrvValue,
|
|
181
|
+
pulseScale: 1,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
this.particles.set(id, particle);
|
|
185
|
+
this.particleOrder.push(id);
|
|
186
|
+
emittedIds.push(id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return emittedIds;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Add or update a persistent particle (for memory particles that don't expire)
|
|
194
|
+
*/
|
|
195
|
+
setParticle(
|
|
196
|
+
id: string,
|
|
197
|
+
particle: Omit<Particle3D, 'acceleration' | 'pulseScale'>
|
|
198
|
+
): void {
|
|
199
|
+
const existing = this.particles.get(id);
|
|
200
|
+
if (existing) {
|
|
201
|
+
// Update existing
|
|
202
|
+
Object.assign(existing, particle);
|
|
203
|
+
existing.acceleration = new Vector3(0, 0, 0);
|
|
204
|
+
} else {
|
|
205
|
+
// Add new
|
|
206
|
+
if (this.particles.size >= this.maxParticles) {
|
|
207
|
+
// Remove oldest non-persistent particle
|
|
208
|
+
for (let i = 0; i < this.particleOrder.length; i++) {
|
|
209
|
+
const oldId = this.particleOrder[i];
|
|
210
|
+
const oldParticle = this.particles.get(oldId);
|
|
211
|
+
if (oldParticle && oldParticle.life !== Infinity) {
|
|
212
|
+
this.particles.delete(oldId);
|
|
213
|
+
this.particleOrder.splice(i, 1);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const newParticle: Particle3D = {
|
|
220
|
+
...particle,
|
|
221
|
+
acceleration: new Vector3(0, 0, 0),
|
|
222
|
+
pulseScale: 1,
|
|
223
|
+
};
|
|
224
|
+
this.particles.set(id, newParticle);
|
|
225
|
+
this.particleOrder.push(id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remove a particle by ID
|
|
231
|
+
*/
|
|
232
|
+
removeParticle(id: string): void {
|
|
233
|
+
this.particles.delete(id);
|
|
234
|
+
const idx = this.particleOrder.indexOf(id);
|
|
235
|
+
if (idx >= 0) {
|
|
236
|
+
this.particleOrder.splice(idx, 1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get a particle by ID
|
|
242
|
+
*/
|
|
243
|
+
getParticle(id: string): Particle3D | undefined {
|
|
244
|
+
return this.particles.get(id);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Apply a force field to all particles
|
|
249
|
+
*/
|
|
250
|
+
applyForceField(field: (pos: Vector3) => Vector3): void {
|
|
251
|
+
Array.from(this.particles.values()).forEach((particle) => {
|
|
252
|
+
const force = field(particle.position);
|
|
253
|
+
particle.acceleration.x += force.x / particle.mass;
|
|
254
|
+
particle.acceleration.y += force.y / particle.mass;
|
|
255
|
+
particle.acceleration.z += force.z / particle.mass;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Apply an attractor force
|
|
261
|
+
*/
|
|
262
|
+
applyAttractor(
|
|
263
|
+
center: Vector3,
|
|
264
|
+
strength: number,
|
|
265
|
+
falloffRadius: number
|
|
266
|
+
): void {
|
|
267
|
+
Array.from(this.particles.values()).forEach((particle) => {
|
|
268
|
+
const dx = center.x - particle.position.x;
|
|
269
|
+
const dy = center.y - particle.position.y;
|
|
270
|
+
const dz = center.z - particle.position.z;
|
|
271
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
272
|
+
|
|
273
|
+
if (dist > 0.001) {
|
|
274
|
+
const falloff = Math.max(0, 1 - dist / falloffRadius);
|
|
275
|
+
const force = (strength * falloff * falloff) / (dist * dist);
|
|
276
|
+
const invDist = 1 / dist;
|
|
277
|
+
particle.acceleration.x += (dx * invDist * force) / particle.mass;
|
|
278
|
+
particle.acceleration.y += (dy * invDist * force) / particle.mass;
|
|
279
|
+
particle.acceleration.z += (dz * invDist * force) / particle.mass;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Apply velocity from a fluid simulation
|
|
286
|
+
*/
|
|
287
|
+
applyFluidVelocity(fluid: StableFluids3D, strength: number = 1): void {
|
|
288
|
+
Array.from(this.particles.values()).forEach((particle) => {
|
|
289
|
+
const fluidVel = fluid.getVelocityAt(particle.position);
|
|
290
|
+
particle.velocity.x += fluidVel.x * strength;
|
|
291
|
+
particle.velocity.y += fluidVel.y * strength;
|
|
292
|
+
particle.velocity.z += fluidVel.z * strength;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Update all particles
|
|
298
|
+
*/
|
|
299
|
+
update(dt: number): void {
|
|
300
|
+
this.time += dt;
|
|
301
|
+
const deadParticles: string[] = [];
|
|
302
|
+
|
|
303
|
+
Array.from(this.particles.values()).forEach((particle) => {
|
|
304
|
+
// Apply gravity
|
|
305
|
+
particle.acceleration.x += this.gravity.x;
|
|
306
|
+
particle.acceleration.y += this.gravity.y;
|
|
307
|
+
particle.acceleration.z += this.gravity.z;
|
|
308
|
+
|
|
309
|
+
// Integrate velocity
|
|
310
|
+
particle.velocity.x += particle.acceleration.x * dt;
|
|
311
|
+
particle.velocity.y += particle.acceleration.y * dt;
|
|
312
|
+
particle.velocity.z += particle.acceleration.z * dt;
|
|
313
|
+
|
|
314
|
+
// Apply drag
|
|
315
|
+
particle.velocity.x *= this.drag;
|
|
316
|
+
particle.velocity.y *= this.drag;
|
|
317
|
+
particle.velocity.z *= this.drag;
|
|
318
|
+
|
|
319
|
+
// Integrate position
|
|
320
|
+
particle.position.x += particle.velocity.x * dt;
|
|
321
|
+
particle.position.y += particle.velocity.y * dt;
|
|
322
|
+
particle.position.z += particle.velocity.z * dt;
|
|
323
|
+
|
|
324
|
+
// Reset acceleration
|
|
325
|
+
particle.acceleration.x = 0;
|
|
326
|
+
particle.acceleration.y = 0;
|
|
327
|
+
particle.acceleration.z = 0;
|
|
328
|
+
|
|
329
|
+
// Sphere bounds collision
|
|
330
|
+
if (this.boundsSphereRadius < Infinity) {
|
|
331
|
+
const dx = particle.position.x - this.boundsCenter.x;
|
|
332
|
+
const dy = particle.position.y - this.boundsCenter.y;
|
|
333
|
+
const dz = particle.position.z - this.boundsCenter.z;
|
|
334
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
335
|
+
|
|
336
|
+
if (dist > this.boundsSphereRadius) {
|
|
337
|
+
// Push back inside
|
|
338
|
+
const invDist = 1 / dist;
|
|
339
|
+
const nx = dx * invDist;
|
|
340
|
+
const ny = dy * invDist;
|
|
341
|
+
const nz = dz * invDist;
|
|
342
|
+
|
|
343
|
+
particle.position.x =
|
|
344
|
+
this.boundsCenter.x + nx * this.boundsSphereRadius * 0.99;
|
|
345
|
+
particle.position.y =
|
|
346
|
+
this.boundsCenter.y + ny * this.boundsSphereRadius * 0.99;
|
|
347
|
+
particle.position.z =
|
|
348
|
+
this.boundsCenter.z + nz * this.boundsSphereRadius * 0.99;
|
|
349
|
+
|
|
350
|
+
// Reflect velocity
|
|
351
|
+
const dot =
|
|
352
|
+
particle.velocity.x * nx +
|
|
353
|
+
particle.velocity.y * ny +
|
|
354
|
+
particle.velocity.z * nz;
|
|
355
|
+
particle.velocity.x =
|
|
356
|
+
(particle.velocity.x - 2 * dot * nx) * this.bounceFactor;
|
|
357
|
+
particle.velocity.y =
|
|
358
|
+
(particle.velocity.y - 2 * dot * ny) * this.bounceFactor;
|
|
359
|
+
particle.velocity.z =
|
|
360
|
+
(particle.velocity.z - 2 * dot * nz) * this.bounceFactor;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Update biometric pulse
|
|
365
|
+
if (particle.heartRate) {
|
|
366
|
+
const pulseFreq = particle.heartRate / 60; // Hz
|
|
367
|
+
const pulse = Math.sin(this.time * pulseFreq * Math.PI * 2);
|
|
368
|
+
particle.pulseScale = 1 + pulse * 0.15; // 15% size variation
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Decrease life (skip for Infinity life)
|
|
372
|
+
if (particle.life !== Infinity) {
|
|
373
|
+
particle.life -= dt;
|
|
374
|
+
if (particle.life <= 0) {
|
|
375
|
+
deadParticles.push(particle.id);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Remove dead particles
|
|
381
|
+
deadParticles.forEach((id) => {
|
|
382
|
+
this.particles.delete(id);
|
|
383
|
+
const idx = this.particleOrder.indexOf(id);
|
|
384
|
+
if (idx >= 0) {
|
|
385
|
+
this.particleOrder.splice(idx, 1);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get instance data for React Three Fiber InstancedMesh
|
|
392
|
+
*/
|
|
393
|
+
getInstanceData(): InstanceData {
|
|
394
|
+
const count = this.particles.size;
|
|
395
|
+
const ids: string[] = [];
|
|
396
|
+
|
|
397
|
+
let i = 0;
|
|
398
|
+
const entries = Array.from(this.particles.entries());
|
|
399
|
+
for (let j = 0; j < entries.length && i < this.maxParticles; j++) {
|
|
400
|
+
const [id, particle] = entries[j];
|
|
401
|
+
|
|
402
|
+
// Position
|
|
403
|
+
this.positionBuffer[i * 3] = particle.position.x;
|
|
404
|
+
this.positionBuffer[i * 3 + 1] = particle.position.y;
|
|
405
|
+
this.positionBuffer[i * 3 + 2] = particle.position.z;
|
|
406
|
+
|
|
407
|
+
// Color with life-based alpha (encoded in color for now)
|
|
408
|
+
const lifeFactor =
|
|
409
|
+
particle.life === Infinity
|
|
410
|
+
? 1
|
|
411
|
+
: Math.min(1, particle.life / particle.maxLife);
|
|
412
|
+
this.colorBuffer[i * 3] = particle.color[0] * lifeFactor;
|
|
413
|
+
this.colorBuffer[i * 3 + 1] = particle.color[1] * lifeFactor;
|
|
414
|
+
this.colorBuffer[i * 3 + 2] = particle.color[2] * lifeFactor;
|
|
415
|
+
|
|
416
|
+
// Scale with pulse
|
|
417
|
+
this.scaleBuffer[i] = particle.size * (particle.pulseScale ?? 1);
|
|
418
|
+
|
|
419
|
+
ids.push(id);
|
|
420
|
+
i++;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
positions: this.positionBuffer.subarray(0, count * 3),
|
|
425
|
+
colors: this.colorBuffer.subarray(0, count * 3),
|
|
426
|
+
scales: this.scaleBuffer.subarray(0, count),
|
|
427
|
+
count,
|
|
428
|
+
ids,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get all particles
|
|
434
|
+
*/
|
|
435
|
+
getParticles(): Particle3D[] {
|
|
436
|
+
return Array.from(this.particles.values());
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get particle count
|
|
441
|
+
*/
|
|
442
|
+
getCount(): number {
|
|
443
|
+
return this.particles.size;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Clear all particles
|
|
448
|
+
*/
|
|
449
|
+
clear(): void {
|
|
450
|
+
this.particles.clear();
|
|
451
|
+
this.particleOrder = [];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Find particle nearest to a point
|
|
456
|
+
*/
|
|
457
|
+
findNearest(
|
|
458
|
+
point: Vector3,
|
|
459
|
+
maxDistance: number = Infinity
|
|
460
|
+
): Particle3D | null {
|
|
461
|
+
let nearest: Particle3D | null = null;
|
|
462
|
+
let nearestDist = maxDistance;
|
|
463
|
+
|
|
464
|
+
Array.from(this.particles.values()).forEach((particle) => {
|
|
465
|
+
const dist = particle.position.distanceTo(point);
|
|
466
|
+
if (dist < nearestDist) {
|
|
467
|
+
nearest = particle;
|
|
468
|
+
nearestDist = dist;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return nearest;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Find all particles within a radius
|
|
477
|
+
*/
|
|
478
|
+
findWithinRadius(center: Vector3, radius: number): Particle3D[] {
|
|
479
|
+
const results: Particle3D[] = [];
|
|
480
|
+
const r2 = radius * radius;
|
|
481
|
+
|
|
482
|
+
Array.from(this.particles.values()).forEach((particle) => {
|
|
483
|
+
const dx = particle.position.x - center.x;
|
|
484
|
+
const dy = particle.position.y - center.y;
|
|
485
|
+
const dz = particle.position.z - center.z;
|
|
486
|
+
if (dx * dx + dy * dy + dz * dz <= r2) {
|
|
487
|
+
results.push(particle);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return results;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Pensieve-specific particle system with memory visualization presets
|
|
497
|
+
*/
|
|
498
|
+
export class PensieveParticleSystem extends ParticleSystem3D {
|
|
499
|
+
constructor(maxParticles: number = 10000) {
|
|
500
|
+
super({
|
|
501
|
+
maxParticles,
|
|
502
|
+
gravity: new Vector3(0, -0.1, 0), // Gentle downward drift
|
|
503
|
+
drag: 0.995,
|
|
504
|
+
boundsSphereRadius: 50,
|
|
505
|
+
boundsCenter: new Vector3(0, 0, 0),
|
|
506
|
+
bounceFactor: 0.3,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Add a memory particle with reflection data
|
|
512
|
+
*/
|
|
513
|
+
addMemoryParticle(
|
|
514
|
+
id: string,
|
|
515
|
+
position: Vector3,
|
|
516
|
+
color: [number, number, number],
|
|
517
|
+
options: {
|
|
518
|
+
intensity?: number;
|
|
519
|
+
heartRate?: number;
|
|
520
|
+
hrvValue?: number;
|
|
521
|
+
ageFactor?: number;
|
|
522
|
+
userData?: Record<string, unknown>;
|
|
523
|
+
} = {}
|
|
524
|
+
): void {
|
|
525
|
+
const size = 0.5 + (options.intensity ?? 0.5) * 0.5;
|
|
526
|
+
const ageFactor = options.ageFactor ?? 1;
|
|
527
|
+
|
|
528
|
+
this.setParticle(id, {
|
|
529
|
+
id,
|
|
530
|
+
position,
|
|
531
|
+
velocity: new Vector3(
|
|
532
|
+
(Math.random() - 0.5) * 0.5,
|
|
533
|
+
(Math.random() - 0.5) * 0.5,
|
|
534
|
+
(Math.random() - 0.5) * 0.5
|
|
535
|
+
),
|
|
536
|
+
color: [color[0] * ageFactor, color[1] * ageFactor, color[2] * ageFactor],
|
|
537
|
+
size,
|
|
538
|
+
life: Infinity, // Memory particles don't expire
|
|
539
|
+
maxLife: Infinity,
|
|
540
|
+
mass: 1,
|
|
541
|
+
heartRate: options.heartRate,
|
|
542
|
+
hrvValue: options.hrvValue,
|
|
543
|
+
userData: options.userData,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
package/src/algorithms/index.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Algorithms Module Exports
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @module algorithms
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
13
|
-
export * from './
|
|
7
|
+
// 2D algorithms (existing)
|
|
8
|
+
export * from './GraphAlgorithms';
|
|
9
|
+
export * from './FlowField';
|
|
10
|
+
export * from './ParticleSystem';
|
|
11
|
+
export * from './FluidSimulation';
|
|
12
|
+
export * from './AdvancedStatistics';
|
|
13
|
+
export * from './BayesianStatistics';
|
|
14
|
+
export * from './OutlierDetection';
|
|
15
|
+
|
|
16
|
+
// 3D algorithms (new - for Pensieve)
|
|
17
|
+
export * from './FluidSimulation3D';
|
|
18
|
+
export * from './ParticleSystem3D';
|
|
19
|
+
export * from './FlowField3D';
|
package/src/compat.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Backward compatibility layer for Photo and GridItem
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides conversion utilities to ensure existing Photo-based code
|
|
5
5
|
* continues to work while new code can use GridItem.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Photo, GridItem } from './types'
|
|
8
|
+
import type { Photo, GridItem } from './types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Convert Photo to GridItem for backward compatibility
|
|
@@ -37,7 +37,7 @@ export function photoToGridItem(photo: Photo): GridItem<Photo> {
|
|
|
37
37
|
sourceUrl: photo.sourceUrl,
|
|
38
38
|
createdAt: photo.createdAt,
|
|
39
39
|
velocity: photo.velocity,
|
|
40
|
-
}
|
|
40
|
+
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -46,9 +46,9 @@ export function photoToGridItem(photo: Photo): GridItem<Photo> {
|
|
|
46
46
|
export function gridItemToPhoto(item: GridItem<Photo>): Photo | null {
|
|
47
47
|
// If item contains original Photo data, return it
|
|
48
48
|
if (item.type === 'photo' && item.data) {
|
|
49
|
-
return item.data
|
|
49
|
+
return item.data;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
// Fallback: construct Photo from GridItem fields
|
|
53
53
|
if (item.imageUrl || item.url) {
|
|
54
54
|
return {
|
|
@@ -73,17 +73,17 @@ export function gridItemToPhoto(item: GridItem<Photo>): Photo | null {
|
|
|
73
73
|
comments: item.comments,
|
|
74
74
|
dominantColor: item.dominantColor,
|
|
75
75
|
velocity: item.velocity,
|
|
76
|
-
}
|
|
76
|
+
};
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
return null
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Convert an array of Photos to GridItems
|
|
84
84
|
*/
|
|
85
85
|
export function photosToGridItems(photos: Photo[]): GridItem<Photo>[] {
|
|
86
|
-
return photos.map(photoToGridItem)
|
|
86
|
+
return photos.map(photoToGridItem);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
@@ -92,5 +92,5 @@ export function photosToGridItems(photos: Photo[]): GridItem<Photo>[] {
|
|
|
92
92
|
export function gridItemsToPhotos(items: GridItem<Photo>[]): Photo[] {
|
|
93
93
|
return items
|
|
94
94
|
.map(gridItemToPhoto)
|
|
95
|
-
.filter((photo): photo is Photo => photo !== null)
|
|
95
|
+
.filter((photo): photo is Photo => photo !== null);
|
|
96
96
|
}
|
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
import type { CSSProperties } from 'react';
|
|
1
|
+
import type { CSSProperties, RefObject } from 'react';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
Photo as PhotoType,
|
|
5
|
+
HexGridProps as HexGridPropsType,
|
|
6
|
+
HexGridFeatureFlags,
|
|
7
|
+
} from '../types';
|
|
3
8
|
|
|
4
|
-
export
|
|
5
|
-
id: string;
|
|
6
|
-
title: string;
|
|
7
|
-
alt: string;
|
|
8
|
-
imageUrl: string;
|
|
9
|
-
thumbnailUrl: string;
|
|
10
|
-
category: string;
|
|
11
|
-
description: string;
|
|
12
|
-
source: string;
|
|
13
|
-
createdAt: string;
|
|
14
|
-
velocity: number;
|
|
15
|
-
sourceUrl: string;
|
|
16
|
-
likes: number;
|
|
17
|
-
age_in_hours: number;
|
|
18
|
-
}
|
|
9
|
+
export type Photo = PhotoType;
|
|
19
10
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
className?: string;
|
|
23
|
-
style?: CSSProperties;
|
|
24
|
-
onPhotoClick?: (photo: Photo) => void;
|
|
25
|
-
}
|
|
11
|
+
// Re-export the proper HexGridProps from types.ts
|
|
12
|
+
export type { HexGridProps } from '../types';
|
|
26
13
|
|
|
27
|
-
export function HexGrid(_props:
|
|
14
|
+
export function HexGrid(_props: HexGridPropsType): React.JSX.Element | null {
|
|
28
15
|
return null;
|
|
29
16
|
}
|
|
30
17
|
|