# nedgelaw

**Lars Rocha's mathematics and world tools, ready to import.**

```python
import nedgelaw as ned

river = ned.River("k2v-1 k2em")
print(river.frame(12.5).head())
print(ned.Almanac().frame(0, 8))
```

An independent development package built from Lars Rocha's SPSP Python tools and
RangeBlends JavaScript. It includes edge-law lookups, indexed rivers, terrain,
riverside scenery, settlements, periodic weather, wildlife, and table helpers.
Original source locations and differences are recorded in `docs/INVENTORY.md`.

## Planet One: Earth-scale streaming expedition

Open **`examples/planet.html`** for the separate spherical prototype. Choose a
generated site or latitude/longitude, then **Land here**. W A S D walks, dragging
or arrow keys changes the view, Shift hurries, and Escape returns to orbit.
The default radius is 6,371,000 meters. Scroll or use **Travel altitude** to
descend through progressively finer cube-face patches, down to roughly meter
sampling near your feet. Two background workers build terrain where supported;
the old 2 km landing boundary is removed. No global full-resolution map is
allocated. Coarse parents cover unfinished refinements; skirts mask LOD joins.

```python
planet = ned.Planet("k2v-1 k2em", recipe="epic")   # planet-epic-v1, the default in the demo
print(planet.sample(28, -144.1))                    # a summit near 7,500 m
print(planet.sample(20, 30))
patch = planet.patch("+X", 12, 100, 200, segments=16)
print(planet.cache_info())
```

**Epic ranges** (`recipe="epic"`, `planet-epic-v1`) is the ranges law at
range.html's amplitude: ridges rise to 9,500 m before a knee at 5,200 m eases
them toward 9,000, so the tallest summits stand near 7,500 m with snow, and the
surface detail is scaled by ridge height (`0.35 + 1.3·ridge`) so summits are
cragged and plains calm. Continents, basins, rivers, settlements, and trees are
the ranges recipe's, unchanged; `massif` is now a parameter on both
(`Planet(seed, recipe="ranges", massif=0.5)` is exactly `planet-v3`).

**Clock and weather:** `ned.Environment(seed).read(day, minute, lat, lon)` gives
the moment's sun direction, daylight, Sundial palette and light colour, and the
Almanac's cloud/rain/wind for that region — plus `forecast`, `rain`,
`next_rain`, `split_moment`, `world_clock` and `valid_moment`. Same in
JavaScript (`ned.Environment`, `ned.splitMoment`, `ned.worldClock`); Python
matches within 1e-9.

JavaScript exposes `new ned.Planet("k2v-1 k2em")` with `sample`, `patch`, and
`cacheInfo`. This is a new, versioned spherical recipe using Rocha's indexed
hash and cumulative ruler ladder; it does not change `World` or `Landscape`,
and does not yet port their settlements, wildlife, or rivers. See
`docs/PLANET.md` for units, limitations, and the checked-off roadmap.

**Publishing by drag-and-drop:** `python tools/export_clean.py` copies exactly the
files git tracks into `~/pushtogithub` (review images excluded), a folder you can
drag onto the GitHub web uploader. It reads only the local repository; there
are no remotes or network calls anywhere in this project.

Rebuild with `python tools/build_planet.py`. The generated `planet.html` embeds
its dependencies. The repository root `index.html` is a landing page linking
the Atlas, Planet One, and the library demo, so the whole folder deploys as one
static site (Netlify: publish directory `.`). `planet-blocky.html` is now a
build alias of `planet.html` — the blocky look lives in the **Terrain look**
selector — and is kept only so older links still open.

**Optional blocky style preview:** open `examples/planet-blocky.html`, then
choose **Angled close-up**. The **Terrain look** selector compares Blocky and
Smooth at the same location. Blocky adds two-meter display terraces, crisp
cell tops, and earthy side walls; it does not alter `Planet.height()` or add
trees/rivers. Rebuild this separate preview with
`python tools/build_planet.py --output planet-blocky.html` to leave the current
`planet.html` untouched. Details and limitations are in `docs/PLANET.md`.

## Install locally

Python 3.10 or newer. In the virtual environment where you want to use it:

```sh
python -m pip install -e ~/nedgelaw
```

