diff --git a/src/app/features/galaxy-system/system-orbits-renderer.spec.ts b/src/app/features/galaxy-system/system-orbits-renderer.spec.ts index 8aa8c62..ae69d21 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.spec.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.spec.ts @@ -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. diff --git a/src/app/features/galaxy-system/system-orbits-renderer.ts b/src/app/features/galaxy-system/system-orbits-renderer.ts index 54186ac..08b8041 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.ts @@ -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) {