From a9e910c2eac2b2dfa2d13ae41a6309cfd97345e7 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Wed, 30 Sep 2026 17:27:27 +0200 Subject: [PATCH] Keep a body page's camera on its Sun's side when the clock takes the Sun across the equator, aimed at the body in that frame The page put its camera on the side of the equator its Sun lights once, when a body was shown. The Clock tab the same page gained in 74535ac changes the date without showing the body again, so on Saturn's page, opened today with the Sun and the camera south of the rings, Go to 2045 moved the Sun 25.7 degrees north (sunLight y +3.063) and left the camera south (-0.6): the unlit face of the rings, the view the camera side was there to avoid. A clock left running through the May 2025 equinox did the same. tick now remembers which side the Sun stood on at the last frame, and moves the camera across only when that side changes: when a body is shown (the side is reset to 0 then) and when the Sun crosses the equator, as the clock runs or is set. Between crossings the camera is the reader's to orbit, below the rings if they like. The move came after the controls had aimed the camera for the frame, which was drawn straight after with the body 22.6 degrees (2 atan(0.6/3)) off the middle of the view; the camera now looks at the controls' target again before that frame. Live on :4301 (this worktree's ng serve), Saturn's page opened today: camera -0.6, Sun -0.935. Clock tab, 2045-06-01T00:00, Go: camera +0.6, Sun +3.063, the rings' lit face; 265 frames drawn across the move, every one aimed within 0.00 degrees of Saturn. The camera then put south by hand stayed there (-0.6 a second later). On Earth's page the Sun crosses twice a year, so at a month a second the camera changes side about every six seconds. Tests: the page followed into 2045 with the camera north and aimed at Saturn in that frame; and a camera the reader takes over the rings while the Sun stays south is left there, which the old single-flip rule only held through the flag it cleared and no test covered. Guarded mutants (full suite), each failing only its named test: the side chosen only when a body is shown ('follows the Sun across Saturn's equator'); the side never remembered, so the camera is mirrored back every frame ('leaves the camera where the reader orbits it'); no lookAt after the move ('follows the Sun across', on the aim). Unit suite 860/860, tsc -p tsconfig.app.json and etl:typecheck clean. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../body-detail-scene.component.spec.ts | 24 +++++++++++++++++ .../body-detail-scene.component.ts | 26 ++++++++++++------- 2 files changed, 41 insertions(+), 9 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 af961ad..de7694d 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 @@ -181,4 +181,28 @@ describe('BodyDetailSceneComponent', () => { expect(page.sunLight.position.y).toBeGreaterThan(0); expect(camera.position.y).toBeGreaterThan(0); }); + + it('follows the Sun across Saturn’s equator when the clock is set past the 2039 equinox, and aims at Saturn in that same frame', async () => { + await open('saturn', '2032-06-01T12:00Z'); + const camera = engine.getCamera(); + expect(camera.position.y).toBeLessThan(0); + // What the page's own Clock tab does: 2045, the Sun 25.7 degrees north of the rings. + time.setDate(new Date('2045-06-01T12:00Z')); + engine.tick(0.016); + expect(page.sunLight.position.y).toBeGreaterThan(0); + expect(camera.position.y).toBeGreaterThan(0); + // The frame drawn straight after the move: aimed from where the camera was, it had Saturn 22.6 + // degrees off the middle of the view. + const toSaturn = new THREE.Vector3().sub(camera.position); + expect(camera.getWorldDirection(new THREE.Vector3()).angleTo(toSaturn)).toBeLessThan(1e-9); + }); + + 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(); + // Taken over the rings, to their unlit face, on purpose. + camera.position.y = 0.6; + engine.tick(0.016); + expect(camera.position.y).toBeGreaterThan(0); + }); }); 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 f8828db..100c3e1 100644 --- a/src/app/features/body-detail/body-detail-scene.component.ts +++ b/src/app/features/body-detail/body-detail-scene.component.ts @@ -104,8 +104,8 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { readonly notFound = signal(false); /** The date the body is drawn for, as the dock's strip prints it; empty at the present. */ readonly date = signal(''); - /** Set when a body is shown, until the next frame has put the camera on its Sun's side. */ - private cameraToSunSide = false; + /** The side of the equator the Sun stood on at the last frame, 1 north or -1 south; 0 once a body is shown. */ + private sunSide = 0; constructor( private readonly engine: EngineService, @@ -193,7 +193,7 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { this.body = this.bodies.find((body) => body.id === viewModel.id); this.planet?.rotation.set(0, 0, 0); this.sunLight?.position.copy(SUN_LIGHT_POSITION); - this.cameraToSunSide = true; + this.sunSide = 0; this.disposeRing(); this.disposeGlow(); @@ -295,13 +295,21 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { } else { this.planet.rotation.y += deltaSeconds * 0.08; } - if (this.cameraToSunSide) { - // Above or below the equator, whichever side the Sun is on. 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. + const sunSide = this.sunLight.position.y < 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. + this.sunSide = sunSide; const camera = this.engine.getCamera(); - camera.position.y = Math.abs(camera.position.y) * (this.sunLight.position.y < 0 ? -1 : 1); - this.cameraToSunSide = false; + camera.position.y = Math.abs(camera.position.y) * sunSide; + // Aimed again before this frame is drawn: the controls aimed it from where it was, and the + // frame drawn from here otherwise had the body 22.6 degrees off the middle of the view. + if (this.controls) { + camera.lookAt(this.controls.target); + } } }