solargod/js/scale.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

92 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SOLARGOD view transform — the one module that owns MEGA/TRUE compression and
// radius exaggeration. Truth (heliocentric AU) goes in, view-world units come
// 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, clamp, lerp, smoothstep } from './lib.js';
let K = 9.0;
let P = 0.4; // live exponent: MEGA=0.4, TRUE=1.0
let P_mega = 0.4, P_true = 1.0;
let planetE = 1200, sunE = 60;
let transitionMs = 2500;
let mode = 'mega';
let tween = null; // { from, to, elapsed } while animating
export function init(CONFIG) {
K = CONFIG.scale.K;
P_mega = CONFIG.scale.P_mega;
P_true = CONFIG.scale.P_true;
P = P_mega;
planetE = CONFIG.scale.planetE;
sunE = CONFIG.scale.sunE;
transitionMs = CONFIG.scale.transitionMs;
moonBase = CONFIG.scale.moonBase;
moonK = CONFIG.scale.moonK;
}
let moonBase = 1.8, moonK = 1.2;
// Radial compression: AU → view units. Direction is never touched, so MEGA↔TRUE
// preserves every body's heliocentric bearing (verify gate: unit dot ≈ 1).
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) {
// 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
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;
}
// Body draw radius in view units. E defaults to the planet exaggeration; the Sun
// and close-focus lerp pass their own.
export function drawRadius(radiusKm, E = planetE) {
return (radiusKm / AU_KM) * K * E;
}
// Local moon distance from parent centre, view units. MEGA lifts every moon clear
// of the exaggerated parent surface and preserves ordering; TRUE is plain linear
// true distance. Blends with the same P animation.
export function moonDistance(dKm, parentRkm, parentDrawView) {
const megaD = parentDrawView * (moonBase + moonK * Math.log10(1 + dKm / parentRkm));
const trueD = (dKm / AU_KM) * K;
const blend = clamp((P - P_mega) / (P_true - P_mega), 0, 1);
return lerp(megaD, trueD, blend);
}
// ---- mode / transition ----
export function getMode() { return mode; }
export function getP() { return P; }
export function isTransitioning() { return tween !== null; }
export function setMode(next) {
if (next === mode && !tween) return;
mode = next;
tween = { from: P, to: next === 'true' ? P_true : P_mega, elapsed: 0 };
}
export function toggle() { setMode(mode === 'mega' ? 'true' : 'mega'); }
// Advance the MEGA↔TRUE tween. Call once per frame with wall dt (ms).
export function update(dtMs) {
if (!tween) return;
tween.elapsed += dtMs;
const t = clamp(tween.elapsed / transitionMs, 0, 1);
P = lerp(tween.from, tween.to, smoothstep(t));
if (t >= 1) { P = tween.to; tween = null; }
}
// ---- exaggeration (HUD slider) ----
export function setPlanetE(e) { planetE = e; }
export function getPlanetE() { return planetE; }
export function getSunE() { return sunE; }
export function getK() { return K; }