This installs only the namespaced `nedgelaw` package; it does not replace your
existing top-level `edgelaw`, `dfkit`, or `edgeframe` imports. Use
`python -c "import nedgelaw; print(nedgelaw.__file__)"` to verify the path.

The initial standalone copies at the workspace root are preserved snapshots.
Active package development belongs in `src/nedgelaw/`, not those root files.

The core has no runtime dependencies. Tables use your existing pandas if present,
or the bundled `dfkit.Frame` otherwise. To explicitly request pandas:

```sh
python -m pip install -e "$HOME/nedgelaw[pandas]"
```

Run `python ~/nedgelaw/examples/river.py` after installation. It queries the river,
checks an exact return, prints weather and dataframe examples, and inspects a
world tile. No server, credentials, GPU, or browser connection is needed.

## Python API

```python
import nedgelaw as ned

ned.edge(255)
ned.generations_frame(0, 16)
ned.table_frame()

river = ned.River("k2v-1 k2em", order=7, count=480)
river.drop(0, time=12.5)
river.snapshot(1_000_000_000)
river.frame(-12.5)
river.segments_frame()
ned.streams_frame(river.basin)
ned.statistics_frame(river.basin)
ned.confluences_frame(river)

key = ned.stream_of("k2v-1 k2em")
ridge = ned.build_ridge(27941, 256, 8, 0.45)
grid = ned.valley_grid(key)
ned.grid_frame(grid)
scene = ned.bank_scene(key)
ned.add_settlements(scene)
ned.props_frame(scene)
ned.heron_at(scene, 0, 12.5)
ned.fish_at(scene, 0, 12.5)
ned.bird_at(key, 0, 12.5)
ned.sun_state(0.5)

weather = ned.Almanac(driver=14)
weather.at(1_000_000_000)
weather.next_change("rain", 100)
weather.frame(100, 16)

world = ned.World("k2v-1 k2em")
world.tile(31, 31)
ned.next_settlement(100, level=4)

ground = ned.Landscape("k2v-1 k2em")
ground.height_at(31 * 16, 31 * 16)
ground.column(496, 496)
ground.chunk(31, 31)

sequence = ned.EdgeSequence("k2v-1 k2em", mode="bursty")
sequence.rows(49, 12)
ned.extended_edge(2**90 - 1, mode="bursty", seed="k2v-1 k2em")
experimental = ned.World("k2v-1 k2em", edge_mode="bursty", edge_strength=0.65)
experimental.tile(31, 31)

ranged = ned.World("k2v-1 k2em", edge_mode="range", edge_strength=1, massif=0.45)
ranged.tile(196, 58)
ned.ridge_at(27941, 9, 0.45)              # buildRidge's profile at one column, no window
ned.Planet("k2v-1 k2em", recipe="epic").sample(28, -144.1)   # a 7,500 m summit
ned.nth_settlement(100, level=4, n=3)
ned.distance_to(100, level=4)
ned.census(4096, 1004096)
ned.biggest_between(4096, 1004096)
ned.horton(river.basin)
ned.horton_frame(river.basin)
ned.deep_run_at(2**30 - 1)                # the ladder 49 rungs deep
ned.facts("lars")                         # yours.html's six true things
```

Droplet IDs are **zero-based**. `drop()` and `snapshot()` return
`[x, y, stream_order, route_fraction]`; table helpers name those columns.
Confluence table times are relative to the start of the stored route, not a
schedule of every repeating event. `next_settlement` is inclusive: it may return
the supplied index if that index already qualifies.

The `nedgelaw.edgeframe` module retains your original period-analysis tables:
`from nedgelaw import edgeframe`. Those particular dataframe functions still
require pandas and may perform substantial initial computation. The new
`ned.table_frame()` and other helpers use optional-pandas `dfkit` instead.

## JavaScript: separate, dependency-free

