Rebuild the map chrome against the Star Citizen starmap
Until now this was built from memory — the reference site is blocked by this environment's egress policy, so the resemblance was asserted rather than checked. Five screenshots of the real thing arrived, and this is what comparing against them changed. Labels say what a thing is, not just what it is called. Every label is now two lines: the name, then its type in smaller, wider-tracked, dimmer capitals. This is the single most characteristic element of the reference and it appears in every frame of it. It also settles a real ambiguity — in a map that mixes scales, "Orion" is an arm, a nebula and a constellation, and nothing about a bare name said which one a label pointed at. For stars the type line distinguishes "System" from "Star", which is the one thing it can say that the map could not otherwise show: which points are somewhere you can actually go. The system view had no body labels at all, where the reference labels every planet. It does now, which turned out to need two supporting changes. The overlay had only ever added and removed labels, never moved them, because stars do not move; planets do, so an existing label is now repositioned rather than left where the body used to be. And the inner four planets printed on top of each other in exactly the clump the star labels were already spread to avoid — so that logic is now shared rather than duplicated, with system bodies ordered outermost-first. Closing in reverses it by itself: the outer orbits leave the frame, their labels drop, and the inner planets take the space. The chrome follows the reference's layout. The scale ladder is a row of chamfered tabs at the top left rather than a vertical list of diamonds at the middle left, and a nameplate across the top centre says what the view is holding. The centre reticle is a hexagon, which is how the reference locks onto a body, and stays distinct from the rectangular panel chrome. Not copied: the ARK/RSI logos, wordmarks, and the bottom-right tool tabs. The first two are someone else's brand, and the third would be four tabs opening features this app does not have. Two e2e assertions moved off bare text matches onto the readout panel's own title. The nameplate names the same thing the panel does, so "is 'Local Stars' on screen" became ambiguous — the assertion, not the design, was what had to give. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import * as THREE from 'three/webgpu';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { StarLabelOverlay } from './star-label-overlay';
|
||||
|
||||
describe('StarLabelOverlay', () => {
|
||||
let scene: THREE.Scene;
|
||||
let overlay: StarLabelOverlay;
|
||||
let camera: THREE.PerspectiveCamera;
|
||||
|
||||
/**
|
||||
* Every label element currently in the layer, in DOM order.
|
||||
*
|
||||
* `CSS2DRenderer` only attaches an element to its layer when it projects it, so the labels do
|
||||
* not exist in the DOM until something has been rendered — which is why this draws a frame
|
||||
* rather than reading straight off `domElement`.
|
||||
*/
|
||||
function labels(): HTMLElement[] {
|
||||
overlay.render(camera);
|
||||
return [...overlay.domElement.querySelectorAll<HTMLElement>('.map-label')];
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
scene = new THREE.Scene();
|
||||
overlay = new StarLabelOverlay(scene);
|
||||
overlay.setSize(800, 600);
|
||||
camera = new THREE.PerspectiveCamera(50, 800 / 600, 0.1, 1000);
|
||||
camera.position.set(0, 0, 20);
|
||||
camera.updateMatrixWorld();
|
||||
});
|
||||
|
||||
it('prints what a thing is under what it is called', () => {
|
||||
overlay.update([{ id: 1, name: 'Sirius', kind: 'System', x: 1, y: 2, z: 3 }]);
|
||||
|
||||
const label = labels()[0];
|
||||
expect(label.querySelector('.map-label-name')?.textContent).toBe('Sirius');
|
||||
expect(label.querySelector('.map-label-kind')?.textContent).toBe('System');
|
||||
});
|
||||
|
||||
it('prints the name alone when there is no type to give', () => {
|
||||
// The second line is optional, and an empty one would still cost its line height — which
|
||||
// over a screen of labels shifts every name off the point it is anchored to.
|
||||
overlay.update([{ id: 1, name: 'Sirius', x: 1, y: 2, z: 3 }]);
|
||||
|
||||
expect(labels()[0].querySelector('.map-label-name')?.textContent).toBe('Sirius');
|
||||
expect(labels()[0].querySelector('.map-label-kind')).toBeNull();
|
||||
});
|
||||
|
||||
it('places the label at the point it belongs to', () => {
|
||||
overlay.update([{ id: 7, name: 'Vega', kind: 'Star', x: 4, y: -5, z: 6 }]);
|
||||
|
||||
const object = scene.children.find((child) => child.type === 'Object3D');
|
||||
expect(object?.position.toArray()).toEqual([4, -5, 6]);
|
||||
});
|
||||
|
||||
it('touches the DOM only for labels that actually changed', () => {
|
||||
overlay.update([
|
||||
{ id: 1, name: 'Sirius', kind: 'Star', x: 1, y: 0, z: 0 },
|
||||
{ id: 2, name: 'Vega', kind: 'Star', x: 0, y: 1, z: 0 }
|
||||
]);
|
||||
const sirius = labels()[0];
|
||||
|
||||
overlay.update([
|
||||
{ id: 1, name: 'Sirius', kind: 'Star', x: 1, y: 0, z: 0 },
|
||||
{ id: 3, name: 'Altair', kind: 'Star', x: 0, y: 0, z: 1 }
|
||||
]);
|
||||
|
||||
// Same element instance: the label that stayed was not torn down and rebuilt.
|
||||
expect(labels()[0]).toBe(sirius);
|
||||
expect(labels().map((label) => label.querySelector('.map-label-name')?.textContent)).toEqual(['Sirius', 'Altair']);
|
||||
});
|
||||
|
||||
it('drops every label when given none', () => {
|
||||
overlay.update([{ id: 1, name: 'Sirius', kind: 'Star', x: 1, y: 0, z: 0 }]);
|
||||
overlay.update([]);
|
||||
|
||||
expect(labels()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('leaves nothing behind when disposed', () => {
|
||||
overlay.update([
|
||||
{ id: 1, name: 'Sirius', kind: 'Star', x: 1, y: 0, z: 0 },
|
||||
{ id: 'ngc:224', name: 'Andromeda', kind: 'GALAXY', x: 0, y: 1, z: 0 }
|
||||
]);
|
||||
overlay.dispose();
|
||||
|
||||
expect(labels()).toHaveLength(0);
|
||||
expect(scene.children).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user