# Planet One — an addressable world

Mathematics and original indexed primitives: **Lars Rocha's RangeBlends**.
This milestone adds a new spherical terrain recipe and renderer. It is not a
copy of Earth's geography, a physical climate simulation, or a completed
planetary game engine.

## Try the streaming expedition

Open `examples/planet.html` directly. It needs WebGL but no server, credentials,
external assets, or map downloads. The default seed is `k2v-1 k2em`.

1. Select a generated **Site**, or enter latitude and longitude and choose
   **Visit coordinates**. The selected location is centered under the crosshair.
2. Choose **Land here** to descend continuously. The camera may pause its
   descent while finer patches arrive. Watch the altitude and detail readouts.
3. Walk using W A S D or the on-screen arrows; drag or use keyboard arrows to
   look around. Shift increases travel speed. There is no landing-area boundary.
4. Press Escape or **Return to orbit**, then visit a distant coordinate.
5. Use the **Travel altitude** slider or scroll wheel to stop between ground
   and orbit. W A S D flies at these intermediate heights; speed scales with
   altitude. Return to the same seed and address to regenerate the same terrain.

Movement follows the analytic terrain/sea surface with 1.7 m eye clearance;
the rendered triangles approximate that surface. There is no jump, gravity
simulation, digging, prop collision, player save, or existing Atlas minimap
in this separate prototype. Ocean travel surveys the water plane. Descent
readiness uses an estimated mesh error, not a physical collision guarantee.

## Optional blocky style preview

Open **`examples/planet-blocky.html`** for a separate, self-contained preview.
The existing `planet.html`, Phase 1 snapshot, Atlas, and root `index.html` are
not replaced. Choose **Angled close-up** to descend to 120 m with a tilted
perspective view. Use **Terrain look** to compare Blocky and Smooth at the
same seed and coordinates; switching replaces view meshes without changing
the Planet recipe. **Land here** still descends to walking height.

Blocky mode uses one sampled center per displayed cell and rounds its display
height down to a fixed **2 m** terrace. It creates radial column tops, narrow
dark rims, and vertical exposed sides; colors do not interpolate between
neighboring cells. These are surface columns conforming to the sphere, not
an editable volumetric voxel world. The API's original elevation and biome
readouts remain unchanged. Distant cells summarize larger areas and may pop
when replaced by finer cells. The finest cells use a fixed level-24 cube-face
address (level-20 patches with 16 cells per edge), independent of visit order.

Walking clearance uses the same finest-cell center and quantization as the
block mesher. Final descent waits for level-20 coverage beneath the camera.
Steps are automatic; this is not gravity, jumping, or wall collision. There
are still no trees, river channels, buildings, or props in this style pass.
The reference's terrain palette and columns are the inspiration, not a port
of its entire riverbank scene.

Extra top rims and side faces cost more geometry: a block patch is bounded
below 400,000 vertex bytes, versus 69,120 bytes for a smooth patch. The same
256-patch retention limit therefore permits under 98 MiB of block vertex
buffers, excluding transient uploads and other browser/GPU memory. The live
readout measures actual vertex bytes. Style changes cancel/discard stale work,
clear old GPU buffers, and regenerate coarse coverage before refining.

Build only this preview with:

```sh
python tools/build_planet.py --output planet-blocky.html
```

The builder's default output is still `planet.html`; use the explicit preview
filename to preserve that demo. Automated checks cover terrace heights,
normals/winding on all six faces, shared borders, worker style changes,
camera clearance, standalone assembly, and location-preserving style switches.
Browser/GPU visual review of this preview remains a separate step.

## Address and scale contract

- Radius defaults to **6,371,000 meters**. Accepted radii: 100,000–100,000,000 m.
- Elevation is meters above sea level, not the World generator's 0–1 index.
- Latitude is degrees in −90…90. Longitudes normalize to −180…180; pole samples
  have one canonical direction regardless of longitude.
- Planet Cartesian axes: +Y north pole, +Z latitude 0 / longitude 0,
  +X latitude 0 / longitude 90° east.
- Six faces (`+X`, `-X`, `+Y`, `-Y`, `+Z`, `-Z`) organize patches. `across` and
  `upward` coordinates span −1…1 and project radially onto the sphere. This is
  not an equal-area projection, so patch dimensions vary by location.
- Patch address: face, level, column, row. Level 0 has one patch per face;
  subdivision doubles each axis. Levels 0…24 are supported. The terrain recipe
  does **not** use the level or face as a random seed.
- A patch uses 2…64 power-of-two segments per edge, inclusive endpoints.
  Common samples between parent and child patches use the same rational face
  coordinates; neighboring face edges sample the same global direction.
