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
@@ -129,20 +129,11 @@ const ORBIT_LINE_NAME = 'orbit-line';
* ellipse fixed at one date has the Moon up to 2 sin 5.16° of its distance, 69 000 km, off its own
* line nine years on.
*
* The shape is the epoch's. The planets' axes and eccentricities do drift, but Pluto's axis, the
* fastest, moves 0.0045 AU a century against 39.5, which no drawn line shows.
* The shape is redrawn by {@link reshapeOrbitLine} as the planets' axes and eccentricities drift.
*/
function buildOrbitLine(elements: OrbitalElements, kind: SystemMemberKind, frame: THREE.Quaternion): THREE.Line {
const points = orbitEllipsePoints({ ...elements, inclinationDeg: 0, longitudeOfAscendingNodeDeg: 0, argumentOfPeriapsisDeg: 0 });
const positions = new Float32Array(points.length * 3);
points.forEach((point, index) => {
positions[index * 3] = point.x;
positions[index * 3 + 1] = point.y;
positions[index * 3 + 2] = point.z;
});
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('position', new THREE.BufferAttribute(ellipseInItsPlane(elements, new Float32Array((ORBIT_LINE_SEGMENTS + 1) * 3)), 3));
const material = new THREE.LineBasicMaterial({
color: colorForKind(kind),
@@ -152,10 +143,49 @@ function buildOrbitLine(elements: OrbitalElements, kind: SystemMemberKind, frame
const line = new THREE.Line(geometry, material);
line.name = ORBIT_LINE_NAME;
line.userData = { semiMajorAxisAu: elements.semiMajorAxisAu, eccentricity: elements.eccentricity };
orientOrbit(line.quaternion, elements, frame);
return line;
}
const ORBIT_LINE_SEGMENTS = 128;
/** The orbit's ellipse in its own plane, periapsis along +X, written into `positions`. */
function ellipseInItsPlane(elements: OrbitalElements, positions: Float32Array): Float32Array {
orbitEllipsePoints({ ...elements, inclinationDeg: 0, longitudeOfAscendingNodeDeg: 0, argumentOfPeriapsisDeg: 0 }, ORBIT_LINE_SEGMENTS).forEach((point, index) => {
positions[index * 3] = point.x;
positions[index * 3 + 1] = point.y;
positions[index * 3 + 2] = point.z;
});
return positions;
}
/**
* How far, in AU, an orbit's drawn ellipse may be from its current one before it is drawn again:
* well under the 128 chords' own sag from the true curve, 0.0005 AU for Mars and 0.003 for Saturn.
*/
const ORBIT_RESHAPE_AU = 1e-4;
/**
* Draws an orbit line's ellipse again once the axis and eccentricity it was drawn with have drifted
* from `elements`' by more than {@link ORBIT_RESHAPE_AU}. Standish's rates move Saturn's
* eccentricity 0.0064 in twenty centuries, and left at J2000's, the line passed 0.056 AU, 8.4
* million km, from Saturn at AD 1; Jupiter 0.016 AU there, Pluto 0.021 at AD 3000. The moons' and
* the exoplanets' elements carry no such rates, so their lines are drawn once.
*/
function reshapeOrbitLine(line: THREE.Line, elements: OrbitalElements): void {
const drawn = line.userData as { semiMajorAxisAu: number; eccentricity: number };
const driftAu = Math.abs(elements.semiMajorAxisAu - drawn.semiMajorAxisAu) + elements.semiMajorAxisAu * Math.abs(elements.eccentricity - drawn.eccentricity);
if (driftAu <= ORBIT_RESHAPE_AU) {
return;
}
const position = line.geometry.getAttribute('position') as THREE.BufferAttribute;
ellipseInItsPlane(elements, position.array as Float32Array);
position.needsUpdate = true;
line.geometry.computeBoundingSphere();
line.userData = { semiMajorAxisAu: elements.semiMajorAxisAu, eccentricity: elements.eccentricity };
}
/**
* A marker sphere, surfaced with the body's own photograph where one has ever been taken, and
* with a texture derived from its measurements where none has — and lit by its star either way,
@@ -498,6 +528,7 @@ export class SystemOrbitsRenderer {
body.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(body.frame);
body.marker.position.copy(body.position);
orientOrbit(body.orbitLine.quaternion, current, body.frame);
reshapeOrbitLine(body.orbitLine, current);
if (body.rotationalElements) {
bodyOrientation(body.rotationalElements, epochJd, body.marker.quaternion, body.id === 'earth');
} else if (body.rotationPeriodHours) {