🔧 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 (id === 'sun') { out.x = out.y = out.z = 0; return out; }
|
||||||
if (b.ephemId) return ephem.helioEcl(b.ephemId, jd, out);
|
if (b.ephemId) return ephem.helioEcl(b.ephemId, jd, out);
|
||||||
if (b.elements) return ephem.smallBodyEcl(b.elements, 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;
|
out.x = out.y = out.z = 0; return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export default function create(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refresh(jd) {
|
function refresh(jd) {
|
||||||
|
if (!geo || !elements.length) return; // toggled on before SBDB load completed
|
||||||
for (let k = 0; k < elements.length; k++) {
|
for (let k = 0; k < elements.length; k++) {
|
||||||
ephem.smallBodyEcl(elements[k], jd, _e);
|
ephem.smallBodyEcl(elements[k], jd, _e);
|
||||||
scale.viewFromEcl(_e, _v);
|
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 }));
|
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);
|
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 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);
|
fillOrbit(c);
|
||||||
|
c.lastFillP = scale.getP();
|
||||||
comets.push(c);
|
comets.push(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ export default function create(ctx) {
|
|||||||
id: 'comets',
|
id: 'comets',
|
||||||
onClockTick(simMs, jd) {
|
onClockTick(simMs, jd) {
|
||||||
if (!on) return;
|
if (!on) return;
|
||||||
const transitioning = scale.isTransitioning();
|
const P = scale.getP();
|
||||||
const camDist = ctx.camera.position.length();
|
const camDist = ctx.camera.position.length();
|
||||||
// label DOM writes throttled ≤5 Hz (brief §11)
|
// label DOM writes throttled ≤5 Hz (brief §11)
|
||||||
const nowMs = performance.now();
|
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;
|
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;
|
c.tgeo.attributes.position.needsUpdate = true;
|
||||||
if (updateText) c.lbl.div.textContent = `${c.name} · ${lib.fmtAU(r)}`;
|
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;
|
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) { 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) {
|
handlePick(raycaster) {
|
||||||
|
|||||||
@ -35,9 +35,10 @@ export default function create(ctx) {
|
|||||||
line.frustumCulled = false;
|
line.frustumCulled = false;
|
||||||
worldGroup.add(line);
|
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);
|
buildAuPath(e);
|
||||||
fillView(e);
|
fillView(e);
|
||||||
|
e.lastFillP = scale.getP();
|
||||||
entries.push(e);
|
entries.push(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ export default function create(ctx) {
|
|||||||
return {
|
return {
|
||||||
id: 'planets',
|
id: 'planets',
|
||||||
onClockTick(simMs, jd, isLive) {
|
onClockTick(simMs, jd, isLive) {
|
||||||
const transitioning = scale.isTransitioning();
|
const P = scale.getP();
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
const abs = ctx.bodyWorld[e.id];
|
const abs = ctx.bodyWorld[e.id];
|
||||||
if (!abs) continue;
|
if (!abs) continue;
|
||||||
@ -83,10 +84,11 @@ export default function create(ctx) {
|
|||||||
}
|
}
|
||||||
// focus highlight
|
// focus highlight
|
||||||
e.lbl.div.classList.toggle('focused', e.id === focus.id);
|
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;
|
const needAu = Math.abs(lib.centuriesSinceJ2000(jd) - e.lastT) > CONFIG.orbitRebuildCy;
|
||||||
if (needAu) buildAuPath(e);
|
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
|
// 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.
|
// 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 K = 9.0;
|
||||||
let P = 0.4; // live exponent: MEGA=0.4, TRUE=1.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); }
|
export function radial(rAU) { return K * Math.pow(rAU, P); }
|
||||||
|
|
||||||
// Heliocentric ecliptic AU → view-world Vector3 (radial compression + frame map).
|
// 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) {
|
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; }
|
if (r === 0) { out.x = out.y = out.z = 0; return out; }
|
||||||
const s = radial(r) / r; // scale factor along the true direction
|
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;
|
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))
|
bump(_parse_epoch_to_unix(tok))
|
||||||
if "STOP_TIME" not in q and "TLIST" not in q:
|
if "STOP_TIME" not in q and "TLIST" not in q:
|
||||||
return False
|
return False
|
||||||
elif name == "cad":
|
# CAD is NOT immutable even for past dates: close-approach records keep growing
|
||||||
for raw in q.get("date-max", []):
|
# as new NEOs are discovered. SBDB elements advance too. Both get the 24 h TTL.
|
||||||
bump(_parse_epoch_to_unix(raw))
|
else:
|
||||||
if "date-max" not in q:
|
|
||||||
return False
|
|
||||||
else: # sbdb elements query — has no time span, treat as mutable (TTL)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Margin: only call it immutable if the latest epoch is > 1 day ago.
|
# Margin: only call it immutable if the latest epoch is > 1 day ago.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user