468c98b14a7387e9a40a22693afc46b18a0a285e
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
468c98b14a |
Surface the system view with the photographs it already had, lit by its star
Every body in the system view was an unlit sphere wearing a 32 by 16 pixel procedural texture — the size chosen when a marker was a few pixels across and what survived was its average colour. The thirteen real photographs in `src/assets/textures/bodies/` were used only by the detail page. So Mars was a pale grey ball with invented polar caps while its own NASA mosaic sat unread in the repository, and nothing had a day side or a night side. Each marker now takes its own photograph where one exists, at the size the detail page uses, and the derived texture only where none does — the five moons no probe mapped, and every exoplanet, none of which has ever been imaged. The material is lit, and the light is a point at the star, so each world shows the terminator where it really falls. The light does not fall off with distance. Under the inverse square that real light obeys, Neptune receives a thousandth of what Mercury does and reads as black; the map is a set of worlds to look at rather than a light meter, so each is lit as a photograph of it would be. That is the same concession the pixel floor makes for size, and it is only about brightness: the *direction* is real. Spheres are 32 by 24 rather than 16 by 12, since at true scale a body is drawn anywhere from a pixel to the whole frame and the old silhouette was visibly faceted at the near end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
225d676ab0 |
Turn each body at its own rate, from Horizons' own figures
The view had one rotation in it — the planet on the detail page, at 0.08 rad/s, a number with no source. Nothing in the system view turned at all. The data was already on disk: every cached Horizons page carries how its body spins, in one of five forms. The rate in radians per second is preferred where it appears, because it is signed — that is how Venus and Uranus are known to turn backwards — then a period in hours or days, then the `9h 55m 29.711 s` the giant planets use, and finally the word every major moon here carries instead of a number: Synchronous. A tidally locked moon's day is its orbit, so Kepler supplies it from the elements already parsed and the parent it goes round. Seventeen of the eighteen bodies come out within 1% of their published period — Earth 23.934 h, Jupiter 9.925 h, Venus -5832.5 h, Io 42.5 h, Callisto 400.5 h. Titan is the exception: its page states no period at all, so it is left still rather than turned at an invented rate. The axis is the orbit normal tilted by the obliquity about the orbit's ascending node, which is where an obliquity is measured from and the only line in the orbit the elements name. The phase at the epoch is published for none of these bodies, so the face turned toward the camera is not a claim; the rate and the direction are. At true rates nothing is visible moving — Earth turns 15 degrees an hour. A clock the reader can run faster is the next piece, and the audit asks for it anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi |
||
|
|
3f0abf8717 |
Draw the system at true scale, drop the halo, and refuse to enter what is off screen
Three changes to what the system view claims, all of them the same claim: that the sizes on screen mean something. **The halo is gone.** It was a sprite sized against the arrival frame — 1.12 AU for the Sun — so it stayed that wide as the camera closed in and ended up a flat gradient filling the screen, over the photograph it was meant to dress. It existed to keep the star visible at a framing that holds the whole system, which is now handled in pixels instead. **Bodies are drawn at their own radius.** The old marker size was exaggerated and scaled to the system span, and clamped: Jupiter and Ganymede both ran past the ceiling and were drawn at one radius, so every moon orbited inside its planet, and Phobos and Triton sat entirely within Mars and Neptune. True scale needs no rule against that — physics already puts a moon outside the planet it orbits. What it costs is visibility at the arrival framing, where every body is sub-pixel, so the scene floors each marker at 3 px on screen and holds a moon to half its planet's drawn size. Measured in the Sun's system: at arrival, planets 3 px and moons 1.5 px, against 3 px for everything before; at Jupiter, the planet 10.8 px at scale 1 with the Galilean moons on their orbits outside it. The Sun is drawn at its own radius too. Every other star keeps a size derived from its innermost orbit, because no stellar radius reaches the app — Gaia's `radius_gspphot` is the obvious next fetch. **A click cannot enter a system that is not on screen.** The picker tested depth but not the frame, and a star's hit area is its drawn size plus a slop, so a click in the last pixels of the view could fly into a system outside it, with nothing on screen to explain where it had gone. Co-Authored-By: Claude Opus 5 (1M context) <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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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> @ |