- Cache keys are scoped to the Planet instance. For persistent storage or
  sharing, include generator version, seed, radius, face, level, column, row,
  and segment count. Changing a recipe requires a new generator version.

## Shared Python and JavaScript API

```python
import nedgelaw as ned

planet = ned.Planet("k2v-1 k2em", radius=6371000, cache_limit=16)
location = planet.sample(20, 30)
patch = planet.patch("+X", 12, 100, 200, segments=16)
frame = planet.frame(20, 30)
direction = planet.local_direction(frame, 10, 0)
elevation_ten_meters_east = planet.height(direction)
print(location, planet.cache_info())
```

```js
const planet = new ned.Planet("k2v-1 k2em", {radius: 6371000, cacheLimit: 16});
const location = planet.sample(20, 30);
const patch = planet.patch("+X", 12, 100, 200, 16);
const frame = planet.frame(20, 30);
const elevationTenMetersEast = planet.height(planet.localDirection(frame, 10, 0));
console.log(location, planet.cacheInfo());
```

`height` accepts any finite nonzero three-component direction and normalizes
it. `patch` returns row-major directions and heights; JS results are frozen,
and Python height/direction arrays are tuples. The default LRU cache holds
16 patches, configurable from 0 (disabled) to 64. No cache stores a whole planet.
`frame` gives orthonormal east, north, and up axes. `localDirection` projects a
tangent offset back to the sphere; it is bounded to ±10 km per axis and is a
local projection, not an exact long-distance geodesic-distance API.

Both implementations share `planet-v1`. Tests compare sampled heights within
1e-6 meters across Python/JS, with exact addresses at tested locations. This
is not a promise of bitwise equality across all trig implementations or a
promise of stable rounding for points arbitrarily close to cell boundaries.

## How the terrain uses the mathematics

The unchanged RangeBlends indexed `at` / `mix` / `unit` functions address the
corners of a 3-D value-noise lattice. The existing smooth interpolation blends
those corners. Using the sphere's global direction avoids separate face-wise
noise and therefore avoids longitude and cube-face texture seams.

Eight spatial wavelengths are 2,400 km, 700 km, 180 km, 35 km, 5 km, 700 m,
90 m, and 12 m. Base amplitudes in meters are 4,800, 1,400, 650, 220, 65,
18, 4, and 1. Each is weighted by `0.75 + 0.5 * CUM[index+1] / CUM[8]`.
Layers 1–3 apply an explicit ridge blend; the total begins at −450 m.
The exact expressions live in `planet.py` and `planet.js`. This is a designed
planetary application of the primitives, not the existing planar `ranges`
recipe or an extension of the measured edge table. Its biomes are simple
elevation/latitude classifications, not ecology or a weather system.

## planet-epic-v1: Epic ranges

`Planet(seed, recipe="epic")` / `new ned.Planet(seed, {recipe: "epic"})`, the
demo's default. The ranges law unchanged, with the amplitude range.html gives
it: `-3000 + 3500·roll + 9500·ridge·(0.15 + 0.85·roll)`, a knee at 5,200 m
easing toward 9,000 (`5200 + 3800·(1 − e^{−t})`), and detail of 60/16/3 m at
700/90/12 m wavelengths scaled by `0.35 + 1.3·ridge`, so summits are cragged
and plains calm. Basins, the river valley carve, shorelines, settlements
(20–1,400 m) and trees (14–1,500 m) are exactly planet-v3's. On `k2v-1 k2em`
the summit near 28°N 144.1°W stands at about 7,500 m where v3 has 3,400.
`massif` is a parameter on both spherical recipes; the default 0.5 reproduces
planet-v3 to the bit. A changed law means a new version string.

## planet-v3: Ranges, rivers & settlements

A second, versioned recipe: `new ned.Planet(seed, {recipe: "ranges"})` /
`ned.Planet(seed, recipe="ranges")`. The default stays `smooth` (`planet-v1`), so
every existing address keeps its height. (`planet-v2` was the first cut of this
recipe; v3 gave rivers sources and mouths, angled tributaries, and settlements.
A changed law is a new suffix, never a silent edit.)

The law is RangeBlends' `buildRidge` read on the cube-sphere. A face is a chart
of 2^20 cells per edge (about 9.5 m at Earth radius). A ridge of scale `v`
(8…18, 2.4 km…2,500 km) stands where the cell index is `k·2^v − 1`, with
amplitude from `CUM[v]`, half-width `WIDTH`, and the `squareRamp` massif slide —
which is exactly where the quadtree has an edge at level `20 − v`. The ruler
sequence and the quadtree are the same counting, so the mountains sit on the
address, and a deeper zoom is a longer index, not a new octave table.

