@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.
- package/dist/adapters/DashAdapter.d.ts.map +1 -1
- package/dist/adapters/DashAdapter.js +4 -3
- package/dist/algorithms/FluidEngineFactory.d.ts.map +1 -1
- package/dist/algorithms/FluidEngineFactory.js +6 -5
- package/dist/components/HexGrid.d.ts.map +1 -1
- package/dist/components/HexGrid.js +114 -51
- package/dist/lib/logger.d.ts +2 -8
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +2 -2
- package/dist/wasm/HexGridWasmWrapper.d.ts.map +1 -1
- package/dist/wasm/HexGridWasmWrapper.js +3 -2
- package/dist/webgpu/WebGPUContext.d.ts.map +1 -1
- package/dist/webgpu/WebGPUContext.js +6 -5
- package/dist/webnn/WebNNContext.d.ts.map +1 -1
- package/dist/webnn/WebNNContext.js +6 -5
- package/dist/workers/hexgrid-worker.worker.js +97 -74
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/DashAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
19
|
+
logger.log("Fluid Engine: Using WebNN (NPU)");
|
|
19
20
|
return webnn;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
catch (e) {
|
|
23
|
-
|
|
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
|
-
|
|
31
|
+
logger.log("Fluid Engine: Using WebGPU");
|
|
31
32
|
return webgpu;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
catch (e) {
|
|
35
|
-
|
|
36
|
+
logger.warn("Fluid Engine: WebGPU init failed", e);
|
|
36
37
|
}
|
|
37
38
|
// 3. Fallback to CPU
|
|
38
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
308
|
-
debugLogs:
|
|
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
|
-
//
|
|
2069
|
-
const
|
|
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
|
-
|
|
2691
|
-
const
|
|
2692
|
-
const
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
2825
|
+
const pulseInfo = pulseMap.get(index);
|
|
2766
2826
|
let pulseProgress = 0;
|
|
2767
2827
|
if (pulseInfo) {
|
|
2768
|
-
const elapsed =
|
|
2828
|
+
const elapsed = now - pulseInfo.start;
|
|
2769
2829
|
pulseProgress = Math.max(0, Math.min(1, elapsed / pulseInfo.duration));
|
|
2770
2830
|
if (pulseProgress >= 1)
|
|
2771
|
-
|
|
2831
|
+
pulseMap.delete(index);
|
|
2772
2832
|
}
|
|
2773
2833
|
// Use projected position and scale for drawing
|
|
2774
|
-
const proj =
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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];
|
package/dist/lib/logger.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
export declare const logger: {
|
|
2
2
|
debug: (..._args: unknown[]) => void;
|
|
3
|
-
log:
|
|
4
|
-
|
|
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;
|
package/dist/lib/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,MAAM;sBANK,OAAO,EAAE
|
|
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"}
|
package/dist/lib/logger.js
CHANGED
|
@@ -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":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"HexGridWasmWrapper.d.ts","sourceRoot":"","sources":["../../src/wasm/HexGridWasmWrapper.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AA6CD;;;GAGG;AACH,qBAAa,kBAAkB;IAe3B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAfzB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAkC;IACvD,OAAO,CAAC,MAAM,CAAC,OAAO,CAAkD;IACxE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAS;IAElC,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,YAAY,CAMJ;IAEhB,OAAO;IAKP;;OAEG;WACU,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAgC3C;;OAEG;WACU,MAAM,CACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,CAAC;IA4D9B;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAO/B;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQ5C;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAQtD;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IASvC;;OAEG;IACH,aAAa,CACX,aAAa,GAAE,MAAY,EAC3B,kBAAkB,GAAE,MAAY,GAC/B,MAAM,EAAE;IAoCX;;OAEG;IACH,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAmC7C;;OAEG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAsBxC;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,EAAE;IAkEvE;;OAEG;IACH,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAsBzC;;OAEG;IACH,WAAW,IAAI,MAAM;IAwBrB;;OAEG;IACH,cAAc,IAAI,MAAM;IAoBxB;;OAEG;IACH,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,GAAE,MAAW,GAAG,MAAM,EAAE;IAgF3D;;OAEG;IACH,IAAI,IAAI,MAAM;IAId;;OAEG;IACH,KAAK,IAAI,IAAI;IASb;;OAEG;IACH,OAAO,IAAI,IAAI;CAOhB;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAU7B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVzB,OAAO,CAAC,YAAY,CAAsC;IAC1D,OAAO,CAAC,YAAY,CAKJ;IAEhB,OAAO;WAKM,MAAM,CACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,oBAAoB,CAAC;IA4BhC,WAAW,IAAI,OAAO;IAItB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAqBvD,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAoBvD,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAmC9C,iBAAiB,IAAI,YAAY;IAuBjC,WAAW,IAAI,YAAY;IAuB3B,KAAK,IAAI,IAAI;IASb,OAAO,IAAI,IAAI;CAOhB"}
|
|
@@ -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
|
-
|
|
40
|
+
logger.log('[HexGrid] WASM module loaded successfully');
|
|
40
41
|
return this.module;
|
|
41
42
|
}
|
|
42
43
|
catch (error) {
|
|
43
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
logger.log('WebGPU initialized successfully.');
|
|
43
44
|
return true;
|
|
44
45
|
}
|
|
45
46
|
catch (e) {
|
|
46
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
logger.log(`WebNN initialized successfully on ${preference}`);
|
|
33
34
|
return true;
|
|
34
35
|
}
|
|
35
36
|
catch (e) {
|
|
36
|
-
|
|
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
|
-
|
|
47
|
+
logger.log(`WebNN initialized successfully on fallback ${fallback}`);
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
49
50
|
catch (err) {
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
+
debugLog('[findConnectedComponents] Component', componentCount, 'complete:', comp.length, 'nodes,', iterations, 'iterations');
|
|
228
250
|
comps.push(comp);
|
|
229
251
|
}
|
|
230
|
-
|
|
252
|
+
debugLog('[findConnectedComponents] Complete:', comps.length, 'components found');
|
|
231
253
|
const elapsed = performance.now() - startMarker;
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
+
debugLog('[calculatePhotoCentroids] Processing photo', photoNum, '/', byPhoto.size, 'photoId=', photoId, 'indices=', inds.length);
|
|
268
290
|
try {
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1230
|
+
debugLog('[evolve] Step 4: Calculating centroids...');
|
|
1209
1231
|
const centroids = calculatePhotoCentroids(infectionsMap, positions, hexRadius);
|
|
1210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1549
|
+
debugLog('[evolve] Entropy decay: removed', entropyDecayCount, 'cells from dominant successful photos');
|
|
1528
1550
|
}
|
|
1529
1551
|
}
|
|
1530
1552
|
}
|
|
1531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1603
|
+
debugLog('[evolve] Step 8: Optimization merge pass...');
|
|
1582
1604
|
// Conservative merge pass (opt-in)
|
|
1583
1605
|
postOptimizationMerge(newInfections, positions, hexRadius, !!workerDebug.mergeLogs);
|
|
1584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1692
|
+
debugLog('[hexgrid-worker] Processed', i + 1, '/', positions.length, 'positions');
|
|
1671
1693
|
}
|
|
1672
1694
|
}
|
|
1673
1695
|
const elapsed = Date.now() - startTime;
|
|
1674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1865
|
+
debugLog('[hexgrid-worker] ❌ evolveInfectionSystem returned null!');
|
|
1844
1866
|
return;
|
|
1845
1867
|
}
|
|
1846
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2037
|
+
debugLog('[hexgrid-worker] ready');
|