From 2e5daa0f97b3a90e2ecc3140c339b5f5c38f31f2 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Thu, 24 Sep 2026 20:20:51 +0200 Subject: [PATCH] Give every body the period it is drawn going round in, and say where its orbit comes from Audit #38: Europa's card listed its axis, eccentricity and inclination but no period, while the scene turned it round Jupiter all the same: heliocentricPeriodDays refused every moon, since the catalogue carried no planet masses. Every solar-system body's period is now 360 over the JPL mean motion that carries it round the scene, filed under Measured since that is JPL's published figure: Europa 3.55 d, the Moon 27.3 d, Saturn 29.5 yr on the live cards, Earth 365.2564 d. heliocentricPeriodDays is gone. Exoplanets keep the archive's period, or none. The card's provenance line now ends with where the orbit comes from, "Orbit: JPL SSD satellite mean elements, epoch 1997 Jan 16." for Europa, "Orbit: JPL approximate mean elements (Standish), fit for 3000 BC to AD 3000." for a planet, and the Sun's system note says so too: "Orbits propagated from JPL mean elements, the planets' fit for 3000 BC to AD 3000, to the current date." Other systems keep "published elements". All three read in the running app. Each has a test that fails without it (moons refused a period, provenance without the orbit, a note without the source). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../features/body-detail/body-detail.model.ts | 7 +-- src/app/features/body-detail/body-readouts.ts | 3 +- .../body-detail/body-view-model.spec.ts | 44 ++++++------------- .../features/body-detail/body-view-model.ts | 26 +++-------- .../galaxy-system-scene.component.spec.ts | 13 ++++++ .../galaxy-system-scene.component.ts | 5 ++- 6 files changed, 43 insertions(+), 55 deletions(-) diff --git a/src/app/features/body-detail/body-detail.model.ts b/src/app/features/body-detail/body-detail.model.ts index eed591b..bc02d0f 100644 --- a/src/app/features/body-detail/body-detail.model.ts +++ b/src/app/features/body-detail/body-detail.model.ts @@ -33,9 +33,8 @@ export interface BodyDetailViewModel { /** True when a real photograph is being shown rather than the derived surface. */ hasPhotography: boolean; /** - * Sidereal orbital period. Measured where the archive published one; otherwise derived from the - * semi-major axis for heliocentric orbits, where the central mass is known exactly. Undefined - * when neither applies — see `heliocentricPeriodDays`. + * Sidereal orbital period. For a solar-system body, 360 degrees over JPL's published mean + * motion; for an exoplanet, the archive's period where it published one, and undefined where not. */ orbitalPeriodDays?: number; /** @@ -44,4 +43,6 @@ export interface BodyDetailViewModel { * derived surface as a photograph. */ orbitalPeriodSource?: 'measured' | 'derived'; + /** Where the orbit comes from and the span it holds over; see `BodyRecord.orbitSource`. */ + orbitSource?: string; } diff --git a/src/app/features/body-detail/body-readouts.ts b/src/app/features/body-detail/body-readouts.ts index 67611fc..9953ade 100644 --- a/src/app/features/body-detail/body-readouts.ts +++ b/src/app/features/body-detail/body-readouts.ts @@ -65,7 +65,8 @@ export function bodyReadouts(body: BodyDetailViewModel): BodyReadouts { derived.push({ label: 'Bulk density', value: formatDensity(body.appearance.bulkDensityGramsPerCm3) }); } - return { kindLabel: KIND_LABELS[body.kind], measured, derived, provenance: provenanceFor(body) }; + const provenance = body.orbitSource ? `${provenanceFor(body)} Orbit: ${body.orbitSource}.` : provenanceFor(body); + return { kindLabel: KIND_LABELS[body.kind], measured, derived, provenance }; } /** diff --git a/src/app/features/body-detail/body-view-model.spec.ts b/src/app/features/body-detail/body-view-model.spec.ts index 4792435..91a1e69 100644 --- a/src/app/features/body-detail/body-view-model.spec.ts +++ b/src/app/features/body-detail/body-view-model.spec.ts @@ -3,7 +3,8 @@ import { describe, expect, it } from 'vitest'; import { BodyRecord, OrbitalElements } from '../../shared/models/body.model'; import { ExoplanetRecord } from '../../shared/models/exoplanet.model'; import { StarRecord, SUN_STAR_ID } from '../../shared/models/star.model'; -import { buildBodyViewModel, heliocentricPeriodDays } from './body-view-model'; +import { bodyReadouts } from './body-readouts'; +import { buildBodyViewModel } from './body-view-model'; const orbit = (overrides: Partial = {}): OrbitalElements => ({ semiMajorAxisAu: 1, @@ -51,43 +52,24 @@ const luna: BodyRecord = { orbitSource: 'JPL SSD satellite mean elements, epoch 2000 Jan 1', }; -describe('heliocentricPeriodDays', () => { - it('recovers a known period from the semi-major axis alone', () => { - // P² = a³ in these units, so Earth must come back a year. - expect(heliocentricPeriodDays(earth)).toBeCloseTo(365.25, 1); - }); - - it('scales as the three-halves power', () => { - const jupiter: BodyRecord = { - ...earth, - id: 'jupiter', - name: 'Jupiter', - orbit: orbit({ semiMajorAxisAu: 5.2044 }), - }; - // Jupiter's real sidereal period is 4332.6 days. - expect(heliocentricPeriodDays(jupiter)).toBeCloseTo(4335, -1); - }); - - it('refuses to compute a period for a moon', () => { - // A moon's elements are relative to its planet, whose mass is not in the catalogue — the - // same arithmetic would be wrong by the ratio of that planet's mass to the Sun's. - expect(heliocentricPeriodDays(luna)).toBeUndefined(); - }); -}); - describe('buildBodyViewModel', () => { const catalogues = { bodies: [earth, luna], exoplanets: [] as ExoplanetRecord[], stars: [sun] }; - it('marks a period computed from the semi-major axis as derived', () => { + it('gives a planet the sidereal year its published mean motion goes round in', () => { const model = buildBodyViewModel('earth', catalogues); - expect(model?.orbitalPeriodSource).toBe('derived'); - expect(model?.orbitalPeriodDays).toBeCloseTo(365.25, 1); + expect(model?.orbitalPeriodSource).toBe('measured'); + expect(model?.orbitalPeriodDays).toBeCloseTo(365.2564, 4); }); - it('leaves a moon without a period rather than inventing one', () => { + it('gives a moon its period too, from the same mean motion that carries it round', () => { + // The card used to refuse, while the scene turned the Moon round the Earth all the same. const model = buildBodyViewModel('luna', catalogues); - expect(model?.orbitalPeriodDays).toBeUndefined(); - expect(model?.orbitalPeriodSource).toBeUndefined(); + expect(model?.orbitalPeriodSource).toBe('measured'); + expect(model?.orbitalPeriodDays).toBeCloseTo(27.32166, 5); + }); + + it('says where the orbit comes from, in the card’s provenance', () => { + expect(bodyReadouts(buildBodyViewModel('luna', catalogues)!).provenance).toContain('Orbit: JPL SSD satellite mean elements, epoch 2000 Jan 1.'); }); it('marks a published exoplanet period as measured, not derived', () => { diff --git a/src/app/features/body-detail/body-view-model.ts b/src/app/features/body-detail/body-view-model.ts index becca17..9df6c18 100644 --- a/src/app/features/body-detail/body-view-model.ts +++ b/src/app/features/body-detail/body-view-model.ts @@ -4,7 +4,7 @@ import { luminositySolar } from '../../shared/astro/stellar'; import { bodyTexturePath } from '../../shared/rendering/texture-catalog'; import { BodyRecord } from '../../shared/models/body.model'; import { ExoplanetRecord } from '../../shared/models/exoplanet.model'; -import { StarRecord, SUN_STAR_ID } from '../../shared/models/star.model'; +import { StarRecord } from '../../shared/models/star.model'; import { BodyDetailViewModel } from './body-detail.model'; /** Everything the view model is assembled from — the three catalogues, already loaded. */ @@ -41,7 +41,10 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body const body = catalogues.bodies.find((candidate) => candidate.id === id); if (body) { const hostStar = catalogues.stars.find((star) => star.id === body.systemStarId); - const periodDays = heliocentricPeriodDays(body); + // The period the map draws, moons included: JPL's own mean motion, which is also what + // carries the body round the scene. Europa's card had no period at all while the scene + // turned it round Jupiter in 3.55 days. + const periodDays = 360 / body.rates.meanMotionDegPerDay; return { id: body.id, name: body.name, @@ -53,7 +56,8 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body appearance: appearanceForBody(body, catalogues.bodies, luminosityOf(hostStar)), hasPhotography: bodyTexturePath(body.id) !== undefined, orbitalPeriodDays: periodDays, - orbitalPeriodSource: periodDays === undefined ? undefined : 'derived', + orbitalPeriodSource: 'measured', + orbitSource: body.orbitSource, }; } @@ -81,19 +85,3 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body orbitalPeriodSource: exoplanet.periodDays === undefined ? undefined : 'measured', }; } - -/** - * Kepler's third law for a body orbiting the Sun: P² = a³ with P in years and a in AU, which - * holds exactly in these units because the Sun's mass is the unit of mass. - * - * Only for heliocentric orbits. A moon's elements are relative to its parent planet, whose mass - * the catalogue does not carry, so the same arithmetic there would be wrong by the ratio of the - * planet's mass to the Sun's — a factor of a thousand for Jupiter. - */ -export function heliocentricPeriodDays(body: BodyRecord): number | undefined { - if (body.parentBodyId !== undefined || body.systemStarId !== SUN_STAR_ID) { - return undefined; - } - const a = body.orbit.semiMajorAxisAu; - return a > 0 ? Math.pow(a, 1.5) * 365.25 : undefined; -} diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts index 89a5a7e..97142b2 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts @@ -831,6 +831,19 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => { expect(navigationStore.viewLevel()).toBe('system'); }); + it('says where a system’s orbits come from, and for the Sun how long they hold', async () => { + const note = (): string => (fixture.componentInstance as unknown as { hudNote: () => string }).hudNote(); + navigationStore.selectStar(SUN.id); + await flushAsync(); + await advanceFrames(engine, 2.5); + expect(note()).toBe('Orbits propagated from JPL mean elements, the planets’ fit for 3000 BC to AD 3000, to the current date.'); + + navigationStore.selectStar(ALPHA_CENTAURI.id); + await flushAsync(); + await advanceFrames(engine, 5); + expect(note()).toBe('Orbits propagated from published elements to the current date.'); + }); + it('performs the floating-origin recenter: the camera lands close to the AU-space origin, not out at parsec-scale coordinates', async () => { navigationStore.selectStar(ALPHA_CENTAURI.id); await flushAsync(); diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index 78454f9..45ca160 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -1760,7 +1760,10 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { ? [{ label: 'Luminosity', value: formatLuminosity(luminosity), derived: true }] : []), ]); - this.hudNote.set(this.time.atNow() ? 'Orbits propagated from published elements to the current date.' : 'Orbits propagated from published elements to the date on the clock.'); + // Where the orbits come from, and for the Sun how far from the present they hold: each + // body's card names its own source and epoch. + const source = this.bodies.some((body) => body.systemStarId === star.id) ? 'JPL mean elements, the planets’ fit for 3000 BC to AD 3000,' : 'published elements'; + this.hudNote.set(`Orbits propagated from ${source} to ${this.time.atNow() ? 'the current date' : 'the date on the clock'}.`); this.hudRange.set( formatAu( this.engine.visibleHalfHeight(