forked from Rockachopa/the-matrix
Applies Replit PR #28 feature on top of current main: - Root sw.js template with __PRECACHE_URLS__ placeholder - generate-sw Vite plugin: reads build manifest, injects actual asset URLs - manifest: true in Vite build config for asset manifest generation - SW registration gated to import.meta.env.PROD (no dev interference) - Preserves existing manifest.json and public/sw.js as dev fallback
143 lines
3.8 KiB
JavaScript
143 lines
3.8 KiB
JavaScript
import { initWorld, onWindowResize, disposeWorld } from './world.js';
|
|
import {
|
|
initAgents, updateAgents, getAgentCount,
|
|
disposeAgents, getAgentStates, applyAgentStates,
|
|
} from './agents.js';
|
|
import { initEffects, updateEffects, disposeEffects } from './effects.js';
|
|
import { initUI, updateUI } from './ui.js';
|
|
import { initInteraction, updateControls, disposeInteraction } from './interaction.js';
|
|
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
|
|
import { initVisitor } from './visitor.js';
|
|
|
|
let running = false;
|
|
let canvas = null;
|
|
|
|
/**
|
|
* Build (or rebuild) the Three.js world.
|
|
*
|
|
* @param {boolean} firstInit
|
|
* true — first page load: also starts UI, WebSocket, and visitor
|
|
* false — context-restore reinit: skips UI/WS (they survive context loss)
|
|
* @param {Object.<string,string>|null} stateSnapshot
|
|
* Agent state map captured just before teardown; reapplied after initAgents.
|
|
*/
|
|
function buildWorld(firstInit, stateSnapshot) {
|
|
const { scene, camera, renderer } = initWorld(canvas);
|
|
canvas = renderer.domElement;
|
|
|
|
initEffects(scene);
|
|
initAgents(scene);
|
|
|
|
if (stateSnapshot) {
|
|
applyAgentStates(stateSnapshot);
|
|
}
|
|
|
|
initInteraction(camera, renderer);
|
|
|
|
if (firstInit) {
|
|
initUI();
|
|
initWebSocket(scene);
|
|
initVisitor();
|
|
|
|
// Dismiss loading screen
|
|
const loadingScreen = document.getElementById('loading-screen');
|
|
if (loadingScreen) loadingScreen.classList.add('hidden');
|
|
}
|
|
|
|
// Debounce resize to 1 call per frame
|
|
const ac = new AbortController();
|
|
let resizeFrame = null;
|
|
window.addEventListener('resize', () => {
|
|
if (resizeFrame) cancelAnimationFrame(resizeFrame);
|
|
resizeFrame = requestAnimationFrame(() => onWindowResize(camera, renderer));
|
|
}, { signal: ac.signal });
|
|
|
|
let frameCount = 0;
|
|
let lastFpsTime = performance.now();
|
|
let currentFps = 0;
|
|
let rafId = null;
|
|
|
|
running = true;
|
|
|
|
function animate() {
|
|
if (!running) return;
|
|
rafId = requestAnimationFrame(animate);
|
|
|
|
const now = performance.now();
|
|
frameCount++;
|
|
if (now - lastFpsTime >= 1000) {
|
|
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
|
|
frameCount = 0;
|
|
lastFpsTime = now;
|
|
}
|
|
|
|
updateControls();
|
|
updateEffects(now);
|
|
updateAgents(now);
|
|
updateUI({
|
|
fps: currentFps,
|
|
agentCount: getAgentCount(),
|
|
jobCount: getJobCount(),
|
|
connectionState: getConnectionState(),
|
|
});
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Pause rendering when tab is backgrounded (saves battery on iPad PWA)
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
rafId = null;
|
|
running = false;
|
|
}
|
|
} else {
|
|
if (!running) {
|
|
running = true;
|
|
animate();
|
|
}
|
|
}
|
|
});
|
|
|
|
animate();
|
|
|
|
return { scene, renderer, ac };
|
|
}
|
|
|
|
function teardown({ scene, renderer, ac }) {
|
|
running = false;
|
|
ac.abort();
|
|
disposeInteraction();
|
|
disposeEffects();
|
|
disposeAgents();
|
|
disposeWorld(renderer, scene);
|
|
}
|
|
|
|
function main() {
|
|
const $overlay = document.getElementById('webgl-recovery-overlay');
|
|
|
|
let handle = buildWorld(true, null);
|
|
|
|
// WebGL context loss recovery (iPad PWA, GPU driver reset, etc.)
|
|
canvas.addEventListener('webglcontextlost', event => {
|
|
event.preventDefault();
|
|
running = false;
|
|
if ($overlay) $overlay.style.display = 'flex';
|
|
});
|
|
|
|
canvas.addEventListener('webglcontextrestored', () => {
|
|
const snapshot = getAgentStates();
|
|
teardown(handle);
|
|
handle = buildWorld(false, snapshot);
|
|
if ($overlay) $overlay.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
main();
|
|
|
|
// Register service worker only in production builds
|
|
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
}
|