29 KiB
SOLARGOD — execution brief
For: Opus 4.8. Read this whole file before writing any code.
SOLARGOD is a standalone, browser-based 3D solar system explorer — the sibling of
GODSIGH (the CesiumJS OSINT globe at /Users/m3ultra/Documents/GODSIGH, read-only
reference; do not modify it). GODSIGH proved the house architecture: no build step,
ES modules, a layer-factory contract, a HUD with per-layer status lines, a scrubbable
time slider with live/scrubbed gating, shareable hash-state URLs, and a zero-dependency
serve.py that statically serves the app and proxies the feeds a browser can't call
directly. SOLARGOD transplants all of that onto Three.js — Cesium is an
Earth-geodesy engine and is wrong for interplanetary space.
The experience: you load the page and you're floating above a compressed-scale ("MEGA mode") solar system — every planet visible at once, moving on real ephemerides, with a time bar you can scrub across centuries. Click Jupiter and the camera flies out and settles into orbit around it, moons circling. Toggle TRUE scale and the system animates out into its real, humbling emptiness. Live layers show where Voyager 1, JWST and Parker Solar Probe are right now, real asteroids, comets, and close approaches. Everything client-side, free, keyless.
0. Ground rules
- No build step. No npm. No framework. No TypeScript. Plain ES modules loaded by
the browser, Three.js via CDN importmap (pin an exact version, e.g.
three@0.170.0from jsdelivr; you need the core +three/addons/for OrbitControls, CSS2DRenderer, Lensflare). Vendoring the two Three.js files intovendor/instead of the CDN is acceptable and preferred if trivial, so the app works offline. - serve.py: stdlib-only Python static server + API proxy (see §6). Default port
8147 (GODSIGH owns 8137). Quickstart must be exactly
python3 serve.py→ openhttp://127.0.0.1:8147. - Deterministic first: the core scene (Sun, planets, moons, orbits, time travel) must work with zero network calls — planet positions are computed locally from Keplerian elements (§3), the way GODSIGH computes satellites from TLEs with SGP4. Network-dependent layers (spacecraft, asteroids, comets) are additive and must fail soft with a status line in the HUD, never break the scene.
- Truth before view: every position is computed in real heliocentric J2000-ecliptic AU first, then passed through the view transform (§4). Never compute directly in view units; never mix the two spaces. One module owns the transform.
- No n-body integration. The Standish Keplerian approximation IS the spec. Resist the urge to be more accurate than the mission needs.
- Verification gates are mandatory (§9). Each stage ends with its gate passing; don't move on with a failing gate. Measurement beats reasoning.
- Git: work on
stage/N-namebranches, merge tomainwhen the stage gate passes, push toorigin(Gitea, already configured). Commit style matches the house: emoji prefix + short message +(opus)suffix, e.g.🪐 kepler: planets on rails — ephemeris engine + orbit paths (opus).
1. Repo layout
solargod/
index.html # importmap, canvas, HUD skeleton, time bar
serve.py # static + Horizons/SBDB/CAD proxy + disk cache
css/style.css # dark UI, HUD, labels, time bar
js/
config.js # tunables: scale constants, camera, layer defaults, body registry refs
lib.js # time (JD/centuries), math helpers, eclToWorld(), formatters
ephem.js # Keplerian propagator + element tables (§3)
bodies.js # physical data registry: radii, tilts, rotation, colors, texture paths
scale.js # THE view transform: MEGA/TRUE compression, radius exaggeration (§4)
ui.js # HUD rows, status lines, LIVE/SCRUBBED chip (port GODSIGH's ui.js idioms)
main.js # bootstrap: renderer, clock, focus/camera system, layer loader, picking, hash state
layers/
planets.js # Stage 1
moons.js # Stage 3
rings.js # Stage 3
spacecraft.js # Stage 4
asteroids.js # Stage 5
comets.js # Stage 5
neos.js # Stage 5
assets/textures/ # 2k textures + CREDITS.md (§7)
cache/ # serve.py disk cache — gitignored
verify.html # in-browser test runner (§9)
SOLARGOD_BRIEF.md # this file
Layer contract — identical to GODSIGH (js/layers/*.js default-exports a factory):
export default function create(ctx) {
const { scene, camera, CONFIG, lib, ui, scale, ephem, bodies, focus, clock } = ctx;
// ui.addLayer(id, name, defaultOn, onToggle); ui.setStatus(id, text, 'ok'|'warn'|'err')
return {
id: 'x',
onClockTick(simDate, jd, isLive) {}, // every frame; keep allocation-free
handlePick(intersect) { return false; },
clearPick() {},
};
}
main.js loads layers with dynamic import() + Promise.allSettled so one broken
layer never kills the app (copy GODSIGH main.js:179-208 idiom).
2. Time system
- Simulation clock lives in
main.js: a float64 unix-mssimTimeMs, advanced each frame byrate * dtWallMs.lib.jsprovidesjdFromUnixMs(ms)(JD = ms/86400000- 2440587.5) and
centuriesSinceJ2000(jd)(T = (jd − 2451545.0)/36525). Ignore TT/TDB−UTC (~69 s) — irrelevant at this scale; note it in a comment.
- 2440587.5) and
- Range: 1800–2050 (Table 1 validity). Clamp scrubbing to it. An extended-range mode (Table 2a/2b, 3000 BC–3000 AD) is a stretch goal — build the table plumbing so it's a config swap, but don't build UI for it in v1.
- Controls (bottom bar): NOW button, play/pause, a rate control with presets
1×real · 1 min/s · 1 hr/s · 1 day/s · 1 wk/s · 1 mo/s · 1 yr/s(also negatives for reverse), a scrubbable timeline strip spanning the full range with a zoomed inner strip around the cursor, and a date readout (UTC). - LIVE / SCRUBBED chip exactly like GODSIGH: live = |sim − wallclock| ≤ 60 s at 1× rate. Live-only network layers pause off-live where that matters (spacecraft layer interpolates its cached span instead).
3. Ephemeris engine (ephem.js) — Standish approximate elements
Source: JPL SSD "Approximate Positions of the Planets"
(https://ssd.jpl.nasa.gov/planets/approx_pos.html). The tables below were extracted
from that page on 2026-07-15 — they are authoritative, use them verbatim.
(The page's e column header says "rad" — that's the page's own typo; e is
dimensionless. Angles are degrees, rates per Julian century.)
Table 1 — J2000 ecliptic elements + rates/century, valid 1800–2050 AD
(rows: a[au] e I[deg] L[deg] long.peri ϖ[deg] long.node Ω[deg], second line = rates/Cy):
Mercury 0.38709927 0.20563593 7.00497902 252.25032350 77.45779628 48.33076593
0.00000037 0.00001906 -0.00594749 149472.67411175 0.16047689 -0.12534081
Venus 0.72333566 0.00677672 3.39467605 181.97909950 131.60246718 76.67984255
0.00000390 -0.00004107 -0.00078890 58517.81538729 0.00268329 -0.27769418
EM Bary 1.00000261 0.01671123 -0.00001531 100.46457166 102.93768193 0.0
0.00000562 -0.00004392 -0.01294668 35999.37244981 0.32327364 0.0
Mars 1.52371034 0.09339410 1.84969142 -4.55343205 -23.94362959 49.55953891
0.00001847 0.00007882 -0.00813131 19140.30268499 0.44441088 -0.29257343
Jupiter 5.20288700 0.04838624 1.30439695 34.39644051 14.72847983 100.47390909
-0.00011607 -0.00013253 -0.00183714 3034.74612775 0.21252668 0.20469106
Saturn 9.53667594 0.05386179 2.48599187 49.95424423 92.59887831 113.66242448
-0.00125060 -0.00050991 0.00193609 1222.49362201 -0.41897216 -0.28867794
Uranus 19.18916464 0.04725744 0.77263783 313.23810451 170.95427630 74.01692503
-0.00196176 -0.00004397 -0.00242939 428.48202785 0.40805281 0.04240589
Neptune 30.06992276 0.00859048 1.77004347 -55.12002969 44.96476227 131.78422574
0.00026291 0.00005105 0.00035372 218.45945325 -0.32241464 -0.00508664
Table 2a — same format, valid 3000 BC–3000 AD (ship it in the file, unused by v1 UI):
Mercury 0.38709843 0.20563661 7.00559432 252.25166724 77.45771895 48.33961819
0.00000000 0.00002123 -0.00590158 149472.67486623 0.15940013 -0.12214182
Venus 0.72332102 0.00676399 3.39777545 181.97970850 131.76755713 76.67261496
-0.00000026 -0.00005107 0.00043494 58517.81560260 0.05679648 -0.27274174
EM Bary 1.00000018 0.01673163 -0.00054346 100.46691572 102.93005885 -5.11260389
-0.00000003 -0.00003661 -0.01337178 35999.37306329 0.31795260 -0.24123856
Mars 1.52371243 0.09336511 1.85181869 -4.56813164 -23.91744784 49.71320984
0.00000097 0.00009149 -0.00724757 19140.29934243 0.45223625 -0.26852431
Jupiter 5.20248019 0.04853590 1.29861416 34.33479152 14.27495244 100.29282654
-0.00002864 0.00018026 -0.00322699 3034.90371757 0.18199196 0.13024619
Saturn 9.54149883 0.05550825 2.49424102 50.07571329 92.86136063 113.63998702
-0.00003065 -0.00032044 0.00451969 1222.11494724 0.54179478 -0.25015002
Uranus 19.18797948 0.04685740 0.77298127 314.20276625 172.43404441 73.96250215
-0.00020455 -0.00001550 -0.00180155 428.49512595 0.09266985 0.05739699
Neptune 30.06952752 0.00895439 1.77005520 304.22289287 46.68158724 131.78635853
0.00006447 0.00000818 0.00022400 218.46515314 0.01009938 -0.00606302
Table 2b — extra terms added to M for Jupiter–Neptune, Table 2a only
(M += b·T² + c·cos(f·T) + s·sin(f·T), everything in degrees, T in centuries):
Jupiter -0.00012452 0.06064060 -0.35635438 38.35125000
Saturn 0.00025899 -0.13434469 0.87320147 38.35125000
Uranus 0.00058331 -0.97731848 0.17689245 7.67025000
Neptune -0.00041348 0.68346318 -0.10162547 7.67025000
Algorithm (per body, per tick — this is the whole engine, ~80 lines):
T = centuriesSinceJ2000(jd); each elementx = x0 + ẋ·T.- Argument of perihelion
ω = ϖ − Ω; mean anomalyM = L − ϖ(+ Table 2b terms if using Table 2a for the outer planets). Normalize M to (−180°, +180°]. - Solve Kepler's equation
E − e·sin E = M(radians) by Newton:E₀ = M + e·sin M, iterateE -= (E − e·sinE − M)/(1 − e·cosE), tol 1e-9 rad, max 20 iterations. - Orbital-plane coordinates:
x′ = a(cos E − e),y′ = a·√(1−e²)·sin E. - Rotate to heliocentric J2000 ecliptic:
x = (cosω·cosΩ − sinω·sinΩ·cosI)·x′ + (−sinω·cosΩ − cosω·sinΩ·cosI)·y′ y = (cosω·sinΩ + sinω·cosΩ·cosI)·x′ + (−sinω·sinΩ + cosω·cosΩ·cosI)·y′ z = (sinω·sinI)·x′ + (cosω·sinI)·y′ - Earth = EM Bary for rendering purposes (the barycenter offset is ~4 700 km, invisible at any scale we draw). The Moon is drawn by the moons layer around Earth.
Expose ephem.helioEcl(bodyId, jd) -> {x,y,z} [AU] and
ephem.orbitPath(bodyId, jd, nSamples) -> Float64Array (one full period sampled in
mean anomaly, re-used for orbit lines; regenerate lazily when |T| drifts, elements
change slowly). The same keplerToEcl(elements, jd) core must be reusable for
asteroids/comets in Stage 5 (they come as epoch elements, not element rates:
propagate with M = M0 + n·(jd − epoch), n from a via Kepler's third law,
n[deg/day] = 0.9856076686 / a^1.5). Skip hyperbolic/parabolic bodies (e ≥ 0.98)
in v1 — filter them out with a status note.
Frame convention (fix once in lib.eclToWorld(v)): ecliptic X → Three.js x,
ecliptic Z (north) → Three.js y, ecliptic Y → Three.js −z (keeps the frame
right-handed with Y-up). All of Three-space is "world"; nothing outside lib.js
does this mapping by hand.
4. The view transform (scale.js) — "compressed mega"
The whole product lives or dies here. Two scale modes, one smooth parameter.
- MEGA mode (default): compress heliocentric radius only, keep direction true:
r_view = K · r_AU^Pwith P = 0.4, K = 9.0 as starting tunables inconfig.js(puts Mercury ≈ 6.2, Earth 9, Jupiter ≈ 17.4, Neptune ≈ 35 scene units — the whole system fits in one ~80-unit view). Orbit paths must be built by compressing the sampled true positions with the same function so body and path always agree. - TRUE mode:
r_view = K · r_AU(linear, 1 AU = K units). Mostly empty space. That's the point. - The transition is a feature: animate exponent
Pfrom 0.4 → 1.0 over ~2.5 s (smoothstep) when toggling. Everything — bodies, paths, trails — flows outward. Implement asscale.radial(r_AU)reading a liveP; nothing else changes. - Body draw radius: true radius in AU (
r_km / 1.496e8) × view K × an exaggeration factorE. Default E = 1200 for planets, Sun gets its own (E_sun ≈ 60, else it swallows Mercury). As the camera approaches a focused body, lerp its effective E → 1 based oncameraDistance / (E · trueDrawRadius)so you can descend to something like true scale in close orbit without being inside a balloon. Expose a body-scale slider (1× / 100× / 1200×) in the HUD. - Moon-system compression (Stage 3): moons get local compression around the
parent:
d_view = R_parent_draw · (1.8 + 1.2 · log10(1 + d_true_km / R_parent_km))— guarantees every moon clears the exaggerated parent surface and preserves ordering. In TRUE mode use plain linear true distance. Same animated blend. - Floating origin (critical): float32 GPU precision jitters when the camera sits
1e-4 units from a position of magnitude 30+. Therefore: all true positions are kept
in float64 JS; each frame,
focusPos(the focused body's view-space position) is subtracted from every rendered object's position in JS before writing to the scene graph, and the camera orbits near the scene origin. Focus = Sun at boot. This also makes "in orbit around a moving planet" free — the planet stays at the origin and the sky moves. UselogarithmicDepthBuffer: trueon the renderer for the huge near/far span.
5. Navigation & UX (main.js + ui.js)
- Focus system: click a body (raycast, generous sphere hit targets) or pick from
a body menu → animated camera flight (~2 s eased): tween the focus point from old
to new body while tweening camera distance to
~6× the target's draw radius, then hand over to OrbitControls targeting the origin (floating origin does the rest). Breadcrumb top-left:SOL ▸ JUPITER ▸ EUROPA.Escsteps out one level. - Labels: CSS2DRenderer DOM labels per body — name + (for the focused body's children) distance. Fade by screen density; never let labels overlap the HUD.
- HUD (right, GODSIGH style): layer rows with toggle + live status line
(
spacecraft — 8 tracked, Horizons ok), LIVE/SCRUBBED chip, MEGA/TRUE toggle, body-scale slider, mode credits footer. - Hash state (port GODSIGH
main.js:85-163wholesale):#f=<bodyId>&t=<offsetSec|@unixMs>&s=<mega|true>&L=<on,ids>&cam=<dist,heading,pitch>— poll-serialize 1 Hz withhistory.replaceState, apply once at boot, reject malformed tokens (GODSIGH learned:Number('')is 0 — validate every token). - Info panel (Stage 6, left slide-in on focus): physical fact card (from
bodies.js) + live computed facts — current distance from Sun and from Earth (AU + km), light-time ("sunlight is 43 min old at Jupiter right now"), current orbital speed, length of day/year. These live numbers are the soul of the tool — they come free from the ephemeris.
6. serve.py — static + proxy + cache
stdlib only (http.server, urllib.request, hashlib, json, pathlib). Serves
the repo dir; plus proxy endpoints (the browser cannot call JPL directly — verified:
no CORS headers on ssd.jpl.nasa.gov or ssd-api.jpl.nasa.gov):
/proxy/horizons?<query>→https://ssd.jpl.nasa.gov/api/horizons.api?<query>/proxy/sbdb?<query>→https://ssd-api.jpl.nasa.gov/sbdb_query.api?<query>/proxy/cad?<query>→https://ssd-api.jpl.nasa.gov/cad.api?<query>
Allowlist the upstream hosts + paths (never a general proxy). Disk cache in
cache/ keyed by sha256 of the full upstream URL: responses whose requested time
span lies entirely in the past never expire; anything else TTL 24 h. Be polite to
JPL — the cache is the rate limiter. Print one boot line like GODSIGH's serve.py.
7. Textures & assets
- Planet/moon textures: Solar System Scope texture pack (CC BY 4.0,
https://www.solarsystemscope.com/textures/) — download the 2k maps for Sun, the
8 planets, Moon, plus the 8k→resized-to-4k Milky Way panorama for the skybox and
the Saturn ring texture (PNG with alpha). Commit them under
assets/textures/with anassets/textures/CREDITS.md(CC BY requires attribution; also credit in the HUD footer). Keep the committed set under ~25 MB total; downscale withsipsif needed. - Every textured material needs a procedural fallback (flat color from
bodies.js) so a missing file degrades gracefully. - Moons without textures: colored lambert spheres. Sun: emissive texture + additive sprite glow (+ subtle Lensflare addon if cheap).
- Rings: Saturn — annulus geometry, inner 74 500 km / outer 140 220 km (true km,
scaled with the parent's draw scale), ring texture,
DoubleSide, transparent. Uranus gets a thin schematic gray ring. Tilt with the parent's axial tilt.
7b. Generated art — the MODELBEAST pipeline
The M3 Ultra runs MODELBEAST (/Users/m3ultra/Documents/MODELBEAST, web UI
http://localhost:8777, tailnet http://100.89.131.57:8777) — a local job
queue/load manager with an on-device FLUX operator (flux_local, mflux/MLX,
flux2-klein-4b ungated, seed-reproducible, free and unlimited). Drive it
headless with the mb CLI (see MODELBEAST/AGENTS.md for the full reference):
# shared agent credential — pre-provisioned, just source it (never commit/paste it):
source ~/Documents/MODELBEAST/data/agent.env # sets MB_HOST + MB_TOKEN
MB=~/Documents/MODELBEAST/mb
# house workflow — run 5 seeds, pick the best:
for S in 1 2 3 4 5; do
$MB run flux_local -p prompt="$PROMPT" -p model=flux2-klein-4b \
-p seed=$S -p width=1024 -p height=1024 --wait --download art/candidates/
done
The credential is the agents guest user (verified working 2026-07-15: submit →
generate ≈ 7 s/image → download). Guest = local operators only — flux_local,
bg_remove_local (alpha cutouts), seedvr2_upscale — all free; the owner's paid
fal/OpenRouter operators are blocked server-side, so generate without cost anxiety.
The gpu lane runs one job at a time — queue all 5 and wait, don't spam retries
on queued. If data/agent.env is missing, re-mint it with
cd ~/Documents/MODELBEAST && uv run python scripts/agent_token.py; if MODELBEAST
is down entirely, generate nothing and leave a TODO list of prompts in
assets/art/PROMPTS.md instead — never block a stage on art.
The truth boundary (non-negotiable): SOLARGOD is a scientific instrument. Real worlds get real data textures (§7 — NASA/Solar System Scope maps); FLUX must never invent the surface of Mars or the face of Europa. Generated art owns the aesthetic layer only, always visually distinct from data:
- Splash/boot screen (Stage 0, optional polish): one hero image, brass-and-void observatory aesthetic to match the god-app family.
- Grimoire illustrations (Stage 6): one stylized portrait per body for the info
panel — engraving/astrolabe style, clearly art, framed separately from the
rendered planet. ~800×800, 5-seed runs, pick best, note chosen seed+prompt in
assets/art/PROMPTS.mdso any card can be regenerated in-style. - Effect sprites: comet tail/coma, sun glow, spacecraft glyph icons — generate,
cutout via
fal_bg_remove, save as alpha PNGs. - Optional "DREAM skybox" toggle (off by default): a FLUX nebula panorama as an alternate sky. The default sky stays the real Milky Way panorama — from inside the solar system the real sky is a data statement too. FLUX doesn't produce seamless 2:1 equirects reliably; hide the seam behind the anti-camera side or mirror-wrap, and don't burn time here.
Log every shipped asset in assets/art/PROMPTS.md (prompt, model, seed, operator
chain) — reproducibility is the house rule for art too. The §7 texture budget
(~25 MB committed) covers generated art as well; art candidates/rejects stay in
art/candidates/ (gitignore it).
8. Physical data (bodies.js)
Registry keyed by body id with: name, type, radiusKm, axialTiltDeg, rotationHours (negative = retrograde), color, texture, parent, elements|moonOrbit. Radii to use:
Sun 695 700 · Mercury 2 439.7 · Venus 6 051.8 · Earth 6 371.0 · Mars 3 389.5 ·
Jupiter 69 911 · Saturn 58 232 · Uranus 25 362 · Neptune 24 622 · Moon 1 737.4 ·
Pluto 1 188.3. Source the rest (tilts, rotation periods, moon data) from JPL SSD
(https://ssd.jpl.nasa.gov/planets/phys_par.html and /sats/elem/) — fetch and check,
don't trust training memory for numbers.
Moons v1 roster (circular orbits in the parent's equatorial plane; correct a and
period, phase = deterministic function of jd so scrubbing is stable). Starter values
(verify against JPL): Moon 384 400 km / 27.322 d · Phobos 9 378 / 0.319 · Deimos
23 459 / 1.262 · Io 421 700 / 1.769 · Europa 671 034 / 3.551 · Ganymede 1 070 412 /
7.155 · Callisto 1 882 709 / 16.689 · Mimas 185 539 / 0.942 · Enceladus 238 042 /
1.370 · Tethys 294 672 / 1.888 · Dione 377 415 / 2.737 · Rhea 527 068 / 4.518 ·
Titan 1 221 870 / 15.945 · Iapetus 3 560 851 / 79.33 · Miranda 129 900 / 1.413 ·
Ariel 190 900 / 2.520 · Umbriel 266 000 / 4.144 · Titania 436 300 / 8.706 · Oberon
583 500 / 13.46 · Triton 354 759 / −5.877 (retrograde) · Charon 19 591 / 6.387.
Pluto itself: treat as a Stage 5 small body (SBDB elements), but give it a real
texture-less sphere and Charon.
9. Verification gates (verify.html)
A standalone page that imports lib.js + ephem.js, runs assertions, renders a
PASS/FAIL table. No test framework. Ground truth from JPL Horizons (heliocentric,
J2000 ecliptic, AU, TDB — fetched 2026-07-15 from the live API; JD 2461236.5 =
2026-Jul-15 00:00 TDB):
| Body | X | Y | Z | tolerance |
|---|---|---|---|---|
| Mars (499) | +1.05351078 | +1.00862522 | −0.00469549 | ≤ 0.005 AU each axis |
| Jupiter (599) | −3.01967256 | +4.33218144 | +0.04956505 | ≤ 0.01 AU |
| EM Bary (3) | +0.38381080 | −0.94118486 | +0.00005421 | ≤ 0.003 AU |
Add at least two more epochs (e.g. J2000.0 itself and 1900-01-01) by querying
Horizons through the proxy from verify.html and comparing live — that also gates
the proxy. Additional gates: Kepler solver converges for e = 0.97; orbitPath
endpoints join; eclToWorld round-trips; MEGA→TRUE transition keeps direction
(unit-vector dot ≈ 1). Spacecraft gate (Stage 4): Voyager 1 (COMMAND='-31') on
2026-07-15 must plot at (−32.07, −136.21, +98.55) AU ecliptic — ~172 AU out,
high above the plane; JWST (-170) must hug Earth (Δ < 0.02 AU).
10. Stages
Branch per stage; gate before merge; one commit per coherent step.
- Stage 0 — GENESIS (scaffold): repo layout,
.gitignore(cache/,.DS_Store), serve.py static-only, index.html + importmap, renderer with logarithmic depth, Milky Way skybox, Sun with glow, OrbitControls, HUD + time-bar skeletons (static), 60 fps. Gate: boots clean, no console errors,verify.htmlscaffolding runs. - Stage 1 — KEPLER (the engine):
ephem.js+ tables, planets layer (textured, exaggerated), orbit paths, labels, full time system (§2) driving it, MEGA layout. Gate: §9 Horizons checkpoints pass; scrub 1800→2050 smoothly; retrograde loops of Mars visible when watched from Earth-focus (eyeball check). - Stage 2 — HELM (navigation): focus system, camera flights, floating origin, breadcrumb, MEGA/TRUE animated toggle, body-scale slider, hash-state URLs, proxy endpoints in serve.py (+ cache). Gate: jump Sun→Jupiter→back at 60 fps with zero visible jitter while clock runs at 1 mo/s; a pasted hash URL reproduces the view.
- Stage 3 — RETINUE (worlds become places): moons layer w/ local compression, axial tilts, rotation (textures spin at true rates), sun-lit materials (day/night terminator falls out of a single point light at the Sun — verify Earth's night side faces away), Saturn/Uranus rings. Gate: Galilean moons orbit with correct relative periods (Io:Europa:Ganymede ≈ 1:2:4 — count laps at 1 day/s); Triton runs backwards.
- Stage 4 — PROBES (the live layer): spacecraft via Horizons proxy. Roster:
Voyager 1
-31, Voyager 2-32, New Horizons-98, JWST-170, Parker Solar Probe-96, plus 2–3 more that resolve cleanly (check Juno, Lucy, Psyche ids via Horizons lookup API). Fetch VECTORS at 1-day steps spanning the clock window, through the cache; cubic-interpolate between samples; glowing marker + fading trail + label with live distance and one-way light time. Fail soft per craft. Gate: §9 spacecraft checkpoints. - Stage 5 — SWARM (small bodies): main-belt sample via SBDB query (~1–2 k
asteroids with
a,e,i,om,w,ma,epoch, propagated by the shared Kepler core, InstancedMesh points); NEO close approaches this ±30 d via CAD API (highlighted, clickable, miss distance in lunar distances); famous comets via SBDB lookup (1P/Halley, 2P/Encke, 67P, 96P — elliptical only) with full drawn orbits and anti-sunward tail sprite scaled by 1/r². All three are separate HUD layers, off-by-default except comets. Gate: belt sits between Mars and Jupiter (not a sphere, not a line); Halley's aphelion beyond Neptune's orbit… in the compressed view, near it. - Stage 6 — ALMANAC (knowing): info panel with live facts (§5), per-body prose
blurbs (2–3 sentences, hand-written, grimoire voice), grimoire illustrations via
MODELBEAST (§7b — skip gracefully if no token), Earth Easter egg — focusing
Earth shows "▸ open GODSIGH world view" linking
http://127.0.0.1:8137, README.md with the full GODSIGH-style layer table and screenshots (docs/), HUD credits. Gate: every clickable body has a card; README quickstart works from a fresh clone. - Stage 7 — RESONANCE (optional, only if explicitly requested later): the
godstrument bridge — serve.py WebSocket broadcasting
{body: {helioLonDeg, rAU, speedKms}}at 10 Hz for sonification (music of the spheres: orbital periods as polyrhythms). Do not build in this pass.
11. Performance rules
- Zero allocations in
onClockTickpaths — module-scope scratchVector3s. - Orbit paths: build once, rebuild only when elements drift (|ΔT| > 0.1 Cy) or scale
mode changes; the MEGA/TRUE transition may rebuild per-frame during the 2.5 s
tween only (or better: write paths in true AU and compress in a tiny custom shader
with
Pas a uniform — do this only if the CPU rebuild visibly hitches). - Asteroids: one InstancedMesh (or Points), positions updated at ≤ 10 Hz, not every frame, unless rate > 1 day/s.
- Labels: update DOM at ≤ 5 Hz; cull labels behind the camera.
- Target: 60 fps with all layers on, MacBook-class GPU; test with
stats-style frame readout available behind?debug=1.
12. Known traps (read twice)
- Float32 jitter — floating origin is not optional (§4). Symptom: focused planet "swims" at close zoom while time runs.
- Mixing spaces — if a body and its orbit path disagree, someone compressed twice or not at all. One transform module, everything flows through it.
M = L − ϖcan exceed ±360° enormously for Mercury (L rate 149 473°/Cy) — normalize after the subtraction, in float64.- Horizons is text-first: with
format=jsonthe payload is still one big text blob inresult— parse the$$SOE…$$EOEblock by line. RequestVEC_TABLE='1'&REF_PLANE='ECLIPTIC'&CENTER='500@10'&OUT_UNITS='AU-D'. - Don't hammer JPL while iterating — the disk cache exists so a dev reload costs zero upstream calls. Check the cache works before Stage 4 fetch loops.
- Texture download links from solarsystemscope.com occasionally move — if a fetch
404s, fall back to procedural colors and leave a
warnstatus, don't block. - Retrograde ≠ negative inclination — Triton is retrograde via i > 90° (or the negative-period convention in §8; pick one and comment it).
- First Horizons call for a spacecraft can take 2–5 s — status line must say
fetching…so it doesn't read as broken.