1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/adapters
karti acf4d1a510 feat: the state becomes California, and the port fills with ships
**Stage 1 of one California.** The owner's complaint had two halves and this is
the first: the state board was a CROPPED SLAB. `california.ts` stopped at 38.05 N,
so the board disagreed with its own minimap about the shape of California in a
single frame, and Bug Fire's 93,733 acres burned off-frame while the panel said
all clear. Bounds now run 32.50-42.05 N / -124.50 to -114.0 W — Cape Mendocino,
the ruled Oregon parallel, the 120th-meridian corner into the Nevada diagonal.

**And it got cheaper.** 391,169 triangles to 375,351, while gaining the North
Coast, the Sacramento Valley, the Klamath knot, the Cascade arc, Shasta at 4,320 m
and Lassen at 3,190 m. Extending the bounds alone would have doubled the lattice
to 168,813 points and blown the mobile cap; coarsening cellLat 0.022 -> 0.0312 and
cellLng 0.027 -> 0.0383 holds it at ~83,800. The cell as a FRACTION of the board
moves 0.0030 -> 0.0033 — unchanged in frame — because the camera retreats to frame
whatever it is given. That argument was already written in the pack's own comment.

The second half — three boards becoming one world you zoom through — is NOT here.
Merging at Bay density would be 34.04M triangles, 13x the highest budget, and
merging at SoCal density would downgrade San Francisco from 40 m lots to 164 m.
Both delete the board every marketing still is shot from. `sf.ts` and `socal.ts`
are untouched by design.

**Aerial perspective, which the state board could not have had before.** The old
fog started at 1.15 board spans = 944 km, on a board whose longest diagonal is
820 km — so no pixel could ever be fogged. Fog now responds to camera altitude,
clamped to the authored pair as a ceiling.

`Atmosphere.aerial(env, view)` is a second pure method returning `{ near, far }`
and **deliberately no colour**. That is structural, not stylistic: it is why a
future camera-dependent term cannot reach `environmentKey()`'s colour fingerprint
and start rebuilding the PMREM cubemap on every camera step. Coarsening the
fingerprint instead would have hidden one instance and armed the mechanism. A
mutation-tested seam guard fails if anyone merges the two paths back together.

**The port.** Terminal Island rendered as a bare tan polygon with generic white
blocks while the chapter text called it the busiest port complex in the
hemisphere. Now six container yards drawn as canvas atlases, 56 gantry cranes at
varied boom angles, the 13 km San Pedro breakwater, the dredged channel. Five
buckets merging ACROSS ports the way airports.ts merges across fields, so a
second complex costs no extra draws: +11 draws and +4,377 triangles for all of it.

At vertical exaggeration 3.4 a 130 m gantry is 1.132 units tall against a 400 m
ship's 1.024 long — the crane is the taller object, and it is what makes a port
read as a port from altitude.

**Ships, and the wake carries the information.** Moored hulls have no foam,
verified at three terminals; a tug under way in the Main Channel trails a clean
Kelvin V. One hull geometry, one InstancedMesh, orientation from the BERTH rather
than the wire. The AIS gate strips sog 102.3, heading 511 and cog 360 — all mean
"not available" — with an explicit test that cog 358.7 SURVIVES, because a naive
range check on cog eats real headings near north.

"Empty or full" is not in AIS position reports and is not invented per ship. The
honest answer is at port level and is a better story: 348,691 of 460,467 boxes
left Los Angeles empty in July 2026, corroborated by FBX01 $7,491 inbound against
FBX02 $347 outbound.

**Radar and birds ship dark, and say why.** California is 0.47% wet and migration
is nocturnal and seasonal, so both layers have nothing to say on most days. The
panel reads "No radar feed is configured, so this board draws no weather. That is
a fact about this box, not about the sky."

Also recorded, and it matters beyond this commit: **the GPU on amd-server never
leaves 500 MHz of a possible 2725**, traced across 80 seconds of sustained load.
`bay-area/desktop` is fragment-bound at that clock and sits on the vsync deadline,
so a trivial change in fragment work flips it between 16.8 and 33.3 with geometry
identical to the digit. Every frame-time number measured on this box is a floor.
Two investigations reached two different wrong conclusions from single-run
comparisons before this was traced. Geometry is the gate; frame time is advisory.
No cap was raised.

