From e853fe312e2efbffe4a547bb2ad019a078254955 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Fri, 21 Aug 2026 14:50:19 +0200 Subject: [PATCH] Answer the review: one reference viewport, one lookup for the card Two of the three comments were worth taking. The star field and the rings drawn over it each carried their own copy of the reference viewport and field of view the angular sizes are figured against. They agree today, and nothing would have told anyone when they stopped: a ring would just sit a little wide of its star at some window sizes. One module now holds the three constants and says what they are for. The leader line to the object card looked the card's panel up by selector on every frame it was drawn. The host element is stable and the panel inside it only changes when a different body is selected, so the lookup is derived once per change instead of sixty times a second. Left alone: replacing `positions.set([x, y, z], i * 3)` with an index-by-index loop to avoid a temporary array per host. It runs once, over six hundred and thirty-four stars, at bootstrap, and the version with the temporary reads better than the version without. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi --- .../features/galaxy-system/angular-size.ts | 19 +++++++++++++++++++ .../galaxy-system-scene.component.ts | 10 ++++++++-- .../features/galaxy-system/host-star-rings.ts | 5 +---- .../galaxy-system/star-field-renderer.ts | 11 +---------- 4 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 src/app/features/galaxy-system/angular-size.ts diff --git a/src/app/features/galaxy-system/angular-size.ts b/src/app/features/galaxy-system/angular-size.ts new file mode 100644 index 0000000..c10cfc6 --- /dev/null +++ b/src/app/features/galaxy-system/angular-size.ts @@ -0,0 +1,19 @@ +/** + * How a size in pixels becomes a size the scene can draw. + * + * Sprites in this view are sized angularly rather than in world units, so a star holds the same + * share of the screen however far away it is and whatever the window is doing. Pixels are what + * the figures are chosen in, though — "a star is between one and a half and six pixels across" + * is a statement someone can check by looking — so the two are related through a reference + * viewport and field of view, and the pixel figures are exact only at that height. + * + * Shared rather than restated per renderer: the star field and the rings drawn over it have to + * agree, or a ring sits a little wide of the star it belongs to at some window sizes and not at + * others. + */ + +export const REFERENCE_VIEWPORT_HEIGHT_PX = 900; +export const REFERENCE_FOV_DEGREES = 55; + +/** Multiply a size in reference pixels by this to get the angular size the material wants. */ +export const PIXELS_TO_ANGULAR_SIZE = (2 * Math.tan((REFERENCE_FOV_DEGREES * Math.PI) / 180 / 2)) / REFERENCE_VIEWPORT_HEIGHT_PX; 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 0f4b1df..7a33269 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, effect, ElementRef, OnDestroy, signal, viewChild } from '@angular/core'; +import { AfterViewInit, Component, computed, effect, ElementRef, OnDestroy, signal, viewChild } from '@angular/core'; import { Router } from '@angular/router'; import * as THREE from 'three/webgpu'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; @@ -195,6 +195,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { private readonly labelHostRef = viewChild.required>('labelHost'); private readonly leaderRef = viewChild.required>('leader'); private readonly objectCardRef = viewChild>(SystemObjectCardComponent, { read: ElementRef }); + /** + * The card's own box, looked up when the card changes rather than in the render loop that + * draws the leader to it. The host element is a stable wrapper; the panel inside it is what + * moves, and it is only replaced when a different body is selected. + */ + private readonly objectCardElement = computed(() => this.objectCardRef()?.nativeElement.querySelector('[data-testid="object-card"]') ?? null); private readonly raycaster = new THREE.Raycaster(); private readonly galaxyGroup = new THREE.Group(); @@ -645,7 +651,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { const world = member.marker.getWorldPosition(new THREE.Vector3()); this.labelOverlay?.setSelection(world); - const card = this.objectCardRef()?.nativeElement.querySelector('[data-testid="object-card"]'); + const card = this.objectCardElement(); const canvas = this.canvasRef().nativeElement; const projected = world.clone().project(camera); if (!card || projected.z > 1) { diff --git a/src/app/features/galaxy-system/host-star-rings.ts b/src/app/features/galaxy-system/host-star-rings.ts index 2b348ec..7469f17 100644 --- a/src/app/features/galaxy-system/host-star-rings.ts +++ b/src/app/features/galaxy-system/host-star-rings.ts @@ -2,14 +2,11 @@ import * as THREE from 'three/webgpu'; import { color, float, instancedBufferAttribute, smoothstep, uniform, uv, vec2 } from 'three/tsl'; import { StarRecord } from '../../shared/models/star.model'; +import { PIXELS_TO_ANGULAR_SIZE } from './angular-size'; /** Ring diameter in screen pixels at the reference viewport — angular, like the star points. */ const RING_SIZE_PX = 12; const RING_PEAK_OPACITY = 0.35; -/** Same reference as `StarFieldRenderer`, so a ring and its star agree on what a pixel is. */ -const REFERENCE_VIEWPORT_HEIGHT_PX = 900; -const REFERENCE_FOV_DEGREES = 55; -const PIXELS_TO_ANGULAR_SIZE = (2 * Math.tan((REFERENCE_FOV_DEGREES * Math.PI) / 180 / 2)) / REFERENCE_VIEWPORT_HEIGHT_PX; /** Ring radius and stroke half-width in quad-uv units (the quad runs 0..1, centre 0.5). */ const RING_RADIUS_UV = 0.42; const RING_STROKE_UV = 0.06; diff --git a/src/app/features/galaxy-system/star-field-renderer.ts b/src/app/features/galaxy-system/star-field-renderer.ts index ae20ea6..78491f4 100644 --- a/src/app/features/galaxy-system/star-field-renderer.ts +++ b/src/app/features/galaxy-system/star-field-renderer.ts @@ -2,22 +2,13 @@ import * as THREE from 'three/webgpu'; import { instancedBufferAttribute, smoothstep, uv, vec2 } from 'three/tsl'; import { spectralTypeToColorIndex } from '../../shared/astro/spectral'; +import { PIXELS_TO_ANGULAR_SIZE, REFERENCE_VIEWPORT_HEIGHT_PX } from './angular-size'; import { StarRecord } from '../../shared/models/star.model'; /** Apparent star diameters, in pixels at {@link REFERENCE_VIEWPORT_HEIGHT_PX}. */ const MIN_POINT_SIZE = 1.5; const MAX_POINT_SIZE = 6; -/** - * Star size is expressed in pixels for readability, but the material works in angular size, so - * the two are related through the scene's vertical field of view and a reference viewport. - * Because the size is angular, a star keeps the same share of the screen at any window size — - * these pixel figures are exact only at this reference height. - */ -const REFERENCE_VIEWPORT_HEIGHT_PX = 900; -const REFERENCE_FOV_DEGREES = 55; -const PIXELS_TO_ANGULAR_SIZE = - (2 * Math.tan((REFERENCE_FOV_DEGREES * Math.PI) / 180 / 2)) / REFERENCE_VIEWPORT_HEIGHT_PX; /** * Extra click forgiveness added to a star's drawn radius, in NDC — roughly 4 px on the