From d5a5a0fc3a75fd8af9d9ed1a741826a3e2c3c17e Mon Sep 17 00:00:00 2001 From: Senrokai Date: Wed, 30 Sep 2026 19:33:04 +0200 Subject: [PATCH] Move a body page's camera across the equator only once the Sun is 3 degrees past it a9e910c mirrored the camera whenever the Sun's height above the body's equator changed sign. The side is chosen for Saturn's rings, whose Sun goes 26.7 degrees either side, but the rule held for every body: Mercury's Sun, never more than 0.034 degrees off its equator, crosses it 8.3 times a year, so at a month a second the camera went from one side to the other every 1.45 seconds, mirroring wherever the reader had orbited it, with both sides lit alike. Venus's Sun reaches 2.6 degrees and the Moon's 1.6. The side is still chosen whenever a body is shown, but afterwards it changes only when the Sun is more than 3 degrees across (SUN_SIDE_MIN_SINE, on the sine of its latitude). Measured in the app on :4301, camera side changes while the clock ran at a month a second: - Mercury, 12 s from 2026-10-20: 0 (the Sun at most 0.0339 degrees off). - Venus, 12 s: 0 (2.64). The Moon, 15 s: 0 (1.58). - Earth, 12 s from 2025: 2, on 2025-03-28 and 2025-09-30, a week after each equinox. - Saturn, 20 s from 2038-06: 1, on 2039-08-03, half a year after its equinox (it followed at 2039-01-19 before). Reviewers had measured 7 or 8 changes in 10 to 12 s on Mercury's page, 3 on Venus's and 1 or 2 on the Moon's. Test: 'leaves the camera on its side while the Sun only grazes the equator: Mercury through two crossings' steps Mercury's page a day at a time from 2026-10-20 over 60 days, through the Sun's crossings about 1 November and 6 December, and expects the camera south throughout. Guarded mutants, each failing that test alone: the dead band removed (the sign alone chooses), and the dead band applied to a body just shown too (Mercury then opens with the camera on the side away from its Sun). The Saturn tests (2032 and past the 2039 equinox, set to 2045) pass unchanged. Unit suite 867 passed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../body-detail-scene.component.spec.ts | 26 ++++++++++++++++++- .../body-detail-scene.component.ts | 21 +++++++++++---- 2 files changed, 41 insertions(+), 6 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 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;