From d397aaa7e056a3ec026c2f278d7d6febb1dd6bf4 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Fri, 21 Aug 2026 15:08:40 +0200 Subject: [PATCH 1/2] Let the Solar System be kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Sun's catalogue id is 0, and the readout's keep control was shown by `@if (keepableStarId(); as starId)` — which reads zero as "there is no star here". Of the six hundred and thirty-four systems the map can be inside, the one nobody could keep was Sol. Checked against null now, with a test that keeps star zero, because this is the sort of thing that comes back. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi --- src/app/features/hud/hud-dock.component.spec.ts | 15 +++++++++++++++ src/app/features/hud/hud-dock.component.ts | 15 +++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/features/hud/hud-dock.component.spec.ts b/src/app/features/hud/hud-dock.component.spec.ts index 1e43963..8fdf075 100644 --- a/src/app/features/hud/hud-dock.component.spec.ts +++ b/src/app/features/hud/hud-dock.component.spec.ts @@ -210,6 +210,21 @@ describe('HudDockComponent', () => { expect(host().querySelector('[aria-label="Forget Sirius"]')?.getAttribute('aria-pressed')).toBe('true'); }); + it('keeps the one system whose catalogue id is zero, which truthiness would have lost', () => { + // The Sun is star 0. A `@if (id; as ...)` reads that as "no star" and hides the control. + const bookmarks = TestBed.inject(BookmarksStore); + setReadout(); + fixture.componentRef.setInput('title', 'Sol'); + fixture.componentRef.setInput('keepableStarId', 0); + fixture.componentRef.setInput('defaultTab', 'readout'); + fixture.detectChanges(); + + host().querySelector('[aria-label="Keep Sol"]')?.click(); + fixture.detectChanges(); + + expect(bookmarks.has('star', 0)).toBe(true); + }); + it('offers nothing to keep where the readout is a scale rather than a place', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'readout'); diff --git a/src/app/features/hud/hud-dock.component.ts b/src/app/features/hud/hud-dock.component.ts index 2cfe244..6358467 100644 --- a/src/app/features/hud/hud-dock.component.ts +++ b/src/app/features/hud/hud-dock.component.ts @@ -91,16 +91,19 @@ function isWideViewport(): boolean {

{{ eyebrow() }}

{{ title() }}

- @if (keepableStarId(); as starId) { + + @if (keepableStarId() !== null) { }
From b05324337cf495690effe9d35d8d11f61631e1eb Mon Sep 17 00:00:00 2001 From: Senrokai Date: Fri, 21 Aug 2026 15:13:24 +0200 Subject: [PATCH 2/2] Answer the review: the plan view was flat against the wrong plane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings, both right, and the second was the feature not doing what its own commit message said. The depth range stopped being updated under a plan view. It is worked out in perspective terms — near from the distance, far from eight times it — and the orthographic camera derives its own range from that one, so skipping the calculation left the far plane wherever it had been when the projection changed. Flying out to the whole Galaxy from a plan view clipped away most of it. The range is written to the perspective camera whichever one is live now, and the plan view goes on deriving from it every frame. And the galaxy-scale plan looked down the celestial pole. "The plane the current scale is read against" is this system's orbital plane inside a system, and the galactic plane outside one — but the fallback was the scene's own z, which is the Earth's rotation axis. The normal is the north galactic pole now, and up is the direction of the galactic centre, so a plan of the Galaxy is laid out the way the model that draws it is described. The arms are face-on. Verified: build clean, 596/596 unit, 16/16 end-to-end, and the Milky Way photographed flat from 28.3 kpc with nothing clipped. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi --- .../galaxy-system-scene.component.ts | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) 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 bb48552..dfe04be 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -4,7 +4,13 @@ import * as THREE from 'three/webgpu'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { dateToJulianDate } from '../../shared/astro/constants'; -import { galacticCentrePositionPc, galacticToEquatorial, MILKY_WAY_ARMS, SUN_GALACTOCENTRIC_RADIUS_PC } from '../../shared/astro/galaxy'; +import { + GALACTIC_BASIS_EQUATORIAL, + MILKY_WAY_ARMS, + SUN_GALACTOCENTRIC_RADIUS_PC, + galacticCentrePositionPc, + galacticToEquatorial +} from '../../shared/astro/galaxy'; import { DataLoaderService } from '../../core/data/data-loader.service'; import { EngineService, SceneCamera } from '../../core/engine/engine.service'; import { BodyRecord } from '../../shared/models/body.model'; @@ -630,7 +636,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { // from inside it, so it cannot also be the sky behind a view of the Galaxy from outside. this.engine.getScene().backgroundIntensity = display.sky ? 1 - this.galacticStrength : 0; - this.applyGalaxyDepthRange(camera, distancePc); + this.applyGalaxyDepthRange(distancePc); const level: ViewLevel = this.galacticStrength >= GALACTIC_LEVEL_THRESHOLD ? 'galactic' : 'galaxy'; if (this.navigationStore.viewLevel() !== level && !this.systemGroup.visible) { this.navigationStore.setViewLevel(level); @@ -657,19 +663,22 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { return halfHeight / Math.tan((this.engine.getPerspectiveCamera().fov * Math.PI) / 360); } - private applyGalaxyDepthRange(camera: SceneCamera, distancePc: number): void { - // The plan view sets its own depth range, symmetric about the camera; see `frameOrthographic`. - if (this.engine.currentProjection === 'orthographic') { - return; - } + private applyGalaxyDepthRange(distancePc: number): void { const near = THREE.MathUtils.clamp(distancePc / 2000, GALAXY_NEAR_PC, GALACTIC_NEAR_PC); const far = THREE.MathUtils.clamp(distancePc * 8, GALAXY_FAR_PC, GALACTIC_FAR_PC); + // Written to the perspective camera whichever one is live, because it is the one this range + // is reasoned in and the one `frameOrthographic` reads its own from. Skipping it under a plan + // view left the far plane wherever it was when the projection changed, so flying out to the + // Galaxy from there clipped away most of it. + const perspective = this.engine.getPerspectiveCamera(); // Only when it has drifted enough to matter, so a slow zoom isn't rebuilding the projection // matrix on every frame of it. - if (Math.abs(near - camera.near) > camera.near * 0.05 || Math.abs(far - camera.far) > camera.far * 0.05) { - camera.near = near; - camera.far = far; - camera.updateProjectionMatrix(); + if (Math.abs(near - perspective.near) > perspective.near * 0.05 || Math.abs(far - perspective.far) > perspective.far * 0.05) { + perspective.near = near; + perspective.far = far; + perspective.updateProjectionMatrix(); + // The plan view's own range is symmetric about the camera and derived from this one; see + // `frameOrthographic`. It is re-derived every frame, so there is nothing to do here. } } @@ -1074,9 +1083,22 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { if (plan) { // Straight down the plane's normal, from where the camera already was. - const normal = this.systemGroup.visible && this.systemRenderer ? new THREE.Vector3(0, 0, 1).applyQuaternion(this.systemRenderer.referenceFrame) : new THREE.Vector3(0, 0, 1); + // Down the normal of the plane this scale is actually read against. Inside a system that + // is the system's own orbital plane; outside it, the galactic plane — whose normal is the + // north galactic pole, not the celestial one. Defaulting to the scene's own z would have + // looked down the Earth's rotation axis and called it the plane of the Galaxy. + const galactic = GALACTIC_BASIS_EQUATORIAL; + const inSystem = this.systemGroup.visible && this.systemRenderer; + const normal = inSystem + ? new THREE.Vector3(0, 0, 1).applyQuaternion(this.systemRenderer!.referenceFrame) + : new THREE.Vector3(galactic.z.x, galactic.z.y, galactic.z.z); next.position.copy(target).add(normal.multiplyScalar(distance)); - next.up.set(0, 1, 0).applyQuaternion(this.systemGroup.visible && this.systemRenderer ? this.systemRenderer.referenceFrame : new THREE.Quaternion()); + if (inSystem) { + next.up.set(0, 1, 0).applyQuaternion(this.systemRenderer!.referenceFrame); + } else { + // Towards the galactic centre, so the plan is oriented the way the model is described. + next.up.set(galactic.x.x, galactic.x.y, galactic.x.z); + } } next.lookAt(target); this.controls.update();