Tests 1,340 -> 1,540, server 280 -> 295.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 23:35:09 -07:00
..

Adapters

Everything in src/engine/ renders data and takes no position on what it means. A Marker is a point with a colorKey; the engine looks that key up in a palette the caller supplies and will never learn that rejected is red. A FlightSource is an interface with a poll() on it. A WeatherObservation is six numbers.

This directory is where those become somebody's actual data. It is the only part of the browser build that knows an API exists, and keeping it here is what lets one renderer serve a private career map, a public sector map and whatever anyone else builds without any of them being a fork. See ARCHITECTURE.md §3.3.

file what it is
sample.ts fabricated demo markers and flight routes, so a fresh clone has something on it
http.ts the real adapter — the Tera API described in src/server/wire.ts, falling back to sample.ts

Devices have a seam of their own next door, in src/devices/adapter.ts, for a reason worth stating: it is the one feed with a write on it, and the write does not degrade the way a read does. See "Devices" below.

The fallback is the product, not the safety net

http.ts never throws and never leaves the map empty. No server, a 404, a timeout, a static host answering /api/v1/markers with its own index.html — all of it lands on the sample data, and the city keeps rendering.

That is deliberate and it is the acceptance test the whole repo is held to: a stranger clones this, runs one command, and gets a city, with no account, no key and no network (CONTRACT.md §0). npm run build deployed to any static host is a working Tera. Pointing it at a server is an upgrade.

Every response carries live: boolean so the difference is visible to the app above. An interface that shows invented companies exactly the way it shows real ones is the one failure mode this arrangement can have, and the flag is there so it does not have to happen.

const tera = createTeraClient();               // same-origin /api/v1
const markers = await tera.markers();
const scene = await createScene(stage, {
  city: SAN_FRANCISCO,
  markerPalette: markers.palette,
  flights: tera.flights(regionOf(SAN_FRANCISCO), sampleRoutesFor("sf")),
});
scene.setMarkers(markers.value);
if (!markers.live) showSampleDataNotice();

Three things about that call are load-bearing.

createScene is async and takes a Stage rather than a canvas: the heightfield is built in a Worker, and the renderer outlives any one city, so the stage is created once for the page and handed to each scene in turn.

flights takes a region, not a client-wide origin. The Bay Area and SoCal are six hundred kilometres apart and a single configured origin served one of them and lied to the other. regionOf(city) derives it from the city's own bounds, so a city pack added later needs no configuration to get its own sky. The second argument is the simulated traffic to fly when the API has none, which is what keeps the zero-config case from showing an empty sky.

And note the ordering: markerPalette is fixed when the scene is built, so the markers have to be awaited first. markers.palette is the sample palette when the feed is the sample set and the palette you passed in TeraApiOptions when it is real — the sample keys are not your keys.

Clicking an aircraft

TrafficSource.detail(id) answers from what poll() last handed over — callsign, ICAO 24-bit address, altitude in both units, heading as degrees and as a compass point, position, distance from the board centre, whether anybody observed it, and the credit lines owed for it. Synchronous, so a click opens a card in the same frame rather than behind a round trip, and null for an aircraft that has left the feed rather than a card showing where something was two minutes ago.

It is available to an anonymous visitor, and that is a decision rather than an oversight. An ADS-B position is broadcast unencrypted by the aircraft to anybody with a forty-dollar receiver; there is nothing here an account could grant access to, and gating it would cost the first-visit moment this map exists for while protecting nothing.

Two fields are about the data rather than about the aeroplane. observed travels with the aircraft because a card is read on its own, away from any corner label, and a fabricated flight number in the same frame as a real one is the confusion live exists to prevent. attribution travels with it because a card is where the data is displayed, which is what an ODbL notice is about. The address is null unless the feed gave a real one: the simulator's ids are route names and the ~-prefixed ids both community feeds emit for TIS-B and MLAT targets are not ICAO addresses, and somebody pastes that field into a registry lookup.

