470 lines
29 KiB
Markdown
470 lines
29 KiB
Markdown
# 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.0`
|
||
from jsdelivr; you need the core + `three/addons/` for OrbitControls, CSS2DRenderer,
|
||
Lensflare). Vendoring the two Three.js files into `vendor/` 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` →
|
||
open `http://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-name` branches, merge to `main` when the stage gate passes,
|
||
push to `origin` (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):
|
||
|
||
```js
|
||
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-ms `simTimeMs`, advanced each
|
||
frame by `rate * dtWallMs`. `lib.js` provides `jdFromUnixMs(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.
|
||
- **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):
|
||
|
||
1. `T = centuriesSinceJ2000(jd)`; each element `x = x0 + ẋ·T`.
|
||
2. Argument of perihelion `ω = ϖ − Ω`; mean anomaly `M = L − ϖ` (+ Table 2b terms if
|
||
using Table 2a for the outer planets). Normalize M to (−180°, +180°].
|
||
3. Solve Kepler's equation `E − e·sin E = M` (radians) by Newton:
|
||
`E₀ = M + e·sin M`, iterate `E -= (E − e·sinE − M)/(1 − e·cosE)`, tol 1e-9 rad,
|
||
max 20 iterations.
|
||
4. Orbital-plane coordinates: `x′ = a(cos E − e)`, `y′ = a·√(1−e²)·sin E`.
|
||
5. 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′
|
||
```
|
||
6. **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^P` with **P = 0.4, K = 9.0** as starting tunables in `config.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 `P` from 0.4 → 1.0 over ~2.5 s
|
||
(smoothstep) when toggling. Everything — bodies, paths, trails — flows outward.
|
||
Implement as `scale.radial(r_AU)` reading a live `P`; nothing else changes.
|
||
- **Body draw radius**: true radius in AU (`r_km / 1.496e8`) × view K × an
|
||
exaggeration factor `E`. 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 on `cameraDistance / (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. Use `logarithmicDepthBuffer: true` on 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`. `Esc` steps 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-163` wholesale):
|
||
`#f=<bodyId>&t=<offsetSec|@unixMs>&s=<mega|true>&L=<on,ids>&cam=<dist,heading,pitch>`
|
||
— poll-serialize 1 Hz with `history.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
|
||
an `assets/textures/CREDITS.md` (CC BY requires attribution; also credit in the HUD
|
||
footer). Keep the committed set under ~25 MB total; downscale with `sips` if 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):
|
||
|
||
```sh
|
||
# 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.md` so 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.html` scaffolding 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 `onClockTick` paths — module-scope scratch `Vector3`s.
|
||
- 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 `P` as 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=json` the payload is still one big text blob
|
||
in `result` — parse the `$$SOE…$$EOE` block by line. Request
|
||
`VEC_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 `warn` status, 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.
|