🔧 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>
This commit is contained in:
parent
3e36a0c16a
commit
4360269ad3
@ -42,7 +42,18 @@ export default function create(ctx) {
|
||||
if (id === 'sun') { out.x = out.y = out.z = 0; return out; }
|
||||
if (b.ephemId) return ephem.helioEcl(b.ephemId, jd, out);
|
||||
if (b.elements) return ephem.smallBodyEcl(b.elements, jd, out);
|
||||
if (b.parent) return helioOf(b.parent, jd, out); // moon ≈ parent at AU scale
|
||||
if (b.moonOrbit && b.parent) {
|
||||
// parent heliocentric + the moon's TRUE orbital offset (AU) — so e.g. the
|
||||
// Moon's distance from Earth is its real ~384,400 km, not 0.
|
||||
helioOf(b.parent, jd, out);
|
||||
const theta = 2 * Math.PI * ((jd - lib.J2000_JD) / b.moonOrbit.periodDays);
|
||||
const rAU = b.moonOrbit.aKm / lib.AU_KM;
|
||||
const tilt = (bodies[b.parent].axialTiltDeg || 0) * lib.DEG;
|
||||
const ex = Math.cos(theta) * rAU, ey = Math.sin(theta) * rAU;
|
||||
out.x += ex; out.y += ey * Math.cos(tilt); out.z += ey * Math.sin(tilt);
|
||||
return out;
|
||||
}
|
||||
if (b.parent) return helioOf(b.parent, jd, out);
|
||||
out.x = out.y = out.z = 0; return out;
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ export default function create(ctx) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@ -62,8 +62,9 @@ export default function create(ctx) {
|
||||
const tail = new THREE.Line(tgeo, new THREE.LineBasicMaterial({ color: new THREE.Color(w.color), transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending, depthWrite: false }));
|
||||
tail.frustumCulled = false; tail.visible = on; scene.add(tail);
|
||||
const lbl = makeLabel(spr, name, 'craft-label'); lbl.div.style.color = w.color; lbl.obj.visible = on;
|
||||
const c = { w, name, el, geo, line, spr, tail, tgeo, lbl, auPath: au, lastT: lib.centuriesSinceJ2000(ctx.clock.jd) };
|
||||
const c = { w, name, el, geo, line, spr, tail, tgeo, lbl, auPath: au, lastT: lib.centuriesSinceJ2000(ctx.clock.jd), lastFillP: NaN };
|
||||
fillOrbit(c);
|
||||
c.lastFillP = scale.getP();
|
||||
comets.push(c);
|
||||
}
|
||||
|
||||
@ -83,7 +84,7 @@ export default function create(ctx) {
|
||||
id: 'comets',
|
||||
onClockTick(simMs, jd) {
|
||||
if (!on) return;
|
||||
const transitioning = scale.isTransitioning();
|
||||
const P = scale.getP();
|
||||
const camDist = ctx.camera.position.length();
|
||||
// label DOM writes throttled ≤5 Hz (brief §11)
|
||||
const nowMs = performance.now();
|
||||
@ -104,10 +105,10 @@ export default function create(ctx) {
|
||||
tp[3] = c.spr.position.x + _dir.x * len; tp[4] = c.spr.position.y + _dir.y * len; tp[5] = c.spr.position.z + _dir.z * len;
|
||||
c.tgeo.attributes.position.needsUpdate = true;
|
||||
if (updateText) c.lbl.div.textContent = `${c.name} · ${lib.fmtAU(r)}`;
|
||||
// orbit rebuild on T-drift or scale transition (brief §11)
|
||||
// orbit rebuild on T-drift or any P change (covers the tween + its final frame)
|
||||
const needAu = Math.abs(lib.centuriesSinceJ2000(jd) - c.lastT) > CONFIG.orbitRebuildCy;
|
||||
if (needAu) { c.auPath = ephem.orbitPathFromElements(c.el, N); c.lastT = lib.centuriesSinceJ2000(jd); }
|
||||
if (needAu || transitioning) fillOrbit(c);
|
||||
if (needAu || P !== c.lastFillP) { fillOrbit(c); c.lastFillP = P; }
|
||||
}
|
||||
},
|
||||
handlePick(raycaster) {
|
||||
|
||||
@ -35,9 +35,10 @@ export default function create(ctx) {
|
||||
line.frustumCulled = false;
|
||||
worldGroup.add(line);
|
||||
|
||||
const e = { id, b, group, mesh, mat, geo, line, lbl, auPath: null, lastT: NaN };
|
||||
const e = { id, b, group, mesh, mat, geo, line, lbl, auPath: null, lastT: NaN, lastFillP: NaN };
|
||||
buildAuPath(e);
|
||||
fillView(e);
|
||||
e.lastFillP = scale.getP();
|
||||
entries.push(e);
|
||||
}
|
||||
|
||||
@ -68,7 +69,7 @@ export default function create(ctx) {
|
||||
return {
|
||||
id: 'planets',
|
||||
onClockTick(simMs, jd, isLive) {
|
||||
const transitioning = scale.isTransitioning();
|
||||
const P = scale.getP();
|
||||
for (const e of entries) {
|
||||
const abs = ctx.bodyWorld[e.id];
|
||||
if (!abs) continue;
|
||||
@ -83,10 +84,11 @@ export default function create(ctx) {
|
||||
}
|
||||
// focus highlight
|
||||
e.lbl.div.classList.toggle('focused', e.id === focus.id);
|
||||
// orbit rebuild only when needed
|
||||
// orbit rebuild: on element drift, or whenever the compression P changed
|
||||
// (covers the whole MEGA↔TRUE tween AND the final frame it settles on).
|
||||
const needAu = Math.abs(lib.centuriesSinceJ2000(jd) - e.lastT) > CONFIG.orbitRebuildCy;
|
||||
if (needAu) buildAuPath(e);
|
||||
if (needAu || transitioning) fillView(e);
|
||||
if (needAu || P !== e.lastFillP) { fillView(e); e.lastFillP = P; }
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
13
js/scale.js
13
js/scale.js
@ -3,7 +3,7 @@
|
||||
// out. Nothing else in the app maps AU→scene by hand (brief §4). "Truth before
|
||||
// view": callers compute positions in AU, then pass through here.
|
||||
|
||||
import { AU_KM, eclToWorld, clamp, lerp, smoothstep } from './lib.js';
|
||||
import { AU_KM, clamp, lerp, smoothstep } from './lib.js';
|
||||
|
||||
let K = 9.0;
|
||||
let P = 0.4; // live exponent: MEGA=0.4, TRUE=1.0
|
||||
@ -32,11 +32,18 @@ let moonBase = 1.8, moonK = 1.2;
|
||||
export function radial(rAU) { return K * Math.pow(rAU, P); }
|
||||
|
||||
// Heliocentric ecliptic AU → view-world Vector3 (radial compression + frame map).
|
||||
// Allocation-free (brief §11): the eclToWorld map is inlined with the scale so no
|
||||
// intermediate object is created — this runs for every body every frame.
|
||||
export function viewFromEcl(ecl, out) {
|
||||
const r = Math.hypot(ecl.x, ecl.y, ecl.z);
|
||||
// Read all components first so this is safe when out === ecl (callers reuse a
|
||||
// single scratch, e.g. viewFromEcl(_v, _v)) and still allocation-free.
|
||||
const x = ecl.x, y = ecl.y, z = ecl.z;
|
||||
const r = Math.hypot(x, y, z);
|
||||
if (r === 0) { out.x = out.y = out.z = 0; return out; }
|
||||
const s = radial(r) / r; // scale factor along the true direction
|
||||
eclToWorld({ x: ecl.x * s, y: ecl.y * s, z: ecl.z * s }, out);
|
||||
out.x = x * s; // ecl X → world x
|
||||
out.y = z * s; // ecl Z (north) → world y
|
||||
out.z = -y * s; // ecl Y → world −z
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
9
serve.py
9
serve.py
@ -82,12 +82,9 @@ def _requested_span_is_past(name, query):
|
||||
bump(_parse_epoch_to_unix(tok))
|
||||
if "STOP_TIME" not in q and "TLIST" not in q:
|
||||
return False
|
||||
elif name == "cad":
|
||||
for raw in q.get("date-max", []):
|
||||
bump(_parse_epoch_to_unix(raw))
|
||||
if "date-max" not in q:
|
||||
return False
|
||||
else: # sbdb elements query — has no time span, treat as mutable (TTL)
|
||||
# CAD is NOT immutable even for past dates: close-approach records keep growing
|
||||
# as new NEOs are discovered. SBDB elements advance too. Both get the 24 h TTL.
|
||||
else:
|
||||
return False
|
||||
|
||||
# Margin: only call it immutable if the latest epoch is > 1 day ago.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user