feat: SFO, LAX, both bridges, a road that reads as a road, and aeroplanes that move
**The aeroplanes were stuck because the wire could not describe motion.** `WireAircraft` carried position, altitude and heading and nothing else, so the client could only interpolate between the last two observations: every aircraft replayed a segment it had already flown, arrived at the newest known point, and sat still until the next poll landed five to fifteen seconds later. The feed had the missing numbers the whole time and the server threw them away. Sampled live from `api.adsb.lol/v2/point` while writing this — `gs` ground speed, `track`, `baro_rate`, plus `r` registration and `t` type designator. They are on the wire now in SI, aircraft dead-reckon along their own track and correct toward the truth when a fix lands, and the click card an anonymous visitor gets says "B739 · N68834". That last part is the enrichment FR24 was wanted for, obtained from an ODbL feed we may actually republish. **SFO and LAX exist.** A new `engine/airports.ts` composes an airport from runways, taxiways, aprons and terminal masses, with markings drawn on a canvas rather than modelled; the pattern of the runways is what the eye recognises from altitude, long before any building does. SFO is the two crossing pairs on the bay fill; LAX is the four parallels either side of the terminal horseshoe, plus the Southland fields under the traffic that actually flies there. **The Golden Gate and the Bay Bridge are those bridges.** One kit in `engine/bridges.ts`, because a suspension bridge is a repeated tower, a catenary main cable, a series of hangers and a deck — so both are configurations rather than two private implementations. The Bay Bridge carries the real 2013 topology: two suspension towers west of Yerba Buena, one east, then the piered causeway. The freeway stopped being a wireframe overlay and became a road, with shoulders, a median, and lane markings as texture. **And the board got faster while all of that landed.** California went from 728,744 triangles and 562 draw calls to 391,169 and 371 — headroom from 2.8% to 47.8%. The Bay Area board is 506,550 triangles lighter than before this work. Two things paid for it: - `transmission: 0.08` on the aircraft cockpit glass. three.js runs a full transmission backdrop pass whenever any rendered material has transmission above zero, re-drawing the entire opaque scene into a second target every frame — so the city was rendering terrain, every block and every freeway piece TWICE. Measured by patching only that number in a copy of the built bundle: 703,267 tris / 562 draws with it, 398,608 / 371 without. The material was already `transparent: true, opacity: 0.86`, so it was buying nothing. - Flatness-adaptive terrain LOD, which collapses runs of lattice cells wherever the height and colour agree with the quad replacing them. The coastline is provably untouched — a patch collapses only when every point is on land and agrees about `park` — and a test asserts the drawn footprint matches the cell-by-cell area to 1e-6. `createTerrain` got *faster*: the vertices it stops emitting cost more than the flatness scan costs to run. **The budget now watches the boards this was built on.** There was no `bay-area` or `socal` cell — so SFO, LAX and both bridges all landed in frames nothing measured, which is how a cap you do not have looks from the inside. Both are in the matrix now with caps set from measurement, and the rationale lives in the harness because JSON cannot hold a comment. Two known defects ship with this, both recorded in TODO.md rather than hidden: - `bay-area.desktop` drops about one frame in twenty (p50 16.7, p95 33.3). It is desktop-only and not fill rate — mobile runs the same 2.26 M triangles at a comparable pixel count and holds 16.7 flat — which points at the 2048 shadow map desktop uses against handheld's 1024. Measured at the commit before this work with the same harness: identical p95 33.3. Pre-existing, and invisible until the cell existed. - The aeroplane glyph is still about 1.5x the Golden Gate's main span at chapter zoom, down from 2.5x. `GLYPH_MAX_SCALE` is 52 because the raw scale at the far end of the California orbit is 51.0 at a 60-degree field of view, and 26 — tried first — put the glyph at 0.0123 of the frame against the 0.012 where the wings stop resolving. The real fix is to clamp against the camera's focus distance rather than the aircraft's, which is a signature change. Tests 1020 -> 1137. Typecheck, build, eight budget cells, no-binaries, provenance, zero-config boot, dependency licences, arena source hashes and the UI smoke across two viewports and two access tiers all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+67
-1
@@ -1069,6 +1069,13 @@ class HttpFlights implements TrafficSource {
|
||||
// Only a live body carries an address, and only a live body was observed.
|
||||
// The plan's aircraft are this repo's own arithmetic and say so.
|
||||
...(wire?.icao24 === undefined ? {} : { icao24: wire.icao24 }),
|
||||
// The tail number and the type designator, off the same ODbL row as the
|
||||
// position. `Aircraft` has nowhere to put either — they move no pixels —
|
||||
// so they travel here, beside the address, for exactly the same reason.
|
||||
// This is the enrichment the owner wanted a commercial feed for; the
|
||||
// community feeds carried it all along and the server was dropping it.
|
||||
...(wire?.registration === undefined ? {} : { registration: wire.registration }),
|
||||
...(wire?.type === undefined ? {} : { type: wire.type }),
|
||||
observed: this.mode === "live",
|
||||
attribution: this.attribution(),
|
||||
from: this.region.center,
|
||||
@@ -1226,7 +1233,46 @@ class HttpFlights implements TrafficSource {
|
||||
this.mode = "fallback";
|
||||
return ELSEWHERE_SECONDS;
|
||||
}
|
||||
this.aircraft = here;
|
||||
/**
|
||||
* How stale these coordinates already are, in seconds, at the moment they
|
||||
* are adopted.
|
||||
*
|
||||
* Two terms, and both of them are real. `WireAircraft.ageSeconds` is how
|
||||
* old the fix was when the *upstream* answered — a fraction of a second on
|
||||
* a healthy feed. `Date.now() - observedAt` is everything since: this
|
||||
* box's cache TTL, which is five to fifteen seconds by design, plus the
|
||||
* request that carried it. A dead-reckoner told only the first term draws
|
||||
* the whole sky a cache-TTL behind, uniformly, which is the sort of error
|
||||
* that never gets noticed because everything is wrong together.
|
||||
*
|
||||
* Computed once here rather than per poll because `poll()` hands back these
|
||||
* same objects for the body's whole life and the flight layer skips a
|
||||
* repeated position without looking at it — the age matters at the instant
|
||||
* the layer first sees the position, and that is this instant.
|
||||
*
|
||||
* Clamped, and the clamp is not decoration: `observedAt` comes off the
|
||||
* wire, so a server with a wrong clock can make this negative (a fix from
|
||||
* the future) or enormous (a fix from last week), and either would be
|
||||
* integrated into a position. Anything outside the window is treated as
|
||||
* "no useful answer" and the position is taken as current.
|
||||
*/
|
||||
const observedAt = Number.isFinite(body.observedAt) ? body.observedAt : Date.now();
|
||||
const latency = clampSeconds((Date.now() - observedAt) / 1000);
|
||||
|
||||
/**
|
||||
* The wire records, as the engine's `Aircraft`.
|
||||
*
|
||||
* A copy rather than a pass-through, which the live path did not need until
|
||||
* the wire started carrying velocity: `WireAircraft` is structurally an
|
||||
* `Aircraft` and always was, but `ageSeconds` is the one field whose value
|
||||
* is different on the two sides of this line. On the wire it means "how old
|
||||
* when the server saw it"; to the engine it means "how old when you were
|
||||
* handed it", and the difference is the round trip.
|
||||
*/
|
||||
this.aircraft = here.map((a) => ({
|
||||
...a,
|
||||
ageSeconds: clampSeconds((a.ageSeconds ?? 0) + latency),
|
||||
}));
|
||||
// Only the live path has anything to record: a plan carries routes, not
|
||||
// transponders. Cleared at the top of this method, so a record that has
|
||||
// left the feed leaves this map with it rather than surviving to answer a
|
||||
@@ -1245,6 +1291,26 @@ class HttpFlights implements TrafficSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A staleness in seconds, or zero for anything that is not a usable one.
|
||||
*
|
||||
* The ceiling is `MAX_STALE_SECONDS` and the floor is zero. A fix cannot be
|
||||
* from the future, however confidently a clock says so, and one older than the
|
||||
* ceiling is not something to advance a position from — `engine/flights.ts`
|
||||
* stops reckoning at a minute for the same reason.
|
||||
*/
|
||||
function clampSeconds(value: number): number {
|
||||
if (!Number.isFinite(value) || value <= 0) return 0;
|
||||
return Math.min(value, MAX_STALE_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The oldest a fix may be said to be. A minute, matching the point at which the
|
||||
* flight layer stops dead-reckoning and the point at which both flight sources
|
||||
* give up holding their last snapshot.
|
||||
*/
|
||||
const MAX_STALE_SECONDS = 60;
|
||||
|
||||
/**
|
||||
* Slack on the region tests, in nautical miles.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user