**Globe explorer:** open `examples/atlas.html`. This generated file embeds its
library, styles, and application scripts, so opening it from disk does not
depend on the browser allowing scripts from neighboring folders.
Drag and the ground stays under your pointer; scroll and the
zoom moves toward whatever you point at, from a whole globe down to about eight
blocks above the surface. Ctrl-drag (or shift-drag, or the right button) tilts
and turns the camera, so ridges have a horizon behind them; the compass button
levels it and faces north again. Double-click dives toward a spot, and a
field-guide location flies you there. Click anywhere on the visible globe to
inspect its actual world tile; "Look closer" drops you to a low pass over it.
The small overview map shows your footprint and also lets you navigate. Arrow
keys pan, shift with arrows tilts and turns, +/− zoom, N restores north, Enter
inspects the center, and Escape returns to the globe. Touch supports dragging
and two-finger pinch. Reduced-motion preferences disable the fly-to animation.
A lower-resolution Canvas fallback supports non-WebGL browsers.

**Edge sequence terrain lab:** the sidebar now offers Original, Smooth,
Rhythmic, and Bursty terrain recipes. The three experiments invent positive
increments beyond the original 49-entry edge table, then use six of those
increments to weight terrain at different spatial scales. **Blend into world**
controls the effect; zero reproduces the original terrain. **Compare Original
here** switches recipes without changing your selected coordinates or orbit
view. Gray/gold lines compare the same 65-tile height slice. **Land here** uses
the active recipe, including its trees and settlements. See `docs/EXTENSION.md`.

**Ranges & rivers** is a fourth recipe, built from RangeBlends' own terrain rules
rather than the edge table: `buildRidge`'s law (peaks at `k·2^v − 1`, amplitude
`CUM[v]`, half-width `WIDTH`, the `squareRamp` massif slide, `RELIEF` valley
deepening) read in two dimensions and bent by a per-tile warp, so ranges arc,
break into segments, cross into massifs, and leave basins that flood into lakes;
rivers are indexed channels — a trunk in every 64-tile valley, tributaries on
the rows the ruler sequence picks. The original continents sit underneath, read
on a cylinder so the recipe closes around the globe without a seam. Every tile
is still `(seed, x, y) → height`; nothing reads a neighbour. Python and
JavaScript agree to twelve decimal places (`tests/test_extension.py`).
`ned.World("k2v-1 k2em", edge_mode="ranges", edge_strength=1).tile(511, 127)`.

**RangeBlends range** is the fifth recipe, and the one range.html would ship:
`buildRidge`'s profile sets the skeleton (a smoothstep bump at every column
`k·2^v − 1`, height `CUM[v]`, half-width `WIDTH·2^(v−1)`, the maximum over
`v`, the `squareRamp` massif slide, `RELIEF` deepening) and the noise puts
weather on it, with the detail scaled by height so peaks get crags and valleys
stay quiet. The atlas's original world only ever added the bare run as a
one-tile needle at every divisible column; this reads Lars Rocha's ridge law
with its shoulders. A slow bend arcs the ranges and the original continents
decide where they stand, so a range makes islands where it meets the sea. The
**Massif** slider is range.html's: turn it up and the peaks merge into fewer,
bigger headlands. `ridge_at(g, vlim, massif)` is `buildRidge` at one column
and is checked against `~/pytools/rangeblends.ridge` peak for peak
(`tests/test_rangeblend.py`). Python and JavaScript agree to twelve decimal
places. `ned.World("k2v-1 k2em", edge_mode="range", edge_strength=1, massif=0.45)`.

**Relief** is the lab's third slider: how tall a height index is, in blocks per
64. It is one number carried with the terrain, read by `ned.Landscape`
(`relief=2.5` makes `height_scale` 160), by the globe's ray-marcher, and by
the props painted on the detail window, so houses and trees stay on the ground
at every setting. The height index never moves, so biomes, settlements, and
every `World` lookup are unchanged; only the blocks and the shading are taller.
Original stays at 1×; choosing the range recipe brings 2.5× with it until you
set your own. The walker's fog still fades at about 46 blocks, so the tall
ranges show best from a low pass out of orbit rather than from their foot.

**Field guides.** The atlas guide now names a **Summit**, the highest tile in
the window with its height in blocks, and a **Massif**, the broadest high
ground. Planet One's destination list ends with the planet's three highest
summits, each climbed to its actual top from a coarse sweep of every face and
named from its level-13 address: on `k2v-1 k2em` under epic ranges, Takuse Peak
at 7,721 m.

