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 0ee9c88..48ada8a 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.spec.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.spec.ts @@ -475,7 +475,7 @@ describe('solar-system bodies against Horizons', () => { // for the planets, planet-centred for the moons) at dates across 1950-2100, so the whole path — // mean elements, their rates, the Laplace planes and the scene's frame — is checked against // JPL's ephemeris rather than against itself. - const RECORDS: Record> = { + const RECORDS: Record> = { earth: {kind: 'planet', orbit: {semiMajorAxisAu: 1.00000018, eccentricity: 0.01673163, inclinationDeg: -0.00054346, longitudeOfAscendingNodeDeg: -5.11260389, argumentOfPeriapsisDeg: 108.04266274, meanAnomalyAtEpochDeg: -2.4631431299999917, epochJd: 2451545}, rates: {meanMotionDegPerDay: 0.9856091187759068, longitudeOfAscendingNodeDegPerDay: -0.000006604751813826146, argumentOfPeriapsisDegPerDay: 0.000015309819575633124, semiMajorAxisAuPerDay: -8.213552361396303e-13, eccentricityPerDay: -1.002327173169062e-9, inclinationDegPerDay: -3.6609938398357287e-7}}, jupiter: {kind: 'planet', orbit: {semiMajorAxisAu: 5.20248019, eccentricity: 0.0485359, inclinationDeg: 1.29861416, longitudeOfAscendingNodeDeg: 100.29282654, argumentOfPeriapsisDeg: -86.0178741, meanAnomalyAtEpochDeg: 20.059839080000003, epochJd: 2451545}, rates: {meanMotionDegPerDay: 0.08309113532019165, longitudeOfAscendingNodeDegPerDay: 0.0000035659463381245725, argumentOfPeriapsisDegPerDay: 0.0000014167219712525667, semiMajorAxisAuPerDay: -7.841204654346339e-10, eccentricityPerDay: 4.935249828884326e-9, inclinationDegPerDay: -8.83501711156742e-8, meanAnomalyTerms: {b: -0.00012452, c: 0.0606406, s: -0.35635438, f: 38.35125}}}, saturn: {kind: 'planet', orbit: {semiMajorAxisAu: 9.54149883, eccentricity: 0.05550825, inclinationDeg: 2.49424102, longitudeOfAscendingNodeDeg: 113.63998702, argumentOfPeriapsisDeg: -20.778626390000014, meanAnomalyAtEpochDeg: -42.78564733999999, epochJd: 2451545}, rates: {meanMotionDegPerDay: 0.033459683702669406, longitudeOfAscendingNodeDegPerDay: -0.000006848734291581108, argumentOfPeriapsisDegPerDay: 0.000021682266940451745, semiMajorAxisAuPerDay: -8.391512662559891e-10, eccentricityPerDay: -8.773169062286106e-9, inclinationDegPerDay: 1.2374236824093085e-7, meanAnomalyTerms: {b: 0.00025899, c: -0.13434469, s: 0.87320147, f: 38.35125}}}, @@ -486,10 +486,14 @@ describe('solar-system bodies against Horizons', () => { europa: {kind: 'moon', orbit: {semiMajorAxisAu: 0.004486026417754354, eccentricity: 0.0094, inclinationDeg: 0.466, longitudeOfAscendingNodeDeg: 219.106, argumentOfPeriapsisDeg: 88.97, meanAnomalyAtEpochDeg: 171.016, epochJd: 2450464.5}, rates: {meanMotionDegPerDay: 101.3747242, longitudeOfAscendingNodeDegPerDay: -0.03265393199600969, argumentOfPeriapsisDegPerDay: -0.7070489837643877}, laplacePole: {raDeg: 268.084, decDeg: 64.506}, parentBodyId: 'jupiter'}, titan: {kind: 'moon', orbit: {semiMajorAxisAu: 0.008167663044150534, eccentricity: 0.0288, inclinationDeg: 0.306, longitudeOfAscendingNodeDeg: 28.06, argumentOfPeriapsisDeg: 180.532, meanAnomalyAtEpochDeg: 163.31, epochJd: 2451545}, rates: {meanMotionDegPerDay: 22.5769756, longitudeOfAscendingNodeDegPerDay: -0.001398845136769169, argumentOfPeriapsisDegPerDay: 0.002799120423059061}, laplacePole: {raDeg: 36.214, decDeg: 83.949}, parentBodyId: 'saturn'}, triton: {kind: 'moon', orbit: {semiMajorAxisAu: 0.002371417442908832, eccentricity: 0, inclinationDeg: 156.865, longitudeOfAscendingNodeDeg: 177.608, argumentOfPeriapsisDeg: 66.142, meanAnomalyAtEpochDeg: 352.257, epochJd: 2451545}, rates: {meanMotionDegPerDay: 61.2572638, longitudeOfAscendingNodeDegPerDay: 0.001433750844964632, argumentOfPeriapsisDegPerDay: 0.0025509841146658433}, laplacePole: {raDeg: 299.456, decDeg: 43.414}, parentBodyId: 'neptune'}, + uranus: {kind: 'planet', orbit: {semiMajorAxisAu: 19.18797948, eccentricity: 0.0468574, inclinationDeg: 0.77298127, longitudeOfAscendingNodeDeg: 73.96250215, argumentOfPeriapsisDeg: 98.47154226, meanAnomalyAtEpochDeg: 141.76872184, epochJd: 2451545}, rates: {meanMotionDegPerDay: 0.011731557178644764, longitudeOfAscendingNodeDegPerDay: 0.0000015714439425051334, argumentOfPeriapsisDegPerDay: 9.65718275154004e-7, semiMajorAxisAuPerDay: -5.600273785078713e-9, eccentricityPerDay: -4.2436687200547574e-10, inclinationDegPerDay: -4.932375085557837e-8, meanAnomalyTerms: {b: 0.00058331, c: -0.97731848, s: 0.17689245, f: 7.67025}}}, + titania: {kind: 'moon', orbit: {semiMajorAxisAu: 0.002916485361445723, eccentricity: 0.0011, inclinationDeg: 0.079, longitudeOfAscendingNodeDeg: 279.771, argumentOfPeriapsisDeg: 284.4, meanAnomalyAtEpochDeg: 24.614, epochJd: 2444239.5}, rates: {meanMotionDegPerDay: 41.3514246, longitudeOfAscendingNodeDegPerDay: -0.005044947168524978, argumentOfPeriapsisDegPerDay: 0.006102004540272753}, laplacePole: {raDeg: 77.311, decDeg: 15.175}, parentBodyId: 'uranus'}, + charon: {kind: 'moon', orbit: {semiMajorAxisAu: 0.00013095774631236113, eccentricity: 0.0002, inclinationDeg: 0.08, longitudeOfAscendingNodeDeg: 26.928, argumentOfPeriapsisDeg: 146.106, meanAnomalyAtEpochDeg: 131.07, epochJd: 2451545}, rates: {meanMotionDegPerDay: 56.362521, longitudeOfAscendingNodeDegPerDay: -0.00010926638529337138, argumentOfPeriapsisDegPerDay: 0.00009683851540842405}, laplacePole: {raDeg: 132.993, decDeg: -6.163}, parentBodyId: 'pluto', massRatio: 0.1220485755631374}, }; // Each ceiling sits just above what these elements measure on that date: Earth 0.003 degrees, // Jupiter 0.063, Saturn 0.164, Pluto 0.054, the Moon 0.72 (no mean ellipse has its evection or - // variation), Io 0.021, Europa 0.036, Titan 0.014, Triton 0.137. + // variation), Io 0.021, Europa 0.036, Titan 0.014, Triton 0.137, Titania 0.62 (against Uranus's + // equator, 120 years from its 1980 epoch), Charon 0.37. const HORIZONS: Array<[id: string, jd: number, x: number, y: number, z: number, maxDeg: number]> = [ ['earth', 2488069.5, -0.1574071329883954, 0.890666220858489, 0.3859132211165683, 0.02], ['jupiter', 2433282.5, 3.406605247558555, -3.425997624196318, -1.551719750032203, 0.1], @@ -500,6 +504,8 @@ describe('solar-system bodies against Horizons', () => { ['europa', 2433282.5, 0.004084372287322533, -0.001665375585011311, -0.0007673072324795899, 0.1], ['titan', 2488069.5, 0.007800850235156121, -0.001556932380983438, -0.0006078959246502567, 0.05], ['triton', 2488069.5, -0.001421151845853369, -0.0001894510477241482, 0.001888790702926415, 0.2], + ['titania', 2488069.5, -0.00151919968294745, -0.0003387914082135071, 0.002465657830788125, 0.75], + ['charon', 2488069.5, -0.00003046411046017432, -0.000009404114448552256, 0.0001270457155789907, 0.5], ]; function record(id: string): BodyRecord { @@ -517,6 +523,36 @@ describe('solar-system bodies against Horizons', () => { }); } + it('puts Pluto where Horizons has it round its barycentre with Charon, 2 131 km out and opposite Charon', () => { + // Horizons, Pluto (999) from the Pluto-system barycentre (9), on JD 2488069.5 (2100). + const horizons = new THREE.Vector3(0.000003313612032581019, 0.000001023040948538272, -0.00001381793390079716); + renderer.update(2488069.5); + const charon = renderer.members.find((member) => member.id === 'charon')!.marker; + const barycentre = charon.parent!.position; + const pluto = renderer.members.find((member) => member.id === 'pluto')!.marker.position.clone().sub(barycentre); + const charonFromBarycentre = charon.position; + + expect((pluto.angleTo(horizons) * 180) / Math.PI).toBeLessThan(0.5); + expect(pluto.length() * 149597870.7).toBeCloseTo(horizons.length() * 149597870.7, -1); + // Opposite, at the inverse of their mass ratio. + expect((pluto.angleTo(charonFromBarycentre) * 180) / Math.PI).toBeCloseTo(180, 6); + expect(charonFromBarycentre.length() / pluto.length()).toBeCloseTo(1 / 0.1220485755631374, 6); + }); + + it('draws Pluto’s own orbit round the barycentre, in the plane it is going round in', () => { + const charon = renderer.members.find((member) => member.id === 'charon')!.marker; + const [charonLine, plutoLine] = charon.parent!.children.filter((child) => child.name === 'orbit-line'); + for (const days of [0, 3000, 30000]) { + renderer.update(DEFAULT_EPOCH_JD + days); + const pluto = renderer.members.find((member) => member.id === 'pluto')!.marker.position.clone().sub(charon.parent!.position); + const normal = new THREE.Vector3(0, 0, 1).applyQuaternion(plutoLine.quaternion); + expect(Math.abs(pluto.clone().normalize().dot(normal))).toBeLessThan(1e-9); + // A near-circle 2 131 km across, a ninth of Charon's. + expect(Math.abs(plutoLine.scale.x) * 0.00013095774631236113).toBeCloseTo(pluto.length(), 8); + expect(charonLine.scale.x / Math.abs(plutoLine.scale.x)).toBeCloseTo(1 / 0.1220485755631374, 9); + } + }); + 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 0922353..d90018d 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.ts @@ -294,6 +294,11 @@ interface TrackedMoon { parentId: string; rotationPeriodHours?: number; obliquityDeg?: number; + /** + * Where the moon and its planet go round a barycentre outside the planet (Charon): the moon's + * mass over the planet's, and the planet's own small orbit round that point. + */ + barycentre?: { massRatio: number; parentOrbitLine: THREE.Line }; } /** @@ -378,7 +383,7 @@ export class SystemOrbitsRenderer { if (!parentTracked) { continue; // orphaned moon reference; skip rather than crash. } - const moon = this.addMoon(body.id, body.orbit, body.rates, body.radiusKm, parentTracked, moonFrame(body), appearanceForBody(body, bodies, hostLuminositySolar), { periodHours: body.rotationPeriodHours, obliquityDeg: body.obliquityDeg }); + const moon = this.addMoon(body.id, body.orbit, body.rates, body.radiusKm, parentTracked, moonFrame(body), appearanceForBody(body, bodies, hostLuminositySolar), { periodHours: body.rotationPeriodHours, obliquityDeg: body.obliquityDeg }, body.massRatio); members.push({ id: body.id, kind: 'moon', marker: moon.marker, parentId: parent.id }); } @@ -466,6 +471,14 @@ export class SystemOrbitsRenderer { const orbital = positionAtEpoch(current); moon.marker.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(moon.frame); orientOrbit(moon.orbitLine.quaternion, current, moon.frame); + if (moon.barycentre) { + // The planet's elements place the pair's barycentre, which is where the pivot is: the + // planet sits the moon's share of their separation back from it, the moon the rest out. + const { massRatio, parentOrbitLine } = moon.barycentre; + parent.marker.position.copy(parent.position).addScaledVector(moon.marker.position, -massRatio / (1 + massRatio)); + moon.marker.position.multiplyScalar(1 / (1 + massRatio)); + parentOrbitLine.quaternion.copy(moon.orbitLine.quaternion); + } if (moon.rotationPeriodHours) { moon.marker.quaternion.copy(spinFor(current, moon.frame, moon.rotationPeriodHours, moon.obliquityDeg, epochJd - moon.elements.epochJd)); } @@ -545,7 +558,8 @@ export class SystemOrbitsRenderer { parent: TrackedTopLevelBody, frame: THREE.Quaternion, appearance?: PlanetAppearance, - rotation?: { periodHours?: number; obliquityDeg?: number } + rotation?: { periodHours?: number; obliquityDeg?: number }, + massRatio?: number ): TrackedMoon { const pivot = new THREE.Group(); const orbitLine = buildOrbitLine(elements, 'moon', frame); @@ -555,7 +569,20 @@ export class SystemOrbitsRenderer { this.trackDisposable(orbitLine.geometry, orbitLine.material as THREE.Material); this.trackDisposable(marker.geometry, marker.material as THREE.Material); - const moon: TrackedMoon = { id, elements, rates, marker, orbitLine, frame, pivot, parentId: parent.id, rotationPeriodHours: rotation?.periodHours, obliquityDeg: rotation?.obliquityDeg }; + let barycentre: TrackedMoon['barycentre']; + if (massRatio !== undefined) { + // Both orbits are the relative one, scaled: the moon's by the planet's share of the mass, + // the planet's by the moon's share and turned half round, since it is always opposite. + // Charon's then spans 17 460 km of radius, Pluto's 2 131, and neither passes through Pluto. + orbitLine.scale.setScalar(1 / (1 + massRatio)); + const parentOrbitLine = buildOrbitLine(elements, parent.kind, frame); + parentOrbitLine.scale.setScalar(-massRatio / (1 + massRatio)); + pivot.add(parentOrbitLine); + this.trackDisposable(parentOrbitLine.geometry, parentOrbitLine.material as THREE.Material); + barycentre = { massRatio, parentOrbitLine }; + } + + const moon: TrackedMoon = { id, elements, rates, marker, orbitLine, frame, pivot, parentId: parent.id, rotationPeriodHours: rotation?.periodHours, obliquityDeg: rotation?.obliquityDeg, barycentre }; this.moons.push(moon); return moon; } diff --git a/src/app/shared/models/body.model.ts b/src/app/shared/models/body.model.ts index d33798b..2d554c7 100644 --- a/src/app/shared/models/body.model.ts +++ b/src/app/shared/models/body.model.ts @@ -56,9 +56,9 @@ export interface BodyRecord { rates: MeanElementRates; /** * The pole of the plane a moon's elements are measured against, where that is its local - * Laplace plane: right ascension and declination in the ICRF. The node is then counted from - * where that plane crosses the ICRF equator. Absent means the J2000 ecliptic, as for the - * planets and the Moon. + * Laplace plane or, for Uranus's and Pluto's moons, the planet's equator: right ascension and + * declination in the ICRF. The node is then counted from where that plane crosses the ICRF + * equator. Absent means the J2000 ecliptic, as for the planets and the Moon. */ laplacePole?: { raDeg: number; decDeg: number }; /** Where the elements come from and the span they hold over, as the card prints it. */ @@ -68,6 +68,14 @@ export interface BodyRecord { * relative to that planet, not heliocentrically. Undefined for planets/dwarfs. */ parentBodyId?: string; + /** + * For a moon heavy enough that it and its planet go round a point outside the planet — Charon, + * an eighth of Pluto's mass, puts it 2 100 km from Pluto's centre, 900 km above its surface — + * the moon's mass over the planet's, from the GMs on their Horizons pages. The planet's own + * elements then place that barycentre, as Standish's "Pluto" does, and both bodies are drawn + * going round it. Absent for every other moon. + */ + massRatio?: number; /** * How the body turns on its own axis: the sidereal rotation period in hours, negative where * Horizons gives a negative rate (Venus, Uranus), and the tilt of that axis from its orbital