Merge branch 'feat/hud-scene' into feat/hud-neighbours

# Conflicts:
#	src/app/features/galaxy-system/galaxy-system-scene.component.ts
This commit is contained in:
2026-08-21 14:51:12 +02:00
4 changed files with 29 additions and 16 deletions
@@ -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;
@@ -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 { Router } from '@angular/router';
import * as THREE from 'three/webgpu'; import * as THREE from 'three/webgpu';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
@@ -217,6 +217,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
private readonly leaderRef = viewChild.required<ElementRef<SVGLineElement>>('leader'); private readonly leaderRef = viewChild.required<ElementRef<SVGLineElement>>('leader');
private readonly objectCardRef = viewChild<SystemObjectCardComponent, ElementRef<HTMLElement>>(SystemObjectCardComponent, { read: ElementRef }); private readonly objectCardRef = viewChild<SystemObjectCardComponent, ElementRef<HTMLElement>>(SystemObjectCardComponent, { read: ElementRef });
private readonly dockRef = viewChild<HudDockComponent, ElementRef<HTMLElement>>(HudDockComponent, { read: ElementRef }); private readonly dockRef = viewChild<HudDockComponent, ElementRef<HTMLElement>>(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 raycaster = new THREE.Raycaster();
private readonly galaxyGroup = new THREE.Group(); private readonly galaxyGroup = new THREE.Group();
@@ -684,7 +690,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
const world = member.marker.getWorldPosition(new THREE.Vector3()); const world = member.marker.getWorldPosition(new THREE.Vector3());
this.labelOverlay?.setSelection(world); this.labelOverlay?.setSelection(world);
const card = this.objectCardRef()?.nativeElement.querySelector('[data-testid="object-card"]'); const card = this.objectCardElement();
const canvas = this.canvasRef().nativeElement; const canvas = this.canvasRef().nativeElement;
const projected = world.clone().project(camera); const projected = world.clone().project(camera);
if (!card || projected.z > 1) { if (!card || projected.z > 1) {
@@ -2,14 +2,11 @@ import * as THREE from 'three/webgpu';
import { color, float, instancedBufferAttribute, smoothstep, uniform, uv, vec2 } from 'three/tsl'; import { color, float, instancedBufferAttribute, smoothstep, uniform, uv, vec2 } from 'three/tsl';
import { StarRecord } from '../../shared/models/star.model'; 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. */ /** Ring diameter in screen pixels at the reference viewport — angular, like the star points. */
const RING_SIZE_PX = 12; const RING_SIZE_PX = 12;
const RING_PEAK_OPACITY = 0.35; 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). */ /** 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_RADIUS_UV = 0.42;
const RING_STROKE_UV = 0.06; const RING_STROKE_UV = 0.06;
@@ -2,22 +2,13 @@ import * as THREE from 'three/webgpu';
import { instancedBufferAttribute, smoothstep, uv, vec2 } from 'three/tsl'; import { instancedBufferAttribute, smoothstep, uv, vec2 } from 'three/tsl';
import { spectralTypeToColorIndex } from '../../shared/astro/spectral'; 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'; import { StarRecord } from '../../shared/models/star.model';
/** Apparent star diameters, in pixels at {@link REFERENCE_VIEWPORT_HEIGHT_PX}. */ /** Apparent star diameters, in pixels at {@link REFERENCE_VIEWPORT_HEIGHT_PX}. */
const MIN_POINT_SIZE = 1.5; const MIN_POINT_SIZE = 1.5;
const MAX_POINT_SIZE = 6; 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 * Extra click forgiveness added to a star's drawn radius, in NDC — roughly 4 px on the