Merge the review fixes, and take Junie's on the hit radius with them

Carries the shared reference-viewport module and the cached card lookup up from
the branch they were reviewed on, and answers the one comment left against this
one.

The orthographic branch of the star field's hit test multiplied the angular size
by the frustum's half-height and then divided the result by that same
half-height. The two cancel: `setProjection` had already sized the sprite as
`angular * halfHeight / tan(REFERENCE_FOV/2)`, so dividing back out by the
half-height leaves the reference field of view and nothing else. Both
projections are one formula over a different angle now — which is also one
fewer division by a number that is zero if the frustum ever degenerates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-08-21 14:55:59 +02:00
co-authored by Claude Fable 5
4 changed files with 35 additions and 26 deletions
@@ -251,6 +251,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
private readonly leaderRef = viewChild.required<ElementRef<SVGLineElement>>('leader');
private readonly objectCardRef = viewChild<SystemObjectCardComponent, ElementRef<HTMLElement>>(SystemObjectCardComponent, { 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 galaxyGroup = new THREE.Group();
@@ -795,7 +801,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 || projected.z < -1) {