1
0

Shadows land on the building, and the sky layers stop repeating themselves

**Shadows were the right size and pointed at nothing.** Last round fixed the
missing `updateProjectionMatrix()`, so the frustum finally became the size
every caller asks for — but nothing aimed it, and `sun.target` sits at the
world origin. A pack's origin is the **north-west corner of its slab**, so
for lumbridge-hq the box was off-centre by half the building: 14.4 m of a
48 m plate, about a third of the floor, fell outside the frustum and
neither cast nor received. Invisible while three's broken ±5 default made
shadows useless everywhere; obvious the moment they started working.

`SceneKitOptions` takes a `shadowTarget` now, both callers pass one, and
the light's target is added to the scene — which is the part that actually
matters, because `LightShadow.updateMatrices` reads `target.matrixWorld`
and an unparented `Object3D` is never reached by the traversal that
updates it. The sun is also placed relative to the target rather than the
origin, so light-to-target is exactly `sunDistance` for every direction,
which is the invariant each caller's `shadowNear`/`shadowFar` were chosen
against.

**`flights.ts` could not be tested, and that is why it was untested.** It
used a TypeScript parameter property — the one piece of TS syntax that
*emits code* rather than annotating a type — so Node's type stripping
refused the whole module. The bundler never cared, so nobody found out
until the first `node --test` file tried to import it. The module carrying
the worst bug this project has shipped was, by construction, the one
module that could not have a test. It has eleven now, including one that
fails if the live-aircraft repeat-skip is removed.

**Robots are on the plan panel** — a turned marker with a bow for heading,
in the one hue left that is neither the people-blue nor the camera-amber.

Review findings cleared across the four new sky/robot modules: a real
24 mm void at the ankle and an 8 mm hole through each forearm, a
per-frame allocation in the robot heading picker, a per-frame sort in the
starlink ranking, `uTime` growing unbounded until the cloud breath
quantises, and `DAY_REFERENCE`'s derivation which did not reproduce.

`createStarlinkMeshLayer` now takes a **board** radius — the same unit its
sibling takes — instead of a dome radius with nothing in the types to tell
them apart. That is the exact confusion that has already caused one real
bug here. `DOME_RADIUS_FACTOR` has one owner and is imported, not copied:
the points and the meshes must be on the same dome or a satellite that
grows geometry also jumps.

