// 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; }