Point at what is selected: arcs, a leader to the card, rings on the systems
Four things the scene did not yet say, all about where to look. Selection. Hovering or pinning a body raised its card in the corner, but nothing in the scene said which point the card was about. Two hairline arcs now bracket the body — the one mark borrowed from the ARK's control disc — and a leader runs from their rim to the card's near edge, in screen space, once per frame, because the body moves and the card's height depends on its content. The selected body's own label swaps to the left of its point, since the leader leaves the right and would otherwise cross the text. Labels choose a side. Right by default; left when the text would run off the right of the view, or into the reach of a label already placed to the right, and never left when that would run off the left. The overlay hangs the label's near edge on the point either way, so the hairline still meets the star. Rings on the systems. A faint accent ring on every star known to host planets — the one binary fact about a point of light worth reading at a glance from the neighbourhood, since it is the one thing that says "there is somewhere to go here". Drawn the way the star field draws stars, as unattenuated instanced sprites with the ring a band of the quad's own uv, so they sit on the field's points at any zoom; a first cut as three.js Points rendered nothing at all under the WebGPU renderer. 634 of them are a lot at the overview, so they are faint, small, fade with the local layer, and have their own toggle — Systems — in the dock's Display tab. Verified: build clean, 537/537 unit, 6/6 end-to-end, design detector clean, screenshots at the overview, at 90 pc, and in Sol and Proxima with a body hovered and pinned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import * as THREE from 'three/webgpu';
|
||||
import { color, float, instancedBufferAttribute, smoothstep, uniform, uv, vec2 } from 'three/tsl';
|
||||
|
||||
import { StarRecord } from '../../shared/models/star.model';
|
||||
|
||||
/** 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;
|
||||
|
||||
/** A unit quad centred on the origin — the billboard every ring instance is drawn on. */
|
||||
function createQuadGeometry(instanceCount: number): THREE.InstancedBufferGeometry {
|
||||
const geometry = new THREE.InstancedBufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]), 3));
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), 2));
|
||||
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
||||
geometry.instanceCount = instanceCount;
|
||||
return geometry;
|
||||
}
|
||||
|
||||
/**
|
||||
* A faint accent ring around every star known to host planets: the one binary fact about a
|
||||
* point of light worth reading at a glance from the neighbourhood view, since it is the one
|
||||
* thing that says "there is somewhere to go here".
|
||||
*
|
||||
* Drawn the way the star field draws its stars — instanced unattenuated sprites — so the rings
|
||||
* sit exactly on the field's own points at any zoom and window size. The ring itself is a band
|
||||
* of the quad's uv distance from centre, not a texture, so it stays a hairline at any scale.
|
||||
*/
|
||||
export class HostStarRings {
|
||||
readonly object: THREE.Mesh;
|
||||
readonly count: number;
|
||||
|
||||
private readonly geometry: THREE.InstancedBufferGeometry;
|
||||
private readonly material: THREE.SpriteNodeMaterial;
|
||||
private readonly opacity = uniform(RING_PEAK_OPACITY);
|
||||
|
||||
constructor(hosts: readonly StarRecord[], accent: number) {
|
||||
const positions = new Float32Array(hosts.length * 3);
|
||||
hosts.forEach((star, i) => positions.set([star.x, star.y, star.z], i * 3));
|
||||
this.geometry = createQuadGeometry(hosts.length);
|
||||
|
||||
this.material = new THREE.SpriteNodeMaterial({ transparent: true, depthWrite: false });
|
||||
this.material.sizeAttenuation = false;
|
||||
this.material.positionNode = instancedBufferAttribute(new THREE.InstancedBufferAttribute(positions, 3), 'vec3');
|
||||
this.material.scaleNode = float(RING_SIZE_PX * PIXELS_TO_ANGULAR_SIZE);
|
||||
this.material.colorNode = color(accent);
|
||||
// Opaque on the ring's centreline, falling to nothing one stroke-width either side.
|
||||
const distanceFromRing = uv().sub(vec2(0.5)).length().sub(RING_RADIUS_UV).abs();
|
||||
this.material.opacityNode = smoothstep(RING_STROKE_UV, 0.0, distanceFromRing).mul(this.opacity);
|
||||
|
||||
this.object = new THREE.Mesh(this.geometry, this.material);
|
||||
this.object.name = 'host-star-rings';
|
||||
// As for the star field: the quad's bounds say nothing about where the instances are.
|
||||
this.object.frustumCulled = false;
|
||||
this.count = hosts.length;
|
||||
}
|
||||
|
||||
/** Crossfaded with the local grid: from outside the Galaxy the rings are noise. */
|
||||
setStrength(strength: number): void {
|
||||
const clamped = THREE.MathUtils.clamp(strength, 0, 1);
|
||||
this.opacity.value = RING_PEAK_OPACITY * clamped;
|
||||
this.object.visible = clamped > 0;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.geometry.dispose();
|
||||
this.material.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user