f6bb2b95847e5b07bd709026716116b1e684a32b
199
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a6b30e84f0 |
Answer the review: a gradient over the names, a zero-distance neighbour, two lost docblocks
Three findings out of forty-one raised survived being argued against, and all three were real. The vignette was painting over the label layer. It lived in the HUD component, which sits after the label host in the same stack with neither carrying a z-index, so paint order was tree order and a decorative gradient was laid over the names — worst at the edge of the frame, which is precisely where the neighbour ring is. Composited, the ghost's distance line fell to 4.02:1, under the floor the CSS next to it claims. The gradient is scene chrome rather than HUD chrome, so it moves down between the canvas and the labels; the authored contrast then holds as written, and every label near the edge — planets included — is read against the sky rather than through a wash of void. A binary companion printed "0.00 pc". A catalogue holds a close pair as two rows at one position, so the nearest neighbour to one of them is the other, zero away — the exact string the readout deliberately suppresses for a star's distance from itself. The query now asks wide and drops any separation that prints as no separation, compared through the formatter rather than against a hand-picked epsilon so the rule survives the formatter changing. And the edit that added all this had been spliced between updateSystemLabels' docblock and its body, leaving a paragraph about labelling planets sitting over the neighbour resolution and the method it described with nothing, plus two divergent copies of the same fourteen lines seventy-five apart. Both are back where they belong. Verified: build clean, 558/558 unit, 7/7 end-to-end, design detector clean, screenshots re-read in Sol and Proxima Centauri. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
44c6a1f15e |
Name the neighbours, from inside the system
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 |
||
|
|
7591bcc0ea |
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 |
||
|
|
8b507427d8 |
Answer the review: sky toggles in system view too, one media query, idempotent openSearch
Three findings from the automated review, all confirmed before acting. The Sky toggle did nothing in system view. Its write lived only in the galaxy crossfade, which the tick parks while the system group is up — and the sky is still on screen there. It is now also written on toggle, in applyDisplay. The suggested form of that fix crashed the scene on mount: the effect that calls applyDisplay fires once at construction, before the engine has a scene, and getScene() throws — the dock rendered no tabs at all. Guarded on engine.isInitialized; verified with a screenshot of Proxima with the sky off. isWideViewport built a fresh MediaQueryList on every document pointer-down; one module-level query is read instead. And openSearch in the e2e support clicked the Search tab unconditionally, which would fold it closed if a test ever called it while already open; it now checks aria-selected first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
4a1cc5240a |
Dock the HUD: every tool and readout on one rail along the bottom
The overlay had grown by accretion: a search box floating top-centre, a readout panel bottom-left, a range readout bottom-right, and nothing that said these were parts of one instrument. This puts them on one rail across the bottom of the viewport — the dock — with a tab strip pinned to the bottom edge and whichever panel is open growing upward from it. The top of the screen keeps only the scale ladder and the nameplate, so the map itself is what fills the frame. Three tabs. SEARCH is the old search, with its field pinned to the bottom of the panel and the results growing upward above it, so the thing being typed into never moves while the list grows. READOUT is the old bottom-left panel. DISPLAY is new: five layer toggles — labels, orbits, grid, deep sky, sky — each a real scene object switched by visibility, except the ones the galaxy crossfade already rewrites every frame, whose toggles fold into that crossfade instead of fighting it. The range readout sits on the strip itself, so it is readable whatever is open. Behaviour worth stating: choosing a search result hands the panel straight back to the readout, since the thing to look at is now the scene. `/` opens the search from anywhere. Below `sm` the dock is the strip alone; a tap opens a panel as a sheet, a tap on the scene folds it away. The body-detail page gets the same dock with only the search — the info panel is its reading. Two things found on the way. CSS2DRenderer gives every label its own z-index for depth order, and the label host created no stacking context, so labels painted over every HUD panel; `isolate` on the host keeps them under. And starmap-hud's readout tests were really tests of the panel that moved, so they moved with it. Verified: build clean, 535/535 unit, 6/6 end-to-end, design detector clean, screenshots at 1440×900 and 390×844 across galaxy, galactic, system, body-detail, all three tabs and the layers-off state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
9ae87e83fb |
Restyle the HUD as an observatory instrument (#4)
Restyle the HUD as an observatory instrument |
||
|
|
650b3b37a0 |
Fix what the review confirmed: anchoring, overlap, honesty, one voice
Fourteen findings from an adversarial review pass, applied together because most share two roots — geometry written against assumptions the renderer does not hold, and a design language spelled out longhand until it drifted. Correctness. The leader line pointed at empty space: CSS2DObject centres a label on its anchor by default, and margin only nudges the centred box — the overlay now anchors the left edge (center 0, 0.5) so the hairline meets the star. The info panel and not-found panel returned to top-4 at sm, under a search field that is 26rem wide and centred, covering the back button on every viewport from 640 to 1088px; they now wait for xl. The search empty state asserted "nothing matches" while the catalogues were still loading — and forever if they failed — so it now waits for the index. "Matches 8" was the page size wearing the costume of a count; the header now reports the real total, with the cap stated when it bites. The nameplate un-hides at lg instead of sm, clear of the rail and the object card. theme-color matches the void again. Accessibility. Tabbing to search changed one hairline's hue; the wrapper now carries the old ring as a visible focus indicator. Structure. hud-surface names the panel recipe that had been inlined at seven sites and had already forked into /85 and /92; the backdrop blur it carried is gone from every near-opaque panel — the canvas beneath redraws every frame, so each blur was re-sampled continuously for an effect the fill hid — and survives only as blur-sm on the genuinely translucent search shell. type-label and type-eyebrow name the two label voices, retiring 0.14em and the stray hover:bg-accent/10. The Measured/Derived rows both cards pasted twice each live once in ReadoutSectionsComponent; the reticle and chevron each draw from a single geometry, the reticle's stroke held in screen pixels so one mark serves every size. The acquire wipe plays once per search, not once per keystroke, because both outcomes now share one panel. hud-banner, which styled nothing since the rule was deleted, is a data-testid — the convention its own file already used. 527 unit tests, 6/6 end-to-end, production build, and a driven screenshot: every label now hangs off its star with the hairline touching the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
0dcd49c777 |
Merge main again: readouts built once, said in the instrument's voice
Main moved while the previous merge was being verified. It brought the shared bodyReadouts builder — one source for a body's measured and derived rows, used by the detail page and the system view's new object card — plus the derived-value asterisk in the HUD readout panel. All of that data flow is kept. The templates it arrived in are restyled to this branch's idiom: the info panel and the object card share the same organism (header, full-bleed readout rows, provenance line, route rail), the object card's route rail sits at the bottom because there the route is the next step rather than the way back, and the derived asterisk and its footnote keep their meaning in sentence case. The card also needed the restyle to render at all — it arrived wearing hud-panel, a class the observatory system no longer defines. Verified: build clean, 527/527 unit, 6/6 e2e, design detector clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
a965aed1db |
Merge main, carrying the observatory instrument onto its rebuilt chrome
Two HUD redesigns happened in parallel: this branch restyled the old chrome as a precision observatory instrument, while main rebuilt the chrome against screenshots of the Star Citizen starmap — a scale ladder, a nameplate, a readout panel, and two-line labels that say what a thing is, not just what it is called. This merge keeps everything main's rebuild learned and says it in this branch's voice. Kept: the ladder's reachability semantics and test hooks, the two-line name/kind labels and their shared declutter logic, system-view body labels, every readout. Restyled: chamfered clip-path panels become hairline frames with corner-tick brackets; Orbitron is gone and one readout face carries the hierarchy; the hexagon reticle becomes the same circle-and-ticks mark the search field wears; focus states move to real focus-visible outlines; the long uppercase caveat notes drop to sentence case so they read as sentences. Two merge-borne fixes along the way: the labels' translate offset moved into .map-label as a margin (CSS2DRenderer rewrites the inline transform every frame, the margin is the offset it cannot touch), and the info panel's new Derived section picked up the horizontal padding it lost when the panel moved to per-block padding. Verified: build clean, 496/496 unit, 6/6 e2e (on a free port — 4300 is occupied on this machine), design detector clean, screenshots reviewed at 1440x900 and 390x844 across all four views plus search states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
8e55964b14 |
Refresh the catalogues on a schedule, and republish when they change
Mondays 05:23 UTC: re-run the ETL cold against the live archives, and if the output differs by a byte, gate it on the unit suite and a production build, commit it to main, and dispatch the Pages deploy. If nothing changed, say so in the run summary and touch nothing. The gates run inside this workflow because they cannot run after it: a push made with GITHUB_TOKEN fires no push workflows at all — GitHub's recursion guard — so an unguarded push would deploy nothing and be checked by nothing. The same guard is why the deploy and a visible CI record are dispatched explicitly afterwards; dispatch events do go through where push events do not. ci.yml gains a workflow_dispatch trigger for exactly that call. Also corrects pages.yml's claim that configure-pages enables Pages on first run. It cannot: the action's `enablement` input requires an admin-scoped token, which GITHUB_TOKEN is not. If the site has never been enabled, the first deploy fails at that step and the one-time fix is Settings → Pages → Source: GitHub Actions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
9a5e41495b |
Make a cold-cache ETL run a pure function of the archives
Two order dependencies surfaced while designing the scheduled refresh, both invisible on a developer machine with a warm cache and both guaranteed to haunt a weekly CI run that starts cold. The Exoplanet Archive query had no ORDER BY, so the archive was free to return rows in any order it liked — and exoplanets.json preserves row order, so a re-run could rewrite the file, and commit a diff, when nothing was actually published. Ordered by pl_name, which is unique among default_flag=1 rows, so the order is total. The deep-sky sort tie-broke with localeCompare, whose collation belongs to the ICU build of whichever Node runs the ETL. Compared code points instead: the file must not reorder because the runner's ICU disagrees with the machine that wrote it last. Both may reorder the committed files once, on the next real refresh. After that, byte-identity between runs means what it should: the archives published nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
836816ffbd |
Restyle the HUD as an observatory instrument
Replaces the generic rounded dark cards floating over the scene with a single instrument language: hairline frames cut by accent corner ticks, a viewport frame that reads the whole screen as one panel, and readouts whose figures are what the eye lands on. - styles.css: retune the tokens (deeper void/panel, brighter text/muted so every label clears 4.5:1 even over the brightest part of the skybox) and add two utilities — `hud-brackets` draws the four corner ticks from eight background gradients so no panel pays for them in DOM, `hud-acquire` wipes a panel down on mount as its one authored moment (reduced-motion aware). - Drop Orbitron: every string in this UI is a measurement, an identifier, or a catalog label, so one readout face carries the hierarchy through weight, size, and tracking — and the HUD paints one font request sooner. - Search: reticle mark instead of a magnifier, transparent field inside a framed shell, results as a dense two-column list with a match count, and the previously missing empty state. - Info panel: return rail, name/class header, and a readout table with tabular figures and the unit tinted back off the number. - Star labels: a hairline leader back to the star they name. The offset moved from `translate-*` to a margin because CSS2DRenderer overwrites the inline transform every frame, which silently beat the old classes. - Both top-anchored overlays drop below the search field under `sm` so they no longer stack on top of it on a phone, and focus moved from a removed outline plus ring to real `focus-visible` outlines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
7d07921d69 |
Say what the project is, in the two places people arrive
The README opened without mentioning that the thing is deployed, and the HTML document carried a title and nothing else — no description, no link preview. Both matter more now that there is a public URL to land on. README: the live link up top; the object card and the measured/derived split from #3, which were built but never written down; `shared/format/`, `public/` and `.github/workflows/` in the layout. index.html: a description, a theme colour matching --color-void so the browser chrome does not flash white around a black sky, and Open Graph/Twitter tags so a shared link renders as something other than a bare URL. Those URLs are absolute and name the deployment outright — a scraper has no document to resolve a relative path against, so they cannot follow <base> the way the rest of the app's paths do. A custom domain later means editing these three lines. The preview image is the galactic view, copied into `public/` so it is served by the site itself rather than linked out to raw.githubusercontent. Verified: the built document keeps every tag, the base href still rewrites to /star-map/, the image answers 200 at the path the og:image names, and both the root and a deep link still render. 527 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
433bcca8a2 |
Stop publishing the repo as a plugin marketplace, and refresh the run skill
Three small things. Removed `.claude-plugin/marketplace.json`. It is the file `/plugin marketplace add` reads, so without it this repo no longer offers itself as a marketplace — which is the odd part of an Angular app carrying one. The caveman plugin's own files stay, so it can be listed from a marketplace of its own later; only the listing is gone. That left four documents asserting an install path that no longer exists, including a README section handing out `/plugin marketplace add` and `/plugin install` commands that would now fail. All four now say what is actually true. `.junie/plans/nasa-star-map.md` still describes the repo as containing only a marketplace, and is left alone: it records what was here before the app was written, and is not a claim about the present. The run skill's numbers had drifted a release behind — 496 tests in 28 files against a real 527 in 31, and a build timed at 9s that now takes 12. Measured rather than guessed. The pinned Playwright version it names was correct. It also gains the Pages build, which is not a plain `npm run build`: the base href and the 404.html copy are both required for a project site, and the artifact root is `dist/star-map/browser` rather than its parent. Plus the matching gotcha, since a Pages build served at `/` looks broken in a way that tells you nothing about whether it would work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
cca652d71f |
Publish the map to GitHub Pages
Builds on a push to main and deploys the result, so the thing is reachable without checking it out and running a dev server. Two details a project site needs that a root deployment does not: The base href is set from the repository name at build time. The app is served from a subdirectory there, and `DataLoaderService` fetches its catalogues with relative URLs — those resolve against `<base>` rather than the current path, so without it a deep link would ask for /body/assets/data/stars.bin. Pages serves a static tree with no rewrite rules, so /body/mars has no file behind it and returns 404. Answering that 404 with the app lets the router render the route. The status stays 404, which crawlers will notice and readers will not; the alternative is hash URLs, which everyone notices. Verified against a server that mimics both behaviours: the root loads the star field with all six catalogue assets at 200, and /body/mars boots through 404.html and renders Mars at 3,390 km with its assets still resolving. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
cfad718b0a |
Check develop after a merge, not just main
`develop` became the integration branch when #3 merged into it, but CI's push trigger still named only `main` — so the merge commit itself ran nothing. A pull request is checked before the merge, not after, which leaves the state of the branch people actually build from unverified whenever two green pull requests conflict semantically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
ca89724b88 |
Say what is known about a world, and how it is known (#3)
Fills the gaps in the HUD and the body cards, distinguishes measured quantities from derived ones, raises an object card over the live system view instead of navigating away from it, and adds a Junie code-review workflow beside CI. Five commits kept separate: the feature, the review follow-up collapsing bodyReadouts, a doc-comment fix, the review workflow, and the fix that stops that workflow failing a pull request when no API key is set. |
||
|
|
4d5e3a9914 |
Let the Junie review skip rather than fail without a key
The workflow as written failed on
|
||
|
|
ddf805e61f |
Have Junie review each pull request
CI says whether the code works. Nothing says whether it reads well, and the one review this repository has had so far arrived by hand. Kept as a separate workflow rather than a third job in ci.yml so a review can never turn the build red — the two answer different questions and should be able to disagree. It skips drafts, and skips pull requests from forks: GitHub withholds secrets from `pull_request` runs on a forked head, so the job would fail on a missing JUNIE_API_KEY rather than say anything about the code. Each push supersedes the previous review rather than stacking another comment beside it. Requires a JUNIE_API_KEY repository secret, generated at junie.jetbrains.com/cli. Without it the workflow is inert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
b14ce78c5f |
Say what formatRadiusKm actually returns
Its doc comment promised a fallback to Earth radii that the function has never had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
7b0f32f71a |
Assemble a body's readouts once, not once per panel
Follow-up to review on #3. The card and the detail page each built their own Measured/Derived split, kind label and provenance sentence — the drift buildBodyViewModel exists to prevent, re-forked one layer up, and the drift would have been in which side of the measured/derived line a quantity falls on, which is the distinction those panels exist to draw. One bodyReadouts(body) now returns both blocks and the sentence, and both templates iterate it. The two surfaces render identical rows as a result, and the card gains the inclination the detail page already showed. Also from that review: - KIND_LABELS was duplicated between the two panels; it now lives beside bodyReadouts. The third copy the review pointed at is a different union (search results are star/body/exoplanet, and label a body "Body"), so it stays where it is. - CardRow was HudReadout renamed. Both are now Readout, which HudReadout extends with its derived flag. - The enterable-systems count was a 21-line lazy memo over arrays that are already in hand; it is one expression where those arrays are assigned. - buildBodyViewModel now carries hostStarId, so the detail scene stops rescanning both catalogues for something the builder had already resolved. - heliocentricPeriodDays was called twice for the same body. - The superscript helper was a split/map/join; it is a replace. - info-panel had five computed() each wrapping one pure call with a non-null assertion, beside a template that inlined the same kind of call directly. They are gone with the shared readouts. - Dropped a tautological test that compared a pure function to itself. Replaced with one that asserts the host star id the builder now carries. - Removed the orphaned doc comment left behind when formatParsecs/formatAu moved out. And one the review raised as out of scope but is worth taking: formatRadiusKm grouped thousands above its decimal threshold and not below it, so 69,911 km sat beside a bare 6371 km. Both are grouped now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
1019727a39 |
Say what is known about a world, and how it is known
Four changes to the readout panel and the body cards, which between them were showing less than the catalogues hold and not always distinguishing a measurement from an inference. Picking a planet used to navigate straight to /body/:id. That tore down the system scene and the camera with it, so comparing two planets meant flying back into the system between each. Hovering a body now raises a card over the live view and clicking pins it; Full view still opens the route for the full 3D inspection. Clicking empty space unpins, and leaving the system clears it. The card and the detail page were assembling "what do we know about this world" independently, which is the shape of bug where a planet reads 255 K in one panel and 254 K in the other. Both now build from one shared view model. Orbital period was absent everywhere. For a heliocentric orbit it follows exactly from the semi-major axis, because in these units the Sun's mass is the unit of mass — Mars comes back 687.0 d against a published 686.98. It is deliberately not computed for moons, whose elements are relative to a parent planet the catalogue has no mass for, nor for exoplanets: periodDays is populated for none of the 6319 shipped records and hostStarMassSolar for none either, so any figure would assume a solar-mass host and mis-state every planet around an M dwarf. Where a period does exist it is filed under Measured or Derived according to which it is, not by its field name. The system readout showed a flat 0.00 pc for the Sun's distance, which is arithmetically right and reads as a bug — the distance from here to here is not a measurement, so it is suppressed. It gains the host star's luminosity, marked as derived, and counts moons separately from planets. The neighbourhood readout gains the one thing the star field cannot show: how many of those points can actually be entered. Derived readouts carry a marker and a footnote saying so. Every quantity now formats through one module whose precision follows magnitude, rather than a fixed decimal count per call site that read as false precision at one end and lost real information at the other: 0.0026 AU stays legible instead of rounding to 0.00, and Pluto's period reads 248 yr rather than 90560 d. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
60b3726a84 |
Merge pull request #2 from avalon-vanguard/claude/project-development-ehm7mw
Give the repository a way to actually run itself |
||
|
|
c5ec752961 |
Give the repository a way to actually run itself
Nothing in the repo explains how to launch this app in a headless container, and three separate things stop it from starting — none of them discoverable without hitting each in turn. The container's default node is v22.22.2, one patch below the floor `engines` declares. A satisfying build sits at /opt/node (v22.23.2), but PATH finds /opt/node22 first, and the names invert what you would guess. `ng serve` reports only the version and exits. The pinned Playwright wants a Chromium revision this image does not carry, under a directory layout the older build does not use, so `npm run e2e` cannot launch a browser at all until the expected path is shimmed. Downloads are blocked, so `playwright install` is not the answer. And the scene is software-rasterized here: entering a system takes a camera flight that has to be waited on rather than slept through, a single canvas click lands before picking is wired, and a screenshot taken the moment a DOM assertion passes catches a half-drawn frame. driver.mjs handles all of it — picks a node satisfying `engines`, owns the dev server, drives the camera to each scale, and either screenshots or dumps the HUD's labels as JSON for checking a change without eyeballing a picture. SKILL.md documents only commands that were run here, and records the traps in Gotchas, including that no `pkill -f` is safe for stopping the server: it matches your own command line and kills the shell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
bf7ca0da5b |
Merge pull request #1 from avalon-vanguard/claude/project-development-ehm7mw
Galactic scale, derived surfaces, a 68 388-star catalogue, and CI |
||
|
|
748cb8927e |
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 |
||
|
|
39e7f29086 |
Declare the Node versions this project actually needs
Angular 22 requires Node ^22.22.3 || ^24.15.0 || >=26.0.0, and package.json said nothing about it. On a machine half a patch below that floor — 22.22.2, which is what this development container has on PATH — every `ng` command refuses to start, so `npm test` and `npm run build` fail with a version error rather than a test failure. Nothing in the repository pointed at the cause; you had to already know. The range is Angular's, taken verbatim rather than guessed at, because it is the binding constraint. Every other direct dependency is looser and fully contained by it: vitest wants ^20 || ^22 || >=24, jsdom ^20.19 || ^22.12 || >=24, tsx >=18, typescript >=14.17, Playwright >=20. This moves the complaint earlier and makes it name the project. `npm ci` now prints EBADENGINE for star-map itself before anything is installed, instead of the first Angular command failing several steps later. It stays a warning rather than an error — turning it into one needs engine-strict in .npmrc, which would hard-fail installs on any unlisted version, and that is a stricter policy than this change is claiming to make. The lockfile carries the field too: npm mirrors the root package's engines into it, and a lockfile that disagrees with package.json is one npm has to resolve rather than trust. Regenerated with --package-lock-only, which changed those three lines and no dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
2e67c3ea9a |
Commit the lockfile, and give the repository CI
Nothing has ever checked this project automatically. There are no workflows, so the pull request's green tick means only that the checks were run by hand on one machine, and nothing would catch a regression pushed later. The lockfile had to come first. `npm ci` is the only install that guarantees CI tests the dependency tree that is actually committed, and it refuses to run without package-lock.json — which was gitignored. Un-ignoring it also pins the 617 packages this was built and verified against; without it, a transitive release could change what CI runs from one day to the next with no commit to point at. Checked before committing: every entry resolves to registry.npmjs.org, and it carries no credentials. Two jobs rather than one, run in parallel. The typecheck/unit/build job is fast and deterministic; the end-to-end job drives a real headless browser through WebGL2 software rendering and is the one that will be slow and, if anything here is going to be flaky, flaky. Keeping them apart means a browser timeout cannot hide a failing unit test behind it. Between the four steps, all four TypeScript projects are compiled: the ETL and end-to-end configs explicitly, since nothing else ever builds them, and the spec and app configs by `ng test` and `ng build` respectively. One thing this cannot verify from here: the runner installs Chromium to match the pinned Playwright, where this container ships an older build. The suite was run locally against that older browser instead, and passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
8191254b3c |
Put the four views in the README, and correct what one of them showed
The README described the app without showing it. Adds a screenshot to each of the four sections it describes, captured from a real run at the current state of the code rather than assembled or touched up. Capturing them caught a claim that had gone stale. The galactic view's readout still said everything inside 50 pc was real, which was true when that string was written and has been wrong since the catalogue reached 250 pc. It now quotes the catalogue's own size and reach, so it cannot drift again — and, usefully, that makes a stale screenshot self-evident: the numbers are in the picture. JPEG rather than PNG, at 1.1 MB for all four instead of about five. These are dark scenes with fine gradients, where JPEG can band, so they were checked at quality 88 rather than assumed to be fine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
efa9e4084a |
Draw the whole catalogue, and build the aggregation the rest would need
Two things, one verified and one that cannot be. The render budget is now the whole catalogue: 68388 stars, one instanced draw call, which is what a GPU should be asked to do. The budget itself stays, because the catalogue is meant to grow past what any machine should draw at once — Gaia alone could contribute a million — and at that point the selection is what keeps the field legible rather than a grey wash. A `?stars=` override handles the machines that cannot, including the software rasterizer the end-to-end suite runs against, whose frame rate is two orders of magnitude below a real GPU's and which was measuring the rasterizer rather than the app. The aggregation is the second thing, and none of it has run. Every ESA, NOIRLab, SDSS and Euclid endpoint is unreachable from here — only GitHub raw is, which is why HYG and OpenNGC are the current sources. So this is infrastructure and a Gaia query written against the published DR3 schema, not data. What the framework encodes is that these surveys are not interchangeable. The distinction is not size but whether a catalogue knows how far away its objects are, because a 3D map cannot place a star it only has a direction for. Gaia is the only one of the five that can add stars here, because it is the only one that measures parallaxes. DECaPS2 has fifty times Gaia's object count and photometry alone — not one of its 3.32 billion objects can be placed in depth. Euclid's bulge is 8 kpc away, where a parallax is microarcseconds; its contribution would be imagery. SDSS-V and SAGA are keyed to stars something else already places, so they enrich rather than extend. Those roles are recorded as data the ETL prints, not as prose that can drift. Overlapping catalogues are reconciled on direction rather than on 3D proximity, which is the one non-obvious part. Two surveys agree on a star's direction to within an arcsecond and disagree on its distance by tens of per cent, so a star at 200 pc is 50 pc from itself between catalogues while being unmistakably the same object. Matching in 3D would need a tolerance so loose it swallowed real neighbours. The better parallax wins where both reach; where only one does, the star stays. Names become dense-with-holes with a source dictionary, because a survey catalogue has no proper names — writing "Gaia DR3 4472832130942575872" once per star would cost 25 MB per million to repeat what two adjacent fields already say. An empty entry costs three bytes and is regenerated on load. The Sun needed its own case in the merge: it sits at the origin, has no direction to compare, and appears in every catalogue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
29fd92d118 |
Widen the star catalogue, and separate what is drawn from what is known
The map held 8750 stars within 50 pc and rendered 371 systems. Both were lower than they needed to be, for different reasons. The star catalogue was capped by its own encoding as much as by the cutoff: one JSON object per star, eight key names repeated each time, 157 bytes a star. At the range HYG actually reaches that is 17 MB to download and parse before the first frame. So the numbers move into two binary column stores — positions in stars.bin, which the GPU is handed verbatim, and id/magnitude/colour/spectral index in stars-meta.bin — and the JSON keeps only the strings, with 2600 distinct spectral classifications collapsed to a dictionary. The layout is defined once, in star-catalog.ts, and the ETL and the app both use it, so the writer and the reader cannot drift. The cutoff then goes to 250 pc: 68388 stars, 7.8x as many for 1.7x the bytes. That is where HYG's measurements stop rather than a round number — 98.6% of its rows are Hipparcos, whose parallaxes are good to about a milliarcsecond, so beyond 250 pc it would be plotting noise. Drawing all of them is a separate question from knowing them, and it is answered separately. The field draws a budget: every star inside 25 pc, because the nearest are faint red dwarfs and Proxima Centauri is magnitude 11, then the brightest of everything beyond. Search, navigation and the planet cross-reference still see the whole catalogue. A real GPU would draw all 68388 without noticing; the budget is for the machines that would not, and it is one constant. Systems were limited by something else entirely. The archive data already shipped named 4735 host stars and only 388 resolved, because the rest lay outside a 50 pc catalogue — and the cross-reference kept only its own result, so redoing it meant re-downloading an archive that is not reachable from here. Host coordinates are now stored with each planet, and the match is re-resolved at build time against whatever catalogue the run produced. Even name matching alone, which needs no coordinates and so works on the records already shipped, rescues 335 planets across 238 systems: 371 renderable systems become 609. Two selection rules were tuned for a 50 pc bubble and no longer fit. Tethers followed the Sun's nearest neighbours, which are a speck at this range, and now follow the brightest; labels were ranked by proximity, which named whatever sat nearest the middle of the screen, and are now ranked by brightness — so the view names Canopus, Achernar and Spica rather than a clump of catalogue designations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
029162ff52 |
Lower the star halo floor so the inner orbits stay legible
The floor that stopped the Sun disappearing reached past Venus and up to Earth, covering the two orbits it most needed to leave alone. Halved, from 3.5% of the framed radius to 2%. The halo's visual radius is half its extent, so that puts its edge at 1% of the framed radius, and the orbits it has to clear sit at their own fraction of the same radius: in the solar system, framed to hold Pluto, Venus is at 1.3% and Earth at 1.8%. Both are now outside it, and the star still reads at about nine pixels across on a typical window. Mercury, at 0.7%, is still inside — and would be at any halo large enough to see, since its orbit is only three pixels wide at that range. That is now a pinned test rather than an oversight. The floor was only ever the lower bound; the tests now state the upper one too, in the terms the trade is actually made in — pixels on screen for visibility, AU against real orbits for clearance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
be19d9cbcc |
Keep the star visible at the distance that frames its system
Framing the whole system pushed the camera far enough back that the star at the centre became a speck — about a pixel across for the Sun. The cause is a constraint that cannot be tuned away. A star is sized against its system's innermost orbit, because it must never swallow its closest planet, while the camera is placed to frame the outermost ring. In the solar system those differ by a factor of a hundred: at the distance that fits Pluto in view, a disc that stays clear of Mercury is a pixel across. No radius satisfies both, because the information genuinely does not fit on one screen at that zoom. So the disc stays honest to the orbits and the halo carries the visibility. Light is not a surface: a glow that reaches past the innermost orbit says the star is bright, not that it is large. Its extent is still a multiple of the star — so a compact system keeps exactly the corona it had — but floored against the framed radius, which is what the wide systems needed. The disc grows a little too: it may now reach 45% of the innermost orbit rather than 35%, which still leaves clear space between the star's limb and the closest orbit. Also makes createGlowSprite take the extent it will draw rather than a radius and a multiplier. The two were only ever multiplied together, and how large a star's halo should be is not a property of the star — it depends on how its system is framed, which is a decision that belongs with the framing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
6019987fc4 |
Frame the system view from the camera it actually has
The grid overflowed the frame in 368 of the 371 systems the datasets contain — median fill 1.11, and the outermost ring cut off by the viewport edge in almost every one. Two compounding causes. The framing distance was a fixed multiple of the outermost orbit, tuned by eye against a 55-degree field of view; the engine's camera is 50. And it framed the outermost *orbit*, while the widest thing actually drawn is the grid's outer ring, which by construction always sits beyond it. Neither is fixable by adjusting the multiple, because a multiple is the wrong shape of answer: what has to fit is a radius on screen, and how much radius a given distance buys depends entirely on the lens. So the distance now comes from the camera's own vertical field of view and aspect — picking whichever screen axis is the tighter one, so a portrait window backs off further rather than clipping — applied to the grid's outer ring with an explicit margin around it. The ceiling goes up with it. Eighty AU could not frame the solar system out to Pluto once the real field of view was accounted for; that needs 120 on a landscape display and 140 on a portrait one. Only companions hundreds of AU out reach the new ceiling, and those still arrive framed on their inner region. Measured across every system in the data, at three window shapes: the overflow count drops from 368 to 2, the fill settles at exactly 0.89 — the margin, uniformly — and the outer ring still encloses the outermost orbit everywhere, so neither invariant was traded for the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
ac296f5133 |
Derive a surface for every body that was never photographed
Fifteen bodies here have a real photograph. Every exoplanet does not, and never will on current instruments — none has ever been imaged — and nor do several of the solar system's own moons. Those all shared one crude stand-in: a few noisy bands tinted by category, cached per colour, so every exoplanet in the app was literally the same picture. They now get a surface reasoned from what has actually been measured. The chain is standard at every link. A host star's luminosity comes from its catalogued apparent magnitude and its parallax distance — that pair is exactly an absolute magnitude — plus a bolometric correction for its spectral class. The correction is not optional: an M dwarf radiates most of its light in the infrared, so its visual magnitude understates it more than tenfold, and M dwarfs are what most nearby planet hosts are. Luminosity and the semi-major axis then give an equilibrium temperature, mass and radius give a bulk density, and size, temperature and density together give a class of world. Checked against the solar system the temperatures land on Earth 255 K, Jupiter 112 K, Neptune 46 K, all within a kelvin or two of published values, and 51 Pegasi b comes out at 1227 K against a published 1200. Each class carries a palette reasoned from its chemistry — methane absorbs red light, which is why the ice giants are blue — and a structure: zonal bands for a body with a fluid envelope, because a rapidly rotating atmosphere organises into them, and fractal terrain for one with a solid surface. Polar caps grow and shrink with the derived temperature, which is the clearest visible consequence of the whole chain. The generator samples three-dimensional noise along the sphere rather than a flat field, so there is no seam to stitch at the antimeridian and no pinching at the poles, and it writes into a byte array rather than a canvas — a pure function, testable, with no 2D context to be unavailable. Two things the derivation cannot do, both stated on screen next to the measurements it rests on. Equilibrium temperature ignores greenhouse warming and internal heat, so Venus comes out at 300 K against a real surface of 737 K and Io, kept molten by tides, classifies as ice. And these are illustrations: reasoned, but not observations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
a84e2d3a69 |
Put a reference grid under the system view
A system was a handful of ellipses floating in the dark. You could see that one orbit was bigger than another, but not how big, and not that a planet sat above or below the plane the others share. Adds the same plane-and-tether reading aid the outer scales got: a polar grid in the system's own reference plane, with a drop line from each body onto it. Ring radii snap to a 1-2-5 ladder rather than dividing the system evenly, because the point is to put a number on a distance — 5, 10, 15 AU can be read at a glance and 4.34, 8.68, 13.02 cannot. That holds across the four orders of magnitude real systems span: the solar system gets 5 AU rings, TRAPPIST-1 gets 0.01 AU ones. The outermost ring encloses the outermost orbit rather than falling just inside it. The rings are dashed. Solid ones would sit in the same plane as the orbit ellipses, which are themselves rings, and at a glance a reference circle and a circular orbit are the same picture. Dashes are cut by dropping whole segments rather than by a dashed material: the ring is already built from independent segment pairs, so a material's dash pattern would restart at every one. Drawing the grid exposed a framing bug it made unmissable. The camera settled along one fixed direction derived from the ecliptic, which is face-on only for the one system whose elements are ecliptic. Every exoplanet system — measured against the plane of the sky, perpendicular to the line of sight to its own host star — was being presented nearly edge-on, a smear of overlapping ellipses. The settle direction is now taken relative to whichever plane the system was measured in, so all of them read as discs. The solar system is unmoved, which a test pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
2e525fb5c3 |
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 |
||
|
|
2f45fa7fef |
Measure exoplanet inclination from the plane of the sky
The Exoplanet Archive measures orbital inclination from the plane of the sky — the plane perpendicular to our line of sight to the host star. Ninety degrees means edge-on as seen from Earth, which is why transiting planets pile up there: 1643 of the 2061 published inclinations are within five degrees of 90. The renderer fed that straight into a propagator that reads inclination as an angle from the reference plane, so every transiting system was tilted against a plane its inclination was never measured against. Each body's elements are now rotated out of their own reference plane into the scene by a per-body quaternion. Solar-system elements keep the ecliptic rotation from the previous commit. Exoplanets get a rotation carrying the elements' +Z onto the line of sight to their host, which is exactly the star's own position — so an inclination of i means the orbit's normal sits i from our line of sight, which is the definition. The rotation about that axis is the node's position angle on the sky. The archive does not publish it and the ETL does not request it, so the shortest arc is used: deterministic, and no less arbitrary than anything else given no data. Planets with no published inclination default to face-on, which is the honest reading of an unconstrained orbit rather than a guess at a tilt. Unifying this replaced the direct eclipticToEquatorial call in the renderer, so solar-system bodies and moons come out exactly where they did before — verified against Sol side by side. Tests: 253 passing, up from 247. The strongest one is the definition itself: a 90-degree planet must pass through our line of sight to the star, which is what a transit is. One test of mine had to be corrected rather than the code — it asserted that two systems at the same inclination must occupy different planes, which is not guaranteed once the node angle is arbitrary, while each still sits at the correct angle to its own host. Note the e2e camera-flight test flaked once under parallel load during this work, then passed in isolation and on two further full runs. Its click-until- entered poll has a fixed 15s budget that a loaded machine can exceed; that is pre-existing and unrelated to this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
2293585940 |
Put orbits and stars in the same reference frame
The app's two sources disagree about which frame they are in, and nothing reconciled them. HYG star positions are equatorial J2000 — that is what raDecDistanceToXyz produces and what the galaxy view renders directly. Orbital elements come from JPL Horizons, whose default reference plane for element output is the ecliptic, and the ETL never overrides it. The two are tilted 23.4 degrees apart, so the orbits sat that far off the sky they are drawn against. Confirmed rather than assumed, from both ends: the Horizons request in lib/horizons.ts sets no REF_PLANE, and the resulting solar-system inclinations are 0 to 17 degrees with Earth exactly 0.00 — which is only true of the ecliptic, since Earth's orbit defines it. eclipticToEquatorial now rotates orbit positions into the scene frame, so a direction means the same thing in the galaxy view and the system view. The rotation is about the vernal-equinox axis, which both frames share. That exposed a presentation problem the old code had been hiding. The renderer mapped the propagator's z straight onto the scene's vertical, which silently redefined the frame but did make systems render flat. In a properly equatorial scene, orbital planes lie 23.4 degrees off the scene's own axes, so a system would be presented edge-on. Rather than rotate the world back into a comfortable pose — which would only put the orbits at odds with the sky again — the camera now settles relative to the orbital plane: a three-quarter view about 37 degrees off the ecliptic normal. The arrival still begins along the approach direction and swings round as it settles, so the transition stays continuous, and the framing is now the same every time rather than inherited from wherever the camera happened to be. Tests: 247 passing, up from 237. The frame tests are the discriminating kind — Earth's orbit must lie perpendicular to the ecliptic pole rather than to the scene's vertical, and must reach 23.4 degrees of declination a quarter orbit on, where it used to read zero. Verified in a browser against Sol and Gl 357. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
dad4b93e8a |
Rank search results instead of taking the first eight
Search scanned the index in construction order — 8750 stars, then 18 bodies, then 6319 exoplanets — collected substring matches, and stopped at eight. With no scoring, position in the index decided everything. Typing "Io" returned eight stars named "Iot Cas", "Iot Eri" and so on, and never reached the moon Io — despite Io being an *exact* match, because the scan had already filled up 8750 entries before the bodies begin. "Kepler" returned Kepler-939 b, Kepler-1292 b, Kepler-223 d and five more in whatever order the archive happened to list them. Everything is now scored before anything is taken, so where an entry sits in the index cannot hide a better match. Exact beats prefix beats word-start beats substring, with the gaps wide enough that a worse kind of match can never outrank a better one. Ties break on kind — the eighteen solar-system bodies first, then stars, then exoplanets, so "Proxima" offers the star before its own planets — then on name length, then alphabetically, so the order is fully determined rather than inherited from the input. Matching is punctuation insensitive as well as case insensitive, so "gl357" finds "Gl 357". "Io" now returns the moon first. "Kepler" returns Kepler-4 b through Kepler-9 d. The index is pre-normalised once on load rather than per keystroke. Lowercasing, stripping punctuation and splitting 15,000 names on every character typed costs about 11 ms, which is most of a frame, and search shares a thread with the render loop — so it stuttered the scene while typing. Precomputing takes a broad query down to 2.5 ms and a narrow one to 0.5 ms, for one 15 ms build during the existing data load. Adds the first tests this component has had, alongside the ranking's own. Tests: 237 passing, up from 206. Verified in a real browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
f2c77fb5ad |
Scale the system view to the system it is showing
Star size, planet marker size and camera distance were all fixed constants in
AU, tuned against the solar system's 30 AU span. Real systems span four orders
of magnitude, and the fixed values served only the wide end. Measured across the
370 systems that draw planets:
- 170 had their innermost orbit inside the 0.2 AU star sphere, and for 107 of
those every orbit was inside it, so the system rendered as a lone sphere.
- 193 were framed from the 3 AU distance floor — for TRAPPIST-1 that is 48x
the width of the entire system, reducing it to a cluster of specks.
- Planet markers were effectively a flat 0.09 AU, since almost every body
clamps to the maximum. Inside Gl 357's 0.204 AU system that is wider than
the orbits themselves: one planet swallowed the whole view.
All three are now derived from the system's own measurements. The star is a
fraction of the innermost orbit, so it can never reach the closest one. The
camera is a multiple of the outermost orbit, so everything fits. Markers scale
with the span against the solar system as the reference, so the constants that
were tuned by eye keep their meaning. Because star, markers and camera all
scale together, a compact system now looks like a wide one — same apparent star,
same legible spread of orbits.
Gl 357 is the case that motivated this. It gained three planets in the previous
commit and still rendered as a bare star, because all three orbits were inside
the star sphere. It now shows its star and all three orbits.
The renderer measures the span before building anything, since markers are sized
against it as they are created, which also removes the reduce over tracked
bodies that used to compute it afterwards. The star sphere is rebuilt per system
rather than shared, so its geometry is now disposed on each transition.
Sol is deliberately unchanged: its innermost orbit is Mercury at 0.387 AU, so
the star lands just under the old fixed radius, and the reference span makes the
marker scale factor 1. Verified side by side.
Tests: 206 passing, up from 199. Verified in a real browser against both ends of
the range — Gl 357 at 0.2 AU and Sol at 30 AU.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
|
||
|
|
f241b093eb |
Draw the 1509 exoplanets that were being silently dropped
The system renderer required both a semi-major axis and an eccentricity before it would place an exoplanet, even though resolveOrbitalElements already defaults every other missing element. The archive publishes an axis far more often than an eccentricity: 3895 records have one and only 2386 have both, so 1509 planets were dropped for want of a value that can simply be assumed. A missing eccentricity now defaults to 0, a circle. That is the conventional assumption for an orbit whose shape has not been constrained, and it is the only honest option available, since the axis alone says nothing about elongation. The effect is not subtle. 18 systems gain planets, and seven of them previously rendered as a bare star with nothing around it at all: Gl 357 goes from zero planets to three, HD 176986 likewise. Beyond the effect today, a user could already reach one of these planets through search and its detail page, then jump to its system and find it missing from the very system it belongs to. isPropagatableOrbit replaces the old inline guard and also rejects what the old one never checked: a non-positive axis, and an eccentricity of 1 or more. Those are escape trajectories that no ellipse describes, and propagating them anyway does not throw — it yields NaN, which reaches the vertex buffer and poisons the geometry's bounding sphere, disabling culling for the whole object rather than just the bad orbit. Being a type guard, it also lets the caller drop a seven-line field-by-field copy of the orbit. Fixes a label leak found while verifying this in the browser. Galaxy star labels were being cleared on entering system space but immediately recomputed, because the tick gated them on `currentStarId`, which is not assigned until the arrival flight finishes a second later — so parsec-scale names sat pinned over the system. Both label and orbit updates now gate on which group is actually visible, which is true throughout the transition rather than only at the end of it. Tests: 185 passing, up from 171. Verified in a real browser: GJ 1151 draws the orbit and marker it gained, and no labels survive into the system view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
8d8c65bdb2 |
Propagate exoplanets with their real orbital period
Every exoplanet was propagated with gmForParent(undefined) — the Sun's gravitational parameter — so the whole catalogue orbited as though each host were exactly one solar mass. Most hosts are red dwarfs far lighter than that, and a heavier central mass pulls harder and shortens the period, so their planets were whirling round much too fast: TRAPPIST-1 is 0.09 solar masses, and its planets were completing an orbit in roughly a third of the true time. pl_orbper was already in the TAP query and was being discarded on the way into the record. It is now kept, along with st_mass. A period and a semi-major axis together pin the host's gravitational parameter exactly, via GM = n^2 a^3 — no stellar model, no assumption, just the inverse of the orbitalPeriodDays helper that was already there. resolveGravitationalParameter picks the best available source: the measured period, else the published host mass, else one solar mass as before. A derived value implying something outside 0.01-150 solar masses is rejected and falls through, since a period and axis taken from disagreeing solutions would otherwise send a planet spinning at a visibly absurd rate. Note the direction of the error, which is the opposite of what it looks like: assuming a *heavier* host than reality makes a planet orbit *faster*. A test pins it, and caught me stating it backwards first. The NASA Exoplanet Archive is unreachable from this environment (egress policy returns 403 on CONNECT), so exoplanets.json cannot be regenerated here and still carries no periods. Behaviour is therefore unchanged until `npm run etl` is run somewhere with archive access, at which point every planet with a published period starts moving correctly with no further code changes. build.ts reports how many records gained a period, and rejects non-positive ones. Tests: 171 passing, up from 151, including a new end-to-end check that TRAPPIST-1 b with its real period completes exactly one orbit in 1.51088 days and sits a full diameter away at half that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
c1feb4b74e |
Rewrite the star field as instanced billboards
Plan step 3 promises glow and size driven by magnitude and spectral type, but the star field was a THREE.Points cloud and the WebGPU backend — the renderer this app targets — caps point primitives at a single pixel. Every one of the 8750 stars drew as an identical 1 px dot with a hard edge, discarding the magnitude sizing entirely; the class comment already admitted sizeNode only did anything on the WebGL2 fallback. Each star is now an instanced camera-facing quad on a SpriteNodeMaterial, which behaves the same on both backends. That material takes each instance's centre from positionNode rather than from an instance matrix, so position, colour and size ride on instanced buffer attributes and the mesh itself never moves. A radial falloff in opacityNode gives each star a bright core inside a soft halo. Sizes are angular rather than world-space. That keeps a star the same apparent size at any camera distance, which is both what the old screen-space points did and what is physically right: real stars are unresolvable point sources, so apparent size follows brightness, not distance. World-space quads would instead have made the whole field vanish at the camera's 2000 pc limit. Picking had to be rebuilt. Billboarding happens in the vertex shader, so the CPU-side geometry is one quad at the origin and Raycaster cannot see the star field at all. Selection is now done in screen space against the size each star is actually drawn at, which is strictly better than the fixed 1.2 pc world radius it replaces — that radius was over-permissive up close and sub-pixel at the far end of a 4000x camera range. Stars behind the camera need an explicit depth guard, because project() mirrors them back onto the screen. Two things only caught by running it. The colour attribute was declared with node type 'color', which is not a GLSL type, so the generated shader failed to compile — it has to be vec3. And the click tolerance was first written as a floor on the drawn radius, which flattened every star to one hit size, since a floor generous enough for the faintest star exceeds the brightest star's radius; adding the slop instead keeps a brighter star the easier target. Tests: 151 passing, up from 145. Verified in a real browser — shaders compile clean and the Playwright click-to-select flight passes against the new picking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
4aca223027 |
Regenerate the star catalogue, fixing 2331 names and 875 colours
Two ETL bugs, both fixed at the source and then re-run against HYG. Star ids,
ordering and positions are all unchanged, so stars.bin is byte-identical and
every exoplanet cross-reference still resolves.
Names. HYG's `gl` column already carries its own catalogue prefix ("Gl 581",
"GJ 3512"), unlike the bare numbers in `hd` and `hip`, so prefixing it again
produced 2331 of 8750 stars named "Gl GJ 1076". That corrupted three surfaces at
once: search, the on-screen labels, and exoplanet host-star name matching, which
compares normalised names and could never match "glgj1076" to "gj1076".
Colours. `Number(row['ci']) || 0` cannot tell a blank cell from a real zero, and
0 is a real B-V colour index meaning a hot blue-white A-type star. All 875
affected stars turned out to be blanks — the catalogue contains no genuine zero
inside the distance cutoff — so several hundred red dwarfs were rendering
blue-white. colorIndex is now `number | null` rather than defaulted, because any
numeric default is indistinguishable from a measurement.
Consumers resolve the gap from the spectral type instead. That needs real
parsing: HYG's `spect` column runs to 134 distinct spellings among the affected
stars alone, including a bare lowercase "m" for 354 of them, plus "k-m" ranges,
"dM4" luminosity prefixes and "K:" uncertainty flags. 622 of the 875 recover a
class this way — 497 of them M-class — and the remaining 253, which carry no
classification at all, fall back to neutral white.
The parse is anchored at the start of the string rather than scanning it. A scan
is the obvious implementation and is quietly wrong: the ETL writes the literal
"Unknown" for unclassified stars, that contains a K, and every one of those 253
would have been classified as an orange K-type. A test covers it.
Also lifts parseOptionalNumber out of fetchExoplanets into lib/csv, where both
fetchers now use it, and gives magnitude a faint default instead of 0 — no
current star is affected, but 0 would mean "as bright as Vega" and render an
unphotometered star as one of the largest points on the map.
Tests: 145 passing, up from 116, including the first coverage of
StarFieldRenderer. Build, both typechecks and the Playwright suite are green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
|
||
|
|
06cf7d2a15 |
Fix four defects a user hits in the first minute
Found by surveying the codebase against the plan; each was verified against the
committed assets or the running app before being touched.
TRAPPIST-1 was orbiting the Sun. The Exoplanet Archive leaves sy_dist blank for
some systems, and fetchExoplanets.ts read it with bare Number() — Number('') is
0, which is finite, so it slipped past the Number.isFinite guard in
resolveHostStarId, placed the host at the origin, and matched Sol at distance
exactly 0. 127 records shipped with hostStarId 0, all seven TRAPPIST-1 planets
among them, and the system view filters on that id, so drilling into Sol drew
127 alien worlds inside the real solar system.
Fixed in three places: resolveHostStarId now rejects a non-positive distance
(the robust guard, covering every caller), fetchExoplanets.ts uses the
parseOptionalNumber that already sat unused in that same file for ra/dec/dist,
and validateExoplanets asserts nothing ever resolves to the Sun again — the Sun
has no exoplanets, so that tripwire costs nothing and is permanent.
The archive's endpoint is blocked by this environment's egress policy, so the
ETL cannot be re-run here. The committed asset was corrected in place instead,
which is safe because the outcome is deterministic: the name path runs first and
none of the 127 resolve by name, so all of them reached id 0 positionally and
the fixed pipeline yields null for exactly that set. Cross-referenced hosts drop
from 761 to 634; record count is unchanged.
Dragging to rotate selected stars. Selection was bound to the raw click event,
which browsers fire on release however far the pointer travelled and which
OrbitControls does not suppress — so any drag ending over a star launched a
camera flight, and in system view routed away to /body/:id. Now tracks
pointerdown and ignores a release more than 5 px from it.
Ghost systems accumulated on every star-to-star hop. SystemOrbitsRenderer.dispose
released geometries and materials but never detached its group, so old orbit
lines stayed parented forever — still traversed and re-uploaded each frame with
disposed geometries, drawn over the new system and unpickable. dispose() now
detaches and clears.
Galaxy star labels stayed pinned inside the system view. They are CSS2D objects
parented to the scene rather than to galaxyGroup, so hiding the group left up to
15 parsec-space names clumped over the system's star. Cleared on entry. Also
gated the per-frame Kepler propagation on actually being in a system; it ran in
galaxy view too, because the renderer is never nulled on exit.
Tests: 116 passing, up from 112. Build, both typechecks and the Playwright suite
are green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
|
||
|
|
3a859360ba |
Add the deep-sky backdrop, the last unbuilt piece of the plan
The design doc scopes deep-sky objects as a galaxy-view backdrop and lists fetchDeepSky.ts, deepsky.json and deepsky.model.ts, but none of it existed — it was the only part of the plan with no implementation behind it. ETL: fetchDeepSky.ts pulls the OpenNGC catalog, classifies each object as a galaxy/nebula/cluster, and keeps the ~460 worth drawing (everything Messier, everything with a common name, and anything brighter than magnitude 9) out of ~12,000 mostly-anonymous rows. build.ts runs it and validates the output. Distances are the hard part: OpenNGC has no distance column, and both fallbacks fail for the best-known objects. M31, M33 and M42 are Local Group members whose redshift is negative or absent, and a galaxy's catalog parallax comes from a cross-matched foreground star — 6 mas for M31 would put a 780 kpc galaxy at 167 pc. So records store a unit direction on the celestial sphere rather than a position (the line of sight is always known precisely, and the objects are drawn on a fixed backdrop shell where true distance is unusable anyway), and distance is optional metadata carrying its own provenance. Parallax is trusted only for galactic objects, redshift only above z=0.003 where expansion outweighs peculiar velocity. 330 of 463 get a distance; the rest honestly report none. Rendering: DeepSkyRenderer paints the objects as soft additive billboards on a 2500 pc shell — clear of the 50 pc star field, beyond the camera's 2000 pc orbit limit, and inside its 5000 pc far plane. Size comes from real angular extent, so Andromeda is six times wider than the full Moon, clamped at both ends. Sprites rather than points because the WebGPU backend caps point primitives at one pixel; materials are shared per kind and brightness band, so 460 objects cost nine of them. The brightest dozen get permanent labels, which needed the label overlay to accept string ids alongside numeric star ids. The backdrop is decorative, so a failure to load its dataset is logged and the star field comes up regardless. Also documents the app in the README, which until now covered only the plugin marketplace. Tests: 112 passing, up from 54. Build, both typechecks and the Playwright suite are green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G |
||
|
|
d7e8ea1d4d |
@
Add star-map Angular app, ETL pipeline, and caveman plugin Angular 3D star map (galaxy/system/body views, Three.js rendering, navigation store) plus the NASA ETL tooling that builds the star, exoplanet and solar-system datasets, Playwright e2e suite, and the cs:caveman Claude Code plugin (command, agent, skill). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> @ |
||
|
|
1e1b58b0e9 | Initial commit |