merge perihelion/o-closer → main: README wave-2 refresh, event band, directorial moon tint, HERALD prep (fable)
This commit is contained in:
commit
5185c6fa41
77
README.md
77
README.md
@ -3,11 +3,13 @@
|
|||||||
A standalone, browser-based 3D solar-system explorer — the interplanetary sibling
|
A standalone, browser-based 3D solar-system explorer — the interplanetary sibling
|
||||||
of [GODSIGH](../GODSIGH), which watches Earth. Float above a compressed-scale
|
of [GODSIGH](../GODSIGH), which watches Earth. Float above a compressed-scale
|
||||||
("MEGA") solar system with every world visible at once, moving on **real JPL
|
("MEGA") solar system with every world visible at once, moving on **real JPL
|
||||||
ephemerides**; scrub a time bar across centuries; fly the camera into orbit around
|
ephemerides**; scrub a time bar across **six thousand years**; fly the camera into
|
||||||
any planet, its moons circling; toggle **TRUE scale** and watch the system animate
|
orbit around any planet, its moons circling, textured and tidally locked; toggle
|
||||||
out into its real, humbling emptiness. Live layers plot where Voyager 1, JWST and
|
**TRUE scale** and watch the system animate out into its real, humbling emptiness.
|
||||||
Parker Solar Probe are *right now*, plus real asteroids, comets and Earth
|
Live layers plot where Voyager 1, JWST and Parker Solar Probe are *right now*, real
|
||||||
close-approaches. Everything is client-side, free, and keyless.
|
main-belt asteroids, comets, and near-Earth close-approaches on their **true
|
||||||
|
orbits** — and a computed almanac of oppositions and elongations on the time bar.
|
||||||
|
Everything is client-side, free, keyless, and runs **fully offline**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -20,24 +22,29 @@ python3 serve.py # stdlib only — no npm, no build step
|
|||||||
|
|
||||||
`serve.py` statically serves the app and proxies the JPL feeds a browser can't call
|
`serve.py` statically serves the app and proxies the JPL feeds a browser can't call
|
||||||
directly (Horizons / SBDB / CAD — no CORS), with a disk cache in `cache/`. Three.js
|
directly (Horizons / SBDB / CAD — no CORS), with a disk cache in `cache/`. Three.js
|
||||||
loads from a pinned CDN importmap. The deterministic core (Sun, planets, moons,
|
is **vendored** under `vendor/three/`, so the app runs with no CDN. The
|
||||||
orbits, time travel) runs with **zero network calls** — positions are computed
|
deterministic core (Sun, planets, moons, orbits, sky events, time travel) runs with
|
||||||
locally from Keplerian elements; network layers are additive and fail soft.
|
**zero network calls** — positions are computed locally from Keplerian elements;
|
||||||
|
network layers (spacecraft, asteroids, comets, close-approaches) are additive and
|
||||||
|
fail soft.
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
Open **http://127.0.0.1:8147/verify.html** — an in-browser gate runner that checks
|
Open **http://127.0.0.1:8147/verify.html** — an in-browser gate runner that checks
|
||||||
the ephemeris against **JPL Horizons ground truth** (hard-coded checkpoints + live
|
the ephemeris against **JPL Horizons ground truth** (hard-coded checkpoints + live
|
||||||
queries through the proxy). All 13 gates should read PASS, e.g.:
|
queries through the proxy). All **22 gates** should read PASS, e.g.:
|
||||||
|
|
||||||
| Gate | Result |
|
| Gate | Result |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| Mars / Jupiter / Earth @ 2026-07-15 (heliocentric ecliptic AU) | Δ ≤ 3.3e-4 / 2.9e-3 / 3.9e-5 AU |
|
| Mars / Jupiter / Earth @ 2026-07-15 (heliocentric ecliptic AU) | Δ ≤ 3.3e-4 / 2.9e-3 / 3.9e-5 AU |
|
||||||
| Kepler solver converges at e=0.97 · orbitPath closes · frame round-trips | exact |
|
| Kepler @ e=0.97 · orbitPath closes · frame round-trips · MEGA→TRUE keeps direction | exact |
|
||||||
| MEGA→TRUE preserves direction (unit dot ≈ 1) | 1.000000000000 |
|
|
||||||
| Mars @ J2000 · Jupiter @ 1900 vs **live** Horizons | Δ 4.8e-5 / 2.0e-3 AU |
|
| Mars @ J2000 · Jupiter @ 1900 vs **live** Horizons | Δ 4.8e-5 / 2.0e-3 AU |
|
||||||
| Main belt between Mars & Jupiter · Halley aphelion beyond Neptune | pass |
|
| Main belt between Mars & Jupiter · Halley aphelion beyond Neptune | pass |
|
||||||
| Voyager 1 @ 2026-07-15 = (−32.07, −136.21, +98.55) AU · JWST hugs Earth | Δ 0.002 / 0.011 AU |
|
| Voyager 1 @ 2026-07-15 = (−32.07, −136.21, +98.55) AU · JWST hugs Earth | Δ 0.002 / 0.011 AU |
|
||||||
|
| **NEO** propagated miss vs CAD (real orbit) | ≤ 25% (median ~0.6%) |
|
||||||
|
| **Sky events** — Mars opposition ±2 d of 2027-02-19 · Venus elongation cadence · nextEvent | pass |
|
||||||
|
| **Deep time** — Jupiter @ 1000 BC · Mercury @ 2500 vs live Horizons (Table 2a/2b) | Δ 0.004 / 3.3e-5 AU |
|
||||||
|
| **Event band** maps the ±2 yr window to full width | pass |
|
||||||
|
|
||||||
## Layers
|
## Layers
|
||||||
|
|
||||||
@ -45,54 +52,60 @@ queries through the proxy). All 13 gates should read PASS, e.g.:
|
|||||||
|-------|--------|:------:|-------|
|
|-------|--------|:------:|-------|
|
||||||
| **Planets & orbits** | JPL SSD approximate elements (Standish) | on | 8 planets + Pluto, textured, MEGA-compressed orbit paths |
|
| **Planets & orbits** | JPL SSD approximate elements (Standish) | on | 8 planets + Pluto, textured, MEGA-compressed orbit paths |
|
||||||
| **Body labels** | — | on | names; the focused body's children also get distance labels |
|
| **Body labels** | — | on | names; the focused body's children also get distance labels |
|
||||||
| **Moons** | JPL satellite data | on | 21 moons, per-parent local compression, retrograde Triton |
|
| **Moons** | NASA/USGS mosaics | on | 21 moons, per-parent compression, retrograde Triton; 8 with real surfaces, **tidally locked**; grayscale mosaics directorially tinted (Titan orange, Europa tan…) |
|
||||||
| **Rings** | true km, scaled to parent | on | Saturn (textured annulus) · Uranus (schematic) |
|
| **Rings** | true km, scaled to parent | on | Saturn (textured annulus) · Uranus (schematic) |
|
||||||
| **Spacecraft** | JPL **Horizons** (proxy) | on | Voyager 1/2, New Horizons, JWST, Parker, Juno, Lucy, Psyche — live distance + one-way light-time |
|
| **Spacecraft** | JPL **Horizons** (proxy) | on | Voyager 1/2, New Horizons, JWST, Parker, Juno, Lucy, Psyche — live distance + one-way light-time |
|
||||||
| **Main belt** | JPL **SBDB** (proxy) | off | ~1500 main-belt asteroids, shared Kepler core |
|
| **Main belt** | JPL **SBDB** (proxy) | off | ~1500 main-belt asteroids, shared Kepler core |
|
||||||
| **Comets** | JPL **SBDB** (proxy) | on | 1P/Halley, 2P/Encke, 67P, 96P — full orbits + anti-sunward 1/r² tail |
|
| **Comets** | JPL **SBDB** (proxy) | on | 1P/Halley, 2P/Encke, 67P, 96P — full orbits + anti-sunward 1/r² tail |
|
||||||
| **Close approaches** | JPL **CAD** (proxy) | off | ±30-day NEO radar around Earth, miss distance in lunar distances |
|
| **Close approaches** | JPL **CAD + SBDB** (proxy) | off | ±30-day NEOs at their **true propagated positions** with real orbits (per-object fail-soft to a schematic ring); miss distance in lunar distances |
|
||||||
|
| **Sky events** | computed (no network) | on | oppositions, conjunctions & greatest elongations as diamonds on the timeline's **event band**; click one to jump the clock |
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
- **Focus** — click any body (or use the Focus menu) → the camera flies into orbit
|
- **Focus** — click any body (or use the Focus menu) → the camera flies into orbit
|
||||||
around it; breadcrumb `SOL ▸ JUPITER ▸ EUROPA`; `Esc` steps out one level.
|
around it; breadcrumb `SOL ▸ JUPITER ▸ EUROPA`; `Esc` steps out one level.
|
||||||
- **MEGA / TRUE** — toggle the scale mode; the transition animates over ~2.5 s.
|
- **MEGA / TRUE** — toggle the scale mode; the transition animates over ~2.5 s.
|
||||||
|
- **Epoch** — `1800–2050` / `3000 BC – 3000 AD`: switch to the extended
|
||||||
|
Table 2a/2b range (reloads the session, carried in the hash as `&e=1`).
|
||||||
- **Body scale** — 1× / 100× / 1200× radius exaggeration.
|
- **Body scale** — 1× / 100× / 1200× radius exaggeration.
|
||||||
- **Time bar** — NOW, play/pause, reverse, rate presets (`1× · 1 min/s … 1 yr/s`),
|
- **Time bar** — NOW, play/pause, reverse, rate presets (`1× · 1 min/s … 1 yr/s`,
|
||||||
and a scrubbable 1800–2050 timeline. A **LIVE / SCRUBBED** chip mirrors GODSIGH.
|
plus `1 decade/s · 1 century/s` for deep time), a scrubbable timeline, and an
|
||||||
|
**event band** below it mapping the computed ±2 yr window to full width (a bracket
|
||||||
|
on the main strip marks its extent). A **LIVE / SCRUBBED** chip mirrors GODSIGH.
|
||||||
- **Info panel** — appears on focus: a grimoire card with a physical fact sheet,
|
- **Info panel** — appears on focus: a grimoire card with a physical fact sheet,
|
||||||
hand-written prose, a stylized portrait, and **live computed** facts (distance
|
hand-written prose, a stylized portrait, and **live computed** facts (distance
|
||||||
from Sun & Earth, sunlight age, orbital speed, day & year length). Focusing Earth
|
from Sun & Earth, sunlight age, orbital speed, day & year length, and the next
|
||||||
reveals a link to the GODSIGH world view.
|
opposition/elongation). Focusing Earth reveals a link to the GODSIGH world view.
|
||||||
- **Shareable URLs** — the view serializes to the hash
|
- **Shareable URLs** — the view serializes to the hash
|
||||||
(`#f=<body>&t=<time>&s=<mega|true>&L=<layers>&cam=…`); paste one to reproduce a view.
|
(`#f=<body>&t=<time>&s=<mega|true>&L=<layers>&cam=…&e=<0|1>`); paste one to reproduce a view.
|
||||||
- **`?debug=1`** — frame-rate / scale readout.
|
- **`?debug=1`** — frame-rate / scale readout.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
No build step, no framework, plain ES modules. One module owns the truth→view
|
No build step, no framework, plain ES modules; Three.js vendored offline. One
|
||||||
transform (`scale.js`); every position is computed in real heliocentric J2000
|
module owns the truth→view transform (`scale.js`); every position is computed in
|
||||||
ecliptic AU first (`ephem.js`) then compressed. A **floating origin** subtracts the
|
real heliocentric J2000 ecliptic AU first (`ephem.js`) then compressed. A
|
||||||
focused body's position in float64 before writing to the scene graph, so a focused
|
**floating origin** subtracts the focused body's position in float64 before writing
|
||||||
planet never jitters at close zoom.
|
to the scene graph, so a focused planet never jitters at close zoom.
|
||||||
|
|
||||||
```
|
```
|
||||||
index.html · serve.py · verify.html
|
index.html · serve.py · verify.html
|
||||||
css/style.css
|
css/style.css · vendor/three/ (vendored, MIT)
|
||||||
js/ config lib ephem bodies scale ui main
|
js/ config lib ephem bodies scale ui main
|
||||||
js/layers/ planets moons rings spacecraft asteroids comets neos almanac
|
js/layers/ planets moons rings spacecraft asteroids comets neos events almanac
|
||||||
assets/textures/ (CC BY) · assets/art/grimoire/ (generated)
|
assets/textures/ (CC BY + NASA/USGS public domain) · assets/art/grimoire/ (generated)
|
||||||
```
|
```
|
||||||
|
|
||||||
The full execution spec is [SOLARGOD_BRIEF.md](SOLARGOD_BRIEF.md).
|
The full execution spec is [SOLARGOD_BRIEF.md](SOLARGOD_BRIEF.md); wave-2 polish is
|
||||||
|
[PERIHELION_BRIEF.md](PERIHELION_BRIEF.md).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- **Positions** — [JPL SSD](https://ssd.jpl.nasa.gov/planets/approx_pos.html)
|
- **Positions** — [JPL SSD](https://ssd.jpl.nasa.gov/planets/approx_pos.html)
|
||||||
approximate elements; live feeds via **JPL Horizons / SBDB / CAD**.
|
approximate elements; live feeds via **JPL Horizons / SBDB / CAD**.
|
||||||
- **Textures** — [Solar System Scope](https://www.solarsystemscope.com/textures/),
|
- **Planet/Sun/ring/sky textures** — [Solar System Scope](https://www.solarsystemscope.com/textures/), CC BY 4.0.
|
||||||
CC BY 4.0 (see `assets/textures/CREDITS.md`).
|
- **Moon surfaces** — NASA/USGS Astrogeology mosaics (public domain); grayscale ones
|
||||||
|
directorially colour-tinted. See `assets/textures/CREDITS.md`.
|
||||||
- **Grimoire art** — generated locally with MODELBEAST / FLUX (see
|
- **Grimoire art** — generated locally with MODELBEAST / FLUX (see
|
||||||
`assets/art/PROMPTS.md`); the *aesthetic* layer only — real worlds get real data
|
`assets/art/PROMPTS.md`); the *aesthetic* layer only — real worlds get real data.
|
||||||
textures.
|
- Built on [Three.js](https://threejs.org/) (r170, vendored, MIT).
|
||||||
- Built on [Three.js](https://threejs.org/).
|
|
||||||
|
|||||||
@ -53,6 +53,13 @@ under budget). Downloaded 2026-07-16.
|
|||||||
Host base URL: `https://planetarymaps.usgs.gov/mosaic/` (served from the USGS
|
Host base URL: `https://planetarymaps.usgs.gov/mosaic/` (served from the USGS
|
||||||
`asc-pds-services` bucket). USGS terms: <https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits>.
|
`asc-pds-services` bucket). USGS terms: <https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits>.
|
||||||
|
|
||||||
|
**Directorial colourisation (PERIHELION closer).** USGS has no colour equirect for
|
||||||
|
Europa, Callisto, Titan, Phobos and Charon, so those mosaics ship grayscale. The
|
||||||
|
app multiplies each by its characteristic hue at render (`bodies.js` `tint`:
|
||||||
|
Titan orange, Europa/Charon tan, Callisto/Phobos warm-brown) — the *real surface
|
||||||
|
detail is preserved*; only the colour is an explicit directorial choice, not data.
|
||||||
|
Io, Ganymede and Triton are USGS colour mosaics and render untinted.
|
||||||
|
|
||||||
**Truth boundary (brief §7b):** these are the *real data* textures — real worlds
|
**Truth boundary (brief §7b):** these are the *real data* textures — real worlds
|
||||||
get real maps. The stylized grimoire portraits in `../art/grimoire/` are the
|
get real maps. The stylized grimoire portraits in `../art/grimoire/` are the
|
||||||
*aesthetic* layer (generated art), kept visually and directorially separate.
|
*aesthetic* layer (generated art), kept visually and directorially separate.
|
||||||
|
|||||||
@ -202,3 +202,20 @@ html, body {
|
|||||||
.ev-elong { top: -8px; background: #7fc7d6; box-shadow: 0 0 5px rgba(127, 199, 214, 0.6); }
|
.ev-elong { top: -8px; background: #7fc7d6; box-shadow: 0 0 5px rgba(127, 199, 214, 0.6); }
|
||||||
.ev-conj { top: 2px; width: 5px; height: 5px; background: var(--brass-dim); opacity: 0.6; }
|
.ev-conj { top: 2px; width: 5px; height: 5px; background: var(--brass-dim); opacity: 0.6; }
|
||||||
.ev-conj:hover { opacity: 1; }
|
.ev-conj:hover { opacity: 1; }
|
||||||
|
|
||||||
|
/* PERIHELION-O — event band: the computed ±2 yr window mapped to full width in a
|
||||||
|
strip below the timeline (diamonds spread out); a bracket on the main strip
|
||||||
|
marks the window's real extent. Reuses the ev-* diamond classes above. */
|
||||||
|
#event-band {
|
||||||
|
position: relative; height: 14px; margin-top: 6px;
|
||||||
|
border-radius: 5px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
#event-band #event-marks { inset: 1px 0; }
|
||||||
|
#event-band .ev-mark { top: 50%; transform: translate(-50%, -50%) rotate(45deg); }
|
||||||
|
#event-band .ev-mark:hover { transform: translate(-50%, -50%) rotate(45deg) scale(1.7); }
|
||||||
|
#event-band .ev-conj { top: 50%; }
|
||||||
|
#event-window-bracket {
|
||||||
|
position: absolute; top: -3px; bottom: -3px; z-index: 3; pointer-events: none;
|
||||||
|
border: 1px solid var(--brass); border-radius: 4px;
|
||||||
|
background: rgba(217, 164, 65, 0.14); box-shadow: 0 0 7px rgba(217, 164, 65, 0.45);
|
||||||
|
}
|
||||||
|
|||||||
28
deploy/HERALD.md
Normal file
28
deploy/HERALD.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# HERALD — deploying SOLARGOD
|
||||||
|
|
||||||
|
SOLARGOD is a static site + a stdlib `serve.py` that also proxies JPL
|
||||||
|
(Horizons/SBDB/CAD) and disk-caches responses. `serve.py` binds **127.0.0.1 only**,
|
||||||
|
so production is the same shape as godstrument.pro: **systemd runs serve.py on the
|
||||||
|
loopback, nginx reverse-proxies the public domain to it, certbot adds TLS.**
|
||||||
|
|
||||||
|
## What this directory gives you
|
||||||
|
- `solargod.service` — systemd unit (`python3 serve.py 8147` in `/opt/solargod`).
|
||||||
|
- `nginx-solargod.conf` — nginx server block → `127.0.0.1:8147` (replace `SOLARGOD_DOMAIN`).
|
||||||
|
- `deploy.sh` — one-shot rsync + install + enable + reload (idempotent; re-run to update).
|
||||||
|
|
||||||
|
## One-time
|
||||||
|
```sh
|
||||||
|
DOMAIN=orrery.example.com VPS=user@vps-host ./deploy/deploy.sh
|
||||||
|
ssh $VPS 'sudo certbot --nginx -d orrery.example.com'
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠ Blocked on you (cannot be done from the dev box)
|
||||||
|
1. **Domain / DNS** — pick the hostname and point an A record at the VPS. (I don't
|
||||||
|
have your DNS or a chosen name — the whole lane waited on this per Part ω.2.)
|
||||||
|
2. **VPS access** — the `VPS=user@host` with sudo, on the same box that serves
|
||||||
|
godstrument.pro (so the reverse-proxy pattern matches).
|
||||||
|
3. Confirm Python 3 + nginx + certbot are present on the VPS (they are for godstrument).
|
||||||
|
|
||||||
|
Give me the domain + VPS target and I'll run `deploy.sh` and verify the live site
|
||||||
|
(orrery loads, planets move, `/verify.html` green, `/proxy/*` reaches JPL). Until
|
||||||
|
then these artifacts are prepared but **not applied** — nothing has been deployed.
|
||||||
29
deploy/deploy.sh
Executable file
29
deploy/deploy.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# HERALD — deploy SOLARGOD to the VPS (rsync + systemd + nginx), mirroring the
|
||||||
|
# godstrument.pro shape. Run from the repo root AFTER the DNS A record resolves.
|
||||||
|
# DOMAIN=orrery.example.com VPS=user@host ./deploy/deploy.sh
|
||||||
|
# Idempotent: re-run to push updates (rsync --delete + service restart).
|
||||||
|
set -euo pipefail
|
||||||
|
: "${VPS:?set VPS=user@host}"
|
||||||
|
: "${DOMAIN:?set DOMAIN=your.domain}"
|
||||||
|
APP=/opt/solargod
|
||||||
|
|
||||||
|
echo "→ syncing repo to $VPS:$APP"
|
||||||
|
rsync -az --delete \
|
||||||
|
--exclude '.git' --exclude 'cache' --exclude 'assets/art/candidates' \
|
||||||
|
./ "$VPS:$APP/"
|
||||||
|
|
||||||
|
echo "→ installing service + nginx site on $VPS (domain: $DOMAIN)"
|
||||||
|
ssh "$VPS" "bash -euo pipefail -s" <<EOF
|
||||||
|
sudo install -m 644 $APP/deploy/solargod.service /etc/systemd/system/solargod.service
|
||||||
|
sed 's/SOLARGOD_DOMAIN/$DOMAIN/g' $APP/deploy/nginx-solargod.conf \
|
||||||
|
| sudo tee /etc/nginx/sites-available/solargod >/dev/null
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/solargod /etc/nginx/sites-enabled/solargod
|
||||||
|
sudo mkdir -p $APP/cache && sudo chown -R www-data:www-data $APP/cache
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now solargod
|
||||||
|
sudo systemctl restart solargod
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✓ deployed. TLS: ssh $VPS 'sudo certbot --nginx -d $DOMAIN'"
|
||||||
17
deploy/nginx-solargod.conf
Normal file
17
deploy/nginx-solargod.conf
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# HERALD — nginx reverse proxy for SOLARGOD. Replace SOLARGOD_DOMAIN, then run
|
||||||
|
# certbot for TLS once the A record resolves. serve.py handles both static files
|
||||||
|
# and the /proxy/* JPL endpoints (with its disk cache), so we proxy everything.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name SOLARGOD_DOMAIN;
|
||||||
|
|
||||||
|
# (optional perf) serve committed static assets straight from disk instead of
|
||||||
|
# through Python: root /opt/solargod; try_files $uri @app; location @app { ... }
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8147;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 90s; # first cold Horizons spacecraft fetch can take 2–5 s
|
||||||
|
}
|
||||||
|
}
|
||||||
18
deploy/solargod.service
Normal file
18
deploy/solargod.service
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# HERALD — systemd unit for SOLARGOD. serve.py binds 127.0.0.1 only; nginx
|
||||||
|
# reverse-proxies the public domain to it (mirror of the godstrument.pro shape).
|
||||||
|
# Install: sudo cp deploy/solargod.service /etc/systemd/system/ && sudo systemctl enable --now solargod
|
||||||
|
[Unit]
|
||||||
|
Description=SOLARGOD orrery — serve.py (static + JPL Horizons/SBDB/CAD proxy + disk cache)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
WorkingDirectory=/opt/solargod
|
||||||
|
ExecStart=/usr/bin/python3 /opt/solargod/serve.py 8147
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
# serve.py writes its disk cache under WorkingDirectory/cache — keep it writable.
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
10
js/bodies.js
10
js/bodies.js
@ -62,18 +62,18 @@ export const BODIES = {
|
|||||||
parent: 'earth', moonOrbit: { aKm: 384400, periodDays: 27.322 } },
|
parent: 'earth', moonOrbit: { aKm: 384400, periodDays: 27.322 } },
|
||||||
|
|
||||||
phobos: { name: 'Phobos', type: 'moon', radiusKm: 11.27, color: '#8a7c6e',
|
phobos: { name: 'Phobos', type: 'moon', radiusKm: 11.27, color: '#8a7c6e',
|
||||||
texture: '1k_phobos.jpg', parent: 'mars', moonOrbit: { aKm: 9378, periodDays: 0.319 } },
|
texture: '1k_phobos.jpg', tint: '#a08a76', parent: 'mars', moonOrbit: { aKm: 9378, periodDays: 0.319 } },
|
||||||
deimos: { name: 'Deimos', type: 'moon', radiusKm: 6.2, color: '#9a8c7a',
|
deimos: { name: 'Deimos', type: 'moon', radiusKm: 6.2, color: '#9a8c7a',
|
||||||
texture: null, parent: 'mars', moonOrbit: { aKm: 23459, periodDays: 1.262 } },
|
texture: null, parent: 'mars', moonOrbit: { aKm: 23459, periodDays: 1.262 } },
|
||||||
|
|
||||||
io: { name: 'Io', type: 'moon', radiusKm: 1821.6, color: '#e8d24b',
|
io: { name: 'Io', type: 'moon', radiusKm: 1821.6, color: '#e8d24b',
|
||||||
texture: '1k_io.jpg', parent: 'jupiter', moonOrbit: { aKm: 421700, periodDays: 1.769 } },
|
texture: '1k_io.jpg', parent: 'jupiter', moonOrbit: { aKm: 421700, periodDays: 1.769 } },
|
||||||
europa: { name: 'Europa', type: 'moon', radiusKm: 1560.8, color: '#c9b79c',
|
europa: { name: 'Europa', type: 'moon', radiusKm: 1560.8, color: '#c9b79c',
|
||||||
texture: '1k_europa.jpg', parent: 'jupiter', moonOrbit: { aKm: 671034, periodDays: 3.551 } },
|
texture: '1k_europa.jpg', tint: '#e6dcc8', parent: 'jupiter', moonOrbit: { aKm: 671034, periodDays: 3.551 } },
|
||||||
ganymede: { name: 'Ganymede', type: 'moon', radiusKm: 2634.1, color: '#9b8e7e',
|
ganymede: { name: 'Ganymede', type: 'moon', radiusKm: 2634.1, color: '#9b8e7e',
|
||||||
texture: '1k_ganymede.jpg', parent: 'jupiter', moonOrbit: { aKm: 1070412, periodDays: 7.155 } },
|
texture: '1k_ganymede.jpg', parent: 'jupiter', moonOrbit: { aKm: 1070412, periodDays: 7.155 } },
|
||||||
callisto: { name: 'Callisto', type: 'moon', radiusKm: 2410.3, color: '#6b5f52',
|
callisto: { name: 'Callisto', type: 'moon', radiusKm: 2410.3, color: '#6b5f52',
|
||||||
texture: '1k_callisto.jpg', parent: 'jupiter', moonOrbit: { aKm: 1882709, periodDays: 16.689 } },
|
texture: '1k_callisto.jpg', tint: '#9a8778', parent: 'jupiter', moonOrbit: { aKm: 1882709, periodDays: 16.689 } },
|
||||||
|
|
||||||
mimas: { name: 'Mimas', type: 'moon', radiusKm: 198.2, color: '#c8c8c8',
|
mimas: { name: 'Mimas', type: 'moon', radiusKm: 198.2, color: '#c8c8c8',
|
||||||
texture: null, parent: 'saturn', moonOrbit: { aKm: 185539, periodDays: 0.942 } },
|
texture: null, parent: 'saturn', moonOrbit: { aKm: 185539, periodDays: 0.942 } },
|
||||||
@ -86,7 +86,7 @@ export const BODIES = {
|
|||||||
rhea: { name: 'Rhea', type: 'moon', radiusKm: 763.8, color: '#c4c4c4',
|
rhea: { name: 'Rhea', type: 'moon', radiusKm: 763.8, color: '#c4c4c4',
|
||||||
texture: null, parent: 'saturn', moonOrbit: { aKm: 527068, periodDays: 4.518 } },
|
texture: null, parent: 'saturn', moonOrbit: { aKm: 527068, periodDays: 4.518 } },
|
||||||
titan: { name: 'Titan', type: 'moon', radiusKm: 2574.7, color: '#d9a441',
|
titan: { name: 'Titan', type: 'moon', radiusKm: 2574.7, color: '#d9a441',
|
||||||
texture: '1k_titan.jpg', parent: 'saturn', moonOrbit: { aKm: 1221870, periodDays: 15.945 } },
|
texture: '1k_titan.jpg', tint: '#e0a63f', parent: 'saturn', moonOrbit: { aKm: 1221870, periodDays: 15.945 } },
|
||||||
iapetus: { name: 'Iapetus', type: 'moon', radiusKm: 734.5, color: '#8a7a5e',
|
iapetus: { name: 'Iapetus', type: 'moon', radiusKm: 734.5, color: '#8a7a5e',
|
||||||
texture: null, parent: 'saturn', moonOrbit: { aKm: 3560851, periodDays: 79.33 } },
|
texture: null, parent: 'saturn', moonOrbit: { aKm: 3560851, periodDays: 79.33 } },
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ export const BODIES = {
|
|||||||
w: 113.76329, ma: 14.86012204, epoch: 2451545.0 },
|
w: 113.76329, ma: 14.86012204, epoch: 2451545.0 },
|
||||||
},
|
},
|
||||||
charon: { name: 'Charon', type: 'moon', radiusKm: 606.0, color: '#b3a894',
|
charon: { name: 'Charon', type: 'moon', radiusKm: 606.0, color: '#b3a894',
|
||||||
texture: '1k_charon.jpg', parent: 'pluto', moonOrbit: { aKm: 19591, periodDays: 6.387 } },
|
texture: '1k_charon.jpg', tint: '#c4b7a4', parent: 'pluto', moonOrbit: { aKm: 19591, periodDays: 6.387 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ordered planet ids (draw / menu order).
|
// Ordered planet ids (draw / menu order).
|
||||||
|
|||||||
@ -140,6 +140,13 @@ export function nextEvent(bodyId, jd) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fraction of an instant within the event band's [winStart, winEnd] window (0..1).
|
||||||
|
// The band maps the ±2 yr window to full width regardless of near/deep epoch.
|
||||||
|
// Exported so verify.html gates the mapping without a DOM.
|
||||||
|
export function windowFraction(ms, winStartMs, winEndMs) {
|
||||||
|
return (ms - winStartMs) / ((winEndMs - winStartMs) || 1);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- default factory: timeline diamonds + HUD row ----
|
// ---- default factory: timeline diamonds + HUD row ----
|
||||||
const WINDOW_DAYS = 730; // ±2 years, computed lazily around the current sim date
|
const WINDOW_DAYS = 730; // ±2 years, computed lazily around the current sim date
|
||||||
function cssKind(type) { return type === 'opposition' ? 'opp' : type.startsWith('elongation') ? 'elong' : 'conj'; }
|
function cssKind(type) { return type === 'opposition' ? 'opp' : type.startsWith('elongation') ? 'elong' : 'conj'; }
|
||||||
@ -149,9 +156,19 @@ export default function create(ctx) {
|
|||||||
const strip = document.getElementById('timeline-outer');
|
const strip = document.getElementById('timeline-outer');
|
||||||
if (!strip) return null; // no time bar (e.g. headless) — plain exports still work
|
if (!strip) return null; // no time bar (e.g. headless) — plain exports still work
|
||||||
|
|
||||||
|
// PERIHELION-O event band — the ±2 yr computed window mapped to FULL width in a
|
||||||
|
// second strip below the timeline, so diamonds spread out instead of clustering
|
||||||
|
// into ~1.6% of the 250-yr strip (worse in deep time). A bracket on the main
|
||||||
|
// strip marks the window's true extent, tying the two together.
|
||||||
|
const band = document.createElement('div');
|
||||||
|
band.id = 'event-band';
|
||||||
const marks = document.createElement('div');
|
const marks = document.createElement('div');
|
||||||
marks.id = 'event-marks';
|
marks.id = 'event-marks';
|
||||||
strip.appendChild(marks);
|
band.appendChild(marks);
|
||||||
|
strip.parentNode.insertBefore(band, strip.nextSibling); // directly under timeline-outer
|
||||||
|
const bracket = document.createElement('div');
|
||||||
|
bracket.id = 'event-window-bracket';
|
||||||
|
strip.appendChild(bracket);
|
||||||
|
|
||||||
const minMs = CONFIG.time.minMs, maxMs = CONFIG.time.maxMs, spanMs = maxMs - minMs;
|
const minMs = CONFIG.time.minMs, maxMs = CONFIG.time.maxMs, spanMs = maxMs - minMs;
|
||||||
let on = true, events = [];
|
let on = true, events = [];
|
||||||
@ -183,9 +200,10 @@ export default function create(ctx) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
marks.textContent = '';
|
marks.textContent = '';
|
||||||
|
const winStartMs = unixMsFromJd(winStart), winEndMs = unixMsFromJd(winEnd);
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
const ms = unixMsFromJd(e.jd);
|
const ms = unixMsFromJd(e.jd);
|
||||||
const f = (ms - minMs) / spanMs;
|
const f = windowFraction(ms, winStartMs, winEndMs); // position WITHIN the band
|
||||||
if (f < 0 || f > 1) continue;
|
if (f < 0 || f > 1) continue;
|
||||||
const d = document.createElement('div');
|
const d = document.createElement('div');
|
||||||
d.className = `ev-mark ev-${cssKind(e.type)}`;
|
d.className = `ev-mark ev-${cssKind(e.type)}`;
|
||||||
@ -197,6 +215,12 @@ export default function create(ctx) {
|
|||||||
});
|
});
|
||||||
marks.appendChild(d);
|
marks.appendChild(d);
|
||||||
}
|
}
|
||||||
|
// bracket: where the ±2 yr window sits within the full CONFIG.time range (tiny
|
||||||
|
// in near mode, tinier in deep time — that's the point of the band below it).
|
||||||
|
const bl = ((winStartMs - minMs) / spanMs) * 100;
|
||||||
|
const bw = ((winEndMs - winStartMs) / spanMs) * 100;
|
||||||
|
bracket.style.left = `${Math.min(Math.max(0, bl), 100).toFixed(4)}%`;
|
||||||
|
bracket.style.width = `${Math.max(0.5, Math.min(bw, 100)).toFixed(4)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -17,7 +17,17 @@ export default function create(ctx) {
|
|||||||
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 24, 16), mat);
|
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 24, 16), mat);
|
||||||
mesh.visible = true;
|
mesh.visible = true;
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
loadTextureInto(mat, b.texture); // NASA/USGS map when present; flat color otherwise (§7 fallback)
|
if (b.tint) {
|
||||||
|
// Directorial colourisation: the USGS mosaics for these moons are grayscale
|
||||||
|
// (no color equirect exists), so multiply the real surface by the moon's
|
||||||
|
// characteristic hue — keeps true detail, adds the iconic colour. On a load
|
||||||
|
// error the flat b.color already set on the material stands (§7 fallback).
|
||||||
|
new THREE.TextureLoader().load(CONFIG.textures.path + b.texture, (tex) => {
|
||||||
|
tex.colorSpace = THREE.SRGBColorSpace; mat.map = tex; mat.color.set(b.tint); mat.needsUpdate = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadTextureInto(mat, b.texture); // NASA/USGS map when present; flat color otherwise (§7 fallback)
|
||||||
|
}
|
||||||
registerPick(mesh, id);
|
registerPick(mesh, id);
|
||||||
const lbl = makeLabel(mesh, b.name, 'body-label moon');
|
const lbl = makeLabel(mesh, b.name, 'body-label moon');
|
||||||
lbl.obj.visible = false; // CSS2DRenderer honors obj.visible (not div.style.display)
|
lbl.obj.visible = false; // CSS2DRenderer honors obj.visible (not div.style.display)
|
||||||
|
|||||||
17
verify.html
17
verify.html
@ -286,6 +286,23 @@
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PERIHELION-O — event band: the computed ±2 yr window maps to full band width.
|
||||||
|
windowFraction is pure; the click-jump-in-band is a browser real-input test. */
|
||||||
|
{
|
||||||
|
const { windowFraction, findEvents } = await import('./js/layers/events.js');
|
||||||
|
const okEnds = windowFraction(1000, 1000, 2000) === 0
|
||||||
|
&& windowFraction(2000, 1000, 2000) === 1
|
||||||
|
&& windowFraction(1500, 1000, 2000) === 0.5;
|
||||||
|
// a real event (Mars 2027 opposition) lands strictly inside a ±2 yr band.
|
||||||
|
const c = lib.jdFromUnixMs(Date.UTC(2026, 6, 16));
|
||||||
|
const wS = lib.unixMsFromJd(c - 730), wE = lib.unixMsFromJd(c + 730);
|
||||||
|
const opp = findEvents(c - 730, c + 730).find((e) => e.bodies[0] === 'mars' && e.type === 'opposition');
|
||||||
|
const fr = opp ? windowFraction(lib.unixMsFromJd(opp.jd), wS, wE) : NaN;
|
||||||
|
row('event band maps ±2 yr window to full width', okEnds && !!opp && fr > 0 && fr < 1,
|
||||||
|
`ends 0/0.5/1 ok=${okEnds} · Mars-opp band-fraction=${opp ? fr.toFixed(3) : 'n/a'}`);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
/* PERIHELION-V — deep-time (extended Table 2a/2b) accuracy gates.
|
/* PERIHELION-V — deep-time (extended Table 2a/2b) accuracy gates.
|
||||||
Capture the "mine" positions with the extended range ON, then restore it OFF
|
Capture the "mine" positions with the extended range ON, then restore it OFF
|
||||||
synchronously and atomically: ES modules are singletons, so setExtendedRange
|
synchronously and atomically: ES modules are singletons, so setExtendedRange
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user