4bd8481be1
The owner asked twice why there are still three separate boards. The honest answer, and what this round executes: **it feels like three boards, but not because the scale jumps 20x — because the three packs draw three different Californias, and the loudest difference is that the mountains are four times taller on one of them.** **THE 20x HORIZONTAL SCALE JUMP IS INVISIBLE**, and measuring that collapsed the cost of this whole round. `World.project` is a uniform scale in x/z with no vertical term, and a uniform scale leaves a perspective image identical — so a camera carried across the seam on matched true-metre offsets draws a pixel-identical horizontal frame. 1,919 -> 94 m/unit costs nothing to look at. Rescaling was never the problem. A boot card, a tab strip and a 4.17x vertical deflation were. **THE PAUSE WAS MOSTLY FAKE.** A switch covered the screen for 1,715 ms but only 608 ms blocked the main thread; the page drew 46 of 69 frames with nothing to show, because the outgoing board had already been disposed. `mountCity` now retains it: the incoming board builds BEHIND a live, interactive picture, and `stage.setScene` fires only on completion. Measured across all six directions, three runs each — boot card yes -> **no**, opaque cover 726-1,415 ms -> **0**, blank frames 21-46 -> **exactly 1**, wall clock down 12-29%, blocked main thread down 15-47%. A return to a board already seen links **zero** shader programs and blocks **zero** milliseconds: 298-312 ms of camera flight where it was ~1,600 ms behind a card. Disposal had been throwing away the shader cache too — linkProgram ran 38, 59, 78, 109, 127 across five mounts and never reused one. **The transition is a fog dip, not a crossfade**, through the `setAerialFog` seam built last round. Every both-boards-live crossfade breaks a budget — ca+sf is 2,640,307 triangles against bay-area's 2,600,000 cap — and a fade never lands inside the harness's sample window, which is the "a cap you do not measure is a cap you do not have" failure this repo already argues against. The dip costs zero triangles and zero draw calls, and it hides the 4.17x deflation, the 4,025 m projection disagreement and the vanishing 2 km freeway symbols at once, because all three happen at maximum obscuration. It is also diegetic: a descent through haze. The first dip was wrong and the photograph caught it: collapsing to 6% of board SPAN turned the whole night frame into one flat field — the exact "turns the map off" failure the risk list named. Re-anchored to 70% of camera STAND-OFF, so the coastline survives and only the relief melts. **One ladder, one places list.** 26 authored chapters become 24 rungs sorted descending by STAND-OFF, not altitude — by altitude they interleave badly and altitude cannot tell a low oblique from a high plan. The three-board tab strip is off by default; the left column is now one scrolling list of all 24 rungs under three region headings that does not change when the board does. Only which row is lit changes. Label collisions are resolved in the ladder and never in a pack, so the 29 index-aimed capture guards are untouched. The minimap stops turning through 90 degrees between boards: every board is pinned to a rectangle with California's proportions. **SF and SoCal are not regressed**, and that was the acceptance that mattered: 95.9-98.8% of board pixels are delta-0 against a baseline hash-verified identical to what the live site serves, and every one of the 34-70 surviving pixels per frame is an aircraft or a hull. **A real defect found only by photograph:** `minimap.setMarkers()` had zero call sites. Every marker on every board was gone — the LA studio's door dot, the Bay Area's eight company markers — dropped when the minimap went per-board. Typecheck, tests, budgets and the console were all green with that bug in. Also fixed: two capture presets that lied. `look.mjs`'s `glyph-la` and `glyph-sf` claimed California chapter closeups and returned SoCal and Bay Area frames, because they aimed by chapter index and the indices had moved. Aiming is now by identity, with a guard test. NOT SHIPPED, DELIBERATELY: the pack merge. At Bay density it is 34.04M triangles, 13x the highest budget — dead, not a trade. At SoCal density it is 1.99M and fits today, and the price is San Francisco rendering at 164 m lots instead of 40 m, i.e. SF looking the way SoCal looks now. SF and SoCal carry every marketing still on the site. That is the owner's decision and it is worthless as an argument and decisive as a photograph, so it ships as a measurement artifact with a side-by-side still and is wired into nothing. The four data reconciliations that would make one world honest — one exaggeration rule, roads in metres, one projection centre, one coastline convention — are behind TERA_RECONCILE, default OFF. Tests 1,570 -> 1,651, server 295. All ten budget cells pass, no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1065 lines
58 KiB
Markdown
1065 lines
58 KiB
Markdown
# Tera — architecture
|
||
|
||
**Tera** is the map view: California and its cities from above, rendered in three.js, public and
|
||
self-hostable. **Spaces** is the other half — the offices you walk into. They
|
||
are one engine and one asset library, seen from outside and from inside.
|
||
|
||
Lives at `tera.lumbridgecorp.com`. Apache 2.0, on the fleet's own Gitea.
|
||
|
||
## The names
|
||
|
||
Getting these straight early matters, because two of them were nearly overloaded
|
||
onto things that already exist.
|
||
|
||
| name | what it is |
|
||
| --- | --- |
|
||
| **Lumbridge Simulate** | the product line these sit under |
|
||
| **Tera** | the map view. Cities — SF, LA, NYC — from above. Public, no login. |
|
||
| **Spaces** | the interiors product. Offices you enter, host, and self-host. |
|
||
| **Office** | one building's interior, belonging to a tenant. You go *into* an office. |
|
||
| **Space** | a room or zone inside an Office — a desk bay, a meeting room, a lounge. |
|
||
|
||
Two things this avoids. `lumbridge-simulate` is already a **Rust crate** in the
|
||
private monorepo — the Governor's memory-admission simulator — and in its
|
||
vocabulary a **"Scene" is a set of models**, not a three.js scene. This engine
|
||
therefore does not take either word for itself: it is Tera, and its three.js
|
||
scene stays an implementation detail behind `createScene()`.
|
||
|
||
One overload survives on purpose: *Spaces* the product versus *a Space* the
|
||
room. It sits at two different levels and matches how people already talk about
|
||
offices, so it is kept rather than invented around.
|
||
|
||
---
|
||
|
||
## 1. Why this is its own repo, and why it is public
|
||
|
||
Tera began as `/live` inside Workie, the private career portal. That is now
|
||
**deleted**, not deprecated — a 3D city engine has no business living in a job
|
||
tracker, and Workie is a private repo besides.
|
||
|
||
**You cannot cleanly Apache-2.0 a subdirectory of a private repo.** The licence
|
||
attaches to a distribution, and a consumer has to be able to clone the thing the
|
||
licence covers. A folder inside `lumbridgecorp-website` or inside
|
||
`lumbridge-runtime` would mean every release is a manual extraction, and every
|
||
extraction is a chance to ship something private by accident. The point of this
|
||
project — a reusable asset library other people can build on and self-host — needs
|
||
a repo that *is* the open-source artefact.
|
||
|
||
So: **`lumbridge-public/tera` on the fleet's own Gitea.** Not
|
||
GitHub. Self-hosting the forge is the same claim the engine makes: the whole
|
||
thing runs on hardware you own, and nothing about it requires somebody else's
|
||
platform to stay alive.
|
||
|
||
### The 2D world comes down
|
||
|
||
The existing Lumbridge world at `world1.lumbridgecorp.com` is a Phaser 2D
|
||
office, and its tileset is exactly what stops it being open-sourceable. It has
|
||
no real users, so it is being retired rather than migrated — Tera and Spaces
|
||
replace it outright, with original assets, which is the whole point.
|
||
|
||
That reframes this repo: it is not a map that happens to sit next to the
|
||
platform, it is the platform's renderer. The city is the outside of the world,
|
||
offices are the inside, and a tenant moves between them.
|
||
|
||
---
|
||
|
||
## 2. Layout
|
||
|
||
```
|
||
tera/
|
||
├── src/engine/ # renderer. Knows about terrain, blocks, streets, flights.
|
||
│ ├── types.ts # City, District, Hill, Marker, FlightSource
|
||
│ ├── world.ts # projection + cached heightfield, built per city
|
||
│ ├── terrain.ts # relief, coastline, parks
|
||
│ ├── blocks.ts # the built city, on a per-district street lattice
|
||
│ ├── structures.ts # bridges, streets, freeways
|
||
│ ├── markers.ts # pins. Takes Marker[], knows nothing about companies.
|
||
│ ├── flights.ts # FlightSource interface + a simulated implementation
|
||
│ └── scene.ts # lights, sky, camera flights, render loop
|
||
├── src/interiors/ # the inside of the world — floorplans, desks, presence
|
||
├── src/assets/ # the library. Original meshes and materials, Apache 2.0.
|
||
├── src/transport/ # serialisable route packs + renderer-free fixed-step sim
|
||
├── src/cities/ # data packs. Pure geography, no code.
|
||
│ ├── california.ts # state/corridor scale; LA ↔ SF continuity
|
||
│ ├── sf.ts # ~1000 lines of coastline, hills, districts, landmarks
|
||
│ └── socal.ts # LA / OC / Riverside
|
||
├── src/realtime/ # strict game-state wire/client/interpolation adapters
|
||
├── src/media/ # separate screen signaling, ICE and texture lifecycles
|
||
├── src/profile/ # local profile plus ephemeral webcam consent/capture
|
||
├── src/adapters/ # generic HTTP and sample-data boundaries
|
||
├── src/devices/ # smart hardware: types, a fixed-step simulator, the feed seam
|
||
├── src/arena/ # the RL environments, wrapping the same simulators the renderer drives
|
||
├── src/ui/ # every interface decision, and the one module that writes to the page
|
||
└── src/main.ts # the standalone demo app — assembly, and nothing else
|
||
```
|
||
|
||
The split that matters: **`engine` never imports `cities`, and neither imports
|
||
`adapters`.** A city is data handed to the engine. A marker is a `{id, lat, lng,
|
||
label, colorKey}` handed to the engine. The engine has no idea Workie exists,
|
||
which is what makes it publishable, self-hostable by someone with no Lumbridge
|
||
account at all, and free of anyone's private data.
|
||
|
||
`interiors` and `assets` sit beside `engine` rather than inside it: an office
|
||
and a city share the projection, camera, lighting and render loop, and differ
|
||
only in what they put in the scene. That is why `scene.ts` owns the loop and
|
||
knows nothing about terrain specifically.
|
||
|
||
`ui` is the newest of these and the split inside it is the one worth knowing.
|
||
`ui/chromeState.ts` is a **pure function**: hand it a plain object describing what
|
||
the application is, get a plain object back describing what the interface should
|
||
be. `ui/mount.ts` is the only module in the product that writes to the DOM, and it
|
||
writes exactly what that function returned. `main.ts` therefore makes one call —
|
||
`chrome.apply(chromeState(inputs))` — where it used to make about forty
|
||
`element.hidden = someCondition` decisions with the condition and the write on the
|
||
same line, none of which could be reached without a WebGL context. The rule that
|
||
keeps it that way: a new piece of chrome is a field in `ChromeInputs`, a decision
|
||
in `chromeState` and a write in `mount`, and never a fortieth mutation in the app.
|
||
|
||
`devices` follows the same shape one level down, and for the reason §7 gives about
|
||
presence: a `DeviceDeclaration` is **authored** — it says a microphone exists, what
|
||
it can be asked to do and which prop is its hardware — and lives in the office
|
||
pack, public by construction. A `DeviceState` is what that microphone is *hearing*,
|
||
never appears in a pack, and arrives from a route that can refuse an anonymous
|
||
caller. `src/interiors/devices.ts` is the render layer for the first and
|
||
`src/devices/sim.ts` is the state machine behind the second; the arena imports the
|
||
state machine directly, so what a policy is trained against and what a viewer is
|
||
looking at are the same code.
|
||
|
||
`transport` also stays outside the renderer. A `TransportPack` is plain JSON and
|
||
`VehicleSimulation` returns plain geographic poses. Three.js enters only in the
|
||
`roadTraffic` render layer, which projects poses through the active `World`.
|
||
That separation lets the authoritative session service validate and replay the
|
||
same route state without importing a browser or a GPU.
|
||
|
||
### 2.1 Product/control-plane boundary
|
||
|
||
Tera/Spaces is the authoritative renderer and simulation client. The separate
|
||
`lumbridgecorp` service remains the control plane: identity, tenant registry,
|
||
membership, billing, provisioning, and short-lived launch grants. It must not
|
||
grow a second world renderer or own frame-by-frame simulation.
|
||
|
||
The public Tera build remains zero-config and useful without any service. Hosted
|
||
presence, multiplayer and remote media arrive through adapters whose absence
|
||
cannot stop the city, office, traffic, or sky from running. Local profiles and
|
||
webcam faces require no hosted service; webcam pixels never enter the realtime
|
||
or office-screen transports. This is the replacement path for the retired
|
||
Phaser World 1; it is not a migration of that renderer.
|
||
|
||
The first hosted adapter is now concrete: the Fastify API owns short-lived,
|
||
in-memory authoritative presence sessions and streams validated deltas over
|
||
authenticated POST/SSE. `src/realtime` remains renderer- and transport-neutral;
|
||
the browser transport, interpolation buffer, procedural peer renderer and small
|
||
status UI are separate adapters. A city or full-depth office opts into remote
|
||
peers explicitly, while an anonymous/public scene does not even allocate their
|
||
assets. Interest changes clear the old coordinate frame before a new one is
|
||
shown. Game-state deltas never carry webcam or screen media. Webcam faces are
|
||
local-only; office screens use a separate consent, signaling and authorization
|
||
lifecycle.
|
||
|
||
---
|
||
|
||
## 3. Three rules that keep Apache 2.0 honest
|
||
|
||
Apache 2.0 is a promise that everything in the repo is ours to give away. Three
|
||
things would quietly break that promise, and all three are easy to walk into.
|
||
|
||
### 3.1 No trademarks in the repo
|
||
|
||
Company logos are **trademarks**, not code. Salesforce's logo is not ours to
|
||
relicense no matter how the file got here, and an Apache-2.0 repo containing
|
||
`logos/salesforce.svg` is making a claim it cannot back.
|
||
|
||
The NYC atlas ships 47 of these in `public/logos/`. We do not.
|
||
|
||
- The shipped application does not fetch or display third-party logos.
|
||
- The repo carries zero logo files. Self-hoster asset directories are ignored,
|
||
and the provenance/no-binary gates prevent a logo-shaped binary from quietly
|
||
becoming part of the procedural source library.
|
||
- `NOTICE` carries the standard "trademarks are the property of their
|
||
respective owners; their use here is nominative" line.
|
||
|
||
### 3.2 No OSM-derived coordinates in the repo
|
||
|
||
This is the subtle one, and it is the reason the geography in `cities/sf.ts` is
|
||
hand-traced rather than imported.
|
||
|
||
OpenStreetMap data — including anything that comes out of Nominatim geocoding —
|
||
is **ODbL**. ODbL is share-alike: a "derivative database" has to be released
|
||
under ODbL too. Committing a table of company lat/lngs geocoded from Nominatim
|
||
into an Apache-2.0 repo mixes an incompatible share-alike obligation into a
|
||
permissive one. That is exactly the kind of thing that makes a repo unusable for
|
||
the people we want using it.
|
||
|
||
So:
|
||
|
||
- **Geography** (coastlines, hills, districts) is traced by hand from scratch.
|
||
Original expression, ours, Apache 2.0. This is already true for SF.
|
||
- **Elevation**, when we want real terrain, comes from **USGS/SRTM**, which is
|
||
US-government public domain. Not OSM.
|
||
- **Geocoded company coordinates never enter this repo.** They live in a private
|
||
database and arrive over the API at runtime.
|
||
|
||
There is a real result here: the licence constraint and the privacy constraint
|
||
want the same thing. Company positions and pipeline status both stay behind the
|
||
API; the open-source repo holds the city and the renderer. The tempting shortcut
|
||
— "just commit an `sf-companies.json`" — breaks both at once.
|
||
|
||
#### Correction: containment is not discharge
|
||
|
||
An earlier version of this section stopped at the paragraph above, and it was
|
||
**wrong** in a way worth recording rather than quietly fixing.
|
||
|
||
Keeping OSM-derived coordinates out of the repo solves licence *mixing inside
|
||
the repo*. It does not touch ODbL's actual trigger. Serving a snapshot of those
|
||
coordinates from a public endpoint is **Publicly Using a Derivative Database**,
|
||
which brings ODbL §4.3 attribution and §4.4 share-alike onto the served data
|
||
**regardless of where the rows are stored**. Moving the table off-disk hides the
|
||
obligation; it does not discharge it.
|
||
|
||
So the rule is about the *geocoder*, not the storage:
|
||
|
||
- The sanctioned geocoder is the **US Census Geocoder**
|
||
(`geocoding.geo.census.gov`) — a US Government work in the public domain,
|
||
keyless, covering every city planned here.
|
||
- **Google, Mapbox and HERE do not solve this either.** Their terms restrict
|
||
storing and redistributing returned coordinates, which is precisely what a
|
||
public snapshot does. "Not OSM" is not the test; "may be redistributed" is.
|
||
- Every synced row carries a **provenance** field, and the public-shape
|
||
assertion rejects any row whose provenance is not on a non-ODbL allowlist. The
|
||
gate that already checks field *names* now also checks where a coordinate
|
||
came from.
|
||
|
||
Nothing had been built against the old reasoning when this was caught, which is
|
||
the only reason it was cheap.
|
||
|
||
### 3.3 The engine takes no position on what a marker means
|
||
|
||
`markers.ts` renders `Marker[]`. A marker has a `colorKey`, not a `status`. The
|
||
mapping from "rejected" to red lives in the *adapter*, in the consuming app.
|
||
|
||
That is what lets the same engine serve the private Workie build (coloured by
|
||
pipeline state), the public Lumbridge demo (coloured by sector), and whatever
|
||
someone else builds from this repo, without any of them being a fork.
|
||
|
||
---
|
||
|
||
## 4. Flights: FlightRadar24 is the wrong source for this
|
||
|
||
Real aircraft over the Bay is a great idea. FR24 specifically is a problem, and
|
||
it is worth being clear about why before any code is written against it.
|
||
|
||
FlightRadar24's terms prohibit scraping and prohibit redistributing their data;
|
||
their commercial API is licensed per-seat and explicitly does not grant
|
||
redistribution. An Apache-2.0 repo shipping an FR24 client is publishing
|
||
instructions for violating a ToS, and any FR24 data that reached the browser
|
||
could not be relicensed onward. Neither is fatal to a private deployment, but
|
||
both are fatal to the open-source story that is the point of this repo.
|
||
|
||
The engine therefore takes a **`FlightSource` interface**, and the repo ships
|
||
only sources it can actually give away:
|
||
|
||
| source | licence | notes |
|
||
| --- | --- | --- |
|
||
| **`SimulatedFlights`** | ours, Apache 2.0 | great-circle tracks on real SFO/OAK/SJC approach and departure paths. **This is what ships first**, and it is genuinely enough — the map wants convincing motion, not a spotter's log. |
|
||
| **`adsb.lol` / `airplanes.live`** | community, open terms | free, no key, real aircraft. The default "real data" adapter. |
|
||
| **OpenSky Network** | free, non-commercial | fine for a demo, awkward for a commercial product page. |
|
||
| **your own receiver** | *no licence at all* | an RTL-SDR + `dump1090` on any fleet box in the Bay produces first-party ADS-B. Nothing to comply with, and it is very on-brand. |
|
||
| FlightRadar24 | commercial, no redistribution | if it is ever wanted, it is a **private adapter in the Lumbridge deployment**, not in this repo. |
|
||
|
||
Recommendation: build `SimulatedFlights` now, `adsb.lol` next, and treat a
|
||
receiver as the eventual real answer.
|
||
|
||
---
|
||
|
||
## 5. LA is not "SF but more data"
|
||
|
||
SF is 0.20° × 0.36°. At the 45 m cells the SF heightfield uses, that is 336k
|
||
lattice points and a 1.0 s build. (The pack has since grown to the whole Bay
|
||
Area: 0.53M points and about 2.3 s.)
|
||
|
||
LA + Orange County + Riverside is roughly 0.5° × 1.9° — about **14× the area**.
|
||
The same approach would be 4.6M lattice points and a mesh nobody can load. This
|
||
is the one place where the engine as written does not simply extend, and it is
|
||
better to know that before writing `cities/la.ts` than after.
|
||
|
||
Two changes make it work, and both are cheap now:
|
||
|
||
1. **Cell size is per-city, not a constant.** LA's basin gets coarse cells;
|
||
nothing is lost, because LA's relief is mountains at the edges rather than
|
||
SF's hills every four blocks.
|
||
2. **Cities declare `focusRegions`** — a handful of boxes (DTLA, Santa Monica,
|
||
Culver, Irvine, Pasadena) rendered at fine resolution, with the basin between
|
||
them coarse. This is the LOD story, and it is a per-city data declaration
|
||
rather than engine machinery.
|
||
|
||
SF gets one focus region covering the whole city and behaves exactly as it does
|
||
now. LA gets six. NYC, later, gets Manhattan plus the inner boroughs.
|
||
|
||
### 5.1 The heightfield is built off the main thread
|
||
|
||
Both numbers above are seconds during which nothing rendered and nothing
|
||
responded, because the build ran on the thread that paints. It runs in a Worker
|
||
now (`src/engine/terrain.worker.ts`), which is why `createScene` is async and
|
||
takes a `Stage` rather than a canvas.
|
||
|
||
Three consequences worth knowing before touching it:
|
||
|
||
- **The city pack has to stay structured-cloneable.** It is posted to the
|
||
worker as data. A `City` that acquires a method, a class instance or a
|
||
closure stops being sendable, and the fix is to remove it rather than to
|
||
JSON round-trip around it — a city pack containing code is the thing §2
|
||
exists to prevent.
|
||
- **The sampled accessors stay synchronous.** `elevationAt`, `groundAt` and
|
||
`isLand` are called in tight loops by `terrain.ts`, `blocks.ts` and
|
||
`minimap.ts`; the asynchrony is confined to *becoming ready*, and sampling
|
||
before then is a documented error rather than a silent zero.
|
||
- **There is a main-thread fallback and it is not optional.** An environment
|
||
without Workers — a `file://` open, a locked-down browser — still has to
|
||
build and render, because "clone it and it works" has no exception clause.
|
||
|
||
The same reasoning put Spaces behind an `await import()`: the office is a large
|
||
slice of the bundle and most visitors never open it, so it is fetched when
|
||
somebody reaches for the door rather than by everybody at boot.
|
||
|
||
---
|
||
|
||
## 6. Hosted data and authority boundaries
|
||
|
||
Tera has no database requirement. The optional Fastify API supplies bounded
|
||
weather, flight, satellite, marker, office, realtime and screen-signaling
|
||
adapters behind same-origin `/api/v1/*`; a static clone continues with authored
|
||
geography and simulation when that API is absent.
|
||
|
||
Marker rows cross the strict public-shape and provenance allowlist before the
|
||
renderer sees them. The renderer receives only `Marker[]` and never pipeline or
|
||
tenant records. Realtime peers receive opaque page-scoped actor identifiers and
|
||
validated pose state, never auth subjects or profile faces. Screen media is
|
||
peer-to-peer: the API validates exact authored screen bindings and relays bounded
|
||
signaling, but it never receives media bytes, recordings or source locators.
|
||
|
||
The currently implemented hosted authorization is deployment-member plus global
|
||
admin. It is deliberately not described as tenant/office membership. A control-
|
||
plane membership provider must be added before private tenant offices share the
|
||
same deployment authority boundary. TURN is also optional and fail-closed; the
|
||
credential endpoint being implemented does not mean a public relay is deployed
|
||
or approved.
|
||
|
||
---
|
||
|
||
## 7. Interiors — and why the asset library is the real product
|
||
|
||
The existing Lumbridge world is a Phaser 4 2D office, and Karti's read is right:
|
||
its tileset is the thing that stops it being open-sourceable. Redrawing that in
|
||
3D with original assets is not a graphics exercise, it is a **licensing
|
||
unlock** — it is what lets the walkable-office half of Lumbridge ship under
|
||
Apache 2.0 at all.
|
||
|
||
That makes `src/assets` the highest-leverage part of this repo over time: an
|
||
original, consistent, Apache-2.0 library of desks, chairs, partitions, screens,
|
||
doors, lighting rigs, floor and wall materials. Built once, used by every
|
||
Lumbridge world and by anyone else who wants one.
|
||
|
||
Interiors reuse the engine's renderer, camera machinery and render loop, and
|
||
swap the city layer for a floorplan layer. The city view and the office view are
|
||
the outside and the inside of one world, and moving between them is a scene swap
|
||
rather than a different application.
|
||
|
||
Two claims that stood here earlier were wrong, and CONTRACT.md carries the
|
||
resolutions:
|
||
|
||
- **They do not share a `THREE.Scene`.** They cannot. San Francisco's `latScale`
|
||
puts one scene unit at ~94 m with 3.6× vertical exaggeration; an office renders
|
||
at 1 unit = 1 m. There are two scenes over one renderer, and the city scene is
|
||
*retained and paused* when you step inside rather than disposed — rebuilding
|
||
its 0.53M-point heightfield costs about two and a half seconds on the way back
|
||
out.
|
||
- **A person at a desk is not a `Marker`.** A marker is geographic — it has a
|
||
lat/lng. Presence binds to a **seat id** and never to a coordinate, which is
|
||
what lets the office geometry be public and open-source while who is sitting
|
||
in `eng-04` stays private data behind an API. Same shape as the marker rule,
|
||
one level in.
|
||
|
||
Reference point: the per-office spatial products in this space (Simile and
|
||
friends) are the shape to aim at. The difference Spaces is going for is that this
|
||
one is Apache 2.0 and self-hostable — you can run your own world on your own
|
||
hardware, which is the same claim `lumbridge-compute` makes about compute.
|
||
|
||
That phase is now implemented: offices consume the same procedural library and
|
||
retain the city scene behind a metre-scale interior scene swap.
|
||
|
||
---
|
||
|
||
## 8. Current release topology
|
||
|
||
The repository now ships California corridor, Bay Area and Southern California
|
||
boards; walkable offices; procedural actors, vehicles and aircraft; optional
|
||
authoritative realtime; local webcam faces; and separately authorized office
|
||
screen signaling. `BUILD_PLAN.md` is the milestone evidence ledger.
|
||
|
||
Four things landed together and are worth naming, because each crosses a boundary
|
||
this document describes:
|
||
|
||
- **One environment map for the page.** `src/engine/environmentRig.ts` derives a
|
||
sky (city) or a room (office) from the `LightingState` that `Atmosphere` already
|
||
decided, so the eleven metallic material roles and the Model X's clearcoat have
|
||
something to reflect. It sits at the *end* of §4's one-way street: it is handed a
|
||
decision and never makes one, and it constructs no light. One rig is built beside
|
||
the one renderer and shared by every scene; each scene releases itself from it on
|
||
dispose.
|
||
- **The interface is a pure function plus an applier.** See §2 above.
|
||
- **Smart hardware in the studios**, declared in packs and simulated in the tab
|
||
when a deployment has no device source — the same anon-first fallback
|
||
`SimulatedFlights` and `sample.ts` already make.
|
||
- **A Model X on each studio's arrival apron**, driven by
|
||
`src/transport/vehicleTelemetry.ts`, and overhead traffic in a sited office's
|
||
sky drawn from the same `FlightSource` the board outside is polling. The
|
||
aircraft are placed on a dome at their true bearing and elevation rather than at
|
||
their true range: an airliner at twenty kilometres is past a room-scale far
|
||
plane, and a map symbol drawn in 3-D is what §4's traffic has always been.
|
||
|
||
The source code is Apache-2.0. `src/assets/LICENSE-ART` additionally dedicates
|
||
the artistic output of the procedural asset library under CC0-1.0.
|
||
`PROVENANCE.json`, the dependency allowlist and SPDX SBOM gate the distribution.
|
||
|
||
Remaining questions are deployment questions, not missing renderer contracts:
|
||
|
||
1. Which authoritative control-plane provider proves tenant/office membership?
|
||
2. When do operators approve public TURN/DNS/firewall/certificate changes after
|
||
relay-only and abuse-boundary acceptance?
|
||
3. Which Firefox/WebKit versions become supported browser-test targets?
|
||
4. Which additional worlds or optional asset packs enter through the same
|
||
provenance and performance gates?
|
||
|
||
---
|
||
|
||
## 9. Fire — and why the privacy filter lives on cloud-1
|
||
|
||
Tera can draw the wildfires that are actually burning in California. The data
|
||
behind that is collected by `fleet-tools/fires` on **cloud-1**, into a sqlite
|
||
store that is centred on the owner's *house*. That single fact decides the whole
|
||
architecture of this feature, and it is worth stating before any of the shapes.
|
||
|
||
### 9.1 A projection, not a copy
|
||
|
||
cloud-1 serves `GET /api/fires/incidents` and `/api/fires/detections` as a
|
||
**projection**: a hand-written column list that a home-relative column is not in.
|
||
`tera-api` consumes those over `createUpstream` exactly the way it consumes
|
||
satellites, and never opens the sqlite. The store is not copied, mirrored,
|
||
exported on a cron, or replicated to cloud-2.
|
||
|
||
The reason is not convenience. These columns are computed against the owner's
|
||
home and must never reach a public wire:
|
||
|
||
| Column | Table |
|
||
|---|---|
|
||
| `distance_km` | `observations`, `detections`, `evac_zones` |
|
||
| `bearing_deg` | `observations` |
|
||
| `threat` | `observations` |
|
||
|
||
`threat` is the one that is easy to miss. It is
|
||
`(16.0 / distance_to_house)^2 x log10(acres) x momentum x containment x
|
||
wind-alignment` — so with acreage and containment public, publishing `threat`
|
||
**solves for the distance**. One circle around the house per fire; three fires
|
||
give an intersection. Two more are the coordinate with extra steps: `air.area`
|
||
is the literal string "Norco and Corona", a named two-town area next to the
|
||
house, and the `fires near` CLI prints the full street address in its header, so
|
||
no design may proxy that stdout.
|
||
|
||
If `tera-api` pulled the sqlite, every one of those would be sitting on cloud-2
|
||
one careless `SELECT *` away from a public route. Serving a projection makes the
|
||
leak structurally impossible rather than merely avoided — and it costs this repo
|
||
nothing, because `adapters/http.ts` and `createUpstream` already are exactly this
|
||
shape. It also refuses the failure mode this store exists to prevent: a cron
|
||
export that silently stops looks identical to "no fires", and `BACKUP.md` already
|
||
records what a timer reporting success is worth.
|
||
|
||
Note also that `acres`, `pct_contained` and the rest live in `observations`, not
|
||
in `incidents` — the projection is a join to the latest observation per incident,
|
||
and the join's `SELECT` list is precisely where a lazy `o.*` would put the
|
||
home-relative columns on the wire.
|
||
|
||
### 9.2 The promotion gate is the feature
|
||
|
||
`promote()` in `src/server/fires.ts` decides what is worth drawing:
|
||
|
||
```
|
||
acres >= 10
|
||
AND coalesce(pct_contained, 0) < 80
|
||
AND type != 'RX'
|
||
AND last_seen = (SELECT max(last_seen) FROM incidents)
|
||
```
|
||
|
||
Verified against the live store on a quiet day: **zero** rows inside the SoCal
|
||
board bounds, and exactly **five** on the California board. Same data, same day,
|
||
both answers correct. Ungated, SoCal draws twenty-two orange marks over Los
|
||
Angeles on a day nothing is burning — every one with a NULL acreage, fifteen of
|
||
them nameless LA County dispatch numbers — in a frame that contains no other warm
|
||
colour, so each one is the most salient object on the board.
|
||
|
||
Three of the four clauses are there for a specific, verified reason:
|
||
|
||
- **`last_seen = max(last_seen)`** — `persist()` writes every row and never
|
||
deletes, so a deduplication loser keeps its old `last_seen` forever. 21 of 95
|
||
rows are stale ghosts; MP18 and Timber each appear twice, 850 m apart, with
|
||
different acreage.
|
||
- **`type != 'RX'`** — a prescribed burn is deliberate, scheduled, frequently
|
||
adjacent to a real one, and indistinguishable from a wildfire under a distance
|
||
filter. It must never render as one.
|
||
- **`acres >= 10`** — an untested judgement, stated as such. The store has never
|
||
held a SoCal fire between 1 and 100 acres, so this boundary has never been
|
||
exercised against the case it exists for, and a genuinely dangerous 5-acre fire
|
||
in Griffith Park would be invisible. It is accepted because WFIGS's own LA
|
||
County records make the low-acreage false-positive rate overwhelming, and the
|
||
constant is written where it is obvious and one edit away.
|
||
|
||
An empty board is therefore a **result**, not a gap, and the panel says so with
|
||
the age of the fetch: *"No active fire on this board — CAL FIRE and WFIGS, 4
|
||
minutes ago."* A silent board and a dead feed are indistinguishable without a
|
||
timestamp, which is the same argument `health.ts` already makes for `degraded[]`.
|
||
|
||
### 9.3 A detection is evidence, not an incident
|
||
|
||
Satellite thermal detections are a **separate, visually weaker layer** and are
|
||
never promoted into an incident client-side. There is a permanent industrial heat
|
||
source 4.7 km from the owner's house at FRP ~1.0 that appears on every pass, on
|
||
every day in the store, with no matching incident: drawn as a fire, it draws a
|
||
fire on his house. Two more properties of that table have bitten already —
|
||
`confidence` carries incompatible scales in one column (MODIS is an integer
|
||
0-100, VIIRS is `low`/`nominal`/`high`, so read `sat` first), and the
|
||
highest-FRP detections in the store are in **Nevada**, because `CA_BBOX` is a
|
||
rectangle and `in_ca()` gates incidents only.
|
||
|
||
### 9.4 Two tables that must not be used
|
||
|
||
- **`air`** — `observed_at` is the primary key and `fires.py` does `INSERT OR
|
||
REPLACE` once per parameter per poll, so only the last parameter survives.
|
||
Every row says NO2 / AQI 20 / "Good" while the live feed reports ozone 101 and
|
||
PM2.5 55 at the same instant. Haze driven off it *clears the sky during a smoke
|
||
event*.
|
||
- **`weather`** — every row is `zone='point'`, one grid sample at the owner's
|
||
house, written only inside the NWS alerts loop, so on a quiet day no wind is
|
||
stored at all. Tera's own per-region `WeatherBody.windDirDeg`/`windKph` is
|
||
already fetched, already correct for the board, and is already what the clouds
|
||
drift on. The plumes drift on the same one, because a plume leaning on a
|
||
different wind from the cloud beside it would be two opinions about one sky.
|
||
|
||
### 9.5 Where it attaches in the engine
|
||
|
||
`scene.ts` owns the wiring and constructs no fire geometry itself. The seam is
|
||
`SceneOptions.fires`, a factory with the same shape `createCloudLayer` has, and
|
||
five forwarders on `SceneHandle`:
|
||
|
||
```ts
|
||
type FireLayerFactory = (world: World, options: { span: number }) => FireLayer;
|
||
|
||
interface SceneHandle {
|
||
setFires(view: FireView | null): void; // null = nothing has answered yet
|
||
setFireSmoke(visible: boolean): void; // plumes on/off; the marks stay
|
||
fireSmokeLoadAt(lat: number, lng: number): number; // 0..1, for haze elsewhere
|
||
// and, already present: setLighting, setSolarElevation, setWind
|
||
}
|
||
```
|
||
|
||
`FireView`, `DrawnFireMark` and `FireDetectionMark` are declared in `scene.ts`
|
||
as the **minimum a renderer needs**, and `FirePromotion` — what `promote()` in
|
||
`src/server/fires.ts` actually returns — is assignable to `FireView` with no
|
||
mapping step. The duplication is deliberate and is what keeps the engine from
|
||
importing a wire module: the same rule that keeps `Marker` in
|
||
`engine/types.ts` and the marker row on the server. `src/test/fireSeam.test.ts`
|
||
is the one thing standing between that decision and silent drift — it assigns a
|
||
`FirePromotion` to a `FireView` at compile time, so a renamed column fails there
|
||
rather than in a render loop.
|
||
|
||
`fireSmokeLoadAt` is the one read-back, and it is what couples the LA courtyard
|
||
to the real sky. A fire sixty kilometres away in the San Gabriels is not a flame
|
||
seen from a courtyard: it is a brown horizon, a dimmed orange sun, and air that
|
||
stops being clear closer in. That is one scalar into `daylight.ts`, and zero
|
||
geometry.
|
||
|
||
`ATMOSPHERE IS THE SOLE LIGHT OWNER` still holds (CONTRACT §4). The night glow of
|
||
a fire is emissive material and one additive ground quad — the same two
|
||
mechanisms `nightlights.ts` uses to draw San Francisco's 12,038 street lamps in
|
||
one draw call — and not a `THREE.PointLight`.
|
||
|
||
---
|
||
|
||
## 10. First-party devices, and the anonymous visitor
|
||
|
||
A studio's instruments reach the renderer by one of two strategies, chosen once
|
||
at construction in `createDeviceSource`: the deployment's device route, or the
|
||
fixed-step simulator bundled in the tab. There is no runtime failover, because a
|
||
strategy that silently swapped a real bridge for a simulator mid-session would be
|
||
the `first-party-sensor`/`simulated` confusion `DeviceProvenance` exists to
|
||
prevent, arriving without a word in the interface.
|
||
|
||
**That choice needs two facts and used to be made from one.** `Feeds.devices`
|
||
says the *deployment* has a device source; `Capabilities.liveDevices` says the
|
||
*viewer* may read it. On cloud-2 the first is true — `/health` reports
|
||
`devices: "sim"` — and for an anonymous visitor the second is false, because the
|
||
route is members-only and answers 401. Passing only the deployment's answer sent
|
||
every anonymous visitor down the API strategy to be refused, whereupon
|
||
`apiSource` rendered `atRest()` — a rack of powered-off instruments, forever,
|
||
backing off exponentially against a request that could never pass — beside a
|
||
panel describing a studio that runs locally. Both gates are now applied at the
|
||
one call site in `main.ts`, and the fallback is the simulator that was always
|
||
meant to serve this case.
|
||
|
||
What a first-party studio may honestly mirror is bounded by what the upstream
|
||
actually measures, and two refusals are load-bearing:
|
||
|
||
- **No microphone level.** There is no passive level upstream; `POST /levels`
|
||
*calls* `measureMic` and records 1.5–3 s per mic. "Just add the level meter to
|
||
make it feel alive" builds a continuously-recording microphone and it looks
|
||
like a feature while doing it. No level capability, no JPEG from
|
||
`/cameras/:id/live.jpg` (which *captures* on demand), no `/sleep`, no
|
||
`/automations` — at any tier, behind any flag.
|
||
- **No decibels.** Upstream speaks `gainPct`, normalised over four different
|
||
native scales (Yeti max 50, SMY18 and Anker 100, ThinkPad 63). Rendering 68% as
|
||
"20.6 dB" would look completely plausible and would be a guess presented as a
|
||
measurement — the exact failure this product names elsewhere. Without a
|
||
declared `DeviceDeclaration.ranges`, the gain row is not mirrored at all.
|
||
|
||
---
|
||
|
||
## 11. How the fire reaches the board, and where it stops
|
||
|
||
Sections 9 and 10 describe the two feeds. This one is the wiring, because every
|
||
decision in it is about *which* board and *which* viewer, and those are made in
|
||
exactly one file each.
|
||
|
||
### 11.1 Three gates, and none of them is a permission
|
||
|
||
`main.ts` polls `/api/v1/fires` only when all three hold, and each is about a
|
||
different thing:
|
||
|
||
| Gate | Question | Where |
|
||
|---|---|---|
|
||
| `FIRE_BOARDS.has(id)` | does fire happen on this rectangle? | `main.ts` |
|
||
| `access.feeds.fires` | has this deployment got a projection? | `access.ts`, from `/health` |
|
||
| — | may this viewer see it? | **there is no third gate** |
|
||
|
||
A wildfire is a public agency record. CAL FIRE publishes every one of these on
|
||
its own website, so there is no `Capabilities` twin to `feeds.fires` and there
|
||
must never be one: the fires are exactly as public as the weather and the
|
||
aeroplanes, and the anon-first rule that governs those governs this.
|
||
|
||
The board gate is the one people will want to remove. It is not laziness — the
|
||
Bay Area rectangle has held zero incidents on every day the upstream store has
|
||
existed, and it is the board already carrying the largest frame-time allowance
|
||
in the product. `SceneOptions.fires` is *withheld* rather than passed and left
|
||
empty, so on San Francisco there is no group, no material and no draw call.
|
||
|
||
### 11.2 One body, every board
|
||
|
||
`/fires` takes no query and is not per-region: the whole state's live incident
|
||
set is small, and the clip is `promote()`, which the client has to run anyway to
|
||
apply the tier ladder. That has a visible dividend and `main.ts` spends it —
|
||
`firesBody` is held for the **page** rather than for the board, so switching
|
||
from California to the Southland re-clips the answer already in hand and draws
|
||
the correct, and correctly empty, board in the frame of the switch. Only the
|
||
first board of a session ever shows the "nothing has answered" sentence.
|
||
|
||
The three states are distinct and the panel says which one it is in:
|
||
|
||
- `ageMs === null` — nothing has ever answered. **A fault sentence, never an
|
||
all-clear.** A dead feed and a quiet day are indistinguishable without a
|
||
timestamp, which is the same argument `health.ts` makes for `degraded[]`.
|
||
- `drawn.length === 0, suppressed > 0` — the board is quiet and *n* live records
|
||
were refused by the gate. The Southland today: nothing drawn, twenty-one
|
||
refused, and the panel names the threshold that refused them.
|
||
- `drawn.length === 0, suppressed === 0` — nothing is happening on this
|
||
rectangle at all.
|
||
|
||
### 11.3 Where the panel is, and where it goes away
|
||
|
||
`#fire-section` and `#fire-host` belong to `main.ts`, not to `mount.ts` — the
|
||
same arrangement `#presence-host` has, and the reason `main.ts`'s "write to no
|
||
chrome node" rule survives. The panel is rebuilt per board because its `bounds`
|
||
are the board's, and one rectangle feeds both `promote()` and the caption so the
|
||
picture and the sentence cannot disagree about what is off-frame.
|
||
|
||
**Inside a building the panel appears only when the fires are in that building's
|
||
sky.** Outside, it is always up on a board that draws fire, because "nothing is
|
||
burning here" is the fact worth stating. Inside, a five-item list of incidents
|
||
three hundred kilometres away pushed the room's own controls below the fold on
|
||
the first frame a visitor sees of it. When the courtyard actually goes brown the
|
||
list is the explanation for what is on screen and it belongs there;
|
||
`smokeCaption` says the same thing in one sentence either way, and it sits with
|
||
the room's other disclosures rather than in the fire panel, because it is a
|
||
claim about *this room's picture*: the sky in here is derived from incidents on
|
||
the map and is not a measurement of the air at this address.
|
||
|
||
### 11.4 The office coupling is one argument
|
||
|
||
`officeDaylight(state, site, smokeLoad)` — third argument, clamped, and
|
||
bit-identical to no argument at zero. The scalar is
|
||
`SceneHandle.fireSmokeLoadAt(site.lat, site.lng)`, and the adaptation happens
|
||
once, in `main.ts`, where the rest of the office's rig adaptation already
|
||
happens. A room may not reach into the fire layer and form a second opinion
|
||
about its own sky (CONTRACT §4).
|
||
|
||
One consequence is worth stating rather than hiding: the load is computed from
|
||
the **board you came in from**, so the same building is very slightly hazier
|
||
entered from California than from the Southland, because the two rectangles
|
||
contain different fires. `smokeCaption`'s sentence is worded for exactly that —
|
||
"drawn from the fires currently on the board" — and the alternative, a statewide
|
||
query per building, is a second source of truth about the same sky.
|
||
|
||
---
|
||
|
||
## 12. The state board is the state, and what that cost
|
||
|
||
The default board — the first frame an anonymous visitor sees — used to stop at
|
||
38.05 N. The minimap in the corner of the same frame drew the whole of
|
||
California from the same pack, so one screenshot contained a picture of the
|
||
state and a picture of two thirds of the state, disagreeing about the shape of
|
||
the one silhouette in this product that everybody already knows.
|
||
|
||
`bounds` is now 32.50–42.05 N and -124.50 to -114.0 W: the Mexican border, the
|
||
Oregon line, Cape Mendocino with eight kilometres of ocean west of it, and the
|
||
Colorado past Parker. All four edges are the state's own.
|
||
|
||
### 12.1 The price was paid in the cell, not in the bounds
|
||
|
||
Extending north and west at the old 0.022° × 0.027° spacing takes the lattice
|
||
from **84,924 points to 168,813** — 2.02× — and the terrain mesh with it,
|
||
against a mobile budget that had 70,000 triangles spare and two more layers
|
||
landing on the same board in the same round. Extending the bounds alone does not
|
||
fit and was never going to.
|
||
|
||
Coarsening the cell by 1.42× in each axis puts it back. The argument is the one
|
||
`california.ts` already made when the board grew east and it is worth stating
|
||
once more here, because it is counterintuitive: **what the eye sees is the cell
|
||
as a fraction of the board, not the cell in metres**, because the camera
|
||
retreats to frame whatever it is given.
|
||
|
||
| board | cell | span | cell / span |
|
||
| --- | --- | --- | --- |
|
||
| the corridor, before the east | 0.020° | 284 u | 0.0041 |
|
||
| the southern two thirds | 0.022° | 428 u | 0.0030 |
|
||
| the whole state | 0.0312° | 554 u | 0.0033 |
|
||
|
||
The ground cell went from 2,449 m to 3,473 m — 42% coarser on the earth — and
|
||
*finer* in the frame than the board two revisions ago. Measured after: 85,008
|
||
lattice points against 84,924, and 71,876 terrain triangles against 87,074.
|
||
|
||
`verticalExaggeration` went 13 → 15 in the same change, and for the same kind of
|
||
reason. Relief in the frame is peak-units over board *span*: at 13 the extended
|
||
board is 6.4% where the old one was 8.3%, and 15 puts it at 7.4%, which is
|
||
Southern California's 7.5% almost exactly. Buildings share the exaggeration and
|
||
get *smaller* in frame, not larger — a 250 m tower is 0.0035 of a span where it
|
||
was 0.0040 — so the "bed of nails" objection the old comment raises about
|
||
pushing to 20 does not apply at 15 on a board this size.
|
||
|
||
### 12.2 Fog is a function of camera altitude. Almost nothing else became metric
|
||
|
||
An investigation proposed converting every span-derived constant in the engine
|
||
to physical metres, on the grounds that the three boards disagree by 8× once
|
||
converted and San Francisco is the one that is right. **The diagnosis is
|
||
correct and the prescription is not**, and the arithmetic is worth keeping:
|
||
86 km of clear-day visibility is 0.91 spans on the Bay Area's 1,003-unit board
|
||
and **0.085 spans on the extended state board**, where the camera orbits out to
|
||
1,108 units. Literal metre fog buries California in haze 47 units from the lens
|
||
and there is no pose on that board from which the state is visible at all.
|
||
|
||
So exactly two things changed, and the rule for deciding which is stated here so
|
||
the next person does not have to re-derive it:
|
||
|
||
- **Fog near and far** are now a function of camera altitude —
|
||
`aerialReach()` in `atmosphere.ts` — clamped so the pair the board was
|
||
authored with is the ceiling and is never exceeded. It saturates at any
|
||
whole-board pose, which is what leaves every existing marketing still
|
||
untouched *by construction rather than by measurement*: the Bay Area's opening
|
||
chapter sits 40.6 km up and the curve reaches its ceiling by 6.7 km.
|
||
- **Cloud base altitude** was checked and did not move. It was already
|
||
`BASE_ALTITUDE_M = 1350` passed through `world.metres()`, which is a physical
|
||
metre constant respecting each pack's own exaggeration. Nothing to convert.
|
||
|
||
Everything else stays a span, and the distinction is not "physical vs lazy" — it
|
||
is **whether the constant describes the air or describes the composition**:
|
||
|
||
| constant | stays a span, because |
|
||
| --- | --- |
|
||
| `clouds.ts` `TILE_SPANS` (2.2) and the 14×14 cell layout | tuned over 190 photographed frames; a cell-ownership chain, not a distance |
|
||
| `satellites.ts` dome radius | the file says outright that it is arbitrary — it is where the dome looks right |
|
||
| `fires.ts` `MARK_SPAN_FRACTION` | a glyph sized to be legible, like the aeroplane's 0.42-unit clamp |
|
||
| `terrain.ts` `SWELL_TILE_SPANS` (1/34) | a physically-scaled sea has no visible surface at all; the file argues it at length |
|
||
| camera `far`, `shadowExtent`, orbit limits, minimap layout | properties of a board, not of an atmosphere |
|
||
|
||
The one number a physical model got wrong on its own is worth recording, because
|
||
it was found by taking a photograph rather than by thinking. A camera parked
|
||
223 km from a mountain and 8 km above the ground is, physically, looking at
|
||
something it cannot see — the air supports 231 km and the subject is at 223 of
|
||
it — and the frame came back as a white rectangle with a faint cone in it. A
|
||
board is a **map**, and a map is looked at from outside the atmosphere it
|
||
depicts, which is the same argument `minVisibilityM` was already making. So the
|
||
fog is floored at **6× the camera's own stand-off** (`AERIAL_SUBJECT_CLEARANCE`),
|
||
which with `main.ts`'s 1.15/3.9 ratio puts the near plane at 1.77 stand-offs:
|
||
nothing closer than about 1.8× the camera's distance to its subject is fogged at
|
||
all, and everything past that hazes out toward six.
|
||
|
||
Six rather than four is set by one measurement, not by taste, and it is what
|
||
makes "leaves the existing boards untouched by construction" true rather than
|
||
approximately true. California's opening pose is 67.6 km up and Southern
|
||
California's 17.0 km, so both saturate on the air term alone. The Bay Area's is
|
||
11.3 km up against a 12.4 km saturation altitude — 324 km of air against 369 km
|
||
of authored reach — and it is the stand-off term that carries it over. At a
|
||
clearance of 2.4 that board renders at **88%** of its authored reach and the far
|
||
corner picks up haze it has never had, which would have been a silent change to
|
||
every still shot from the board that carries most of the product's imagery.
|
||
`aerialPerspective.test.ts` asserts all three against the deployed formula, not
|
||
against `cityDaylight`'s narrower one, because the narrower one is not what any
|
||
board on the page uses.
|
||
|
||
### 12.3 What the north is made of
|
||
|
||
The added land is authored, not permitted. Hand-traced by house rule (§3.2): the
|
||
North Coast from Bodega Head to the Oregon line with Cape Mendocino at -124.41,
|
||
the 120th meridian and the corner at Lake Tahoe, the Klamath knot as four chains
|
||
at four bearings because the Klamaths genuinely do not run north-south, the
|
||
southern Cascades, the northern Sierra tapering out into the Cascade arc, the
|
||
Warner Mountains alone in the north-east corner, and the Sacramento Valley as a
|
||
chain rather than a circle.
|
||
|
||
Two failures were made and photographed on the way, both of them re-runs of
|
||
mistakes this pack already documents:
|
||
|
||
- **The northern Sierra shipped as a row of separate domes.** Six hand-placed
|
||
bells 0.35° apart on a 0.24–0.26 radius is 1.4 radii, and a chain only reads
|
||
as a ridge below about 0.9. `ridge()` computes the spacing; the peaks that
|
||
are written by hand are the ones somebody has checked.
|
||
- **Mount Shasta rendered as a 4,320 m cone of pine.** `groundColor` answers
|
||
`inPark` **first**, so a park envelope paints its ground green at any altitude
|
||
and the `alpine` stop is unreachable inside one. The Cascade forest is now two
|
||
polygons with the mountain in the gap between them — which is also true on the
|
||
ground, since the Shasta Valley west of the cone is treeless grassland.
|
||
|
||
No new inland water. Clear Lake, Shasta Lake and Lake Tahoe are all legible at
|
||
this scale and all three were rejected for the same reason: `createWater` puts
|
||
every `inlandWater` polygon at y = 0.05, which is sea level, and those three sit
|
||
at 400 m, 320 m and 1,897 m. At an exaggeration of 15 a lake plate at sea level
|
||
under ground standing 3 to 15 units above it is a hole, not a lake. The Salton
|
||
Sea works because it really is below sea level. Each polygon is also its own
|
||
draw call, and the California mobile cell is the tightest in the project.
|
||
|
||
### 12.4 Three packs draw three Californias, and what one would cost
|
||
|
||
The complaint this section answers is "why does it still feel like three
|
||
boards", and the first thing to record is the measurement that removes the
|
||
obvious suspect: **the 20× horizontal scale jump at the seam is invisible.**
|
||
`World.project` is a uniform scale in x/z with no vertical term, and a uniform
|
||
scale leaves a perspective image identical, so a camera carried across the seam
|
||
on matched true-metre offsets draws a pixel-identical horizontal frame. 1,919 →
|
||
94 m per unit costs nothing to look at. Rescaling was never the problem.
|
||
|
||
What is visible is that the three packs disagree, in data, about what California
|
||
looks like. `src/cities/reconcile.ts` holds four rules that replace those
|
||
disagreements with one authored constant each, behind `?reconcile=` and **off by
|
||
default**, because San Francisco and Southern California carry every marketing
|
||
still on lumbridgecorp.com and a photograph is the only instrument that can say
|
||
whether a rule made a board better.
|
||
|
||
| | rule | california | sf | socal |
|
||
| --- | --- | --- | --- | --- |
|
||
| (a) `verticalExaggeration` | `RELIEF_IN_FRAME` = 0.0724, peak units over span | 15 → **15.00** | 3.6 → **5.78** | 3.4 → **3.41** |
|
||
| (b) road width | true metres, floored at `LEGIBLE_SPAN_FRACTION` = 0.000199 of span | 1,919–2,034 m → **212 m** | 19–32 m → **unchanged** | 31–55 m → **unchanged** |
|
||
| (c) `coastFalloff` | `COAST_RAMP_CELLS` = 0.8 × the coarse ground cell | 0.0250° → **0.0250°** | 0.0018° → **0.0032°** | 0.0045° → **0.0058°** |
|
||
| (c) palette | one family, mixed by how far south and how far inland the board reaches | **unchanged** | mix 0.05 | mix 0.77 |
|
||
| (d) `lngScale` | one `REFERENCE_LAT` = 37.3° for every board | **unchanged** | 87,990 → 88,552 m/° | 92,484 → **88,552** m/° |
|
||
|
||
Three of the four leave the state board exactly where §12.1's photograph put it,
|
||
and that is not luck: each constant is taken *off* California, which is the board
|
||
this document already tuned against a picture. What the rules then say is that
|
||
**San Francisco is the board that is out of family** — 1.6× too flat for its own
|
||
span, and the only pack with no palette at all.
|
||
|
||
Four things worth writing down so they are not re-derived:
|
||
|
||
- **The relief rule narrows the seam step; it does not close it.** Apparent
|
||
relief at a *matched* pose is proportional to the exaggeration itself, not to
|
||
relief-in-frame, so the rule takes California → Bay Area from 4.17× to 2.60×
|
||
and leaves California → Southern California at 4.4×, because those two were
|
||
already on one number. Making it 1.0× means one exaggeration everywhere, which
|
||
puts the state at 1.7% of its own frame. Relief-in-frame and matched-pose
|
||
relief are the same quantity only for boards of the same span.
|
||
- **The relief rule moves buildings too, and on a metro board the buildings are
|
||
the subject.** §12.1's argument that a taller exaggeration makes towers a
|
||
*smaller* fraction of the frame holds only because California's span grew in
|
||
the same change. San Francisco's span does not move, so the Financial District
|
||
comes out 1.60× taller in an unchanged frame — measured and photographed, and
|
||
the one place where (a) makes a board arguably worse rather than better. It is
|
||
the sub-item most likely to be reverted or split into a separate building
|
||
term.
|
||
- **The road floor was measured, not chosen.** The two metro packs, authored
|
||
separately, put their narrowest road at 0.00019940 and 0.00020356 of their own
|
||
span. The floor sits just under the tighter of the two, so neither metro board
|
||
moves and California's two-kilometre freeway symbols fall onto it.
|
||
- **The projection rule costs local aspect.** Southern California draws 4.25%
|
||
narrower in x at 37.3° than at its own 33.82°, and San Francisco 0.64% wider,
|
||
against a registration error that goes from 4,010 m to **zero by
|
||
construction**.
|
||
|
||
Measured with the flag on, against the flag off: California is identical on
|
||
every layer to the digit; the Bay Area gains 10.8% of terrain triangles and 1.0%
|
||
of building instances; Southern California loses 4.3% of instances with the
|
||
narrower board. No cap is approached, let alone raised.
|
||
|
||
### 12.5 The merge, priced
|
||
|
||
`scripts/merge-feasibility.mjs` builds the statewide merged pack in node and
|
||
counts it. It is a measurement artifact: no board id, no route, nothing wired
|
||
into the product.
|
||
|
||
A lot is `LOT = 0.42` **scene units**, so how much ground a building stands on is
|
||
decided entirely by the board's metres-per-unit — 40 m on the Bay Area, 164 m on
|
||
Southern California, 806 m on the state. A merged board must pick one:
|
||
|
||
| density | lot | extent | instances | triangles | vs the 2.6M desktop cap |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| Southern California | 164 m | 2,380 × 2,722 u | 93,253 | 1,119,036 | 0.43× |
|
||
| Southern California, all 116 districts | 164 m | 2,380 × 2,722 u | 165,417 | 1,985,004 | 0.76× |
|
||
| Bay Area | 40 m | 9,856 × 11,269 u | 1,599,401 | 19,192,812 | 7.4× |
|
||
| Bay Area, all 116 districts | 40 m | 9,856 × 11,269 u | 2,829,753 | 33,957,036 | 13.1× |
|
||
|
||
The two rows per density differ by one judgement: whether the state pack's own
|
||
coarse Bay Area and Los Angeles districts are dropped where a metro pack already
|
||
draws that ground. They should be — but both are printed, because a number
|
||
quoted without saying which one it is has already produced a 1.8× disagreement
|
||
inside this round's own brief. Terrain adds 75,996 triangles on California's
|
||
existing 3,473 m cell; it is the cheap half.
|
||
|
||
So **Bay Area density is dead** and **Southern California density fits**, and its
|
||
price is that San Francisco is drawn the way Southern California is drawn now.
|
||
That price is a picture and not an argument: `merge-sf-40m-vs-164m.png` is the
|
||
Financial District at 40 m lots beside the same frame at 164 m, and downtown
|
||
stops being a skyline. The still varies `LOT` in a throwaway tree under `/tmp`
|
||
rather than `latScale`, so the camera, the terrain and every chapter pose are
|
||
held constant and lot size is the only variable.
|
||
|
||
One measurement is noise and is labelled as such: instance and triangle counts
|
||
are deterministic to the digit across runs, and placement milliseconds on this
|
||
box span 323 to 1,989 ms for the same 93,253 lots.
|
||
|
||
|
||
## 13. Four layers, one seam, and two gates each
|
||
|
||
The port kit, the ships, the reflectivity raster and the migration field arrived
|
||
as four independent workstreams and reach the board through one seam:
|
||
`SceneOptions` carries four optional factories — `ports`, `vessels`, `precip`,
|
||
`migration` — and `createScene` builds, lights, ticks and disposes each of them
|
||
exactly as it does `fires`. Every one of those five touch points is a line
|
||
somebody can delete without breaking a compile, so
|
||
`src/test/integration/layerSeams.test.ts` asserts all five from the source, at
|
||
both ends of the seam: the factory is supplied by `main.ts` and it is consumed by
|
||
`scene.ts`.
|
||
|
||
**A layer that is not supplied costs nothing.** Not "draws nothing" — is never
|
||
visited: no group, no material, no geometry, no draw call. That is the whole
|
||
reason the slots are optional rather than a `visible` flag, and it is what the
|
||
budget shows. The California mobile cell measures **370 draws and 373,825
|
||
triangles** with the two sky layers wired and no feed behind them, which is the
|
||
number it measured before they existed.
|
||
|
||
### 13.1 The two gates
|
||
|
||
Each layer answers two questions, and they are different questions:
|
||
|
||
| Layer | Board gate | Deployment gate |
|
||
|---|---|---|
|
||
| `ports` | the pack declares `city.ports` | none — a quay is authored, not fetched |
|
||
| `vessels` | the pack declares `city.ports` | none this round; the hulls are modelled |
|
||
| `precip` | `boardCarriesRaster(bounds)` | `/health` `sources.radar` |
|
||
| `migration` | `boardCarriesRaster(bounds)` | `/health` `sources.birds` |
|
||
|
||
The **board** gate is geometry. `boardCarriesRaster` asks whether a rectangle can
|
||
be described by a quarter-degree cell — twelve cells on the short side, which is
|
||
three degrees — and it answers true for California (9.55° × 10.50°) and false for
|
||
the Southland (1.08° × 1.66°) and the Bay (0.85° × 0.89°). That is the same
|
||
answer an `id === "california"` would have given and it is a fact about the map
|
||
rather than about a name. It gates the birds as well as the rain because both
|
||
feeds are statewide instruments quantised to the same cell: NEXRAD's composite
|
||
and BirdCast's fifty-eight counties. A metro board would show three county discs
|
||
and part of a fourth, which is a picture of the sampling grid rather than of the
|
||
migration.
|
||
|
||
The **deployment** gate is `access.feeds`, read once from `/health`'s `sources`
|
||
block at boot — the same field `drawsFire` reads. `sources.radar` and
|
||
`sources.birds` are published by `server/src/routes/health.ts` off the config, so
|
||
"why is there no rain on this board" is answered by the box rather than by its
|
||
env file. Both are **optional** on `Feeds`, unlike every other feed there, because
|
||
they are newer than some servers this client will meet and `undefined` has to
|
||
mean *do not ask*.
|
||
|
||
### 13.2 The quiet day is the designed case
|
||
|
||
This repo's default is `none` for radar and `none` for birds, and no AIS licence
|
||
has been read, so **the empty state is what nearly every visitor sees**. It is
|
||
never blank. Each gate owns a sentence, each sentence is always complete, and
|
||
each says what is being withheld as well as what is being shown:
|
||
|
||
- no feed at all — *"No radar feed is configured, so this board draws no
|
||
weather. That is a fact about this box, not about the sky."*
|
||
- a feed that answered with nothing — *"Nothing is falling on this board. All 16
|
||
radars reporting. Last scan 5 minutes old."*
|
||
- an instrument that does not measure in daylight — *"Nothing is aloft. BirdCast
|
||
measures migration only after dark. Last night 393,290 birds crossed
|
||
California heading south-east…"*
|
||
- a harbour with modelled hulls — *"…19 hulls: 6 making way, 13 at rest, 16
|
||
alongside a berth. No hull is labelled laden or in ballast: that is a port
|
||
figure, not a ship one."*
|
||
|
||
They are printed into `#sea-section` and `#sky-section`, which are `index.html`
|
||
elements written by `main.ts` rather than by `mount.ts` — the arrangement
|
||
`#fire-section` and `#presence-host` already have, and for the same reason: the
|
||
content is a *board's* instrument reading, it is rebuilt per board, and the
|
||
section is hidden outright on a board that has neither a harbour nor a statewide
|
||
raster. Both sit below **Go**, so the studio's own call to action stays above the
|
||
fold on a phone.
|
||
|
||
### 13.3 The harbour is refreshed as a new fix, never nudged
|
||
|
||
`modelHarbour` stands in for a feed this deployment does not have, and the feed's
|
||
shape sets the cadence. Upstream listens for thirty seconds every fifteen
|
||
minutes; a hull under way moves about five kilometres between two reports;
|
||
`engine/vessels.ts` dead-reckons along the reported course for exactly 900
|
||
seconds and then stops. `main.ts` rebuilds the modelled body on that same
|
||
interval off the once-a-minute clock, which is not a smoothing trick — it is the
|
||
feed's own behaviour, and the small jump a moving hull makes when a new body
|
||
lands is the jump a real fix makes. Nothing anywhere splines between two fixes,
|
||
because the chord between them is not a path anything took.
|
||
|
||
Two clocks, deliberately different:
|
||
|
||
- the **modelled harbour** is built for `currentInstant()`, so a scrubbed sky and
|
||
the ships under it describe the same moment, and `promoteVessels` is handed the
|
||
same number so the body's age is what it really is: zero;
|
||
- the **sky promotions** take `Date.now()`, because `fetchedAt` is a real
|
||
timestamp and "last scan 5 minutes old" has to stay true when somebody drags
|
||
the clock to midnight.
|
||
|
||
The migration layer is given `env.sun.elevation` — the same number the rig is
|
||
lit by — rather than an ephemeris of its own. `LightingState.hemisphere.intensity`
|
||
is **not** a day/night signal: it measures 1.33 at 21° below the horizon against
|
||
0.95 at noon, because `atmosphere.ts` raises the fill to compensate a moonlit
|
||
scene. A layer that read it that way drew nothing at midnight while passing a
|
||
test written against invented numbers.
|
||
|
||
### 13.4 What the boards cost after wiring
|
||
|
||
Measured on this box, against the pre-round baseline:
|
||
|
||
| Cell | Before | After | Cap |
|
||
|---|---|---|---|
|
||
| california · mobile | 368 draws / 389,843 tri | 370 / 373,825 | 415 / 430,000 |
|
||
| socal · desktop | 205 / 1,417,648 | 216 / 1,422,025 | 320 / 1,700,000 |
|
||
| socal · mobile | 137 / 765,596 | 148 / 771,797 | 170 / 900,000 |
|
||
| bay-area · desktop | 206 / 2,264,956 | 209 / 2,264,956 | 320 / 2,600,000 |
|
||
|
||
The whole port complex and its fleet — two ports, a thirteen-kilometre
|
||
breakwater, six container yards, fifty-six gantries and nineteen hulls with
|
||
wakes — costs **eleven draw calls and four to six thousand triangles**, and the
|
||
same eleven on both viewports. That is what
|
||
instancing across buckets buys, and it is the `airports.ts` argument repeated:
|
||
the draw count is a function of how many *kinds* of surface exist, not of how
|
||
many terminals.
|