Several comments were wrong rather than merely stale — a fabricated claim
about `Object3D.clone`, a fabricated attribution to `Plan`, an inverted
`DoubleSide` argument, a triangle ledger citing a function that no longer
exists, and a defensive-call rationale that contradicted the paragraph
above it. In a codebase where the comments are the design record, those
are defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 02:50:25 -07:00
parent af0d4a7d57
commit 51979feea0
13 changed files with 1760 additions and 166 deletions
+570
View File
@@ -0,0 +1,570 @@
/**
* `createFlightLayer`: the difference between a snapshot and an observation.
*
* This layer is handed a list of aircraft on the source's timer and has to turn
* that into continuous motion, a trail, and a decision about when something has
* stopped existing. Every bug it has ever had has been one of those three
* confusing the others, and none of them threw: the layer rendered perfectly and
* lied. The worst of them shipped — live ADS-B traffic could not move or grow a
* trail **at all** — and it survived because a still frame of a broken sky and a
* still frame of a correct one are the same picture.
*
* ## What is actually asserted here
*
* The layer exports `group`, `update`, `tick` and `dispose` and nothing else, so
* everything below is observed through the scene graph rather than by reaching
* into `tracks`:
*
* - **Trail length** comes from `flight-trails`'s draw range. `rebuildTrails`
* writes two vertices per segment and one segment per retained observation
* bar the newest, so `drawRange.count / 2` *is* the number of history points
* minus one, for every track being drawn. That is the number the regression
* destroyed, and it is readable without a GL context.
* - **The aircraft's position** is `mesh.position`, which `tick` copies from
* the interpolated head. Reading it a few seconds after an observation is
* how the measured `span` — a private field — becomes observable: a head
* halfway between two positions can only mean the layer believes the leg
* takes twice as long as it has so far had.
* - **Existence** is a mesh's membership of `group`; **being drawn** is
* `mesh.visible`. The two are deliberately different things in this layer
* and the tests keep them different.
*
* ## Time
*
* `nowSeconds()` reads `performance.now()`, and several of the intervals that
* matter here are tens of seconds long — `TRACK_GRACE_SECONDS` alone is 32. A
* test that waited them out would take a minute and would still be racing the
* clock it was waiting on. So the global `performance.now` is replaced with a
* counter for the duration of this file and moved by `at()`. That is legitimate
* rather than a cheat: the layer's only input from the clock is that one call,
* it reads it fresh every time, and every interval under test is defined in
* terms of it. Nothing else in the file touches wall time.
*
* ## The constants are copied, on purpose
*
* `TRAIL_POINTS`, `MAX_TRACKS` and `TRACK_GRACE_SECONDS` are module-private in
* `flights.ts` and are restated below rather than exported for the tests. That
* is the right way round: a test that imports the constant it is checking
* against asserts only that the code is self-consistent, and would follow a
* typo straight into production. These are the numbers the *comments* in
* `flights.ts` argue for, written out again, so that changing one there without
* meaning to fails here loudly.
*/
import assert from "node:assert/strict";
import { after, before, describe, it } from "node:test";
import * as THREE from "three";
import SAN_FRANCISCO from "../cities/sf.ts";
import { createFlightLayer, type FlightLayer } from "../engine/flights.ts";
import type { Aircraft } from "../engine/types.ts";
import { World } from "../engine/world.ts";
// ---- The clock -------------------------------------------------------------
let clockMs = 0;
const realNow = performance.now;
before(() => {
performance.now = () => clockMs;
});
after(() => {
performance.now = realNow;
});
/** Put the layer's clock at this many seconds. Absolute, not a delta. */
function at(seconds: number): void {
clockMs = seconds * 1000;
}
// ---- The board -------------------------------------------------------------
/**
* A real `World` over the real San Francisco pack, and **not** a hand-made city.
*
* The constructor is arithmetic — `lngScale`, `metresPerUnit`, `lngSquash` — and
* `World`'s own header states that `project` and `metres` work the instant it
* returns. Nothing here calls `ready()`, `groundAt` or anything else that would
* touch the heightfield, so the half-million-sample build never happens and this
* costs nothing beyond parsing the pack.
*
* Using the shipped pack rather than inventing a city matters for exactly one
* reason, and it is the reason this file exists: `JUMP_UNITS_PER_SECOND = 8` is
* calibrated against *this board's* ~94 m per scene unit, and so are the
* distances every test below feeds in. A toy city with a round `latScale` would
* make the teleport tests pass or fail for arithmetic that no deployment runs.
*/
const world = new World(SAN_FRANCISCO);
/** Mirrors of `flights.ts`'s private constants. See the header. */
const TRAIL_POINTS = 72;
const MAX_TRACKS = 192;
const TRACK_GRACE_SECONDS = 32;
/**
* What a live feed's refresh actually costs, in seconds.
*
* The number the whole regression is about: `HttpFlights` is polled at 1 Hz and
* the server caches for 515 s, so this is how far apart two *distinct*
* positions arrive while the poll interval stays at 1.
*/
const REFRESH = 10;
const POLL = 1;
interface Fixture {
layer: FlightLayer;
/** Retained observations minus one, per drawn track, summed. See the header. */
segments(): number;
/** The trail's vertex buffer, sliced to what is actually drawn. */
drawnPositions(): Float32Array;
/** Aircraft meshes in the group, in the order their tracks were created. */
meshes(): THREE.Mesh[];
}
function fixture(): Fixture {
const layer = createFlightLayer(world);
const line = layer.group.getObjectByName("flight-trails") as THREE.LineSegments;
assert.ok(line, "the layer no longer has a trail line to read");
const position = line.geometry.attributes.position as THREE.BufferAttribute;
return {
layer,
segments: () => line.geometry.drawRange.count / 2,
drawnPositions: () =>
(position.array as Float32Array).subarray(0, line.geometry.drawRange.count * 3),
// `type` rather than `instanceof`: the trail is a `LineSegments`, which is a
// `Line` and not a `Mesh`, so this is exactly the aircraft and nothing else.
meshes: () => layer.group.children.filter((c): c is THREE.Mesh => c.type === "Mesh"),
};
}
/** An airliner at cruise, eastbound. Altitude and heading are rarely the point. */
function jet(id: string, lat: number, lng: number, altitude = 9000): Aircraft {
return { id, callsign: id.toUpperCase(), lat, lng, altitude, heading: 90 };
}
/** Assert a scene position, with a tolerance the `Float32Array` can meet. */
function assertNear(actual: number, expected: number, what: string): void {
assert.ok(
Math.abs(actual - expected) < 0.01,
`${what}: ${actual.toFixed(4)} is not ${expected.toFixed(4)}`,
);
}
// ---- The regression --------------------------------------------------------
/**
* A frozen snapshot polled faster than it refreshes.
*
* This is the shape of every live deployment: `update` is called once a second
* with a list that only changes every ten. Before the repeat-skip in `update`,
* each of those nine identical lists was recorded as a fresh observation, which
* made `span` the poll interval instead of the refresh interval — and then the
* tenth call, the one carrying a real ten seconds of flying, was measured
* against a one-second span, tripped the teleport guard, and **wiped the track's
* entire history**. Every refresh. For every aircraft. Forever.
*
* So the trace below is not a stress case, it is the normal case, and the
* numbers are chosen so that the broken code and the correct code disagree by
* more than a margin: 0.025° of longitude on this board is ~23 scene units,
* which is 2.3 units per second across a real refresh (comfortably under the
* ceiling of 8) and 23 units per second across a poll (comfortably over it).
*/
describe("a source that repeats itself between refreshes", () => {
const LAT = 37.62;
const LNG0 = -122.38;
/** ~23 scene units, i.e. an airliner's ten seconds. */
const LEG = 0.025;
/** Poll at 1 Hz from `from` to `until`, handing back the same aircraft. */
function holdSnapshot(f: Fixture, a: Aircraft, from: number, until: number) {
for (let t = from; t < until; t += POLL) {
at(t);
f.layer.update([a]);
}
}
it("keeps the history it has instead of wiping it on every refresh", () => {
const f = fixture();
at(0);
f.layer.update([jet("aal1", LAT, LNG0)]);
assert.equal(f.segments(), 0, "one observation is a point, not a trail");
holdSnapshot(f, jet("aal1", LAT, LNG0), POLL, REFRESH);
assert.equal(f.segments(), 0, "a repeated position must not become a second sample");
at(REFRESH);
f.layer.update([jet("aal1", LAT, LNG0 + LEG)]);
assert.equal(f.segments(), 1, "the first real leg");
holdSnapshot(f, jet("aal1", LAT, LNG0 + LEG), REFRESH + POLL, REFRESH * 2);
assert.equal(f.segments(), 1, "the leg survived nine more repeats of its own end point");
at(REFRESH * 2);
f.layer.update([jet("aal1", LAT, LNG0 + LEG * 2)]);
/**
* Two legs, which is the entire claim. The old code reached this line with
* an empty history and a draw range of zero: the step from `LNG0 + LEG` to
* `LNG0 + LEG * 2` was measured against the one-second gap to the last
* *repeat* rather than the ten-second gap to the last real position, came
* out at ~23 units per second against a ceiling of 8, and took the
* `track.samples.length = 0` branch. Remove the repeat-skip in `update` and
* this assertion reads `0` — as does every one above it that expects a leg.
*/
assert.equal(f.segments(), 2, "the trail was wiped by a refresh");
});
/**
* The other half of the same bug, and the half a user would describe: the
* aircraft did not move. It sat still for ten seconds and jumped.
*
* `span` is private, so it is read here through its only consequence — where
* the head is. Halfway between the two positions, five seconds after an
* observation, can only mean the layer is spreading the leg over the full
* refresh. With `span` mismeasured as one second the head is pinned at the
* newest sample from the first frame onward (and, in the old code, had no
* trail behind it either).
*/
it("spreads a refresh's worth of movement across the whole refresh", () => {
const f = fixture();
at(0);
f.layer.update([jet("aal1", LAT, LNG0)]);
holdSnapshot(f, jet("aal1", LAT, LNG0), POLL, REFRESH);
at(REFRESH);
f.layer.update([jet("aal1", LAT, LNG0 + LEG)]);
const [mesh] = f.meshes();
assert.ok(mesh, "the aircraft has no mesh");
const [x0] = world.project(LAT, LNG0);
const [x1] = world.project(LAT, LNG0 + LEG);
// At the instant of an observation the aircraft is at the *previous* one.
// That is the deliberate one-interval lag: the layer interpolates between
// the last two observations rather than extrapolating past the newest, so
// nothing ever overshoots and snaps back when a feed stutters.
assertNear(mesh.position.x, x0, "the leg should start where the last one ended");
at(REFRESH + REFRESH / 2);
f.layer.tick();
assertNear(mesh.position.x, (x0 + x1) / 2, "the aircraft is not halfway along its leg");
// And it arrives rather than overshooting: `tick` clamps, so polling late
// parks the aircraft on the observation instead of flying it past.
at(REFRESH * 3);
f.layer.tick();
assertNear(mesh.position.x, x1, "the aircraft overshot the observation it was heading for");
});
});
// ---- The guard the repeat-skip works alongside -----------------------------
/**
* The teleport check still has to fire, and the case it exists for is real: a
* `SimulatedFlights` route reaching the end of its leg reappears at the start,
* which on this board is several hundred scene units between two consecutive
* polls. Drawn, it is a bright line straight across San Francisco.
*
* This is the test that stops the fix above from being "delete the guard". The
* repeat-skip changed *what* `span` measures; it must not have changed what
* counts as impossible.
*/
describe("a simulator route wrapping", () => {
it("still clears the history rather than drawing a line across the map", () => {
const f = fixture();
const lng = -122.42;
// Three observations up the peninsula: ~9 units a leg, ~0.9 units a second.
at(0);
f.layer.update([jet("sim-1", 37.60, lng)]);
at(REFRESH);
f.layer.update([jet("sim-1", 37.608, lng)]);
at(REFRESH * 2);
f.layer.update([jet("sim-1", 37.616, lng)]);
assert.equal(f.segments(), 2, "the track should have two legs before it wraps");
// The leg ends and the route restarts at its origin: 0.4° of latitude is
// ~472 scene units, i.e. ~47 units a second against a ceiling of 8.
at(REFRESH * 3);
f.layer.update([jet("sim-1", 37.216, lng)]);
assert.equal(f.segments(), 0, "a wrapped route dragged its old trail across the board");
// The aircraft itself survives — it is the *history* that belonged to a
// different part of the leg, not the track.
const [mesh] = f.meshes();
assert.ok(mesh, "the wrap deleted the aircraft as well as its trail");
const [, z] = world.project(37.216, lng);
assertNear(mesh.position.z, z, "the aircraft did not restart at the head of its route");
});
/**
* And a wrap is not a repeat, which is the interaction worth pinning: the
* repeat-skip runs first, so a guard that only ever saw distinct positions
* would be dead code if `samePosition` were ever loosened into a "did it move
* much" test. It is not, and this is what would notice.
*/
it("is not mistaken for the source repeating itself", () => {
const f = fixture();
at(0);
f.layer.update([jet("sim-1", 37.60, -122.42)]);
at(REFRESH);
f.layer.update([jet("sim-1", 37.60, -122.42)]);
assert.equal(f.segments(), 0, "an unmoved aircraft has nothing to draw");
at(REFRESH * 2);
f.layer.update([jet("sim-1", 37.20, -122.42)]);
assert.equal(f.segments(), 0, "the wrap was recorded as a leg");
});
});
// ---- Targets that go quiet -------------------------------------------------
/**
* `TRACK_GRACE_SECONDS`, from both ends.
*
* An ADS-B receiver losing line of sight for one refresh is routine, and the
* layer used to answer it by deleting the track — throwing away up to
* `TRAIL_SECONDS` of history to survive a gap of one. What makes the grace worth
* having is not that the mesh stays in the group, it is that the history does,
* so the two tests below are "does it come back with its trail" and "does it
* ever actually leave".
*/
describe("an aircraft missing from a snapshot", () => {
const OTHER = "ual2";
const LOST = "swa9";
/** Two aircraft, both with a leg behind them, at t = 0 and t = REFRESH. */
function pair(f: Fixture) {
at(0);
f.layer.update([jet(OTHER, 37.70, -122.40), jet(LOST, 37.50, -122.30)]);
at(REFRESH);
f.layer.update([jet(OTHER, 37.70, -122.375), jet(LOST, 37.50, -122.275)]);
assert.equal(f.meshes().length, 2);
assert.equal(f.segments(), 2, "one leg each");
}
it("survives a gap shorter than the grace period", () => {
const f = fixture();
pair(f);
// Gone from every snapshot from here on. The first one is what sets
// `missingSince`, so the clock that matters starts at 2 × REFRESH.
const lostAt = REFRESH * 2;
at(lostAt);
f.layer.update([jet(OTHER, 37.70, -122.35)]);
at(lostAt + TRACK_GRACE_SECONDS - 1);
f.layer.update([jet(OTHER, 37.70, -122.325)]);
assert.equal(f.meshes().length, 2, "a target one second inside the grace period was dropped");
});
it("is forgotten once the grace period is past", () => {
const f = fixture();
pair(f);
const lostAt = REFRESH * 2;
at(lostAt);
f.layer.update([jet(OTHER, 37.70, -122.35)]);
const gone = f.meshes()[1];
assert.ok(gone, "the second aircraft has no mesh to lose");
at(lostAt + TRACK_GRACE_SECONDS + 1);
f.layer.update([jet(OTHER, 37.70, -122.325)]);
assert.equal(f.meshes().length, 1, "a target well past the grace period is still here");
assert.equal(gone.parent, null, "the mesh was dropped from `tracks` but left in the scene");
});
/**
* The point of holding the track at all: a target that comes back inside the
* window **resumes**. Rebuilding is the failure this replaced, and it is
* invisible in a screenshot — the aircraft is in the right place either way,
* it is just dragging a stub instead of the minute of history it had.
*/
it("resumes its trail rather than rebuilding it", () => {
const f = fixture();
pair(f);
const lostAt = REFRESH * 2;
at(lostAt);
f.layer.update([jet(OTHER, 37.70, -122.35)]);
// Back after 21 s away, having flown on: ~0.028° of longitude is ~26 units,
// spread over a span the layer clamps to `MAX_SPAN`, so ~0.9 units a second
// and nothing like a teleport.
at(lostAt + 21);
f.layer.update([jet(OTHER, 37.70, -122.325), jet(LOST, 37.50, -122.247)]);
/**
* Five legs across the two aircraft: three for the one that never left
* (four observations), and two for the one that came back — its original
* leg, still there, plus the long one it flew while nobody could hear it.
*
* A rebuilt track is what this number is really measuring. Delete the grace
* period and the returning aircraft arrives as a brand-new track with one
* observation and no trail at all, and this reads 3.
*/
assert.equal(f.segments(), 5, "the returning aircraft rebuilt its trail from nothing");
});
});
// ---- The frozen ghost ------------------------------------------------------
/**
* Holding a track is not the same as going on drawing the aeroplane.
*
* With the grace period in and this half missing, a target that genuinely left
* the feed hung in the air at full opacity, trail attached, for thirty-two
* seconds — indistinguishable from an aircraft that had stopped flying. The
* godmode traffic dial made it unmissable: 400 fabricated aircraft turned down
* to zero left 400 darts nailed to the sky.
*
* The fix is gated on having run out of interpolation rather than on being
* missing, which is what this pair of assertions is really about: the *first*
* one is the one that would catch an over-eager fix, because hiding a target the
* instant it is absent makes every aircraft blink on a single dropped refresh.
*/
describe("an aircraft that has left the feed", () => {
const LAT = 37.66;
const LNG = -122.30;
const LEG = 0.02;
function departing(): { f: Fixture; mesh: THREE.Mesh; x0: number; x1: number } {
const f = fixture();
at(0);
f.layer.update([jet("dal4", LAT, LNG)]);
at(REFRESH);
f.layer.update([jet("dal4", LAT, LNG + LEG)]);
const [mesh] = f.meshes();
assert.ok(mesh);
const [x0] = world.project(LAT, LNG);
const [x1] = world.project(LAT, LNG + LEG);
return { f, mesh, x0, x1 };
}
it("keeps flying to where it was last seen, without blinking", () => {
const { f, mesh, x0, x1 } = departing();
at(REFRESH + 1);
f.layer.update([]); // the snapshot it is missing from
assert.equal(mesh.visible, true, "one absent snapshot must not make an aircraft blink");
at(REFRESH + REFRESH / 2);
f.layer.tick();
assert.equal(mesh.visible, true, "hidden while it was still arriving");
assertNear(mesh.position.x, (x0 + x1) / 2, "a missing aircraft stopped moving early");
assert.equal(f.segments(), 1, "its trail should still be drawn while it is");
});
it("stops being drawn once it has finished arriving", () => {
const { f, mesh } = departing();
at(REFRESH + 1);
f.layer.update([]);
// Past the end of the leg — `span` is REFRESH and the newest observation was
// at REFRESH — but still well inside the grace period, so nothing has been
// deleted and this is purely about what is drawn.
at(REFRESH * 3);
f.layer.tick();
assert.equal(mesh.visible, false, "a target that left the feed is frozen in mid-air");
assert.equal(f.segments(), 0, "its trail is still being drawn under a hidden aircraft");
assert.equal(f.meshes().length, 1, "the track itself should be held, not deleted");
assert.notEqual(mesh.parent, null, "the mesh left the group before its grace ran out");
});
});
// ---- The ceiling -----------------------------------------------------------
describe("more aircraft than the trail buffer was sized for", () => {
/**
* `MAX_TRACKS` was declared and then referenced only by the buffer sizing, so
* `tracks` grew without limit and `rebuildTrails` ran off the end of the
* vertex array — which stopped being theoretical the moment the godmode dial
* could put four hundred aircraft in the sky.
*
* Note which half is dropped. Every aircraft keeps its dart; what the ones
* past the ceiling lose is the trail, because a sky missing eight trails reads
* as a sky, and a sky missing eight aeroplanes reads as a bug.
*/
it("gives every aircraft a dart and the first MAX_TRACKS of them a trail", () => {
const f = fixture();
const count = MAX_TRACKS + 8;
const flock = (dLng: number) =>
Array.from({ length: count }, (_, i) =>
jet(`ac${i}`, 37.4 + (i % 20) * 0.01, -122.6 + Math.floor(i / 20) * 0.01 + dLng),
);
at(0);
f.layer.update(flock(0));
at(REFRESH);
f.layer.update(flock(0.02));
assert.equal(f.meshes().length, count, "aircraft past the ceiling lost their dart, not their trail");
// One leg each, so the segment count is the number of tracks being drawn.
assert.equal(f.segments(), MAX_TRACKS, `${count} tracks should draw ${MAX_TRACKS} trails`);
});
/**
* A track longer than the buffer remembers loses its **oldest** end.
*
* The trail is written tail-first, so the natural way to write this loop —
* stop when the buffer is full — drops the segments nearest the aircraft, and
* that is the worst possible end to lose: a streak left floating in open air
* with no aeroplane attached to it reads as a rendering fault rather than as a
* shortened trail.
*
* The same ordering governs `trim`, which is the path that is actually
* reachable here — see the note below the test — so the property is asserted
* where it bites: after more observations than `TRAIL_POINTS`, the drawn range
* still *ends* on the aircraft, and what it no longer contains is the
* beginning of the flight.
*
* Two-second polls rather than ten, so that the count limit is what binds and
* not `TRAIL_SECONDS`: 92 observations at 2 s is 184 s of history against a
* 240 s ceiling, so every sample dropped below is dropped for being old in
* *rank*, which is the thing under test.
*/
it("drops the beginning of a long flight and keeps the end attached to the aircraft", () => {
const f = fixture();
const observations = TRAIL_POINTS + 20;
const lat = 37.74;
const lng0 = -122.5;
// ~1.9 scene units per step, i.e. under a unit a second. Nothing near a jump.
const step = 0.002;
const gap = 2;
for (let i = 0; i < observations; i += 1) {
at(i * gap);
f.layer.update([jet("nrt7", lat, lng0 + i * step)]);
}
assert.equal(
f.segments(),
TRAIL_POINTS - 1,
"the trail should saturate at the retained-sample count, not keep growing",
);
const drawn = f.drawnPositions();
const [mesh] = f.meshes();
assert.ok(mesh);
// The last vertex written is the interpolated head, which is the aircraft.
const lastX = drawn[drawn.length - 3];
assert.ok(lastX !== undefined, "nothing was drawn");
assertNear(lastX, mesh.position.x, "the trail does not reach the aircraft");
// The first is the oldest observation still retained — number 20, not
// number 0 — which is what "loses the oldest, not the newest" means when
// read off the buffer.
const oldestKept = observations - TRAIL_POINTS;
const [xKept] = world.project(lat, lng0 + oldestKept * step);
const [xStart] = world.project(lat, lng0);
const firstX = drawn[0];
assert.ok(firstX !== undefined);
assertNear(firstX, xKept, "the trail starts somewhere other than its oldest retained sample");
assert.ok(
Math.abs(firstX - xStart) > 1,
"the trail still reaches back to the start of the flight, so nothing was trimmed",
);
});
});