Devices: reading is the demo, writing is the account

src/devices/adapter.ts is the seam, and it chooses between two strategies once, at construction:

  • the API, when the deployment has a device source (/health says so) and the viewer may read it; or
  • the simulator in this tabsrc/devices/sim.ts, seeded, deterministic, synthetic: true and live: false — for everybody else.

The second is not a degraded mode, it is the anonymous visitor's studio and the zero-config clone's studio, and it is the same argument the marker fallback makes one section up: a studio is never dark. GET /offices/:id/devices refuses an anonymous caller, and it should — a reading describes a room somebody is standing in — so the refusal produces a working, honestly-labelled instrument rather than a dead one.

Commands do not get the same treatment, and this is the one place in this directory where a failure is reported rather than papered over. On the API strategy a refused command is null and the interface says so; it is never quietly applied to a local copy, because a control that appears to work and changes nothing anybody else can see is worse than one that says no. On the simulated strategy a command is applied locally and openly, because nothing there claims to be a real room.

A command travels as a POST on a route of its own and never in a read body. A shared cache that kept a GET which turned a microphone on could replay it, and that is precisely what the fail-closed Cache-Control default in CONTRACT.md §5 exists to prevent.

src/devices/adapter.ts owns no timer: the simulated strategy is advanced by tick(dt) from whatever render loop already exists, and the API strategy's polling lives in watchDevices here in http.ts, where every other watch's timer already is.

No real company data ships in this repo

Two separate constraints want the same thing here, which is the reason this arrangement is worth the indirection rather than just committing a JSON file.

Privacy. Pipeline status — who is talking to whom, and who said no — is private. A public repo is the wrong place for it.

Licence, which is the sharper one. Real positions are geocoded, and a geocoder built on OpenStreetMap returns ODbL data. ODbL is share-alike, and serving a snapshot of those coordinates from a public endpoint is Publicly Using a Derivative Database — §4.3 attribution and §4.4 share-alike attach to the served data whether or not the rows live in the repo. Keeping the table off-disk hides that obligation; it does not discharge it. See ARCHITECTURE.md §3.2 and the correction in CONTRACT.md §8.

So the rule is about the geocoder, not about storage:

  • Geography in src/cities/ is traced by hand. Original expression, ours, Apache-2.0. Never OSM, never Nominatim.
  • Real markers arrive over the API at runtime, each carrying a CoordinateProvenance, and the server refuses any row whose provenance is not on a non-ODbL allowlist — us-census, hand-placed, synthetic.
  • http.ts passes refused straight through rather than swallowing it, because a gate that drops rows silently is indistinguishable from an empty database. It does not re-run the gate in the browser: the allowlist is the server's, and a self-hoster who added their own provenance value should not watch their own rows vanish client-side.
  • Everything in sample.ts is invented. The companies do not exist, the positions were typed by hand from a general sense of where San Francisco's neighbourhoods are, and the names are absurd on purpose so that none of them can be mistaken for a real business.

Writing your own

http.ts is one adapter, not the adapter. Anything that can produce Marker[], a WeatherObservation and a FlightSource is one — a local JSON file, a Postgres query through your own backend, an SDR on the windowsill. The engine imports nothing from this directory, so an adapter can be deleted, forked or replaced without touching a line of the renderer.

Two things to keep if you write one:

Traffic must not block the render loop. poll() may be synchronous, and HttpFlights is: it answers from whatever is in hand and refreshes on the body's own TTL in the background. A poll() that awaits a slow fetch puts a frame behind a round trip.

FlightRadar24 is not an option. Their terms do not permit scraping and do not permit redistributing the data, so an Apache-2.0 repo containing an FR24 client would be publishing instructions for breaking a ToS and shipping data it has no right to relicense. src/engine/flights.ts ships a simulator and points at the open community ADS-B feeds instead; anything commercial belongs in an adapter in a private deployment. See ARCHITECTURE.md §4.