solargod/js/layers/asteroids.js
monsterrobotparty b37eb88670 ☄ swarm: small bodies — main belt, comets, NEO close approaches (opus)
Stage 5. asteroids.js: 1500-strong main-belt sample from SBDB (sb-class=MBA),
propagated by the shared Kepler core, one Points cloud refreshed ≤10 Hz, off by
default. comets.js: 1P/2P/67P/96P from SBDB (elliptical only) with full drawn
orbits, marker + anti-sunward tail scaled 1/r², on by default. neos.js: CAD ±30d
close-approach radar around Earth, colored by miss distance, clickable, lunar-
distance labels, off by default. All fail-soft. verify.html gate: belt view-radii
12.1–13.9 ∈ (Mars 10.7, Jupiter 17.4); Halley aphelion 35.3 AU > Neptune. Also
serialized verify's live Horizons checks (burst-throttle fix) → 13/13 green.

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

68 lines
2.6 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) {
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);
},
};
}