diff --git a/js/layers/almanac.js b/js/layers/almanac.js index 18c714a..d5a9df3 100644 --- a/js/layers/almanac.js +++ b/js/layers/almanac.js @@ -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; } diff --git a/js/layers/asteroids.js b/js/layers/asteroids.js index b1d58ab..129b378 100644 --- a/js/layers/asteroids.js +++ b/js/layers/asteroids.js @@ -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); diff --git a/js/layers/comets.js b/js/layers/comets.js index 2cf8cfd..d15cf55 100644 --- a/js/layers/comets.js +++ b/js/layers/comets.js @@ -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) { diff --git a/js/layers/planets.js b/js/layers/planets.js index a30f59f..1fafb75 100644 --- a/js/layers/planets.js +++ b/js/layers/planets.js @@ -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; } } }, }; diff --git a/js/scale.js b/js/scale.js index 3a4a107..d20794f 100644 --- a/js/scale.js +++ b/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; } diff --git a/serve.py b/serve.py index dae802a..9729757 100644 --- a/serve.py +++ b/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.