@buley/hexgrid-3d 3.4.0 → 3.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"DashAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/DashAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAM;gBAEN,YAAY,EAAE,GAAG;IAI7B;;;;;OAKG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG;CA6BtD"}
1
+ {"version":3,"file":"DashAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/DashAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAM;gBAEN,YAAY,EAAE,GAAG;IAI7B;;;;;OAKG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG;CA6BtD"}
@@ -4,6 +4,7 @@
4
4
  * This adapter subscribes to a Dash "LiveQuery" which returns pointers to shared memory (SharedArrayBuffer)
5
5
  * or Float32Arrays. It then syncs this data directly to the HexGridWasm instance.
6
6
  */
7
+ import { logger } from '../lib/logger';
7
8
  export class DashAdapter {
8
9
  constructor(dashInstance) {
9
10
  this.dash = dashInstance;
@@ -15,11 +16,11 @@ export class DashAdapter {
15
16
  * @param gridInstance The WASM instance of the HexGrid
16
17
  */
17
18
  bindSemanticSearch(query, particleSystem) {
18
- console.log('[DashAdapter] Binding semantic search:', query);
19
+ logger.log('[DashAdapter] Binding semantic search:', query);
19
20
  // Hypothetical Zero-Copy API from Dash 2.0
20
21
  if (this.dash.liveQueryPtr) {
21
22
  this.dash.liveQueryPtr(`SELECT embedding FROM dash_vec_idx WHERE embedding MATCH '${query}'`).subscribe((handle) => {
22
- console.log(`[DashAdapter] Received ${handle.size} bytes from Dash.`);
23
+ logger.log(`[DashAdapter] Received ${handle.size} bytes from Dash.`);
23
24
  // Assume the handle.buffer contains [pos, color, scale] interleaved or tightly packed
24
25
  // For this MVP, we treat it as just positions
25
26
  const floatView = new Float32Array(handle.buffer);
@@ -36,7 +37,7 @@ export class DashAdapter {
36
37
  });
37
38
  }
38
39
  else {
39
- console.warn('[DashAdapter] Dash instance does not support Zero-Copy liveQueryPtr yet.');
40
+ logger.warn('[DashAdapter] Dash instance does not support Zero-Copy liveQueryPtr yet.');
40
41
  }
41
42
  }
42
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FluidEngineFactory.d.ts","sourceRoot":"","sources":["../../src/algorithms/FluidEngineFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAEvF;;;;;;GAMG;AACH,qBAAa,kBAAkB;WAChB,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC;CA6BjE"}
1
+ {"version":3,"file":"FluidEngineFactory.d.ts","sourceRoot":"","sources":["../../src/algorithms/FluidEngineFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG9D,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAEvF;;;;;;GAMG;AACH,qBAAa,kBAAkB;WAChB,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC;CA6BjE"}
@@ -1,6 +1,7 @@
1
1
  import { StableFluids3D } from './FluidSimulation3D';
2
2
  import { FluidSimulationWebNN } from './FluidSimulationWebNN';
3
3
  import { FluidSimulation3DGPU } from './FluidSimulation3DGPU';
4
+ import { logger } from '../lib/logger';
4
5
  /**
5
6
  * Factory to select the best available Fluid Engine.
6
7
  * Priority:
@@ -15,27 +16,27 @@ export class FluidEngineFactory {
15
16
  const webnn = new FluidSimulationWebNN(config);
16
17
  const webnnSupported = await webnn.initialize();
17
18
  if (webnnSupported) {
18
- console.log("Fluid Engine: Using WebNN (NPU)");
19
+ logger.log("Fluid Engine: Using WebNN (NPU)");
19
20
  return webnn;
20
21
  }
21
22
  }
22
23
  catch (e) {
23
- console.warn("Fluid Engine: WebNN init failed", e);
24
+ logger.warn("Fluid Engine: WebNN init failed", e);
24
25
  }
25
26
  // 2. Try WebGPU
26
27
  try {
27
28
  const webgpu = new FluidSimulation3DGPU(config);
28
29
  const webgpuSupported = await webgpu.initialize();
29
30
  if (webgpuSupported) {
30
- console.log("Fluid Engine: Using WebGPU");
31
+ logger.log("Fluid Engine: Using WebGPU");
31
32
  return webgpu;
32
33
  }
33
34
  }
34
35
  catch (e) {
35
- console.warn("Fluid Engine: WebGPU init failed", e);
36
+ logger.warn("Fluid Engine: WebGPU init failed", e);
36
37
  }
37
38
  // 3. Fallback to CPU
38
- console.log("Fluid Engine: using CPU Fallback");
39
+ logger.log("Fluid Engine: using CPU Fallback");
39
40
  return new StableFluids3D(config);
40
41
  }
41
42
  }
@@ -1 +1 @@
1
- {"version":3,"file":"HexGrid.d.ts","sourceRoot":"","sources":["../../src/components/HexGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAYhF,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAI/C,YAAY,EAAE,KAAK,EAAE,CAAA;AAWrB,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IAEvC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IACrB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;IAGhB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACzC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAEnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;IAC9C,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,KAAK,IAAI,CAAA;IAC5G,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,0BAA0B,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAoLD,eAAO,MAAM,OAAO,GAAI,CAAC,GAAG,OAAO,EAAE,iMAalC,YAAY,CAAC,CAAC,CAAC,4CAytIjB,CAAA;AAk7DD,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"HexGrid.d.ts","sourceRoot":"","sources":["../../src/components/HexGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAYhF,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAI/C,YAAY,EAAE,KAAK,EAAE,CAAA;AAWrB,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IAEvC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IACrB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;IAGhB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACzC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAEnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;IAC9C,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,KAAK,IAAI,CAAA;IAC5G,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,0BAA0B,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAoLD,eAAO,MAAM,OAAO,GAAI,CAAC,GAAG,OAAO,EAAE,iMAalC,YAAY,CAAC,CAAC,CAAC,4CA4xIjB,CAAA;AAk7DD,eAAe,OAAO,CAAC"}
@@ -152,6 +152,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
152
152
  // Stream control refs to coordinate long-running UI streaming from worker
153
153
  const streamTokenRef = useRef(0);
154
154
  const streamActiveRef = useRef(false);
155
+ const streamTouchesOccupancyRef = useRef(false);
155
156
  // Reactive state for telemetry overlay (mirrors streamActiveRef)
156
157
  const [streamingActive, setStreamingActive] = useState(false);
157
158
  // How many tiles remain to stream in the current streaming run
@@ -283,10 +284,9 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
283
284
  // gridScale >1 reduces effective hex radius (more hexes), <1 increases radius (fewer hexes)
284
285
  gridScale: 1,
285
286
  // tileSize: base hex radius in pixels (editable)
286
- // Increased from 6 to 12 for better performance (larger hexes = fewer total hexes = better performance)
287
- tileSize: 12,
287
+ tileSize: 8,
288
288
  // hexSpacing: multiplier for hex size (1.0 = perfect touching, <1.0 = gaps, >1.0 = overlap)
289
- hexSpacing: 1.0,
289
+ hexSpacing: 0.95,
290
290
  // sphericalDensity: multiplier for spherical grid density (1.0 = default, >1.0 = more hexes)
291
291
  sphericalDensity: 1.4,
292
292
  // curvature controls (degrees) - start with a visually pleasing curvature
@@ -304,8 +304,8 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
304
304
  polePower: 0.9,
305
305
  // Render both sides option: when true, draw an antipodal copy so images show on inside/outside
306
306
  renderBothSides: false,
307
- // Worker debug logs (ENABLED for debugging evolution issues)
308
- debugLogs: true,
307
+ // Worker debug logs (disabled by default for performance)
308
+ debugLogs: false,
309
309
  // Cluster tiling defaults: preserve aspect, center anchor, no global alignment,
310
310
  // zero uv inset for perfect alignment (seam blending handled separately), no jitter by default
311
311
  clusterPreserveAspect: true,
@@ -1254,6 +1254,8 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
1254
1254
  dlog('HexGrid: Grid dimensions changed, reinitializing infections', { old: prevGridKeyRef.current, new: gridKey });
1255
1255
  const isSpherical = gridMetadataRef.current?.isSpherical ?? false;
1256
1256
  const initState = initializeInfectionSystem(hexPositions, photos, effectiveHexRadius, workerDebugRef.current?.spawnClusterMax ?? 8, logger, isSpherical);
1257
+ blankNeighborCountGenerationRef.current = -1;
1258
+ blankNeighborCountRef.current = new Map();
1257
1259
  setInfectionState(initState);
1258
1260
  infectionStateRef.current = initState;
1259
1261
  // Post to worker immediately (use sendEvolve helper so generation is logged)
@@ -1858,6 +1860,16 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
1858
1860
  const tileAlphaRef = useRef(new Map());
1859
1861
  // Per-tile arrival pulse timestamps (ms) to show a brief highlight when a tile appears/updates
1860
1862
  const tilePulseRef = useRef(new Map());
1863
+ // Worker-precomputed neighbor blank counts (index -> blank-neighbor count)
1864
+ const blankNeighborCountRef = useRef(new Map());
1865
+ const blankNeighborCountGenerationRef = useRef(-1);
1866
+ // Fallback cache for local blank-neighbor computation when worker data is unavailable/mismatched
1867
+ const localBlankNeighborCacheRef = useRef({
1868
+ infectionMap: null,
1869
+ positions: null,
1870
+ hexRadius: -1,
1871
+ counts: new Map()
1872
+ });
1861
1873
  // Runtime telemetry: frame timing buffer for FPS/ms overlay
1862
1874
  const frameTimesRef = useRef([]);
1863
1875
  const lastFrameTimeRef = useRef(performance.now());
@@ -2065,8 +2077,9 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2065
2077
  const lastDrawTimeRef = useRef(0);
2066
2078
  const lastDrawGenRef = useRef(0);
2067
2079
  const cameraDirtyRef = useRef(true);
2068
- // Minimum ms between draws when idle (30fps)
2069
- const minFrameMs = 1000 / 30;
2080
+ // Draw cadence: 30fps when actively animating, lower cadence while static.
2081
+ const activeFrameMs = 1000 / 30;
2082
+ const idleFrameMs = 250;
2070
2083
  // Helper to get effective batchPerFrame (transient override if present)
2071
2084
  const getEffectiveBatch = () => {
2072
2085
  const base = Math.max(0, Math.floor(workerDebugRef.current?.batchPerFrame ?? 0));
@@ -2250,6 +2263,8 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2250
2263
  lastEvolutionTime: infectionStateRef.current.lastEvolutionTime,
2251
2264
  generation: infectionStateRef.current.generation
2252
2265
  };
2266
+ blankNeighborCountGenerationRef.current = -1;
2267
+ blankNeighborCountRef.current = new Map();
2253
2268
  setInfectionState(filteredState);
2254
2269
  infectionStateRef.current = filteredState;
2255
2270
  if (workerRef.current) {
@@ -2277,6 +2292,8 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2277
2292
  // Use the live spawnClusterMax from workerDebug when available so photo reloads honor debug tuning
2278
2293
  const isSpherical = gridMetadataRef.current?.isSpherical ?? false;
2279
2294
  const initState = initializeInfectionSystem(hexPositions, photos, effectiveHexRadius, workerDebugRef.current?.spawnClusterMax ?? 8, logger, isSpherical);
2295
+ blankNeighborCountGenerationRef.current = -1;
2296
+ blankNeighborCountRef.current = new Map();
2280
2297
  setInfectionState(initState);
2281
2298
  infectionStateRef.current = initState;
2282
2299
  if (workerRef.current) {
@@ -2408,6 +2425,20 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2408
2425
  dlog('Processing evolved state with', data.infections.length, 'gossip entries');
2409
2426
  // Convert back from array to Map
2410
2427
  const newInfectionsMap = new Map(data.infections);
2428
+ if (Array.isArray(data.blankNeighborCounts)) {
2429
+ try {
2430
+ blankNeighborCountRef.current = new Map(data.blankNeighborCounts);
2431
+ blankNeighborCountGenerationRef.current = typeof data.generation === 'number' ? data.generation : -1;
2432
+ }
2433
+ catch (err) {
2434
+ // ignore malformed worker payload
2435
+ }
2436
+ }
2437
+ else {
2438
+ // Fallback to local computation when worker payload doesn't include counts.
2439
+ blankNeighborCountGenerationRef.current = -1;
2440
+ blankNeighborCountRef.current = new Map();
2441
+ }
2411
2442
  // Capture tile centers for debug visualization if available
2412
2443
  if (data.tileCenters && Array.isArray(data.tileCenters)) {
2413
2444
  try {
@@ -2477,6 +2508,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2477
2508
  });
2478
2509
  // If no changes, just replace state
2479
2510
  if (changes.length === 0) {
2511
+ streamTouchesOccupancyRef.current = false;
2480
2512
  setInfectionState({
2481
2513
  infections: newInfectionsMap,
2482
2514
  availableIndices: data.availableIndices,
@@ -2494,6 +2526,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2494
2526
  // Start streaming changes; cancel any previous streamer.
2495
2527
  streamTokenRef.current += 1;
2496
2528
  const token = streamTokenRef.current;
2529
+ streamTouchesOccupancyRef.current = changes.some((ch) => ch.type !== 'update');
2497
2530
  // Mark streaming active so other code (animation loop) can avoid posting new evolves
2498
2531
  streamActiveRef.current = true;
2499
2532
  try {
@@ -2520,6 +2553,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2520
2553
  // stop if cancelled
2521
2554
  if (streamTokenRef.current !== token) {
2522
2555
  streamActiveRef.current = false;
2556
+ streamTouchesOccupancyRef.current = false;
2523
2557
  try {
2524
2558
  setStreamingActive(false);
2525
2559
  }
@@ -2567,6 +2601,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2567
2601
  }
2568
2602
  else {
2569
2603
  streamActiveRef.current = false;
2604
+ streamTouchesOccupancyRef.current = false;
2570
2605
  try {
2571
2606
  setStreamingActive(false);
2572
2607
  }
@@ -2598,6 +2633,10 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2598
2633
  streamActiveRef.current = false;
2599
2634
  }
2600
2635
  catch (e) { }
2636
+ try {
2637
+ streamTouchesOccupancyRef.current = false;
2638
+ }
2639
+ catch (e) { }
2601
2640
  try {
2602
2641
  setStreamingActive(false);
2603
2642
  }
@@ -2657,6 +2696,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2657
2696
  }
2658
2697
  finally {
2659
2698
  streamActiveRef.current = false;
2699
+ streamTouchesOccupancyRef.current = false;
2660
2700
  try {
2661
2701
  setStreamingActive(false);
2662
2702
  }
@@ -2687,25 +2727,54 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2687
2727
  // Clear canvas
2688
2728
  ctx.fillStyle = '#001122';
2689
2729
  ctx.fillRect(0, 0, screenWidth, screenHeight);
2690
- // Precompute which infected hexagons are adjacent to a blank (uninfected) hex
2691
- const infectedIndices = Array.from(infectionState.infections.keys());
2692
- const infectedSet = new Set(infectedIndices);
2693
- const blankNeighborCount = new Map();
2694
- for (const idx of infectedIndices) {
2695
- // Guard: ensure the index is valid before processing
2696
- if (idx < 0 || idx >= hexPositions.length)
2697
- continue;
2698
- const isSpherical = gridMetadataRef.current?.isSpherical ?? false;
2699
- const neighbors = getNeighbors(idx, hexPositions, drawnHexRadius, isSpherical);
2700
- let count = 0;
2701
- for (const n of neighbors) {
2702
- if (!infectedSet.has(n))
2703
- count++;
2730
+ const infections = infectionState.infections;
2731
+ const dbg = workerDebugRef.current;
2732
+ const projectedPositions = projectedPositionsRef.current;
2733
+ const pulseMap = tilePulseRef.current;
2734
+ const prevAlphaMap = tileAlphaRef.current;
2735
+ const smooth = Math.max(0, Math.min(1, dbg?.translucencySmoothing ?? 0.08));
2736
+ const scratchEnabled = !!dbg?.scratchEnabled;
2737
+ const sheenIntensity = dbg?.sheenIntensity ?? 0.12;
2738
+ const sheenEnabled = !!dbg?.sheenEnabled;
2739
+ const seamInset = dbg?.clusterUvInset ?? 0.0;
2740
+ const isSpherical = gridMetadataRef.current?.isSpherical ?? false;
2741
+ const computeLocalBlankNeighborCounts = () => {
2742
+ const cache = localBlankNeighborCacheRef.current;
2743
+ if (cache.infectionMap === infections &&
2744
+ cache.positions === hexPositions &&
2745
+ cache.hexRadius === drawnHexRadius) {
2746
+ return cache.counts;
2747
+ }
2748
+ const infectedIndices = Array.from(infections.keys());
2749
+ const infectedSet = new Set(infectedIndices);
2750
+ const computed = new Map();
2751
+ for (const idx of infectedIndices) {
2752
+ if (idx < 0 || idx >= hexPositions.length)
2753
+ continue;
2754
+ const neighbors = getNeighbors(idx, hexPositions, drawnHexRadius, isSpherical);
2755
+ let count = 0;
2756
+ for (const n of neighbors) {
2757
+ if (!infectedSet.has(n))
2758
+ count++;
2759
+ }
2760
+ computed.set(idx, count);
2704
2761
  }
2705
- blankNeighborCount.set(idx, count);
2706
- }
2762
+ localBlankNeighborCacheRef.current = {
2763
+ infectionMap: infections,
2764
+ positions: hexPositions,
2765
+ hexRadius: drawnHexRadius,
2766
+ counts: computed
2767
+ };
2768
+ return computed;
2769
+ };
2770
+ const canUseWorkerBlankCounts = blankNeighborCountRef.current.size > 0 &&
2771
+ blankNeighborCountGenerationRef.current === infectionState.generation &&
2772
+ !(streamActiveRef.current && streamTouchesOccupancyRef.current);
2773
+ const blankNeighborCount = canUseWorkerBlankCounts
2774
+ ? blankNeighborCountRef.current
2775
+ : computeLocalBlankNeighborCounts();
2707
2776
  // compute sheen progress using configured speed
2708
- const sheenSpeed = Math.max(0.1, workerDebugRef.current?.sheenSpeed || 10);
2777
+ const sheenSpeed = Math.max(0.1, dbg?.sheenSpeed || 10);
2709
2778
  const now = performance.now();
2710
2779
  const sheenProgress = ((now / 1000) % sheenSpeed) / sheenSpeed;
2711
2780
  // Draw hexagons (with alpha smoothing)
@@ -2743,39 +2812,30 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2743
2812
  }
2744
2813
  for (let index = 0; index < hexPositions.length; index++) {
2745
2814
  const position = hexPositions[index];
2746
- // Validate index bounds first
2747
- if (index < 0 || index >= hexPositions.length)
2748
- continue;
2749
- const infection = infectionState.infections.get(index);
2750
- // CRITICAL GUARD: Skip drawing if this is an uninfected cell with no valid position data
2751
- // This prevents "ghost" hexagons from appearing in the background
2752
- if (!infection && !position)
2815
+ if (!position)
2753
2816
  continue;
2817
+ const infection = infections.get(index);
2754
2818
  const blankCount = infection ? (blankNeighborCount.get(index) || 0) : 0;
2755
2819
  // target alpha based on blank count
2756
2820
  const targetAlpha = infection ? (1.0 - Math.min(blankCount, 6) * 0.066) : 1.0;
2757
- const prevAlphaMap = tileAlphaRef.current;
2758
2821
  const prev = prevAlphaMap.get(index) ?? targetAlpha;
2759
- const smooth = Math.max(0, Math.min(1, workerDebugRef.current?.translucencySmoothing ?? 0.08));
2760
2822
  const smoothed = prev + (targetAlpha - prev) * smooth;
2761
2823
  prevAlphaMap.set(index, smoothed);
2762
- const scratchEnabled = !!workerDebugRef.current?.scratchEnabled;
2763
- const sheenIntensity = workerDebugRef.current?.sheenIntensity ?? 0.12;
2764
2824
  // Compute pulse progress for this tile
2765
- const pulseInfo = tilePulseRef.current.get(index);
2825
+ const pulseInfo = pulseMap.get(index);
2766
2826
  let pulseProgress = 0;
2767
2827
  if (pulseInfo) {
2768
- const elapsed = performance.now() - pulseInfo.start;
2828
+ const elapsed = now - pulseInfo.start;
2769
2829
  pulseProgress = Math.max(0, Math.min(1, elapsed / pulseInfo.duration));
2770
2830
  if (pulseProgress >= 1)
2771
- tilePulseRef.current.delete(index);
2831
+ pulseMap.delete(index);
2772
2832
  }
2773
2833
  // Use projected position and scale for drawing
2774
- const proj = projectedPositionsRef.current[index] || [position[0], position[1], 1, 0, 0];
2775
- const projX = proj[0];
2776
- const projY = proj[1];
2777
- const projScale = proj[2];
2778
- const projAngle = proj[3] || 0;
2834
+ const proj = projectedPositions[index];
2835
+ const projX = proj ? proj[0] : position[0];
2836
+ const projY = proj ? proj[1] : position[1];
2837
+ const projScale = proj ? proj[2] : 1;
2838
+ const projAngle = proj ? (proj[3] || 0) : 0;
2779
2839
  // proj[4] is z-depth (not needed for drawing, only for click detection)
2780
2840
  // Selective culling: when low-res mode is active, always draw infected tiles but
2781
2841
  // sample uninfected/background tiles to reduce draw count while preserving layout.
@@ -2790,17 +2850,15 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2790
2850
  }
2791
2851
  }
2792
2852
  // Draw primary - use drawnHexRadius to account for spacing
2793
- const sheenEnabled = !!workerDebugRef.current?.sheenEnabled;
2794
- drawHexagon(ctx, [projX, projY, 0], drawnHexRadius * projScale, infection, textures, index, blankCount, smoothed, sheenProgress, sheenIntensity, sheenEnabled, scratchEnabled, scratchCanvasRef.current, workerDebugRef.current?.clusterUvInset ?? 0.0, pulseProgress, false, false, projAngle);
2853
+ drawHexagon(ctx, [projX, projY, 0], drawnHexRadius * projScale, infection, textures, index, blankCount, smoothed, sheenProgress, sheenIntensity, sheenEnabled, scratchEnabled, scratchCanvasRef.current, seamInset, pulseProgress, false, false, projAngle);
2795
2854
  // Optionally draw antipodal copy (opposite side of sphere).
2796
2855
  // Use the same projection helper as hit-tests so angles/positions match exactly.
2797
- if (workerDebug.renderBothSides) {
2856
+ if (dbg?.renderBothSides) {
2798
2857
  try {
2799
2858
  const anti = mapAndProject(hexPositions[index], true);
2800
2859
  const antiScale = anti.scale || 1;
2801
2860
  const antiAngle = anti.angle || 0;
2802
- const sheenEnabledAnti = !!workerDebugRef.current?.sheenEnabled;
2803
- drawHexagon(ctx, [anti.x, anti.y, 0], drawnHexRadius * antiScale, infection, textures, index, blankCount, smoothed, sheenProgress, sheenIntensity, sheenEnabledAnti, scratchEnabled, scratchCanvasRef.current, workerDebugRef.current?.clusterUvInset ?? 0.0, pulseProgress, false, true, antiAngle);
2861
+ drawHexagon(ctx, [anti.x, anti.y, 0], drawnHexRadius * antiScale, infection, textures, index, blankCount, smoothed, sheenProgress, sheenIntensity, sheenEnabled, scratchEnabled, scratchCanvasRef.current, seamInset, pulseProgress, false, true, antiAngle);
2804
2862
  }
2805
2863
  catch (err) {
2806
2864
  // Skip drawing antipodal hex if projection fails
@@ -2816,10 +2874,11 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2816
2874
  try {
2817
2875
  if (workerDebugRef.current?.debugLogs) {
2818
2876
  // Sample a few random infected hexes to check
2877
+ const infectedIndices = Array.from(infections.keys());
2819
2878
  const sampleSize = Math.min(5, infectedIndices.length);
2820
2879
  for (let s = 0; s < sampleSize; s++) {
2821
2880
  const idx = infectedIndices[Math.floor(Math.random() * infectedIndices.length)];
2822
- const infection = infectionState.infections.get(idx);
2881
+ const infection = infections.get(idx);
2823
2882
  if (!infection)
2824
2883
  continue;
2825
2884
  const texture = textures.get(infection.photo.id);
@@ -2829,7 +2888,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2829
2888
  const isSpherical = gridMetadataRef.current?.isSpherical ?? false;
2830
2889
  const neighbors = getNeighbors(idx, hexPositions, drawnHexRadius, isSpherical);
2831
2890
  for (const nIdx of neighbors) {
2832
- const nInf = infectionState.infections.get(nIdx);
2891
+ const nInf = infections.get(nIdx);
2833
2892
  // Only check neighbors with the same photo (adjacent tiles of same image)
2834
2893
  if (!nInf || nInf.photo.id !== infection.photo.id)
2835
2894
  continue;
@@ -3016,7 +3075,9 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
3016
3075
  // If camera changed recently or infections changed (generation advanced), draw immediately
3017
3076
  const gen = infectionState.generation;
3018
3077
  const genChanged = gen !== lastDrawGenRef.current;
3019
- if (cameraDirtyRef.current || genChanged || timeSinceLast >= minFrameMs) {
3078
+ const hasVisualAnimation = !!workerDebugRef.current?.sheenEnabled || tilePulseRef.current.size > 0 || streamActiveRef.current;
3079
+ const targetFrameMs = hasVisualAnimation ? activeFrameMs : idleFrameMs;
3080
+ if (cameraDirtyRef.current || genChanged || timeSinceLast >= targetFrameMs) {
3020
3081
  draw();
3021
3082
  lastDrawTimeRef.current = now;
3022
3083
  lastDrawGenRef.current = gen;
@@ -3113,6 +3174,8 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
3113
3174
  logger.error('spawnClusterAt: Invalid center index', centerIndex);
3114
3175
  return;
3115
3176
  }
3177
+ blankNeighborCountGenerationRef.current = -1;
3178
+ blankNeighborCountRef.current = new Map();
3116
3179
  setInfectionState(prevState => {
3117
3180
  const newInfections = new Map(prevState.infections);
3118
3181
  const newAvailableIndices = [...prevState.availableIndices];
@@ -1,13 +1,7 @@
1
1
  export declare const logger: {
2
2
  debug: (..._args: unknown[]) => void;
3
- log: {
4
- (...data: any[]): void;
5
- (...data: any[]): void;
6
- };
7
- info: {
8
- (...data: any[]): void;
9
- (...data: any[]): void;
10
- };
3
+ log: (..._args: unknown[]) => void;
4
+ info: (..._args: unknown[]) => void;
11
5
  warn: {
12
6
  (...data: any[]): void;
13
7
  (...data: any[]): void;
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,MAAM;sBANK,OAAO,EAAE;;;;;;;;;;;;;;;;;CAYhC,CAAA"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,MAAM;sBANK,OAAO,EAAE;oBAAT,OAAO,EAAE;qBAAT,OAAO,EAAE;;;;;;;;;CAYhC,CAAA"}
@@ -2,8 +2,8 @@ const noop = (..._args) => { };
2
2
  const isDev = typeof process !== 'undefined' && process?.env?.NODE_ENV !== 'production';
3
3
  export const logger = {
4
4
  debug: isDev ? console.debug.bind(console) : noop,
5
- log: console.log.bind(console),
6
- info: console.info.bind(console),
5
+ log: isDev ? console.log.bind(console) : noop,
6
+ info: isDev ? console.info.bind(console) : noop,
7
7
  warn: console.warn.bind(console),
8
8
  error: console.error.bind(console),
9
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"HexGridWasmWrapper.d.ts","sourceRoot":"","sources":["../../src/wasm/HexGridWasmWrapper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"HexGridWasmWrapper.d.ts","sourceRoot":"","sources":["../../src/wasm/HexGridWasmWrapper.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module wasm/HexGridWasmWrapper
8
8
  */
9
+ import { logger } from '../lib/logger';
9
10
  /**
10
11
  * Wrapper class that provides TypeScript interface to WASM
11
12
  * with automatic fallback
@@ -36,11 +37,11 @@ export class HexGridWasmWrapper {
36
37
  const wasmModule = await import('../rust/pkg/hexgrid_wasm');
37
38
  await wasmModule.default();
38
39
  this.module = wasmModule;
39
- console.log('[HexGrid] WASM module loaded successfully');
40
+ logger.log('[HexGrid] WASM module loaded successfully');
40
41
  return this.module;
41
42
  }
42
43
  catch (error) {
43
- console.warn('[HexGrid] WASM module not available, using fallback:', error);
44
+ logger.warn('[HexGrid] WASM module not available, using fallback:', error);
44
45
  this.loadFailed = true;
45
46
  return null;
46
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WebGPUContext.d.ts","sourceRoot":"","sources":["../../src/webgpu/WebGPUContext.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,WAAW,CAAkB;IAErC,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,aAAa;IAOnC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAoCpC,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,UAAU,IAAI,UAAU,GAAG,IAAI;IAI/B,WAAW,IAAI,OAAO;CAGvB"}
1
+ {"version":3,"file":"WebGPUContext.d.ts","sourceRoot":"","sources":["../../src/webgpu/WebGPUContext.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,WAAW,CAAkB;IAErC,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,aAAa;IAOnC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAoCpC,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,UAAU,IAAI,UAAU,GAAG,IAAI;IAI/B,WAAW,IAAI,OAAO;CAGvB"}
@@ -2,6 +2,7 @@
2
2
  * WebGPU Context Manager
3
3
  * Handles the creation and management of the WebGPU Device and Adapter.
4
4
  */
5
+ import { logger } from '../lib/logger';
5
6
  export class WebGPUContext {
6
7
  constructor() {
7
8
  this.adapter = null;
@@ -19,7 +20,7 @@ export class WebGPUContext {
19
20
  */
20
21
  async initialize() {
21
22
  if (typeof navigator === 'undefined' || !navigator.gpu) {
22
- console.warn('WebGPU is not supported in this environment.');
23
+ logger.warn('WebGPU is not supported in this environment.');
23
24
  this.isSupported = false;
24
25
  return false;
25
26
  }
@@ -28,22 +29,22 @@ export class WebGPUContext {
28
29
  powerPreference: 'high-performance'
29
30
  });
30
31
  if (!this.adapter) {
31
- console.warn('No WebGPU adapter found.');
32
+ logger.warn('No WebGPU adapter found.');
32
33
  this.isSupported = false;
33
34
  return false;
34
35
  }
35
36
  this.device = await this.adapter.requestDevice();
36
37
  this.device.lost.then((info) => {
37
- console.error(`WebGPU device lost: ${info.message}`);
38
+ logger.error(`WebGPU device lost: ${info.message}`);
38
39
  this.device = null;
39
40
  this.isSupported = false;
40
41
  });
41
42
  this.isSupported = true;
42
- console.log('WebGPU initialized successfully.');
43
+ logger.log('WebGPU initialized successfully.');
43
44
  return true;
44
45
  }
45
46
  catch (e) {
46
- console.error('Failed to initialize WebGPU:', e);
47
+ logger.error('Failed to initialize WebGPU:', e);
47
48
  this.isSupported = false;
48
49
  return false;
49
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WebNNContext.d.ts","sourceRoot":"","sources":["../../src/webnn/WebNNContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAe;IACtC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,WAAW,CAAkB;IAErC,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,YAAY;IAOlC;;OAEG;IACG,UAAU,CAAC,UAAU,GAAE,eAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuCvE,UAAU,IAAI,SAAS,GAAG,IAAI;IAI9B,aAAa,IAAI,eAAe;IAIhC,WAAW,IAAI,OAAO;CAGvB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,SAAS;QACjB,EAAE,EAAE;YACF,aAAa,CAAC,OAAO,CAAC,EAAE;gBAAE,UAAU,CAAC,EAAE,MAAM,CAAA;aAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;SACtE,CAAC;KACH;IAED,UAAU,SAAS;QAEjB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;KACtI;IAED,UAAU,OAAO;KAEhB;IAED,UAAU,eAAe;KAExB;CACF"}
1
+ {"version":3,"file":"WebNNContext.d.ts","sourceRoot":"","sources":["../../src/webnn/WebNNContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAe;IACtC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,WAAW,CAAkB;IAErC,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,YAAY;IAOlC;;OAEG;IACG,UAAU,CAAC,UAAU,GAAE,eAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuCvE,UAAU,IAAI,SAAS,GAAG,IAAI;IAI9B,aAAa,IAAI,eAAe;IAIhC,WAAW,IAAI,OAAO;CAGvB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,SAAS;QACjB,EAAE,EAAE;YACF,aAAa,CAAC,OAAO,CAAC,EAAE;gBAAE,UAAU,CAAC,EAAE,MAAM,CAAA;aAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;SACtE,CAAC;KACH;IAED,UAAU,SAAS;QAEjB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;KACtI;IAED,UAAU,OAAO;KAEhB;IAED,UAAU,eAAe;KAExB;CACF"}
@@ -3,6 +3,7 @@
3
3
  * Handles the creation and management of the WebNN MLContext.
4
4
  * Prioritizes NPU -> GPU -> CPU.
5
5
  */
6
+ import { logger } from '../lib/logger';
6
7
  export class WebNNContext {
7
8
  constructor() {
8
9
  this.context = null;
@@ -20,7 +21,7 @@ export class WebNNContext {
20
21
  */
21
22
  async initialize(preference = 'npu') {
22
23
  if (typeof navigator === 'undefined' || !navigator.ml) {
23
- console.warn('WebNN is not supported in this environment.');
24
+ logger.warn('WebNN is not supported in this environment.');
24
25
  this.isSupported = false;
25
26
  return false;
26
27
  }
@@ -29,11 +30,11 @@ export class WebNNContext {
29
30
  this.context = await navigator.ml.createContext({ deviceType: preference });
30
31
  this.deviceType = preference;
31
32
  this.isSupported = true;
32
- console.log(`WebNN initialized successfully on ${preference}`);
33
+ logger.log(`WebNN initialized successfully on ${preference}`);
33
34
  return true;
34
35
  }
35
36
  catch (e) {
36
- console.warn(`Failed to initialize WebNN on ${preference}, trying fallback chain...`, e);
37
+ logger.warn(`Failed to initialize WebNN on ${preference}, trying fallback chain...`, e);
37
38
  // Fallback chain: NPU -> GPU -> CPU
38
39
  const chain = ['npu', 'gpu', 'cpu'];
39
40
  const startIndex = chain.indexOf(preference) + 1;
@@ -43,11 +44,11 @@ export class WebNNContext {
43
44
  this.context = await navigator.ml.createContext({ deviceType: fallback });
44
45
  this.deviceType = fallback;
45
46
  this.isSupported = true;
46
- console.log(`WebNN initialized successfully on fallback ${fallback}`);
47
+ logger.log(`WebNN initialized successfully on fallback ${fallback}`);
47
48
  return true;
48
49
  }
49
50
  catch (err) {
50
- console.warn(`Failed to initialize WebNN on fallback ${fallback}`, err);
51
+ logger.warn(`Failed to initialize WebNN on fallback ${fallback}`, err);
51
52
  }
52
53
  }
53
54
  }
@@ -7,7 +7,11 @@
7
7
  // - defensive guards and error reporting via postMessage({type:'error', ...})
8
8
  import { getGridBounds as _getGridBounds, distanceBetween as _distanceBetween, calculateUvBoundsFromGridPosition as _calculateUvBoundsFromGridPosition, calculateContiguity as _calculateContiguity, calculatePhotoContiguity as _calculatePhotoContiguity, } from './hexgrid-math';
9
9
  const WORKER_ID = Math.random().toString(36).substring(7);
10
- console.log('[hexgrid-worker] loaded id=', WORKER_ID);
10
+ /** Guarded log only emits when workerDebug.debugLogs is true. */
11
+ function debugLog(...args) {
12
+ if (workerDebug.debugLogs)
13
+ console.log(...args);
14
+ }
11
15
  const workerDebug = {
12
16
  cohesionBoost: 6.0, // BOOSTED: strongly favor growth near cluster centroids to build larger regions
13
17
  enableMerges: true, // ENABLED: merge small fragments into nearby larger clusters
@@ -149,10 +153,28 @@ function getNeighborsCached(index, positions, hexRadius) {
149
153
  function calculateUvBoundsFromGridPosition(gridCol, gridRow, tilesX, tilesY) {
150
154
  return _calculateUvBoundsFromGridPosition(gridCol, gridRow, tilesX, tilesY);
151
155
  }
156
+ function buildBlankNeighborCounts(infections, positions, hexRadius) {
157
+ if (!infections || infections.size === 0)
158
+ return [];
159
+ const infectedSet = new Set(infections.keys());
160
+ const out = [];
161
+ for (const idx of infectedSet) {
162
+ if (idx < 0 || idx >= positions.length)
163
+ continue;
164
+ const neighbors = getNeighborsCached(idx, positions, hexRadius);
165
+ let blankCount = 0;
166
+ for (const n of neighbors) {
167
+ if (!infectedSet.has(n))
168
+ blankCount++;
169
+ }
170
+ out.push([idx, blankCount]);
171
+ }
172
+ return out;
173
+ }
152
174
  function findConnectedComponents(indices, positions, hexRadius) {
153
175
  // Immediate synchronous check - if this doesn't log, the function isn't being called or is blocked
154
176
  const startMarker = performance.now();
155
- console.log('[findConnectedComponents] FUNCTION ENTERED - indices.length=', indices.length, 'positions.length=', positions.length, 'hexRadius=', hexRadius, 'marker=', startMarker);
177
+ debugLog('[findConnectedComponents] FUNCTION ENTERED - indices.length=', indices.length, 'positions.length=', positions.length, 'hexRadius=', hexRadius, 'marker=', startMarker);
156
178
  // Validate inputs immediately
157
179
  if (!indices || !Array.isArray(indices)) {
158
180
  console.error('[findConnectedComponents] Invalid indices:', indices);
@@ -166,13 +188,13 @@ function findConnectedComponents(indices, positions, hexRadius) {
166
188
  console.error('[findConnectedComponents] Invalid hexRadius:', hexRadius);
167
189
  return [];
168
190
  }
169
- console.log('[findConnectedComponents] About to enter try block');
191
+ debugLog('[findConnectedComponents] About to enter try block');
170
192
  // Add immediate log after try block entry to confirm execution reaches here
171
193
  let tryBlockEntered = false;
172
194
  try {
173
195
  tryBlockEntered = true;
174
- console.log('[findConnectedComponents] ✅ TRY BLOCK ENTERED - marker=', performance.now() - startMarker, 'ms');
175
- console.log('[findConnectedComponents] Inside try block - Starting with', indices.length, 'indices');
196
+ debugLog('[findConnectedComponents] ✅ TRY BLOCK ENTERED - marker=', performance.now() - startMarker, 'ms');
197
+ debugLog('[findConnectedComponents] Inside try block - Starting with', indices.length, 'indices');
176
198
  const set = new Set(indices);
177
199
  const visited = new Set();
178
200
  const comps = [];
@@ -181,7 +203,7 @@ function findConnectedComponents(indices, positions, hexRadius) {
181
203
  if (visited.has(start))
182
204
  continue;
183
205
  componentCount++;
184
- console.log('[findConnectedComponents] Starting component', componentCount, 'from index', start);
206
+ debugLog('[findConnectedComponents] Starting component', componentCount, 'from index', start);
185
207
  const q = [start];
186
208
  visited.add(start);
187
209
  const comp = [];
@@ -194,7 +216,7 @@ function findConnectedComponents(indices, positions, hexRadius) {
194
216
  break;
195
217
  }
196
218
  if (iterations % 100 === 0) {
197
- console.log('[findConnectedComponents] Component', componentCount, 'iteration', iterations, 'queue length', q.length);
219
+ debugLog('[findConnectedComponents] Component', componentCount, 'iteration', iterations, 'queue length', q.length);
198
220
  }
199
221
  const cur = q.shift();
200
222
  if (cur === undefined || cur === null) {
@@ -224,12 +246,12 @@ function findConnectedComponents(indices, positions, hexRadius) {
224
246
  continue;
225
247
  }
226
248
  }
227
- console.log('[findConnectedComponents] Component', componentCount, 'complete:', comp.length, 'nodes,', iterations, 'iterations');
249
+ debugLog('[findConnectedComponents] Component', componentCount, 'complete:', comp.length, 'nodes,', iterations, 'iterations');
228
250
  comps.push(comp);
229
251
  }
230
- console.log('[findConnectedComponents] Complete:', comps.length, 'components found');
252
+ debugLog('[findConnectedComponents] Complete:', comps.length, 'components found');
231
253
  const elapsed = performance.now() - startMarker;
232
- console.log('[findConnectedComponents] ✅ RETURNING - elapsed=', elapsed, 'ms, components=', comps.length);
254
+ debugLog('[findConnectedComponents] ✅ RETURNING - elapsed=', elapsed, 'ms, components=', comps.length);
233
255
  return comps;
234
256
  }
235
257
  catch (e) {
@@ -250,7 +272,7 @@ function findConnectedComponents(indices, positions, hexRadius) {
250
272
  }
251
273
  function calculatePhotoCentroids(infections, positions, hexRadius) {
252
274
  try {
253
- console.log('[calculatePhotoCentroids] Starting with', infections.size, 'infections');
275
+ debugLog('[calculatePhotoCentroids] Starting with', infections.size, 'infections');
254
276
  const byPhoto = new Map();
255
277
  for (const [idx, inf] of infections) {
256
278
  if (!inf || !inf.photo)
@@ -259,14 +281,14 @@ function calculatePhotoCentroids(infections, positions, hexRadius) {
259
281
  arr.push(idx);
260
282
  byPhoto.set(inf.photo.id, arr);
261
283
  }
262
- console.log('[calculatePhotoCentroids] Grouped into', byPhoto.size, 'photos');
284
+ debugLog('[calculatePhotoCentroids] Grouped into', byPhoto.size, 'photos');
263
285
  const centroids = new Map();
264
286
  let photoNum = 0;
265
287
  for (const [photoId, inds] of byPhoto) {
266
288
  photoNum++;
267
- console.log('[calculatePhotoCentroids] Processing photo', photoNum, '/', byPhoto.size, 'photoId=', photoId, 'indices=', inds.length);
289
+ debugLog('[calculatePhotoCentroids] Processing photo', photoNum, '/', byPhoto.size, 'photoId=', photoId, 'indices=', inds.length);
268
290
  try {
269
- console.log('[calculatePhotoCentroids] About to call findConnectedComponents with', inds.length, 'indices');
291
+ debugLog('[calculatePhotoCentroids] About to call findConnectedComponents with', inds.length, 'indices');
270
292
  const callStartTime = performance.now();
271
293
  let comps;
272
294
  try {
@@ -282,7 +304,7 @@ function calculatePhotoCentroids(infections, positions, hexRadius) {
282
304
  else {
283
305
  comps = findConnectedComponents(inds, positions, hexRadius);
284
306
  const callElapsed = performance.now() - callStartTime;
285
- console.log('[calculatePhotoCentroids] findConnectedComponents RETURNED with', comps.length, 'components after', callElapsed, 'ms');
307
+ debugLog('[calculatePhotoCentroids] findConnectedComponents RETURNED with', comps.length, 'components after', callElapsed, 'ms');
286
308
  }
287
309
  }
288
310
  catch (e) {
@@ -291,8 +313,8 @@ function calculatePhotoCentroids(infections, positions, hexRadius) {
291
313
  // Return empty components on error to allow evolution to continue
292
314
  comps = [];
293
315
  }
294
- console.log('[calculatePhotoCentroids] findConnectedComponents returned', comps.length, 'components');
295
- console.log('[calculatePhotoCentroids] Found', comps.length, 'components for photo', photoId);
316
+ debugLog('[calculatePhotoCentroids] findConnectedComponents returned', comps.length, 'components');
317
+ debugLog('[calculatePhotoCentroids] Found', comps.length, 'components for photo', photoId);
296
318
  const cs = [];
297
319
  for (const comp of comps) {
298
320
  let sx = 0, sy = 0;
@@ -313,7 +335,7 @@ function calculatePhotoCentroids(infections, positions, hexRadius) {
313
335
  centroids.set(photoId, []);
314
336
  }
315
337
  }
316
- console.log('[calculatePhotoCentroids] Completed, returning', centroids.size, 'photo centroids');
338
+ debugLog('[calculatePhotoCentroids] Completed, returning', centroids.size, 'photo centroids');
317
339
  return centroids;
318
340
  }
319
341
  catch (e) {
@@ -330,7 +352,7 @@ function calculateContiguity(indices, positions, hexRadius) {
330
352
  function assignClusterGridPositions(infections, positions, hexRadius) {
331
353
  const debugCenters = [];
332
354
  try {
333
- console.log('[assignClusterGridPositions] Starting with', infections.size, 'infections');
355
+ debugLog('[assignClusterGridPositions] Starting with', infections.size, 'infections');
334
356
  // Group infections by photo
335
357
  const byPhoto = new Map();
336
358
  for (const [idx, inf] of infections) {
@@ -340,7 +362,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
340
362
  arr.push(idx);
341
363
  byPhoto.set(inf.photo.id, arr);
342
364
  }
343
- console.log('[assignClusterGridPositions] Processing', byPhoto.size, 'unique photos');
365
+ debugLog('[assignClusterGridPositions] Processing', byPhoto.size, 'unique photos');
344
366
  // Cluster size analytics
345
367
  let totalClusters = 0;
346
368
  let clusterSizes = [];
@@ -353,7 +375,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
353
375
  if (comp && comp.length > 0)
354
376
  clusterSizes.push(comp.length);
355
377
  }
356
- console.log('[assignClusterGridPositions] Photo', photoId.substring(0, 8), 'has', components.length, 'clusters, sizes:', components.map((c) => c.length).join(','));
378
+ debugLog('[assignClusterGridPositions] Photo', photoId.substring(0, 8), 'has', components.length, 'clusters, sizes:', components.map((c) => c.length).join(','));
357
379
  // Process each cluster separately
358
380
  let clusterIndex = 0;
359
381
  for (const cluster of components) {
@@ -432,7 +454,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
432
454
  tilesY++;
433
455
  }
434
456
  }
435
- console.log('[assignClusterGridPositions][hex-lattice] cluster', photoId.substring(0, 8), 'size', cluster.length, 'latticeCols', latticeCols, 'latticeRows', latticeRows, 'tilesX', tilesX, 'tilesY', tilesY);
457
+ debugLog('[assignClusterGridPositions][hex-lattice] cluster', photoId.substring(0, 8), 'size', cluster.length, 'latticeCols', latticeCols, 'latticeRows', latticeRows, 'tilesX', tilesX, 'tilesY', tilesY);
436
458
  // Build optional serpentine ordering for assignment uniqueness (not strictly needed since lattice mapping is direct)
437
459
  const serpentine = workerDebug.clusterScanMode === 'serpentine';
438
460
  // Assign each infection a gridPosition derived from lattice coordinates compressed into tile grid domain.
@@ -586,7 +608,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
586
608
  }
587
609
  const clusterWidth = Math.max(0, maxX - minX);
588
610
  const clusterHeight = Math.max(0, maxY - minY);
589
- console.log('[assignClusterGridPositions] Cluster bounds:', {
611
+ debugLog('[assignClusterGridPositions] Cluster bounds:', {
590
612
  photoId: photoId.substring(0, 8),
591
613
  clusterIndex,
592
614
  hexCount: cluster.length,
@@ -601,7 +623,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
601
623
  // This ensures the tile grid matches the spatial layout of the cluster
602
624
  const clusterAspect = clusterHeight > 0 ? clusterWidth / clusterHeight : 1.0;
603
625
  const targetTileCount = 16; // Target ~16 tiles total for good image distribution
604
- console.log('[assignClusterGridPositions] Cluster aspect:', clusterAspect.toFixed(3), '(width/height)');
626
+ debugLog('[assignClusterGridPositions] Cluster aspect:', clusterAspect.toFixed(3), '(width/height)');
605
627
  let tilesX;
606
628
  let tilesY;
607
629
  if (cluster.length === 1) {
@@ -659,13 +681,13 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
659
681
  newTilesY = Math.max(1, Math.min(16, newTilesY));
660
682
  tilesX = newTilesX;
661
683
  tilesY = newTilesY;
662
- console.log('[assignClusterGridPositions] Expanded tile grid to', tilesX, 'x', tilesY, '=', tilesX * tilesY, 'tiles');
684
+ debugLog('[assignClusterGridPositions] Expanded tile grid to', tilesX, 'x', tilesY, '=', tilesX * tilesY, 'tiles');
663
685
  }
664
686
  }
665
687
  catch (e) {
666
688
  // if anything goes wrong, keep original tilesX/tilesY
667
689
  }
668
- console.log('[assignClusterGridPositions] Final tile dimensions:', tilesX, 'x', tilesY, '=', tilesX * tilesY, 'tiles for', cluster.length, 'hexes');
690
+ debugLog('[assignClusterGridPositions] Final tile dimensions:', tilesX, 'x', tilesY, '=', tilesX * tilesY, 'tiles for', cluster.length, 'hexes');
669
691
  // Single-hex or degenerate clusters: assign a deterministic tile so single hexes don't all use [0,0]
670
692
  if (cluster.length === 1 ||
671
693
  clusterWidth < 1e-6 ||
@@ -859,7 +881,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
859
881
  const y = normMinY + v * normHeight;
860
882
  return [x, y];
861
883
  };
862
- console.log('[assignClusterGridPositions] Normalized bounds for tiling:', {
884
+ debugLog('[assignClusterGridPositions] Normalized bounds for tiling:', {
863
885
  normMinX: normMinX.toFixed(2),
864
886
  normMinY: normMinY.toFixed(2),
865
887
  normWidth: normWidth.toFixed(2),
@@ -923,8 +945,8 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
923
945
  });
924
946
  }
925
947
  }
926
- console.log('[assignClusterGridPositions] Spatially assigned', cluster.length, 'hexes to nearest tile centers');
927
- console.log('[assignClusterGridPositions] Sample assignments:', assignmentSamples
948
+ debugLog('[assignClusterGridPositions] Spatially assigned', cluster.length, 'hexes to nearest tile centers');
949
+ debugLog('[assignClusterGridPositions] Sample assignments:', assignmentSamples
928
950
  .map((s) => `node#${s.nodeId} at (${s.nodeX.toFixed(1)},${s.nodeY.toFixed(1)}) → tile[${s.tileCol},${s.tileRow}] center(${s.centerX.toFixed(1)},${s.centerY.toFixed(1)}) dist=${s.dist.toFixed(1)}`)
929
951
  .join('\n '));
930
952
  // Optional: Neighborhood-aware refinement to reduce visual seams
@@ -996,7 +1018,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
996
1018
  }
997
1019
  if (adjustments === 0)
998
1020
  break; // Converged
999
- console.log('[assignClusterGridPositions] Neighbor-aware refinement iteration', iter + 1, ':', adjustments, 'adjustments');
1021
+ debugLog('[assignClusterGridPositions] Neighbor-aware refinement iteration', iter + 1, ':', adjustments, 'adjustments');
1000
1022
  }
1001
1023
  }
1002
1024
  // Finally write assignments back into infections with UV bounds/inset
@@ -1028,7 +1050,7 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
1028
1050
  tilesY,
1029
1051
  });
1030
1052
  }
1031
- console.log('[assignClusterGridPositions] Assigned grid positions to', cluster.length, 'hexes in cluster (BFS)');
1053
+ debugLog('[assignClusterGridPositions] Assigned grid positions to', cluster.length, 'hexes in cluster (BFS)');
1032
1054
  }
1033
1055
  catch (e) {
1034
1056
  console.error('[assignClusterGridPositions] BFS assignment failed, falling back to quantization', e);
@@ -1044,9 +1066,9 @@ function assignClusterGridPositions(infections, positions, hexRadius) {
1044
1066
  const medianSize = clusterSizes[Math.floor(clusterSizes.length / 2)];
1045
1067
  const maxSize = clusterSizes[0];
1046
1068
  const smallClusters = clusterSizes.filter((s) => s <= 3).length;
1047
- console.log('[assignClusterGridPositions] CLUSTER STATS: total=', totalClusters, 'avg=', avgSize.toFixed(1), 'median=', medianSize, 'max=', maxSize, 'small(≤3)=', smallClusters, '/', totalClusters, '(', ((100 * smallClusters) / totalClusters).toFixed(0), '%)');
1069
+ debugLog('[assignClusterGridPositions] CLUSTER STATS: total=', totalClusters, 'avg=', avgSize.toFixed(1), 'median=', medianSize, 'max=', maxSize, 'small(≤3)=', smallClusters, '/', totalClusters, '(', ((100 * smallClusters) / totalClusters).toFixed(0), '%)');
1048
1070
  }
1049
- console.log('[assignClusterGridPositions] Complete');
1071
+ debugLog('[assignClusterGridPositions] Complete');
1050
1072
  }
1051
1073
  catch (e) {
1052
1074
  console.error('[assignClusterGridPositions] Error:', e);
@@ -1057,7 +1079,7 @@ function postOptimizationMerge(infections, positions, hexRadius, debug = false)
1057
1079
  try {
1058
1080
  if (!workerDebug || !workerDebug.enableMerges) {
1059
1081
  if (debug && workerDebug.mergeLogs)
1060
- console.log('[merge] disabled');
1082
+ debugLog('[merge] disabled');
1061
1083
  return;
1062
1084
  }
1063
1085
  const threshold = typeof workerDebug.mergeSmallComponentsThreshold === 'number'
@@ -1132,7 +1154,7 @@ function postOptimizationMerge(infections, positions, hexRadius, debug = false)
1132
1154
  }
1133
1155
  merges++;
1134
1156
  if (debug && workerDebug.mergeLogs)
1135
- console.log(`[merge] moved ${s.length} -> ${recipientId}`);
1157
+ debugLog(`[merge] moved ${s.length} -> ${recipientId}`);
1136
1158
  }
1137
1159
  }
1138
1160
  }
@@ -1187,36 +1209,36 @@ function normalizePrevState(prevState) {
1187
1209
  }
1188
1210
  function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentTime, debug = false) {
1189
1211
  try {
1190
- console.log('[evolve] Step 1: Validating positions...');
1212
+ debugLog('[evolve] Step 1: Validating positions...');
1191
1213
  if (!positions || positions.length === 0) {
1192
1214
  safePostError(new Error('positions required for evolve'));
1193
1215
  return null;
1194
1216
  }
1195
- console.log('[evolve] Step 2: Normalizing state...');
1217
+ debugLog('[evolve] Step 2: Normalizing state...');
1196
1218
  const normalized = normalizePrevState(prevState);
1197
1219
  const infectionsMap = normalized.infections;
1198
1220
  const availableSet = new Set(Array.isArray(normalized.availableIndices)
1199
1221
  ? normalized.availableIndices
1200
1222
  : []);
1201
- console.log('[evolve] Step 3: Cleaning infections...');
1223
+ debugLog('[evolve] Step 3: Cleaning infections...');
1202
1224
  for (const [idx, inf] of infectionsMap) {
1203
1225
  if (!inf || !inf.photo) {
1204
1226
  infectionsMap.delete(idx);
1205
1227
  availableSet.add(idx);
1206
1228
  }
1207
1229
  }
1208
- console.log('[evolve] Step 4: Calculating centroids...');
1230
+ debugLog('[evolve] Step 4: Calculating centroids...');
1209
1231
  const centroids = calculatePhotoCentroids(infectionsMap, positions, hexRadius);
1210
- console.log('[evolve] Step 5: Creating new state copies...');
1232
+ debugLog('[evolve] Step 5: Creating new state copies...');
1211
1233
  const newInfections = new Map(infectionsMap);
1212
1234
  const newAvailable = new Set(availableSet);
1213
1235
  const generation = prevState && typeof prevState.generation === 'number'
1214
1236
  ? prevState.generation + 1
1215
1237
  : 0;
1216
- console.log('[evolve] Step 6: Growth step - processing', infectionsMap.size, 'infections...');
1238
+ debugLog('[evolve] Step 6: Growth step - processing', infectionsMap.size, 'infections...');
1217
1239
  // Skip growth step if we have no infections or no photos
1218
1240
  if (infectionsMap.size === 0 || photos.length === 0) {
1219
- console.log('[evolve] Skipping growth - no infections or no photos');
1241
+ debugLog('[evolve] Skipping growth - no infections or no photos');
1220
1242
  }
1221
1243
  else {
1222
1244
  // Cell death step: allow fully surrounded cells to die and respawn for optimization
@@ -1351,7 +1373,7 @@ function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentT
1351
1373
  }
1352
1374
  }
1353
1375
  if (deathCount > 0 || mutationCount > 0 || invaderExpulsions > 0) {
1354
- console.log('[evolve] Cell death: removed', deathCount, 'cells (', invaderExpulsions, 'invaders expelled), mutated', mutationCount, 'cells');
1376
+ debugLog('[evolve] Cell death: removed', deathCount, 'cells (', invaderExpulsions, 'invaders expelled), mutated', mutationCount, 'cells');
1355
1377
  }
1356
1378
  }
1357
1379
  // Growth step: prefer neighbors that increase contiguity and are closer to centroids
@@ -1359,7 +1381,7 @@ function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentT
1359
1381
  for (const [idx, inf] of infectionsMap) {
1360
1382
  growthIterations++;
1361
1383
  if (growthIterations % 10 === 0)
1362
- console.log('[evolve] Growth iteration', growthIterations, '/', infectionsMap.size);
1384
+ debugLog('[evolve] Growth iteration', growthIterations, '/', infectionsMap.size);
1363
1385
  const neighbors = getNeighborsCached(idx, positions, hexRadius);
1364
1386
  for (const n of neighbors) {
1365
1387
  if (!newAvailable.has(n))
@@ -1440,7 +1462,7 @@ function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentT
1440
1462
  }
1441
1463
  }
1442
1464
  }
1443
- console.log('[evolve] Step 6.5: Entropy decay - applying decay to dominant successful photos...');
1465
+ debugLog('[evolve] Step 6.5: Entropy decay - applying decay to dominant successful photos...');
1444
1466
  // Entropy decay: successful/dominant photos decay over time to allow new dominance to emerge
1445
1467
  if (workerDebug.enableEntropyDecay && newInfections.size > 0) {
1446
1468
  // Calculate current territory shares
@@ -1524,14 +1546,14 @@ function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentT
1524
1546
  entropyDecayCount++;
1525
1547
  }
1526
1548
  if (entropyDecayCount > 0) {
1527
- console.log('[evolve] Entropy decay: removed', entropyDecayCount, 'cells from dominant successful photos');
1549
+ debugLog('[evolve] Entropy decay: removed', entropyDecayCount, 'cells from dominant successful photos');
1528
1550
  }
1529
1551
  }
1530
1552
  }
1531
- console.log('[evolve] Step 7: Deterministic fill - processing', newAvailable.size, 'available positions...');
1553
+ debugLog('[evolve] Step 7: Deterministic fill - processing', newAvailable.size, 'available positions...');
1532
1554
  // Skip deterministic fill if we have no photos or no existing infections to base decisions on
1533
1555
  if (photos.length === 0 || newInfections.size === 0) {
1534
- console.log('[evolve] Skipping deterministic fill - no photos or no infections');
1556
+ debugLog('[evolve] Skipping deterministic fill - no photos or no infections');
1535
1557
  }
1536
1558
  else {
1537
1559
  // Deterministic fill for holes with >=2 same-photo neighbors
@@ -1539,7 +1561,7 @@ function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentT
1539
1561
  for (const a of Array.from(newAvailable)) {
1540
1562
  fillIterations++;
1541
1563
  if (fillIterations % 50 === 0)
1542
- console.log('[evolve] Fill iteration', fillIterations, '/', newAvailable.size);
1564
+ debugLog('[evolve] Fill iteration', fillIterations, '/', newAvailable.size);
1543
1565
  const neighbors = getNeighborsCached(a, positions, hexRadius);
1544
1566
  const counts = new Map();
1545
1567
  for (const n of neighbors) {
@@ -1578,13 +1600,13 @@ function evolveInfectionSystem(prevState, positions, photos, hexRadius, currentT
1578
1600
  }
1579
1601
  }
1580
1602
  }
1581
- console.log('[evolve] Step 8: Optimization merge pass...');
1603
+ debugLog('[evolve] Step 8: Optimization merge pass...');
1582
1604
  // Conservative merge pass (opt-in)
1583
1605
  postOptimizationMerge(newInfections, positions, hexRadius, !!workerDebug.mergeLogs);
1584
- console.log('[evolve] Step 9: Assigning cluster-aware grid positions...');
1606
+ debugLog('[evolve] Step 9: Assigning cluster-aware grid positions...');
1585
1607
  // Make clusters self-aware by assigning grid positions based on spatial layout
1586
1608
  const tileCenters = assignClusterGridPositions(newInfections, positions, hexRadius);
1587
- console.log('[evolve] Step 10: Returning result - generation', generation, 'infections', newInfections.size);
1609
+ debugLog('[evolve] Step 10: Returning result - generation', generation, 'infections', newInfections.size);
1588
1610
  return {
1589
1611
  infections: newInfections,
1590
1612
  availableIndices: Array.from(newAvailable),
@@ -1637,7 +1659,7 @@ self.onmessage = function (ev) {
1637
1659
  if (!positions || !Array.isArray(positions))
1638
1660
  return;
1639
1661
  const hexRadius = typeof payload.hexRadius === 'number' ? payload.hexRadius : 24;
1640
- console.log('[hexgrid-worker] Pre-building neighbor cache for', positions.length, 'positions...');
1662
+ debugLog('[hexgrid-worker] Pre-building neighbor cache for', positions.length, 'positions...');
1641
1663
  const startTime = Date.now();
1642
1664
  // Build ALL neighbor relationships in one O(n²) pass instead of n×O(n) passes
1643
1665
  try {
@@ -1667,11 +1689,11 @@ self.onmessage = function (ev) {
1667
1689
  }
1668
1690
  // Log progress every 100 positions
1669
1691
  if ((i + 1) % 100 === 0) {
1670
- console.log('[hexgrid-worker] Processed', i + 1, '/', positions.length, 'positions');
1692
+ debugLog('[hexgrid-worker] Processed', i + 1, '/', positions.length, 'positions');
1671
1693
  }
1672
1694
  }
1673
1695
  const elapsed = Date.now() - startTime;
1674
- console.log('[hexgrid-worker] ✅ Neighbor cache built in', elapsed, 'ms - ready for evolution!');
1696
+ debugLog('[hexgrid-worker] ✅ Neighbor cache built in', elapsed, 'ms - ready for evolution!');
1675
1697
  // Mark cache as ready
1676
1698
  cache.cacheReady = true;
1677
1699
  // Notify main thread that cache is ready
@@ -1694,7 +1716,7 @@ self.onmessage = function (ev) {
1694
1716
  if (type === 'evolve') {
1695
1717
  // Check if neighbor cache is ready before processing evolve
1696
1718
  if (!cache.cacheReady) {
1697
- console.log('[hexgrid-worker] ⏸️ Evolve message received but cache not ready yet - deferring...');
1719
+ debugLog('[hexgrid-worker] ⏸️ Evolve message received but cache not ready yet - deferring...');
1698
1720
  // Defer this evolve message by re-posting it after a short delay
1699
1721
  setTimeout(() => {
1700
1722
  try {
@@ -1714,7 +1736,7 @@ self.onmessage = function (ev) {
1714
1736
  // Diagnostic: log that an evolve was received and the available payload keys (only when debugLogs enabled)
1715
1737
  try {
1716
1738
  if (workerDebug && workerDebug.debugLogs) {
1717
- console.log('[hexgrid-worker] evolve received, payload keys=', Object.keys(payload || {}), 'workerDebug.evolutionIntervalMs=', workerDebug.evolutionIntervalMs, 'workerDebug.evolveIntervalMs=', workerDebug.evolveIntervalMs);
1739
+ debugLog('[hexgrid-worker] evolve received, payload keys=', Object.keys(payload || {}), 'workerDebug.evolutionIntervalMs=', workerDebug.evolutionIntervalMs, 'workerDebug.evolveIntervalMs=', workerDebug.evolveIntervalMs);
1718
1740
  }
1719
1741
  }
1720
1742
  catch (e) { }
@@ -1724,12 +1746,12 @@ self.onmessage = function (ev) {
1724
1746
  : typeof workerDebug.evolveIntervalMs === 'number'
1725
1747
  ? workerDebug.evolveIntervalMs
1726
1748
  : 60000;
1727
- console.log('[hexgrid-worker] Throttle check: interval=', interval, 'lastEvolutionAt=', lastEvolutionAt, 'now=', now, 'diff=', now - lastEvolutionAt, 'willThrottle=', now - lastEvolutionAt < interval);
1749
+ debugLog('[hexgrid-worker] Throttle check: interval=', interval, 'lastEvolutionAt=', lastEvolutionAt, 'now=', now, 'diff=', now - lastEvolutionAt, 'willThrottle=', now - lastEvolutionAt < interval);
1728
1750
  // Throttle: if we're within the interval, notify (debug) and skip processing
1729
1751
  const reason = payload.reason || (raw && raw.reason);
1730
1752
  const bypassThrottle = reason === 'photos-init' || reason === 'reset';
1731
1753
  // Clear, high-signal log for build verification: reports whether the current evolve will bypass the worker throttle
1732
- console.log('[hexgrid-worker] THROTTLE DECISION', {
1754
+ debugLog('[hexgrid-worker] THROTTLE DECISION', {
1733
1755
  interval,
1734
1756
  lastEvolutionAt,
1735
1757
  now,
@@ -1740,7 +1762,7 @@ self.onmessage = function (ev) {
1740
1762
  });
1741
1763
  // Throttle: if we're within the interval and not bypassed, notify (debug) and skip processing
1742
1764
  if (!bypassThrottle && now - lastEvolutionAt < interval) {
1743
- console.log('[hexgrid-worker] ⛔ THROTTLED - skipping evolution processing');
1765
+ debugLog('[hexgrid-worker] ⛔ THROTTLED - skipping evolution processing');
1744
1766
  if (workerDebug && workerDebug.debugLogs) {
1745
1767
  try {
1746
1768
  self.postMessage({
@@ -1759,7 +1781,7 @@ self.onmessage = function (ev) {
1759
1781
  }
1760
1782
  // Mark processed time and send ack for an evolve we will process
1761
1783
  lastEvolutionAt = now;
1762
- console.log('[hexgrid-worker] ✅ PROCESSING evolution - lastEvolutionAt updated to', now);
1784
+ debugLog('[hexgrid-worker] ✅ PROCESSING evolution - lastEvolutionAt updated to', now);
1763
1785
  try {
1764
1786
  if (workerDebug && workerDebug.debugLogs) {
1765
1787
  try {
@@ -1800,12 +1822,12 @@ self.onmessage = function (ev) {
1800
1822
  Boolean(payload.isSpherical) !== cache.isSpherical) {
1801
1823
  invalidateCaches(Boolean(payload.isSpherical));
1802
1824
  }
1803
- console.log('[hexgrid-worker] 🔧 About to call evolveInfectionSystem');
1804
- console.log('[hexgrid-worker] - state generation:', state?.generation);
1805
- console.log('[hexgrid-worker] - state infections:', state?.infections?.length || state?.infections?.size || 0);
1806
- console.log('[hexgrid-worker] - positions:', positions?.length || 0);
1807
- console.log('[hexgrid-worker] - photos:', photos?.length || 0);
1808
- console.log('[hexgrid-worker] - hexRadius:', hexRadius);
1825
+ debugLog('[hexgrid-worker] 🔧 About to call evolveInfectionSystem');
1826
+ debugLog('[hexgrid-worker] - state generation:', state?.generation);
1827
+ debugLog('[hexgrid-worker] - state infections:', state?.infections?.length || state?.infections?.size || 0);
1828
+ debugLog('[hexgrid-worker] - positions:', positions?.length || 0);
1829
+ debugLog('[hexgrid-worker] - photos:', photos?.length || 0);
1830
+ debugLog('[hexgrid-worker] - hexRadius:', hexRadius);
1809
1831
  let res;
1810
1832
  let timeoutId;
1811
1833
  let timedOut = false;
@@ -1822,12 +1844,12 @@ self.onmessage = function (ev) {
1822
1844
  catch (e) { }
1823
1845
  }, 10000);
1824
1846
  try {
1825
- console.log('[hexgrid-worker] 🚀 Calling evolveInfectionSystem NOW...');
1847
+ debugLog('[hexgrid-worker] 🚀 Calling evolveInfectionSystem NOW...');
1826
1848
  const startTime = Date.now();
1827
1849
  res = evolveInfectionSystem(state, positions, photos, hexRadius, now, !!workerDebug.debugLogs);
1828
1850
  const elapsed = Date.now() - startTime;
1829
1851
  clearTimeout(timeoutId);
1830
- console.log('[hexgrid-worker] ✅ evolveInfectionSystem RETURNED successfully in', elapsed, 'ms');
1852
+ debugLog('[hexgrid-worker] ✅ evolveInfectionSystem RETURNED successfully in', elapsed, 'ms');
1831
1853
  }
1832
1854
  catch (err) {
1833
1855
  clearTimeout(timeoutId);
@@ -1840,20 +1862,21 @@ self.onmessage = function (ev) {
1840
1862
  console.error('[hexgrid-worker] ⏱️ Function eventually returned but after timeout was triggered');
1841
1863
  }
1842
1864
  if (!res) {
1843
- console.log('[hexgrid-worker] ❌ evolveInfectionSystem returned null!');
1865
+ debugLog('[hexgrid-worker] ❌ evolveInfectionSystem returned null!');
1844
1866
  return;
1845
1867
  }
1846
- console.log('[hexgrid-worker] ✅ Evolution complete! New generation=', res.generation, 'infections=', res.infections.size);
1868
+ debugLog('[hexgrid-worker] ✅ Evolution complete! New generation=', res.generation, 'infections=', res.infections.size);
1847
1869
  try {
1848
1870
  const payload = {
1849
1871
  infections: Array.from(res.infections.entries()),
1850
1872
  availableIndices: res.availableIndices,
1851
1873
  lastEvolutionTime: res.lastEvolutionTime,
1852
1874
  generation: res.generation,
1875
+ blankNeighborCounts: buildBlankNeighborCounts(res.infections, positions, hexRadius),
1853
1876
  };
1854
1877
  if (res.tileCenters && res.tileCenters.length > 0) {
1855
1878
  payload.tileCenters = res.tileCenters;
1856
- console.log('[hexgrid-worker] Including', res.tileCenters.length, 'tile center sets in evolved message');
1879
+ debugLog('[hexgrid-worker] Including', res.tileCenters.length, 'tile center sets in evolved message');
1857
1880
  }
1858
1881
  self.postMessage({ type: 'evolved', data: payload });
1859
1882
  // Record posted generation/infection count so later auto-triggers can avoid regressing
@@ -1866,7 +1889,7 @@ self.onmessage = function (ev) {
1866
1889
  catch (e) {
1867
1890
  console.error('[hexgrid-worker] ❌ Failed to post evolved message:', e);
1868
1891
  }
1869
- console.log('[hexgrid-worker] 📤 Posted evolved message back to main thread');
1892
+ debugLog('[hexgrid-worker] 📤 Posted evolved message back to main thread');
1870
1893
  // Emit a completion marker so the client can confirm the evolve finished end-to-end
1871
1894
  try {
1872
1895
  if (workerDebug && workerDebug.debugLogs) {
@@ -2011,4 +2034,4 @@ function invalidateCaches(isSpherical) {
2011
2034
  if (typeof isSpherical === 'boolean')
2012
2035
  cache.isSpherical = isSpherical;
2013
2036
  }
2014
- console.log('[hexgrid-worker] ready');
2037
+ debugLog('[hexgrid-worker] ready');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buley/hexgrid-3d",
3
- "version": "3.4.0",
3
+ "version": "3.5.1",
4
4
  "description": "3D hexagonal grid visualization component for React",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",