diff --git a/public/js/exhibit.js b/public/js/exhibit.js index 41976ae..a4a57e1 100644 --- a/public/js/exhibit.js +++ b/public/js/exhibit.js @@ -52,8 +52,10 @@ let detector, poseEst, fuser, cvSolver; let gradients = {}, modelManifest = null, characters = { defaults: {}, byId: {} }; let ghostHeight = 4; // cm, from scene.ghostHeightCm const activeGhosts = new Map(); // uid -> { rec, group } -let tracking = { markers: 0, lastSeen: 0 }; +let tracking = { markers: 0, raw: 0, rawIds: [], lastSeen: 0 }; let netRef = null; +let sceneLoaded = false; +let anchorCount = 0; const rxLog = []; // last few WS message types, newest last function rxTrace(t) { rxLog.push(t); if (rxLog.length > 6) rxLog.shift(); } const clockOffsetSamples = []; @@ -71,7 +73,7 @@ let dbgPose = ''; function dbgStatus() { const ws = netRef && netRef.ws ? ['conn','open','closing','closed'][netRef.ws.readyState] : 'none'; return `ws ${ws} rx ${rxLog.join(',') || '-'}\n` + - `ghost recs ${activeGhosts.size}\n`; + `scene ${sceneLoaded ? anchorCount + ' anchors' : 'NOT LOADED'} ghost recs ${activeGhosts.size}\n`; } function paintDbg() { if (dbgEl) dbgEl.textContent = dbgStatus() + dbgPose + dbgAim; } @@ -123,7 +125,7 @@ function updateDbg(fusedQuat, markerCount, extra) { dbgPose = `eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` + `${modeLine}\n` + - `markers ${markerCount}${extra}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` + + `markers ${markerCount}/${tracking.raw} seen [${tracking.rawIds.join(',')}]${extra}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` + `pos ${v3(camera3.position)}`; paintDbg(); } @@ -222,6 +224,13 @@ async function start() { cvSolver = new CvBoardSolver(); ensureOpenCV(); + // scene over HTTP so tracking works even if the WebSocket is down + try { + applyScene(await (await fetch('/api/scene')).json()); + } catch (e) { + reportError({ kind: 'scene', msg: `scene fetch failed: ${(e && e.message) || e}` }); + } + // dev-mode pose readout + tap-to-cycle modes if (info.mode === 'dev') { freezeGhosts = true; // start static so tracking jitter is isolated from ghost float @@ -266,11 +275,7 @@ async function start() { const net = new ExhibitNet(); netRef = net; const on = (t, fn) => net.on(t, (m) => { rxTrace(t); return fn(m); }); - on('scene', (m) => { - fuser.setScene(m.scene); cvSolver.setScene(m.scene); buildOcclusion(m.scene); - ghostHeight = m.scene.ghostHeightCm || 4; - for (const e of activeGhosts.values()) e.group.userData.setHeight(ghostHeight); - }); + on('scene', (m) => applyScene(m.scene)); on('characters', async (m) => { characters = m.characters || characters; const recs = [...activeGhosts.values()].map(e => e.rec); @@ -301,6 +306,22 @@ function onResize() { camera3.updateProjectionMatrix(); } +/* Scene application. Tracking must NOT depend on the WebSocket: detection filters + * against the scene's marker IDs, so if the socket is slow or flapping the crest + * is detected and then silently discarded ("Point at a Newbury Crest" forever). + * We therefore load the scene over plain HTTP at start; the WS message just + * refreshes it when an operator edits the layout. */ +function applyScene(scene) { + if (!scene) return; + sceneLoaded = true; + anchorCount = (scene.anchors || []).filter(a => a.enabled !== false).length; + fuser.setScene(scene); + cvSolver.setScene(scene); + buildOcclusion(scene); + ghostHeight = scene.ghostHeightCm || 4; + for (const e of activeGhosts.values()) e.group.userData.setHeight(ghostHeight); +} + // ---------- occlusion: invisible depth-only building volumes ---------- const occlusionGroup = new THREE.Group(); function buildOcclusion(scene) { @@ -374,6 +395,8 @@ function loop(t) { const markers = detectMarkers(detector, img, fuser.knownIds()); tracking.markers = markers.length; + tracking.raw = markers.rawCount || 0; + tracking.rawIds = markers.rawIds || []; if (dbgEl) updateAim(markers, W, H); if (markers.length) { tracking.lastSeen = performance.now(); @@ -421,7 +444,11 @@ function loop(t) { if ((frameCount++ & 15) === 0) { paintDbg(); const stale = performance.now() - tracking.lastSeen > 1500; - $('#trk').textContent = stale ? 'Point at a Newbury Crest' : `Tracking ${tracking.markers} crest${tracking.markers === 1 ? '' : 's'}`; + $('#trk').textContent = !stale + ? `Tracking ${tracking.markers} crest${tracking.markers === 1 ? '' : 's'}` + : (tracking.raw > 0 + ? `Crest ${tracking.rawIds.join(',')} not in this layout` // detected, but no anchor for it + : 'Point at a Newbury Crest'); $('#trk').classList.toggle('warn', stale || tracking.markers < 2); $('#cnt').textContent = `${activeGhosts.size} ghost${activeGhosts.size === 1 ? '' : 's'} nearby`; }