Redraw a planet's orbit line as its axis and eccentricity drift, so Saturn stays on it at AD 1

The orbit lines kept the shape of the J2000 elements and only turned with the node, while the
markers moved on Standish's drifting a and e. Saturn's eccentricity falls 0.00032 a century, so
at AD 1 its line passed 0.056 AU (8.4 million km) from Saturn, Jupiter's 0.016 AU from Jupiter,
and Pluto's 0.021 AU from Pluto at AD 3000. The comment that said no drawn line shows the drift
weighed one century of Pluto's axis, not twenty of Saturn's eccentricity.

update() now writes the line's 129 points again once |da| + a |de| since they were drawn passes
1e-4 AU, well under the 128 chords' own sag. Measured in the app on :4301, marker to its own
polyline: Saturn 0.0016 AU at AD 1 and 0.0028 at AD 2999, Jupiter 0.0014 at AD 1, Pluto 0.0078
at AD 2999 and 0.0082 today, all the chord sag.

The existing Mars test checked only that Mars stays in its line's plane. A new test measures the
distance to the drawn chords: Saturn 0.0017 AU and Mars 0.0005 at AD 1 (0.054 and 0.0022 without
the redraw). Guarded mutants: the call removed, and the threshold raised to 1 AU, each fail it
and only it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 15:01:08 +02:00
co-authored by Claude Opus 5.5
parent d449214309
commit c2683da37b
2 changed files with 69 additions and 11 deletions
@@ -690,6 +690,33 @@ describe('solar-system bodies against Horizons', () => {
}
});
/** How far a top-level body is from its own drawn orbit line, in AU: from the nearest of its chords. */
function offLineAu(id: string): number {
const marker = renderer.members.find((member) => member.id === id)!.marker;
const line = renderer.object.children[renderer.object.children.indexOf(marker) - 1] as THREE.Line;
expect(line.name).toBe('orbit-line');
line.updateMatrixWorld();
const position = line.geometry.getAttribute('position');
const vertex = (index: number): THREE.Vector3 => new THREE.Vector3().fromBufferAttribute(position, index).applyMatrix4(line.matrixWorld);
const chord = new THREE.Line3();
const closest = new THREE.Vector3();
let nearest = Number.POSITIVE_INFINITY;
for (let index = 0; index + 1 < position.count; index++) {
nearest = Math.min(nearest, chord.set(vertex(index), vertex(index + 1)).closestPointToPoint(marker.position, true, closest).distanceTo(marker.position));
}
return nearest;
}
it('redraws a planet’s orbit as its axis and eccentricity drift, so Saturn and Mars stay on their lines at AD 1', () => {
// What is left is the 128 chords' own sag from the true ellipse: measured 0.0017 AU for Saturn and
// 0.0005 for Mars at AD 1, and 0.0011 for Saturn at J2000. Drawn at J2000's shape at AD 1, the
// lines were 0.054 AU from Saturn and 0.0022 from Mars.
for (const [id, days, maxAu] of [['saturn', -730000, 0.002], ['mars', -730000, 0.0006], ['saturn', 0, 0.0015]] as const) {
renderer.update(DEFAULT_EPOCH_JD + days);
expect(offLineAu(id)).toBeLessThan(maxAu);
}
});
it('turns the Moon’s drawn orbit with its node, so the Moon stays on its own line', () => {
// Half the node's 18.6-year turn on, the ellipse drawn at the epoch has the Moon 10 degrees off
// its plane at the worst.