1// renderLoop.js â Own requestAnimationFrame render loop 2// 3// Replaces Spacekit's internal animation loop. 4 5import { THREE, renderer, scene, camera, controls } from './sim.js'; 6import { _followTarget } from './sim.js'; 7import { getJdPerSecond, setDateMs, getDateMs, isPaused } from './simClock.js'; 8import { tick as _perfTick } from './perfBudget.js'; 9import { isSurfaceLocked, getSurfaceBodyId, getFocusScenePos } from './focusTracker.js'; 10import { BODY_PARAMS } from './bodyRotation.js'; 11 12let _running = false; 13let _onTick = null; 14let _lastTime = 0; 15let _errorLogged = false; 16let _frameCount = 0; 17const MS_PER_DAY = 86400000; 18const _DEG = Math.PI / 180; 19const _OBLIQUITY = 23.43929111 * _DEG; 20const _cosE = Math.cos(_OBLIQUITY), _sinE = Math.sin(_OBLIQUITY); 21let _prevDateMs = 0; 22 23// Compute ecliptic pole vector for surface-lock co-rotation (cached per bodyId) 24const _poleCache = new Map(); 25function _getPoleEcl(bodyId) { 26 if (_poleCache.has(bodyId)) return _poleCache.get(bodyId); 27 const p = BODY_PARAMS[bodyId]; 28 if (!p) return null; 29 const ra = p.poleRA * _DEG, dec = p.poleDec * _DEG; 30 const px = Math.cos(dec) * Math.cos(ra); 31 const py = Math.cos(dec) * Math.sin(ra); 32 const pz = Math.sin(dec); 33 const v = new THREE.Vector3(px, py * _cosE + pz * _sinE, -py * _sinE + pz * _cosE).normalize(); 34 _poleCache.set(bodyId, v); 35 return v; 36} 37 38// Reusable quaternion for co-rotation 39const _coRotQ = new THREE.Quaternion(); 40const _coRotV = new THREE.Vector3(); 41 42export function setOnTick(fn) { _onTick = fn; } 43 44export function startLoop() { 45 if (_running) return; 46 _running = true; 47 _lastTime = performance.now(); 48 _frame(performance.now()); 49} 50 51export function stopLoop() { 52 _running = false; 53} 54 55function _frame(time) { 56 if (!_running) return; 57 requestAnimationFrame(_frame); 58 59 // Performance budget â measure frame time, update quality multiplier 60 _perfTick(time); 61 62 // Advance simulation time 63 const dt = (time - _lastTime) / 1000; 64 _lastTime = time; 65 if (dt > 0 && dt < 0.5 && !isPaused()) { 66 const jdPerSec = getJdPerSecond();
67 const advanceMs = jdPerSec * dt * MS_PER_DAY; 68 setDateMs(getDateMs() + advanceMs); 69 } 70 71 // User frame logic (wrapped to prevent single-frame errors from killing loop) 72 if (_onTick) { 73 try { _onTick(); } 74 catch (e) { if (!_errorLogged) { console.error('[renderLoop] onTick error:', e); _errorLogged = true; } } 75 } 76 77 // Track followed object (camera follows as planet moves) 78 if (_followTarget) { 79 const pos = _followTarget.getPosition?.(); 80 if (pos) { 81 const newTarget = new THREE.Vector3(pos.x ?? 0, pos.y ?? 0, pos.z ?? 0); 82 if (controls?.target) { 83 const delta = newTarget.clone().sub(controls.target); 84 controls.target.copy(newTarget); 85 camera.position.add(delta); 86 } 87 } 88 } 89 90 // OrbitControls damping 91 if (controls && controls.update) controls.update(); 92 93 // ââ Surface-lock co-rotation ââââââââââââââââââââââââââââââââââââââââââââââ 94 // When surface-locked to a body, rotate camera position around the body's 95 // ecliptic pole axis by pmRate * actualDeltaDays so the surface appears 96 // stationary. Uses actual dateMs delta to match body mesh rotation exactly. 97 if (isSurfaceLocked()) { 98 const bodyId = getSurfaceBodyId(); 99 const params = bodyId ? BODY_PARAMS[bodyId] : null; 100 const pole = bodyId ? _getPoleEcl(bodyId) : null; 101 if (params && pole) { 102 const curDateMs = getDateMs(); 103 const deltaDays = (curDateMs - _prevDateMs) / MS_PER_DAY; 104 if (Math.abs(deltaDays) > 0 && Math.abs(deltaDays) < 100) { 105 const deltaAngle = params.pmRate * deltaDays * _DEG; 106 const focusPos = getFocusScenePos(); 107 // Rotate camera position around focus point along pole axis 108 _coRotV.copy(camera.position).sub(focusPos); 109 _coRotQ.setFromAxisAngle(pole, deltaAngle); 110 _coRotV.applyQuaternion(_coRotQ); 111 camera.position.copy(focusPos).add(_coRotV); 112 // Also rotate OrbitControls target to stay centred 113 if (controls?.target) { 114 controls.target.copy(focusPos); 115 } 116 } 117 } 118 } 119 _prevDateMs = getDateMs(); 120 121 // Note: camera.near/far is set in _onTickBody via getFocusDistAU 122 // (more sophisticated than simple distance â considers focused object's scale) 123 124 // Render 125 renderer.render(scene, camera); 126 127 // Dismiss loading overlay after first few frames 128 _frameCount++; 129 if (_frameCount === 3) { 130 const overlay = document.getElementById('scene-loading'); 131 if (overlay) { 132 overlay.style.opacity = '0'; 133 setTimeout(() => overlay.remove(), 800); 134 } 135 } 136}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.