A system view could say everything about the star it was inside and nothing about where that star was. The four nearest catalogue stars are now named around the edge of it, each with its distance, each a button that flies there — so a chain of neighbours can be walked without pulling back out to the field between hops. These are bearings, not sky positions, and that is the one deliberate compromise here. A true direction was tried first and does not work: at this field of view the visible cone is about 30 degrees, so on average one neighbour in fifteen falls inside the frame — measured, not guessed, at one label of four in Sol and none at all after a small orbit. What survives the ring is the half of the direction a viewer can act on, which way to turn to face it, and the ring reads as instrument rather than as scene because it sits at a fixed radius. Real distance was never an option: Proxima is 268 000 AU from Sol, thirteen far planes out, so the distance goes on the type line. Proximity is answered by a new pure module rather than by a scan. A uniform grid over the catalogue answers both "the k nearest to this star" and "every star within n parsecs", the second being what the jump-link graph in the next PR is built from — one scan per node, and the quadratic would show. Its spec pins the grid against a brute-force sweep of a pseudo-random cloud, because a spatial index is an optimisation and never a different answer. Where the ring meets the HUD, the HUD wins: placement is given the boxes the readout, the strip and the object card occupy, and slides a name along the ring until it clears them, or drops it rather than print it half hidden. That rule is a pure function with its own spec. Four defects found while verifying this, three of them older than it: The dock's flex column was pointer-events-auto and as wide as its strip, so an invisible band above the strip swallowed every click in it — including, but not only, a neighbour's. The column is transparent now and each surface opts back in. The ring was sized against the frame's height alone, which on a phone held upright put it a viewport and a half wide: no neighbour was reachable on any portrait screen. It is sized against the shorter side. Picking a search result reopened the readout, which on a narrow viewport is a sheet over most of the scene — reopening it onto whatever was just flown to. Below sm it now folds away. A selectable label's two lines are adjacent spans, so it announced as "Sirius2.64 pc"; it carries an explicit label saying what it does. Verified: build clean, 558/558 unit, 7/7 end-to-end including a new spec that flies Sol to Barnard's Star by its label, design detector clean, screenshots at 1440x900 and 390x844 in Sol and Proxima Centauri, and the keyboard path walked: both names are in the tab order, focusable, with the accent ring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
223 lines
9.8 KiB
Markdown
223 lines
9.8 KiB
Markdown
---
|
|
name: run-star-map
|
|
description: Build, launch, run, drive, and screenshot the star-map Angular app in this container. Use when asked to run or start the app, take a screenshot of the map or HUD, check a UI change in a real browser, or run the unit/e2e test suites.
|
|
---
|
|
|
|
# Running star-map
|
|
|
|
An Angular 3D star map (three.js, WebGPU renderer falling back to WebGL2). There is no GPU here,
|
|
so the scene is software-rasterized and slow — but it does run, and it screenshots.
|
|
|
|
Drive it with the committed driver, which owns the dev server, the browser, and the camera flights:
|
|
|
|
```
|
|
.claude/skills/run-star-map/driver.mjs
|
|
```
|
|
|
|
All paths below are relative to the repo root.
|
|
|
|
## Prerequisites
|
|
|
|
Two container quirks have to be handled first. **Both are one-liners, and both are silent
|
|
footguns if skipped** — see Gotchas for why.
|
|
|
|
**1. Node.** The default `node` on `PATH` is *below* the floor the Angular CLI enforces. A
|
|
satisfying build is at `/opt/node`. The driver picks it automatically; anything you run by hand
|
|
needs the prefix:
|
|
|
|
```bash
|
|
node -v # v22.22.2 ← too old, ng refuses to start
|
|
/opt/node/bin/node -v # v22.23.2 ← use this
|
|
export PATH=/opt/node/bin:$PATH
|
|
```
|
|
|
|
**2. Playwright browser.** The pinned `@playwright/test` (1.62.1) wants Chromium revision 1234;
|
|
this container ships 1194, under a different internal layout. Do **not** run
|
|
`npx playwright install` (the environment blocks browser downloads). Shim the expected path
|
|
instead — needed only for `npm run e2e`, not for the driver:
|
|
|
|
```bash
|
|
mkdir -p /opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64
|
|
ln -sfn /opt/pw-browsers/chromium_headless_shell-1194/chrome-linux/headless_shell \
|
|
/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell
|
|
touch /opt/pw-browsers/chromium_headless_shell-1234/INSTALLATION_COMPLETE
|
|
```
|
|
|
|
## Install
|
|
|
|
```bash
|
|
PATH=/opt/node/bin:$PATH npm ci # ~25s
|
|
```
|
|
|
|
## Run (agent path) — the driver
|
|
|
|
Takes screenshots of the app at each scale. Starts the dev server on port 4300 if one isn't
|
|
already up, and stops it on exit.
|
|
|
|
```bash
|
|
node .claude/skills/run-star-map/driver.mjs tour
|
|
```
|
|
|
|
```
|
|
starting dev server on http://localhost:4300 (node v22.23.2)
|
|
dev server ready
|
|
field:
|
|
/tmp/star-map-shots/1-star-field.png (level=Solar Neighbourhood)
|
|
galaxy:
|
|
/tmp/star-map-shots/2-galactic.png (level=Milky Way)
|
|
system:
|
|
/tmp/star-map-shots/3-system-sol.png (level=System)
|
|
inner:
|
|
zoomed to 5.53 AU
|
|
/tmp/star-map-shots/4-system-inner.png (level=System)
|
|
```
|
|
|
|
`tour` takes **~3m20s** — most of it camera flights and software rasterization. For one view:
|
|
|
|
```bash
|
|
node .claude/skills/run-star-map/driver.mjs shot galaxy # ~1m
|
|
node .claude/skills/run-star-map/driver.mjs shot field system
|
|
```
|
|
|
|
| View | What it shows |
|
|
|---|---|
|
|
| `field` | Solar-neighbourhood star field — the default landing view |
|
|
| `galaxy` | Galactic scale: arms, bar, Sol's position |
|
|
| `system` | Sol at ~120 AU, outer planets labelled |
|
|
| `inner` | Sol zoomed to <6 AU, inner four planets labelled |
|
|
|
|
**Look at the PNG you produced.** A black frame means the scene never initialized — that is a
|
|
failure, not a dark theme.
|
|
|
|
Flags: `--out=DIR` (default `/tmp/star-map-shots`), `--stars=N` (default 12000), `--port=N`
|
|
(default 4300), `--keep-server`, `--headed`.
|
|
|
|
### Checking HUD state without screenshots
|
|
|
|
Faster than eyeballing a picture, and the right tool for label/HUD changes:
|
|
|
|
```bash
|
|
node .claude/skills/run-star-map/driver.mjs probe inner
|
|
```
|
|
|
|
```json
|
|
{
|
|
"title": "Sol",
|
|
"level": "System",
|
|
"labelCount": 4,
|
|
"labels": [
|
|
{ "name": "Mars", "kind": "Planet" },
|
|
{ "name": "Earth", "kind": "Planet" },
|
|
{ "name": "Mercury", "kind": "Planet" },
|
|
{ "name": "Venus", "kind": "Planet" }
|
|
],
|
|
"neighbours": [
|
|
{ "name": "Proxima Centauri", "distance": "1.30 pc" },
|
|
{ "name": "Barnard's Star", "distance": "1.82 pc" }
|
|
]
|
|
}
|
|
```
|
|
|
|
`probe` accepts the same four view names. Labels are read from the CSS2D layer
|
|
(`.map-label` > `.map-label-name` + `.map-label-kind`). `neighbours` is the ring of nearby
|
|
stars named from inside a system (`.map-label--ghost`); it is absent where there are none.
|
|
|
|
## Run (human path)
|
|
|
|
```bash
|
|
PATH=/opt/node/bin:$PATH npm start -- --port=4300
|
|
```
|
|
|
|
Serves on <http://localhost:4300>. Useless headless on its own — there is no display to look at.
|
|
Use it only when you want a long-lived server for the driver to attach to (the driver reuses an
|
|
already-running server and leaves it alone on exit).
|
|
|
|
## Test
|
|
|
|
```bash
|
|
PATH=/opt/node/bin:$PATH npm test # 527 tests, 31 files, ~6s
|
|
PATH=/opt/node/bin:$PATH npm run build # ~12s
|
|
PATH=/opt/node/bin:$PATH npm run etl:typecheck
|
|
PATH=/opt/node/bin:$PATH npm run e2e:typecheck
|
|
PATH=/opt/node/bin:$PATH npm run e2e -- --workers=1 # 6 tests, 3 files, ~2.3m
|
|
```
|
|
|
|
`--workers=1` on the e2e suite is **not optional here** — see Gotchas.
|
|
|
|
## Build for GitHub Pages
|
|
|
|
`.github/workflows/pages.yml` publishes the app on a push to `main`. To reproduce what it builds:
|
|
|
|
```bash
|
|
export PATH=/opt/node/bin:$PATH
|
|
npm run build -- --base-href "/star-map/"
|
|
cp dist/star-map/browser/index.html dist/star-map/browser/404.html
|
|
```
|
|
|
|
Two things differ from a plain `npm run build`, and both exist because a project site is served
|
|
from a subdirectory rather than a domain root:
|
|
|
|
- **`--base-href`.** `DataLoaderService` fetches its catalogues with relative URLs
|
|
(`assets/data/stars.bin`), which resolve against `<base>` rather than the current path. Get it
|
|
wrong and a deep link asks for `/body/assets/data/stars.bin`. The workflow derives it from the
|
|
repository name so a rename cannot strand it.
|
|
- **`404.html`.** Pages has no rewrite rules, so `/body/mars` has no file behind it. Serving the
|
|
app as the 404 body lets the router render the route. The response stays HTTP 404.
|
|
|
|
The uploaded artifact is `dist/star-map/browser`, **not** `dist/star-map` — the parent also holds
|
|
`3rdpartylicenses.txt` and `prerendered-routes.json`, which are not part of the site.
|
|
|
|
## Gotchas
|
|
|
|
- **The wrong Node is first on `PATH`.** `/opt/node22` is v22.22.2; `/opt/node` is v22.23.2.
|
|
`PATH` finds the *older* one, and `package.json#engines` requires `^22.22.3`. `ng serve` then
|
|
dies with "Node.js version v22.22.2 detected" and nothing else. The names invert what you'd
|
|
guess — `/opt/node` is newer than `/opt/node22`.
|
|
- **`npm run e2e` fails at full parallelism**, passing 4 of 6. The two camera-flight specs time
|
|
out when four browsers share a software rasterizer. The same specs pass alone and pass with
|
|
`--workers=1`. It is CPU contention, not a broken test — don't "fix" the specs.
|
|
- **Never stop the dev server with `pkill -f`.** It matches against every process's full command
|
|
line — including the shell running your own `pkill`, which then kills itself (exit 144). Narrowing
|
|
the pattern does not save you, and neither does the `[n]g` bracket trick: the match is against the
|
|
*whole* command line, so a literal `ng serve` anywhere else in it — inside an `echo`, a comment, a
|
|
later `pgrep` — re-arms the self-match. Kill by port instead, which does no pattern matching at all:
|
|
|
|
```bash
|
|
fuser -k 4300/tcp
|
|
```
|
|
|
|
Or just let the driver clean up after itself, which it does unless you pass `--keep-server`.
|
|
- **The star count must be cut for anything interactive.** The full catalogue is 68 388 stars and
|
|
runs at roughly 1 fps here. `?stars=N` overrides it; the driver defaults to 12 000.
|
|
- **One canvas click is not enough to enter a system.** Bootstrap (data fetch + raycaster wiring)
|
|
finishes asynchronously after load, so an early click hits nothing. The driver polls
|
|
click-until-`hud-current-level`-reads-`System`. Sol is at the origin, dead centre of the default
|
|
camera, so the centre click is what reliably works.
|
|
- **Screenshots need a settle delay.** A DOM assertion passing does not mean the frame is drawn;
|
|
the driver waits 2.5s before every capture. Without it you get half-rendered scenes.
|
|
- **At 120 AU the inner planets have no labels.** They are inside the centre reticle, collapsed to
|
|
a few pixels, and correctly suppressed. This is not a regression — zoom below ~6 AU (the `inner`
|
|
view) to see Mercury/Venus/Earth/Mars labelled.
|
|
- **Port 4300, not Angular's usual 4200** — the project's own convention, set in
|
|
`playwright.config.ts` so it never collides with an unrelated `ng serve`.
|
|
- **A Pages build looks broken if you serve it at the root.** `<base href="/star-map/">` makes
|
|
every asset resolve under that prefix, so opening `dist/star-map/browser` at `/` gets you a
|
|
blank page and a wall of 404s. Serve it under the same prefix as Pages does, with unknown paths
|
|
falling back to `404.html`, or the check tells you nothing.
|
|
- **Don't write driver scripts in `.ts` outside the repo.** `tsx` compiles a bare `.ts` as CJS
|
|
("Top-level await is currently not supported with the cjs output format"), and a script outside
|
|
the repo can't resolve `@playwright/test` at all. The driver is `.mjs` inside the repo for both
|
|
reasons.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Fix |
|
|
|---|---|
|
|
| `Node.js version v22.22.2 detected. The Angular CLI requires...` | `export PATH=/opt/node/bin:$PATH` |
|
|
| `browserType.launch: Executable doesn't exist at .../chromium_headless_shell-1234/...` | Apply the browser shim in Prerequisites. Do **not** run `npx playwright install`. |
|
|
| e2e: 2 specs time out on camera flights | Add `-- --workers=1` |
|
|
| Your shell exits with code 144 while cleaning up | A `pkill -f` matched your own command line. Use `fuser -k 4300/tcp`. |
|
|
| Screenshot is entirely black | Scene never initialized — check the driver's `[pageerror]` lines. |
|
|
| `Top-level await is currently not supported with the "cjs" output format` | Name the script `.mjs` (or `.mts`) and keep it inside the repo. |
|
|
| Driver hangs at "starting dev server" | Another server holds port 4300: `fuser -k 4300/tcp`, or pass `--port=4301`. |
|