solargod/js/layers/asteroids.js
monsterrobotparty 4360269ad3 🔧 review: fix 8 findings from adversarial self-review (opus)
Multi-agent adversarial review (verified findings) — the top finding (dead orbit
drag) fable had already fixed in 3e36a0c; these are the rest, composing on top of
fable's 228caf2/3e36a0c:

- scale.js viewFromEcl: allocation-free + alias-safe (read x/y/z into locals before
  writing out) — it ran per body per frame and allocated an object each call (§11).
- planets.js + comets.js: refill orbit paths on any P change, not just
  isTransitioning() — the MEGA↔TRUE tween's FINAL frame (P settled, tween nulled)
  was skipped, leaving body/orbit desync (§12).
- almanac.js: a moon's heliocentric position adds its true orbital offset, so the
  Moon's "From Earth" is its real ~384,400 km / 1.3 s light, not 0.
- asteroids.js: guard refresh() against null geometry when toggled on before the
  SBDB load completes (was a TypeError).
- serve.py: CAD close-approach responses are never cached immutable — the database
  keeps growing for past dates as new NEOs are discovered (only Horizons ephemeris
  is truly immutable).

Re-verified: 13/13 gates green; orbit zoom dollies 82→73 finite; Moon From Earth
0.003 AU; layer toggles throw nothing; no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:43:40 +10:00

69 lines
2.7 KiB
JavaScript

// SOLARGOD asteroids layer (Stage 5) — a main-belt sample from JPL SBDB, each rock
// propagated by the SAME Kepler core as the planets (ephem.smallBodyEcl over epoch
// elements). One Points cloud, positions refreshed ≤10 Hz. Off by default; additive
// and fail-soft (brief §5, §11). Gate: the belt sits as a torus between Mars and
// Jupiter — not a sphere, not a line.
export default function create(ctx) {
const { THREE, CONFIG, ui, scale, ephem, worldGroup } = ctx;
const CAP = 1500;
let elements = [];
let geo = null, points = null, positions = null;
let on = false, lastWall = 0;
const _e = {}, _v = {};
ui.addLayer('asteroids', 'Main belt', false, (v) => { on = v; if (points) points.visible = v; if (v) refresh(ctx.clock.jd, true); });
ui.setStatus('asteroids', 'off', 'off');
load();
async function load() {
ui.setStatus('asteroids', 'fetching SBDB…', 'warn');
try {
const q = `fields=full_name,a,e,i,om,w,ma,epoch&sb-kind=a&sb-class=MBA&limit=${CAP}`;
const j = await (await fetch(`${CONFIG.proxy.sbdb}?${q}`)).json();
const F = j.fields, ix = (k) => F.indexOf(k);
elements = j.data.map((r) => ({
a: +r[ix('a')], e: +r[ix('e')], i: +r[ix('i')], om: +r[ix('om')], w: +r[ix('w')], ma: +r[ix('ma')], epoch: +r[ix('epoch')],
})).filter((el) => el.a > 0 && el.e < 0.98 && Number.isFinite(el.a) && Number.isFinite(el.ma));
build();
ui.setStatus('asteroids', `${elements.length} main-belt · SBDB`, on ? 'ok' : 'off');
} catch (err) {
console.warn('[solargod] asteroids', err.message);
ui.setStatus('asteroids', 'SBDB unavailable', 'err');
}
}
function build() {
geo = new THREE.BufferGeometry();
positions = new Float32Array(elements.length * 3);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
points = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x9a8464, size: 0.16, sizeAttenuation: true, transparent: true, opacity: 0.85, depthWrite: false }));
points.frustumCulled = false;
points.visible = on;
worldGroup.add(points);
if (on) refresh(ctx.clock.jd, true);
}
function refresh(jd) {
if (!geo || !elements.length) return; // toggled on before SBDB load completed
for (let k = 0; k < elements.length; k++) {
ephem.smallBodyEcl(elements[k], jd, _e);
scale.viewFromEcl(_e, _v);
positions[k * 3] = _v.x; positions[k * 3 + 1] = _v.y; positions[k * 3 + 2] = _v.z;
}
geo.attributes.position.needsUpdate = true;
}
return {
id: 'asteroids',
onClockTick(simMs, jd) {
if (!on || !points) return;
const now = performance.now();
if (now - lastWall < 100) return; // ≤10 Hz
lastWall = now;
refresh(jd);
},
};
}