| Stage | Rule | Locality |
|---|---|---|
| Continents | two 3-D value-noise octaves on the direction (2,400 km, 800 km) | pointwise |
| Bend | two more (600 km) move the chart coordinates up to ±25 km | pointwise |
| Spines | `buildRidge`'s law along both chart axes, `v = 8…18`, gated by a presence signal so ranges break | three candidates per scale per axis |
| Deepening | `best + RELIEF·(best² − best)` | pointwise |
| Basins | a bowl field (300 km, 120 km) lowers plains by up to 320 m, never below +40 m unless the bowl is deep, where it floods to −40 m | pointwise |
| Knee | above 2,600 m heights ease toward 8,800 m; nothing clamps | pointwise |
| Rivers | a trunk per 4,096 cells (~39 km, 0.6–1.3 km wide) that is *alive* only where its own flow signal says so — a source and a mouth every ~150 km — meandering at two wavelengths; tributaries leave the trunk at hashed angles on rows with `v2(j+1) ≥ 1`; distance in widths carves a valley, then water with steep banks | three trunks, three rows |
| Trees | level-17 cells (~76 m) where a 25 km forest field sets the odds; up to four trees per cell, one per quarter, each decided and placed by its own hash, on land 14–1,500 m below 62°; trunk 4–7 m, canopy 4–6 m — `World.treeAt`'s rule on the sphere; patches of level 15 and finer carry them | one cell |
| Settlements | level-13 cells (~1.2 km) where `min(v2(column+1), v2(row+1)) ≥ 3`, on land 20–1,400 m, gentler than 45 m over 60 m, below 66° — `World.villageAt`'s law on the sphere; rank picks hut / cottages / farmstead / great hall (2, 4, 7, 11 houses) laid out by hash on a 5×5 plan; each house has a footprint, wall height, roof pitch and orientation | one cell, five probes |
| Detail | v1's 700 m / 90 m / 12 m octaves, damped at sea and on low plains (full only above 75 m) | pointwise |

