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 7b4d6e7..9a23f43 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'; @@ -217,6 +217,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { private readonly leaderRef = viewChild.required>('leader'); private readonly objectCardRef = viewChild>(SystemObjectCardComponent, { read: ElementRef }); private readonly dockRef = viewChild>(HudDockComponent, { 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(); @@ -684,7 +690,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