From 7080a5a817559a53ad095b6f665af0bfb2334d47 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Wed, 30 Sep 2026 17:33:01 +0200 Subject: [PATCH] Turn Eris, Haumea, Makemake and Nereid on their pages at their measured days on the map's clock The body page's comment said the body is drawn at the clock's date and turns at its rate, but a body with no IAU model turned 0.08 radians a second of wall time whatever the clock said: on Nereid's, Eris's, Haumea's and Makemake's pages the Clock tab's rates and Backwards changed nothing (Nereid 4.58 degrees a second at every rate), while the system view turns them at their measured days on the clock. Hyperion spun the same way on its page and is still in the system view, where it tumbles and has no day. A body whose day is measured but whose pole is not now turns pole up at that day, counted from its orbit's epoch as spinFor counts it; Hyperion is left still; only an exoplanet, whose day no one has measured, still turns for show. The template comment now says so, and the tick doc names all five bodies. Live on :4301, degrees a wall second at clock rates 1, 3600 and -3600: Nereid 0.009, 31.05, 31.08 (360/11.594 = 31.05); Eris 0, 0.951, 0.955 (360/378.504 = 0.951); Hyperion 0, 0, 0; Mars, by its IAU elements, 0.004, 14.67, 14.56. That makes the sphere reset when a body is shown matter for Hyperion and an exoplanet. The test the earlier fix added for that reset checked only the light, and deleting the reset passed the suite; a test now opens Hyperion after Earth and expects the sphere at rest (Earth left it 0.89 radians turned). Eris's page test checks it stands while the clock stands and turns a sixth of a turn in a sixth of its 378.504-hour day, pole up. Guarded mutants (full suite), each failing only its named test: the day ignored and Eris turned for show ('turns a body whose day is measured'); Hyperion turned for show, and the reset deleted (both 'puts the sphere back at rest ... Hyperion after Earth'). Unit suite 862/862, tsc -p tsconfig.app.json and etl:typecheck clean. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../body-detail-scene.component.spec.ts | 32 +++++++++++++++++-- .../body-detail-scene.component.ts | 17 +++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/app/features/body-detail/body-detail-scene.component.spec.ts b/src/app/features/body-detail/body-detail-scene.component.spec.ts index de7694d..194eea2 100644 --- a/src/app/features/body-detail/body-detail-scene.component.spec.ts +++ b/src/app/features/body-detail/body-detail-scene.component.spec.ts @@ -34,9 +34,11 @@ const SATURN: BodyRecord = { rates: {meanMotionDegPerDay: 0.033459683702669406, longitudeOfAscendingNodeDegPerDay: -0.000006848734291581108, argumentOfPeriapsisDegPerDay: 0.000021682266940451745}, rotationalElements: {poleRaDeg: [40.589, -0.036, 0], poleDecDeg: [83.537, -0.004, 0], primeMeridianDeg: [38.9, 810.7939024, 0]} }; -// Eris as it is shipped for this page's purposes: no IAU model, so its page keeps its own light. -const ERIS: BodyRecord = { ...EARTH, id: 'eris', name: 'Eris', kind: 'dwarf', radiusKm: 1163, rotationalElements: undefined }; -const BODIES = [EARTH, SATURN, ERIS]; +// Eris and Hyperion as they are shipped for this page's purposes: no IAU model, so their pages keep +// their own light; Eris's day is measured, and Hyperion tumbles and has none. +const ERIS: BodyRecord = { ...EARTH, id: 'eris', name: 'Eris', kind: 'dwarf', radiusKm: 1163, rotationalElements: undefined, rotationPeriodHours: 378.504 }; +const HYPERION: BodyRecord = { ...ERIS, id: 'hyperion', name: 'Hyperion', kind: 'moon', radiusKm: 135, parentBodyId: 'saturn', rotationPeriodHours: undefined }; +const BODIES = [EARTH, SATURN, ERIS, HYPERION]; /** Stands in for the WebGPU engine: a scene, a camera, and the tick hook, driven by hand. */ class FakeEngineService { @@ -155,6 +157,30 @@ describe('BodyDetailSceneComponent', () => { expect(page.sunLight.position.distanceTo(new THREE.Vector3(4, 3, 5))).toBeLessThan(1e-9); }); + it('puts the sphere back at rest when the next body shown does not turn: Hyperion after Earth', async () => { + await open('earth'); + expect(page.planet.quaternion.angleTo(new THREE.Quaternion())).toBeGreaterThan(0.1); + route.next(convertToParamMap({ id: 'hyperion' })); + await flushAsync(); + engine.tick(0.016); + engine.tick(0.016); + expect(page.planet.quaternion.angleTo(new THREE.Quaternion())).toBeLessThan(1e-9); + }); + + it('turns a body whose day is measured but not its pole at that day on the map’s clock: Eris a sixth of a turn in 63.084 hours', async () => { + await open('eris'); + const start = page.planet.rotation.y; + // The clock stands (the page is opened at rate 0): so does Eris, where it used to turn for show. + engine.tick(1); + expect(page.planet.rotation.y).toBe(start); + time.setDate(new Date(Date.parse('2025-06-01T12:00Z') + (378.504 / 6) * 3600000)); + engine.tick(0.016); + const turned = (((page.planet.rotation.y - start) / (2 * Math.PI)) % 1 + 1) % 1; + expect(turned).toBeCloseTo(1 / 6, 6); + // Pole up, as the system view turns it about its orbit's normal. + expect(new THREE.Vector3(0, 1, 0).applyQuaternion(page.planet.quaternion).y).toBeCloseTo(1, 12); + }); + it('says on its dock the date the body is drawn for, and nothing at the present, and offers the clock', async () => { await open('saturn', '2032-06-01T12:00Z', true); fixture.detectChanges(); diff --git a/src/app/features/body-detail/body-detail-scene.component.ts b/src/app/features/body-detail/body-detail-scene.component.ts index 100c3e1..2e1486c 100644 --- a/src/app/features/body-detail/body-detail-scene.component.ts +++ b/src/app/features/body-detail/body-detail-scene.component.ts @@ -63,8 +63,9 @@ const SUN_LIGHT_POSITION = new THREE.Vector3(4, 3, 5); } + panel is the reading, and the panel's own control is what keeps this body. A solar-system + body is drawn at the clock's date and turns at its rate, so both are shown and can be set + here; an exoplanet, whose day no one has measured, turns for show whatever the clock says. --> ` @@ -280,8 +281,10 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { /** * A body the IAU gives rotational elements for is turned as it is at the map's date, under its - * real Sun, at the rate the map's clock runs (see `bodyPageView`). Any other — an exoplanet, or - * Eris, Haumea or Makemake — turns slowly for show, as the page always turned them. + * real Sun, at the rate the map's clock runs (see `bodyPageView`). Eris, Haumea, Makemake and + * Nereid, whose day is measured but whose pole is not, turn pole up at that day on the same + * clock, as the system view turns them; Hyperion, which tumbles, is left still, as it is there. + * An exoplanet turns slowly for show, as the page always turned it. */ private tick(deltaSeconds: number): void { this.controls?.update(); @@ -292,7 +295,11 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { const sunAzimuth = Math.atan2(SUN_LIGHT_POSITION.x, SUN_LIGHT_POSITION.z); if (this.body && bodyPageView(this.body, this.bodies, this.time.julianDate(), sunAzimuth, this.planet.quaternion, this.sunLight.position)) { this.sunLight.position.multiplyScalar(SUN_LIGHT_POSITION.length()); - } else { + } else if (this.body?.rotationPeriodHours !== undefined) { + // Counted from the orbit's epoch, as `spinFor` counts: where the meridian starts is unknown. + const turns = ((this.time.julianDate() - this.body.orbit.epochJd) * 24) / this.body.rotationPeriodHours; + this.planet.rotation.set(0, (turns % 1) * 2 * Math.PI, 0); + } else if (!this.body) { this.planet.rotation.y += deltaSeconds * 0.08; } const sunSide = this.sunLight.position.y < 0 ? -1 : 1;