**World size.** The masthead's Size control picks the atlas window: 512×256
tiles (8 km around, a half-hour walk at the equator) or 1024×512 (16 km around,
four times the coastline). Nothing in the renderer assumes a size — every
atlas carries its own `geometry` (blocks around, radius in blocks, relief as a
fraction of the radius), and `World(seed, extent=(1024, 512))` tells the
ranges recipe how far its ridge and river identities repeat, so the wider
globe closes without a seam too. Same law, more world: the tile at (200, 117)
is the same tile in both windows.

**Land here:** select a location, then click **Land here** to explore its block
neighborhood. Try **Gevasu** in the `k2v-1 k2em` world (seed `k2v-1 k2em`) for a small house; the demos open on seed `k2v-1 k2em`.
W/A/S/D walk, drag or arrow keys look, Space jumps, and Shift moves faster.
The on-screen buttons also support walking and jumping. Click the ground view
to give it keyboard focus. Water supports swimming; walls and terrain collide.
**Return to orbit** (or Escape while the view is focused) takes you back above
your current tile. Selecting another place or generating a new seed leaves the
ground view. This ground renderer needs WebGL; the globe's Canvas fallback does
not provide first-person rendering.

The globe wraps a 512×256 tile window of `ned.World(seed)` onto a sphere. It
automatically scouts a coastline, woodland, snowy high ground, settlement, and
deep water where those features exist. The details panel shows the exact Python
lookup for a selected location. A perspective camera ray-marches the terrain
itself: heights become real elevation on the sphere at `ned.Landscape`'s own
scale, 64 blocks over a radius of 8,192/2π blocks, so this world's north-south
ranges cast shadows and break the horizon instead of being painted on. It does
not add real Earth data or invent an integrated global climate/geology model.
The rectangular window still has a longitude seam, and the shader averages the
polar rows across the longitudes the projection squeezes together there — a
display choice that leaves `atlas.heights` and every tile reading untouched.
Everything runs locally without fetching maps or libraries.

**Zoom generates, it does not magnify.** Past about 2.6× the atlas builds a
256×256 detail window for the piece of world on screen, sampled with the same
bilinear anchors and detail octaves as `ned.Landscape.heightAt`, and throws it
away when you leave. The status line under the globe reports the current
sampling: 16 blocks per sample for the plain tile texture, down past one sample
per block, where the trees and houses `ned.Landscape.props` would build appear
in place. Nothing is fetched and nothing is cached to disk; a closer look is
more arithmetic, not a bigger download. The camera also refuses to sink below
the ground it is looking at, so a 40-block ridge is a floor, not a wall you
fly through.

Landing goes further than the globe's closest pass: `ned.Landscape` refines each
tile into 16×16 block columns you can collide with, with deterministic local
relief, trees, and hollow houses. The globe reads the same heights from above,
so the hillside you circle at treetop height is the one you walk on.
Every `(tile_x * 16, tile_z * 16)` height anchor exactly matches `World.height`.
Nearby chunks stream in and are discarded when you walk away; they regenerate
from coordinates, not stored history. The demo stops at the finite atlas edges.
This is an explorable heightfield prototype, not editable voxels, caves, a
seamless spherical terrain system, or a saved game. See `docs/GROUND.md` for the
coordinate contract and Python/JavaScript examples.

Double-click **`examples/index.html`** for a local river, bank, weather, and time
return demo. No installation or server is required. It uses the actual bundle,
not another copy of the math in the page.

Classic browser script:

```html
<script src="../js/nedgelaw.js"></script>
<script>
  const river = new ned.River("k2v-1 k2em");
  console.table(river.rows(12.5));
  console.table(new ned.Almanac().rows(0, 8));
</script>
```

Node/CommonJS: `const ned = require("./js");`

ES modules: `import ned, {River} from "./js/index.mjs";`
Browser ES modules generally need a local HTTP server; use the classic script
for double-click HTML. Python uses snake_case and JavaScript uses the original
camelCase names (`stream_of` / `streamOf`, `bank_scene` / `bankScene`).

Canvas renderers and Web Audio (`ned.bankSound`) are browser-only; the data
generators work in Node. Audio is never started on import and requires an
explicit user gesture. Python exposes sound-control values (`day_voices`,
`water_share`), not a Web Audio replacement. JavaScript tables are arrays of
records for `console.table`; the Python dataframe API is not a JavaScript port.

## What determinism means here

