Open the map out to the whole Milky Way

The map stopped at the catalogued 50 pc around the Sun — 0.33% of the
Galaxy's width — and looked like a point cloud with a search box.

Adds the galactic scale above it and the heads-up display the reference
map is built from.

The Galaxy is not a third coordinate space. It is the same parsec space
four orders of magnitude further out, so the model and the star field
crossfade against camera distance instead of switching, and the Sun stays
where it really is: 8.18 kpc out, on the Orion Spur, between the
Sagittarius and Perseus arms. The depth range scales with that distance —
one fixed near/far pair cannot both fly into a star and hold the Galaxy.

The structure in shared/astro/galaxy.ts is measured: the directions of the
centre and the north galactic pole, which fix the disc's 63 degree tilt
against the celestial equator; the Sun's galactocentric distance; and a
radius, azimuth and pitch angle per arm. The particles scattered around it
are not, and cannot be — dust hides the disc, so no catalogue holds the
Galaxy's stars. The view says so, and the model fades out before the
camera reaches the 50 pc where the real stars are.

The rest is the look: polar grids lying in the galactic plane with drop
lines from the Sun's neighbours, a scale ladder, a readout panel, range,
reticle and frame brackets. Two things had to give way for it. The
deep-sky shell is the sky as seen from here, so it dissolves rather than
letting the camera fly through a wall of nebulae, and so does the skybox,
which is a photograph taken from inside the thing now being viewed from
outside. Labels are picked by screen separation rather than distance
alone: the Sun's fifteen nearest neighbours are all inside four parsecs
and printed as one unreadable clump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
Claude
2026-08-04 19:59:01 +00:00
parent 2f45fa7fef
commit 2e525fb5c3
19 changed files with 2166 additions and 46 deletions
+24 -1
View File
@@ -1,11 +1,34 @@
import { expect, test } from '@playwright/test';
import { backButtonLocator } from './support/wait-for-back-button';
test.describe('Galaxy view', () => {
test('boots the app, initializes the 3D scene, and starts in the galaxy overview (no system controls shown)', async ({ page }) => {
await page.goto('/');
await expect(page.getByTestId('scene-canvas')).toBeVisible();
await expect(page.getByPlaceholder('Search stars, planets, exoplanets…')).toBeVisible();
await expect(page.getByRole('button', { name: 'Galaxy' })).toHaveCount(0);
await expect(backButtonLocator(page)).toHaveCount(0);
await expect(page.getByText('Local Stars')).toBeVisible();
});
test('the scale ladder flies out to the whole Galaxy and back to the solar neighbourhood', async ({ page }) => {
// Two multi-second camera flights, either side of a software-rendered scene bootstrap, add
// up to more than the default per-test budget.
test.setTimeout(90_000);
await page.goto('/');
await expect(page.getByTestId('scene-canvas')).toBeVisible();
await page.getByRole('button', { name: 'Milky Way' }).click();
// The flight covers four orders of magnitude, and the readout only switches over once the
// camera is far enough out for the Galaxy model to have taken over from the star field.
await expect(page.getByText('Galactic Scale')).toBeVisible({ timeout: 15_000 });
await expect(page.getByText('Sagittarius A*')).toBeVisible();
// At the outermost scale there is nowhere further out to go.
await expect(page.getByRole('button', { name: 'Milky Way' })).toHaveCount(0);
await page.getByRole('button', { name: 'Solar Neighbourhood' }).click();
await expect(page.getByText('Local Stars')).toBeVisible({ timeout: 15_000 });
});
});
+19 -8
View File
@@ -1,21 +1,31 @@
import { expect, Locator, Page } from '@playwright/test';
/**
* The "Galaxy" back button is only rendered while `NavigationStore.viewLevel() === 'system'`,
* which becomes true only once a full galaxy-to-system camera-flight transition has settled.
* Bootstrap (data fetch + raycaster wiring) finishes asynchronously after `page.goto()`, so
* this repeatedly clicks the canvas (re-checking first, so it never clicks a system-view body
* marker by accident once the transition has already completed) until the button appears.
* The HUD's scale ladder marks the level the view is currently at, and offers the others as
* buttons. `NavigationStore.viewLevel()` becomes `'system'` only once a full galaxy-to-system
* camera-flight transition has settled, so that marker reading "System" is the signal that the
* transition is done.
*/
export function backButtonLocator(page: Page): Locator {
return page.getByRole('button', { name: 'Galaxy' });
export function currentLevelLocator(page: Page): Locator {
return page.getByTestId('hud-current-level');
}
/** The ladder entry that takes the view back out of a system, once there is one to leave. */
export function backButtonLocator(page: Page): Locator {
return page.getByRole('button', { name: 'Solar Neighbourhood' });
}
/**
* Bootstrap (data fetch + raycaster wiring) finishes asynchronously after `page.goto()`, so this
* repeatedly clicks the canvas — re-checking first, so it never clicks a system-view body marker
* by accident once the transition has already completed — until the view reports it is in a
* system.
*/
export async function clickCanvasUntilSystemEntered(page: Page, canvas: Locator, backButton: Locator): Promise<void> {
await expect
.poll(
async () => {
if (await backButton.isVisible()) {
if ((await currentLevelLocator(page).textContent())?.trim() === 'System') {
return true;
}
await canvas.click();
@@ -24,4 +34,5 @@ export async function clickCanvasUntilSystemEntered(page: Page, canvas: Locator,
{ timeout: 15_000, intervals: [300] }
)
.toBe(true);
await expect(backButton).toBeVisible();
}