@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.
@@ -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
+ }
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * Algorithms Module Exports
3
- *
3
+ *
4
4
  * @module algorithms
5
5
  */
6
6
 
7
- export * from './GraphAlgorithms'
8
- export * from './FlowField'
9
- export * from './ParticleSystem'
10
- export * from './FluidSimulation'
11
- export * from './AdvancedStatistics'
12
- export * from './BayesianStatistics'
13
- export * from './OutlierDetection'
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 interface Photo {
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 interface HexGridProps {
21
- photos?: Photo[];
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: HexGridProps): JSX.Element | null {
14
+ export function HexGrid(_props: HexGridPropsType): React.JSX.Element | null {
28
15
  return null;
29
16
  }
30
17