- Paths are generated and stored once. Position is a lookup in time, not an
  accumulated particle simulation. Droplets repeat their routes.
- Revisiting the identical numeric time in the same implementation gives the
  identical coordinates regardless of query order.
- Integer hashes and seeds match JavaScript, including its UTF-16 handling of
  emoji. River topology is compared against the original JavaScript.
- River coordinates use platform `sin`, `cos`, and floating-point arithmetic.
  Cross-language bit-for-bit coordinates are **not promised**. Tiny differences
  in route duration can be magnified at very large times. Tests use explicit
  absolute tolerances: 1e-12 for ordinary times, 1e-5 at ±1e9, 0.005 at ±1e12.
  Discontinuous route restarts/order changes need special care in future ports.
- Time inputs to the river/wildlife interface are finite and within ±1e12.
  Network orders are 1–12, with the original 60,000-segment budget and an explicit
  `truncated` flag. Treat these as local creative tools, not physical hydrology.
- Python `edge()` uses your table through rung 48 and rejects missing rungs.
  `run_at()` / JavaScript `runAt()` preserve RangeBlends' clamp at rung 17.
  The new JavaScript `edge()` shares the Python table but accepts only safe
  integer generation addresses; Python retains arbitrary-size integer lookup.
- The almanac's readings are indexed by **integer tick and driver**, not seed.
  Weather periods are precomputed from the triangular-XOR recurrence. These
  are game-weather signals, not a meteorological forecast.
- Wildlife preserves the original JavaScript remainder and signed-shift
  behavior, including negative-time fish/bird positions outside their usual
  positive-time display range. It is not a rewritten animal simulation.
- The exploration world, ridge window, river network, and bank are separate
  generators sharing mathematical tools—not a globally integrated planet.

## Development and validation

```sh
python tools/build_js.py
python tools/build_atlas.py
PYTHONPATH=src python -m unittest discover -s tests -v
node --test tests/test_*.cjs
python -m pip wheel . --no-deps --no-build-isolation --wheel-dir dist
```

The parity tests run preserved original JavaScript in Node rather than assuming
the new Python implementation is correct. They cover three seeds (including
emoji), topology, routes, terrain, bank props, weather drivers, and large signed
times. Additional tests cover table backends, validation, import formats,
ground-coordinate parity, chunk boundaries, walking collisions, and lifecycle.
They do not constitute browser visual or audio QA.

`tools/build_js.py` builds `js/nedgelaw.js` from preserved `js/vendor` scripts,
the marked pure-function sections of `js/reference` pages, `js/extension.js`, `js/landscape.js`,
and `js/api.js`.
Those reference pages are source snapshots, not standalone working demos.
The edge lookup table is read directly from Python to avoid manual drift.

`tools/build_atlas.py` also rebuilds that JavaScript bundle, then combines it
with `examples/atlas.template.html`, `atlas.css`, `atlas-model.js`,
`ground-model.js`, `ground-view.js`, and `atlas.js`.
Edit those sources and rerun the command; `examples/atlas.html` is the generated
single-file output. Its scripts run in dependency order after the page markup.

## Release status and credit

Local development version **0.3.0**. Nothing is uploaded to PyPI, npm, or hosting.

**0.3.0** adds the RangeBlends range recipe and massif slider to the atlas, the
epic recipe to Planet One, `ridge_at`, the O(1) settlement arithmetic
(`nth_settlement`, `distance_to`, `census`, `biggest_between`), Horton's ratios,
the 49-rung `deep_run_at`, and yours.html's `facts`, all from Lars Rocha's
RangeBlends and `rangeblends.py`, in both languages with parity tests; a Relief
slider and `Landscape(relief=...)`; and summit destinations in both guides.

**0.2.0** adds the ranges recipes (atlas and planet), settlements and trees on
the sphere, the Sundial & Almanac clock as `Environment` in both languages,
flight in both demos, the landing page and guide.
The npm manifest is private to prevent accidental publishing. Choose a release
license with Lars and check registry name availability before public release;
no license grant has been invented here.

**Mathematics and original river/world engines: Lars Rocha's SPSP and RangeBlends.**
This package adds namespacing, Python ports, import adapters, dataframe views,
examples, and cross-language checks. The original projects remain untouched.