`planet.structures(face, level, column, row)` returns the settlement whose
cell holds a patch (level ≥ 13), `planet.settlement(lat, lon)` the one under a
point, and `sample()` carries `{name, kind, houses}`. The renderer builds the
houses into the patch mesh — four walls, a gable roof, a footing — for both
looks, so the walker meets the village the orbit showed. **F** toggles free flight: W A S D at your height, Space climbs, C descends,
`[` `]` or the wheel change speed; F again lands where you are. Walking stops at
house walls and tree trunks (footprint tests on the plan and the cell's trees).
Landing sites prefer the largest settlement on each face — farmsteads and great
halls — and the site buttons carry their names. Patches holding a settlement score 1.6× in the quadtree, so a village
earns its detail before the plain around it.

Near a face edge the direction is read on every face whose axis component is
within 8% of the largest, and the sculpted results are blended; the shared edge
samples are bitwise equal on both faces. Stream names carry `planet-v2`; a
changed law means `planet-v3`, never a silent edit. Python matches JavaScript
within 1e-6 m (`tests/test_planet.py`).

## Sundial and Almanac

The demo has a clock. `examples/planet-environment.js` reuses RangeBlends'
`ned.sunState` and `ned.skyOf` for the hour's sky palette and light, and
`ned.Almanac(14)` for cloud, rain and wind — one tick per world hour, with each
level-4 cube-face region given a seeded offset into the same 64-hour cycle, so
neighbouring regions have different weather from one almanac. World hour is
referenced to longitude 0°; local time adds four minutes per degree. The sun's
equatorial direction rotates with world hour; there is no axial tilt, season,
or polar-day model, and the 06:00–18:00 daylight window is stylised.

Controls: **World day** (±1,000,000), the **World hour** slider, **Play day**
(15 world minutes per second, paused after a hidden tab), **Dawn / Noon / Dusk**
(local 06:15, 12:00, 17:45), **Next rain** (looks ahead one cycle), **Weather
effects** (overcast tint, visibility, rain streaks near the surface), a
six-hour local forecast, and **Bookmark / Return to moment** — one version-1
record under `nedgelaw.planet.moment.v1` in browser storage, validated before
use. Time and weather never change terrain, props, or addresses.

## Sky

A full-screen pass drawn before the terrain, per pixel from the camera ray:
black space with the planet's limb glowing where the ray grazes the atmosphere
(dimmed on the night side), easing below 220 km into the Sundial's horizon-to-
zenith palette for the current local hour with a sun disc where the sun is.
Night keeps a moonlight floor on the terrain so the ground stays legible.
Terrain fog fades toward the horizon colour; rain and overcast shorten it.

## Rendering and bounded work

- **Adaptive cube quadtree:** each patch has 16×16 cells. Selection uses an
  estimated screen-space height error (8 CSS pixel target) and projected
  cell spacing (12 CSS pixel target), conservative horizon/frustum tests,
  and a priority branch beneath the camera. Cell spacing matters even over
  nearly flat coastlines: height error alone left large colored triangles
  around an overly refined center. Limits are 96 selected
  leaves, 192 considered tree nodes, and level 20. Budgets can stop refinement
  before the error target is met. Level 20 spacing varies across a cube face;
  it is approximately meter scale on this Earth-radius demo, not one fixed grid.
- **Coverage while loading:** visible parents remain until every currently
  needed child subtree has coverage. Parent and child surfaces are not drawn
  over one another. Shared source samples remain identical. Radially lowered
  skirts hide most mixed-resolution gaps; this is not a stitched or geomorphed
  mesh. Brief changes in silhouette, normals, or color can still be visible.
- **Bounded background work:** two Blob workers generate and transfer local
  Float32 vertex arrays. Queue priority favors coarse coverage; stale queued
  jobs are cancelled and stale active results discarded. A cooperative timer
  fallback runs one job at a time if workers cannot start. Each worker's Planet
  sample cache retains at most 16 patches. The renderer retains at most 256
  GPU patches (about 16.9 MiB of vertex data; not total browser/GPU memory),
  with a transient upload before eviction. Descriptor caching is capped at
  1,024 entries per active Planet. Actual memory also includes workers, code,
  temporary meshes, the canvas, and driver allocations.
- **Camera-relative precision:** subtract the patch origin in JavaScript
  doubles before storing vertices in Float32. Subtract the camera position
  from each origin in doubles before uploading its offset. Far-to-near depth
  passes separate local meters from planetary distances. All passes use the
  same spherical terrain; no switch to a flat landing world is required.
- **Travel:** altitude interpolates logarithmically, with descent held above
  unfinished near-ground detail. Reduced-motion preferences skip interpolation,
  but still wait for detail. Walking continually reprojects short steps onto
  the sphere and transports heading into the next tangent frame, including
  cube faces, the date line, and poles. This is navigation, not an exact geodesic
  integrator or physics solver. WASD at altitude provides a simple survey flight.

The eight-layer `planet-v1` height recipe and Python/JS parity are unchanged.
The new LOD, worker pool, and camera controller are demo rendering modules,
not new Python API requirements. The earlier `orbitMesh`/`localMesh` helpers
remain available in `planet-model.js` for their existing tests and experiments.

## Roadmap

- [x] Preserve the deployed Atlas and root `index.html`.
- [x] Add versioned Planet APIs in Python and JavaScript.
- [x] Define meter-based spherical coordinates and six connected face addresses.
- [x] Match shared face/patch samples; keep heights independent of query order.
- [x] Generate a coarse Earth-scale globe without a global full-resolution array.
- [x] Visit distant coordinates and generate local landable terrain on demand.
- [x] Bound patch memory and replace local GPU buffers; use local render origins.
- [x] Test seams, subdivision samples, return visits, API parity, controls, and packaging.
- [x] User browser/GPU review of the first expedition.
- [x] Globe quadtree LOD selected by estimated screen-space error, with horizon/frustum culling.
- [x] Crack-resistant patch joins using shared samples, parent fallback, and skirts.
- [x] Near-to-horizon terrain through the adaptive tree, with two-worker generation.
- [x] Continuous orbit-to-ground travel and tangent-frame rebasing beyond the
      old landing-area limit, including heading transport across face boundaries.
- [x] Automated LOD, worker transfer/fallback/cancellation, and controller tests.
- [ ] User browser/GPU review of the streaming expedition, including seams and performance.
- [ ] Geomorphing or explicit stitched LOD edges to reduce refinement popping further.
- [ ] Planet-scale drainage/catchments, then local rivers tied to that network.
- [ ] Deterministic vegetation, settlements, wildlife, and almanac events.
- [ ] Player changes stored as sparse overrides separate from generated terrain.

## Build and publish safely

Run `python tools/build_planet.py` to rebuild only the shared bundle and
`examples/planet.html`. It never rewrites Atlas or the existing root index.
The repository root `index.html` is a landing page that links this demo, the
Atlas, and the library demo; deploy the folder as a static site.
`planet-blocky.html` is a build alias of `planet.html` (same template, same
looks selector), kept for older links. The Python source is independently
included in the package wheel. Do not overwrite your live Atlas unless that
replacement is intentional.

The single HTML embeds its worker source too; no worker URL, CDN, or asset
fetch is required. If a deployment adds a Content Security Policy that blocks
Blob workers, the demo reports **cooperative fallback** in its memory readout.
`planet.template.html` is a build input; open the generated `planet.html` for
the self-contained demo. This milestone is covered by automated tests, not
an automated browser/GPU visual review.
