Commit Graph
11 Commits
Author SHA1 Message Date
Claude 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
2026-08-05 07:40:10 +00:00
Claude 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
2026-08-05 07:30:02 +00:00
Claude 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
2026-08-05 06:52:22 +00:00
Claude 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
2026-08-04 20:32:31 +00:00
Claude 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
2026-08-04 19:59:01 +00:00
Claude 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
2026-08-04 18:05:07 +00:00
Claude 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
2026-08-04 16:10:17 +00:00
Claude 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
2026-08-04 11:11:23 +00:00
Claude 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
2026-08-03 15:19:39 +00:00
Senrokai 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>
@
2026-08-03 16:50:10 +02:00
Senrokai 1e1b58b0e9 Initial commit 2026-07-15 16:54:27 +02:00