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 194eea2..5f344a4 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 @@ -38,7 +38,14 @@ const SATURN: BodyRecord = { // 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]; +// Mercury as shipped, but its 0.01-degree libration: its Sun is never 0.034 degrees off its equator. +const MERCURY: BodyRecord = { + id: 'mercury', systemStarId: 0, name: 'Mercury', kind: 'planet', radiusKm: 2439.4, orbitSource: 'test', + orbit: {semiMajorAxisAu: 0.38709843, eccentricity: 0.20563661, inclinationDeg: 7.00559432, longitudeOfAscendingNodeDeg: 48.33961819, argumentOfPeriapsisDeg: 29.118100759999997, meanAnomalyAtEpochDeg: 174.79394829, epochJd: 2451545}, + rates: {meanMotionDegPerDay: 4.092338805372484, longitudeOfAscendingNodeDegPerDay: -0.0000033440607802874744, argumentOfPeriapsisDegPerDay: 0.000007708198494182067}, + rotationalElements: {poleRaDeg: [281.0103, -0.0328, 0], poleDecDeg: [61.4155, -0.0049, 0], primeMeridianDeg: [329.5988, 6.1385108, 0]} +}; +const BODIES = [EARTH, SATURN, ERIS, HYPERION, MERCURY]; /** Stands in for the WebGPU engine: a scene, a camera, and the tick hook, driven by hand. */ class FakeEngineService { @@ -223,6 +230,23 @@ describe('BodyDetailSceneComponent', () => { expect(camera.getWorldDirection(new THREE.Vector3()).angleTo(toSaturn)).toBeLessThan(1e-9); }); + it('leaves the camera on its side while the Sun only grazes the equator: Mercury through two crossings', async () => { + // The Sun is south of Mercury's equator on 2026-10-20, north from about 1 November, and south + // again from about 6 December, never more than 0.034 degrees either side. + await open('mercury', '2026-10-20T00:00Z'); + const camera = engine.getCamera(); + expect(page.sunLight.position.y).toBeLessThan(0); + expect(camera.position.y).toBeLessThan(0); + const sunSides = new Set(); + for (let day = 1; day <= 60; day++) { + time.setDate(new Date(Date.parse('2026-10-20T00:00Z') + day * 86400000)); + engine.tick(0.016); + sunSides.add(Math.sign(page.sunLight.position.y)); + expect(camera.position.y).toBeLessThan(0); + } + expect([...sunSides].sort()).toEqual([-1, 1]); + }); + it('leaves the camera where the reader orbits it while the Sun stays on one side', async () => { await open('saturn', '2032-06-01T12:00Z'); const camera = engine.getCamera(); 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 2e1486c..98646d0 100644 --- a/src/app/features/body-detail/body-detail-scene.component.ts +++ b/src/app/features/body-detail/body-detail-scene.component.ts @@ -28,6 +28,16 @@ const GAS_GIANT_IDS = new Set(['jupiter', 'saturn', 'uranus', 'neptune']); const GLOW_SCALE = 2.6; /** Where the page's light stands, and the Sun with it wherever the body's real one is known. */ const SUN_LIGHT_POSITION = new THREE.Vector3(4, 3, 5); +/** + * How far from the equator the Sun must stand, as the sine of its latitude, before the camera + * follows it across: 3 degrees. The side is for Saturn's rings, lit on one face only, whose Sun + * goes 26.7 degrees either side. Mercury's never leaves the equator by more than 0.034 degrees + * and crosses it 8.3 times a year, which moved the camera from one side to the other every 1.45 + * seconds at a month a second, both sides lit alike; Venus's reaches 2.6 and the Moon's 1.6. + * Earth's and Saturn's pages still follow their seasons, a week and half a year after each + * equinox (2025-03-28 and 2039-08-03, measured). + */ +const SUN_SIDE_MIN_SINE = Math.sin((3 * Math.PI) / 180); /** * Separate, focused route for inspecting a single planet/moon/exoplanet: its own scene/camera @@ -302,13 +312,14 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { } else if (!this.body) { this.planet.rotation.y += deltaSeconds * 0.08; } - const sunSide = this.sunLight.position.y < 0 ? -1 : 1; + const sunLatitudeSine = this.sunLight.position.y / this.sunLight.position.length(); + const sunSide = this.sunSide !== 0 && Math.abs(sunLatitudeSine) < SUN_SIDE_MIN_SINE ? this.sunSide : sunLatitudeSine < 0 ? -1 : 1; if (sunSide !== this.sunSide) { // Above or below the equator, whichever side the Sun is on, when a body is shown and again - // whenever the Sun crosses it, as the clock runs or is set: held above it, the page opened - // Saturn on the unlit face of its rings from 2025 until 2039, while the Sun is south of them — - // the face Earth does not see either — and the Clock set to 2045 left it on the other one. - // Between crossings the camera is the reader's to orbit where they like. + // whenever the Sun is well across it (SUN_SIDE_MIN_SINE), as the clock runs or is set: held + // above it, the page opened Saturn on the unlit face of its rings from 2025 until 2039, while + // the Sun is south of them — the face Earth does not see either — and the Clock set to 2045 + // left it on the other one. Between crossings the camera is the reader's to orbit where they like. this.sunSide = sunSide; const camera = this.engine.getCamera(); camera.position.y = Math.abs(camera.position.y) * sunSide;