1
0

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>
This commit is contained in:
2026-08-22 23:35:09 -07:00
parent b25f217e3e
commit acf4d1a510
52 changed files with 14436 additions and 142 deletions
+28 -2
View File
@@ -18,13 +18,39 @@ describe("California corridor city", () => {
}
});
it("offers route chapters plus doors into both detailed city boards", () => {
it("offers route chapters, doors into both detailed boards, and the north", () => {
// Order is asserted rather than membership, and the first entry doubly so:
// `createScene` opens on `chapters[0]` and `scripts/brand-assets/shots.mjs`
// reaches the others by *index*, so an insertion anywhere but the end
// re-points every marketing still at a different photograph.
assert.deepEqual(
CALIFORNIA_CITY.chapters.map((chapter) => chapter.id),
["california-overview", "la-sf-us-101", "la-sf-i-5", "los-angeles", "san-francisco"],
[
"california-overview",
"la-sf-us-101",
"la-sf-i-5",
"los-angeles",
"san-francisco",
"shasta-cascades",
],
);
});
it("puts a pose on the half of the state the corridor never reaches", () => {
// The board runs to 42.05 N. Before the north chapter existed every authored
// pose sat below 38, so a visitor who never dragged the camera saw none of
// the two hundred kilometres of state above San Francisco.
const north = CALIFORNIA_CITY.chapters.filter((chapter) => chapter.focus.lat > 39);
assert.ok(north.length >= 1, "no chapter looks at the north of the board");
for (const chapter of north) {
assert.ok(
chapter.focus.lat < CALIFORNIA_CITY.bounds.maxLat &&
chapter.focus.lng > CALIFORNIA_CITY.bounds.minLng,
`chapter "${chapter.id}" points off the board`,
);
}
});
it("uses a state-scale field rather than city-scale cells", () => {
assert.ok(CALIFORNIA_CITY.cellLat >= 0.01);
assert.ok(CALIFORNIA_CITY.cellLng >= 0.01);
+253
View File
@@ -0,0 +1,253 @@
/**
* The migration gate, and the two errors it exists to prevent.
*
* **The six-times headline.** `SELECT SUM(birds_crossed)` over the 58 county
* rows for the night of 2026-08-21 gives 2,360,086. The authoritative figure, on
* the `US-CA` state row for the same night, is 393,290. Both numbers are
* correct — a bird crossing four counties is counted in four of them — and only
* one of them is a crossing count. A headline wrong by six times is the fire
* layer's twenty-two orange marks in a different costume, and the only thing
* standing between this build and it is that nothing here ever adds counties up.
*
* **The state row placed as a county.** `counties` upstream is 59 rows, not 58.
* The 59th is `US-CA`, `kind='state'`, with NULL lat and lon, and in tonight's
* granule it carries 793,141 birds aloft against the largest county's 82,549. A
* per-county path that does not take it out puts a three-quarter-million-bird
* blob at 0,0 — and it looks exactly like the other fifty-eight rows, which is
* why it is refused on three independent grounds and asserted on all three.
*
* The third thing here is the **empty state**, which is not an edge case: 176 of
* 297 granules in the store are daytime and hold 104 rows between them against
* 7,719 at night, so the layer is absent about fourteen hours in every
* twenty-four by construction. Every default frame `scripts/look.mjs` takes is a
* daylight frame. The sentence is the layer, for most visitors, and it is tested
* as such.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
BIRDS_MAX_SOLAR_ELEVATION_DEG,
BIRDS_STATE_ROW_ID,
countyRows,
emptyBirdsPromotion,
headingWords,
isCountyRow,
promoteBirds,
quietMessage,
statewideHeadline,
} from "../../server/birds.ts";
import type { BirdsBody, WireBirdCounty } from "../../server/wire.ts";
/**
* The granule of 2026-08-23T03:20Z, as the store holds it — the six busiest
* counties, plus the row that is not a county at all.
*
* Real numbers, from `migration` joined to `counties`. The `US-CA` row's
* `birds_aloft` of 793,141 is what makes the exclusion visible: it is 9.6 times
* the largest county in the same granule.
*/
const STATE_ROW: WireBirdCounty = {
id: "US-CA",
name: "California",
lat: null as unknown as number,
lon: null as unknown as number,
areaKm2: null as unknown as number,
aloft: 793141,
altitude: 605,
direction: 140.6,
speed: 4.8,
};
const COUNTIES: WireBirdCounty[] = [
{ id: "US-CA-019", name: "Fresno County", lat: 36.761006, lon: -119.655019, areaKm2: 15569, aloft: 82549, altitude: 333, direction: 140.0, speed: 6.9 },
{ id: "US-CA-107", name: "Tulare County", lat: 36.228834, lon: -118.781055, areaKm2: 12531, aloft: 52108, altitude: 446, direction: 130.1, speed: 7.5 },
{ id: "US-CA-027", name: "Inyo County", lat: 36.56216, lon: -117.404209, areaKm2: 26488, aloft: 36214, altitude: 826, direction: 118.0, speed: 6.4 },
{ id: "US-CA-029", name: "Kern County", lat: 35.346629, lon: -118.729506, areaKm2: 21147, aloft: 30492, altitude: 469, direction: 122.3, speed: 7.3 },
{ id: "US-CA-073", name: "San Diego County", lat: 33.023604, lon: -116.776117, areaKm2: 11722, aloft: 29809, altitude: 760, direction: 144.5, speed: 6.6 },
{ id: "US-CA-039", name: "Madera County", lat: 37.209821, lon: -119.749802, areaKm2: 5576, aloft: 28061, altitude: 351, direction: 141.0, speed: 6.0 },
];
/** The state row for the night of 2026-08-21, verbatim. */
const LAST_NIGHT = {
crossed: 393290.37,
peakAloft: 1501193.14,
peakAt: "2026-08-22T06:20:00Z",
meanAltitude: 726.0,
heading: "south-east",
};
/** What summing the 58 county rows for that night gives. The wrong answer. */
const COUNTY_SUM_TRAP = 2360086;
const NOW = Date.parse("2026-08-23T03:41:04Z");
function body(over: Partial<BirdsBody> = {}): BirdsBody {
return {
source: "cloud1",
fetchedAt: "2026-08-23T03:41:04Z",
observedAt: "2026-08-23T03:20:00Z",
counties: [...COUNTIES, STATE_ROW],
statewide: LAST_NIGHT,
quiet: null,
ttlSeconds: 600,
...over,
};
}
// ---- The state row --------------------------------------------------------
describe("the row that is a state and not a county", () => {
it("is refused on all three of the grounds that identify it", () => {
assert.equal(isCountyRow(STATE_ROW), false);
// By id…
assert.equal(isCountyRow({ ...STATE_ROW, lat: 37, lon: -119, areaKm2: 423967 }), false);
// …by a missing coordinate…
assert.equal(isCountyRow({ ...COUNTIES[0]!, lat: null as unknown as number }), false);
// …and by a missing area, because the motes scatter in a disc of it.
assert.equal(isCountyRow({ ...COUNTIES[0]!, areaKm2: 0 }), false);
assert.equal(isCountyRow(COUNTIES[0]), true);
assert.equal(BIRDS_STATE_ROW_ID, "US-CA");
});
it("never reaches any per-county output", () => {
const rows = countyRows([...COUNTIES, STATE_ROW]);
assert.equal(rows.length, COUNTIES.length);
assert.ok(!rows.some((row) => row.id === BIRDS_STATE_ROW_ID));
const out = promoteBirds(body(), { nowMs: NOW, solarElevationDeg: -20 });
assert.ok(out.field);
assert.ok(!out.field.counties.some((c) => c.id === BIRDS_STATE_ROW_ID));
// And the largest thing on the board is the largest COUNTY, not the state.
const biggest = Math.max(...out.field.counties.map((c) => c.aloft));
assert.equal(biggest, 82549);
});
});
// ---- The headline ---------------------------------------------------------
describe("the statewide headline", () => {
it("is the state row's own figure and never the sum of counties", () => {
const head = statewideHeadline(body());
assert.ok(head);
assert.equal(Math.round(head.crossed), 393290);
assert.notEqual(Math.round(head.crossed), COUNTY_SUM_TRAP);
assert.equal(Math.round(head.peakAloft), 1501193);
});
it("is null rather than reconstructed when the state row is missing", () => {
// A box up for less than one night has no crossing count, and the honest
// answer is to have none — not to add the counties together and be wrong by
// six times without saying so.
assert.equal(statewideHeadline(body({ statewide: null })), null);
const out = promoteBirds(body({ statewide: null }), { nowMs: NOW, solarElevationDeg: 20 });
assert.ok(out.field?.quiet);
assert.ok(!out.message.includes("Last night"));
});
it("says which way, in words the mean can support", () => {
assert.equal(headingWords(130.3), "south-east");
assert.equal(headingWords(0), "north");
assert.equal(headingWords(359), "north");
assert.equal(headingWords(181), "south");
});
});
// ---- The empty sky --------------------------------------------------------
describe("the daytime sky", () => {
it("returns an explicit empty-with-reason rather than an empty array", () => {
// The sun is up. This is the case most visitors meet, and a bare `[]` beside
// nothing else is indistinguishable from a dead feed.
const out = promoteBirds(body(), { nowMs: NOW, solarElevationDeg: 34 });
assert.ok(out.field);
assert.deepEqual(out.field.counties, []);
assert.ok(out.field.quiet, "quiet is not optional and must be filled in");
assert.equal(out.field.quiet.reason, "daylight");
});
it("names last night's figures in the reason", () => {
const out = promoteBirds(body(), { nowMs: NOW, solarElevationDeg: 34 });
const message = out.field?.quiet?.message ?? "";
assert.match(message, /BirdCast measures migration only after dark/);
assert.match(message, /393,290/);
assert.match(message, /1,501,193/);
assert.match(message, /south-east/);
assert.match(message, /726 metres/);
// Los Angeles time, not UTC. 06:20Z is 23:20 the previous evening in PDT,
// and a caption that said 06:20 would put the peak at breakfast.
assert.match(message, /23:20/);
assert.equal(out.message, message);
});
it("refuses daytime rows even when the feed sends them", () => {
// 104 daytime rows exist in the store across 176 granules. BirdCast does not
// measure by day, so a daytime row is a measurement of something the
// instrument does not measure, and the sun wins over the feed.
const justAfterDawn = BIRDS_MAX_SOLAR_ELEVATION_DEG + 0.1;
const out = promoteBirds(body(), { nowMs: NOW, solarElevationDeg: justAfterDawn });
assert.deepEqual(out.field?.counties, []);
assert.equal(out.field?.quiet?.reason, "daylight");
assert.equal(out.suppressed, 7, "…and counts every row it put back");
});
it("tells a quiet night from a daytime one and from a dead feed", () => {
const night = promoteBirds(body({ counties: [STATE_ROW] }), { nowMs: NOW, solarElevationDeg: -20 });
assert.equal(night.field?.quiet?.reason, "off-season");
assert.match(night.field?.quiet?.message ?? "", /every county is quiet/);
const dead = promoteBirds(body({ fetchedAt: new Date(0).toISOString() }), { nowMs: NOW });
assert.equal(dead.field, null);
assert.equal(dead.ageMs, null);
assert.match(dead.message, /fact about this box/);
});
it("has an empty promotion that is a sentence and not a blank", () => {
const empty = emptyBirdsPromotion();
assert.equal(empty.source, "none");
assert.equal(Date.parse(empty.fetchedAt), 0);
assert.ok(empty.message.length > 40);
assert.ok(quietMessage("no-data", null).length > 20);
});
});
// ---- A night with something in it -----------------------------------------
describe("a night with birds over it", () => {
it("draws the counties and says where they are going", () => {
const out = promoteBirds(body(), { nowMs: NOW, solarElevationDeg: -20 });
assert.equal(out.field?.quiet, null);
assert.equal(out.field?.counties.length, 6);
assert.match(out.message, /aloft over 6 counties/);
assert.match(out.message, /drifting south-east/);
// And it does NOT print a current statewide total, because the only way to
// get one from what crosses the wire is to add the counties up.
assert.ok(!out.message.includes(String(COUNTY_SUM_TRAP)));
});
it("averages bearings circularly, so the wrap does not point the state south", () => {
const wrap: WireBirdCounty[] = [
{ ...COUNTIES[0]!, direction: 350, aloft: 1000 },
{ ...COUNTIES[1]!, direction: 10, aloft: 1000 },
];
const out = promoteBirds(body({ counties: wrap }), { nowMs: NOW, solarElevationDeg: -20 });
// An arithmetic mean of 350 and 10 is 180: due south, for a state flying
// due north. Tonight's rows all sit in the 120-150 bucket and would survive
// a naive mean, which is exactly why this would ship broken and stay broken.
assert.match(out.message, /drifting north/);
assert.ok(!out.message.includes("drifting south"));
});
it("survives a null, an undefined and a body full of nonsense", () => {
for (const input of [null, undefined, 7, "birds", { source: "cloud1" }]) {
const out = promoteBirds(input as unknown as BirdsBody, { nowMs: NOW });
assert.notEqual(out.message, "");
}
const junk = promoteBirds(
body({ counties: [null, { id: "x" }] as unknown as WireBirdCounty[] }),
{ nowMs: NOW, solarElevationDeg: -20 },
);
assert.deepEqual(junk.field?.counties, []);
});
});
+316
View File
@@ -0,0 +1,316 @@
/**
* The radar gate, held to the three things a screenshot cannot check.
*
* **It refuses sea clutter.** DATA-CATALOG.md:84 already records a 54.5 dBZ cell
* between Long Beach and Catalina on a clear day, and the upstream applies no
* anomalous-propagation filter of any kind. A thunderstorm drawn over the ocean
* on a blue afternoon is the fire layer's twenty-two-orange-marks failure in a
* different palette, and the only place it can be prevented is here — a
* statement about data that lives inside a mesh builder is a statement nobody
* can test without a WebGL context (`engine/fires.ts:16-23`).
*
* **It knows "not on-line" from "off the air".** Six of the sixteen stations in
* the store read `RDA - Maintenance Action Mandatory` right now, including Los
* Angeles and San Diego, and every one of them is transmitting. A gate that
* treated the flag as an outage would grey out the southern half of the state on
* a night with a monsoon over it. That case is asserted below with the real
* strings.
*
* **It knows Nevada from the Pacific.** The board now reaches -114.0 and 42.05 N,
* so a third of it is land the California coastline trace has never described. A
* land test of "inside the pack's polygon" calls a Great Basin thunderstorm sea
* clutter, and tonight's frame has one at 41.375,-117.125 carrying 53 dBZ.
*
* The coastline is the pack's own, not a fake: a rectangle would pass every one
* of these assertions while the shipped board drew rain over Reno.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import CALIFORNIA_CITY from "../../cities/california.ts";
import {
AP_MAX_CELLS,
AP_MIN_DBZ,
buildRadarField,
classifyGround,
emptyRadarPromotion,
promoteRadar,
RADAR_DRY_DBZ,
RADAR_MIN_WET_FRACTION,
RADAR_RAIN_DBZ,
stationIsDown,
type RadarCell,
type RadarStation,
} from "../../server/radar.ts";
import type { RadarBody } from "../../server/wire.ts";
const COAST = CALIFORNIA_CITY.landmasses;
const BOARD = CALIFORNIA_CITY.bounds;
/**
* The station table as it stands, with the operability strings verbatim.
*
* Copied from `station_state` rather than invented. The point of the file is
* that six of these are flagged and none of the six is down.
*/
const STATIONS: RadarStation[] = [
{ id: "KBBX", lat: 39.49611, lon: -121.63165, type: "WSR-88D", operability: "RDA - Maintenance Action Mandatory" },
{ id: "KBHX", lat: 40.49833, lon: -124.29215, type: "WSR-88D", operability: "RDA - On-line" },
{ id: "KDAX", lat: 38.50111, lon: -121.67782, type: "WSR-88D", operability: "RDA - Maintenance Action Mandatory" },
{ id: "KESX", lat: 35.70111, lon: -114.89138, type: "WSR-88D", operability: "RDA - On-line" },
{ id: "KEYX", lat: 35.09777, lon: -117.56074, type: "WSR-88D", operability: "RDA - On-line" },
{ id: "KHNX", lat: 36.31416, lon: -119.63213, type: "WSR-88D", operability: "RDA - On-line" },
{ id: "KMUX", lat: 37.15522, lon: -121.89843, type: "WSR-88D", operability: "RDA - On-line" },
{ id: "KNKX", lat: 32.91888, lon: -117.04193, type: "WSR-88D", operability: "RDA - Maintenance Action Mandatory" },
{ id: "KSOX", lat: 33.81773, lon: -117.63599, type: "WSR-88D", operability: "RDA - Maintenance Action Required" },
{ id: "KVBX", lat: 34.83855, lon: -120.3979, type: "WSR-88D", operability: "RDA - On-line" },
{ id: "KVTX", lat: 34.41166, lon: -119.1786, type: "WSR-88D", operability: "RDA - Maintenance Action Mandatory" },
{ id: "TLAS", lat: 36.144, lon: -115.007, type: "TDWR", operability: "RDA - On-line" },
];
/** A cell centre, on the product's own quarter-degree lattice. */
function cell(lat: number, lon: number, dbz: number): RadarCell {
return { lat, lon, dbz };
}
function build(cells: RadarCell[], stations = STATIONS, wetFraction = 0.02) {
return buildRadarField({
cells,
stations,
bounds: BOARD,
coast: COAST,
observedAt: "2026-08-23T03:55Z",
wetFraction,
});
}
function at(field: NonNullable<ReturnType<typeof build>["field"]>, lat: number, lng: number) {
const row = Math.round((lat - field.minLat) / field.cellLat);
const col = Math.round((lng - field.minLng) / field.cellLng);
assert.ok(row >= 0 && row < field.rows && col >= 0 && col < field.cols, `${lat},${lng} is off the lattice`);
return field.dbz[row * field.cols + col];
}
// ---- Anomalous propagation ------------------------------------------------
describe("the sea-clutter rule", () => {
it("drops 47.5 dBZ off Big Sur and keeps the same value over the Inland Empire", () => {
// 35.625,-121.375 is the lattice cell the design's own probe found carrying
// 47.5 dBZ over 302 pixels of open water. 33.875,-117.375 is Riverside.
assert.equal(classifyGround(35.625, -121.375, COAST), "sea");
assert.equal(classifyGround(33.875, -117.375, COAST), "land");
const out = build([cell(35.625, -121.375, 47.5), cell(33.875, -117.375, 47.5)]);
const field = out.field;
assert.ok(field, "a frame with a real Riverside storm in it must be promoted");
assert.equal(at(field, 35.625, -121.375), RADAR_DRY_DBZ, "the ocean cell must not survive");
assert.equal(at(field, 33.875, -117.375), 47.5, "the land cell must survive untouched");
assert.equal(out.suppressed.length, 1);
assert.equal(out.suppressed[0]?.dbz, 47.5);
});
it("keeps an offshore cell that is part of the same blob as a cell over land", () => {
// Rain coming ashore. The seaward half is over water and reaches 55 dBZ, and
// deleting it would punch a hole in the middle of a real storm — which is
// the failure a per-cell version of this rule actually produced, measured on
// tonight's 03:55Z frame.
// 34.625 N, because at 34.375 the shore is already at -119.6: south of
// Point Conception the coast turns east and the Santa Barbara Channel is all
// water out to Ventura. Getting that wrong is how the first draft of this
// test wrote a cell "ashore near Vandenberg" that was 60 km out to sea.
assert.equal(classifyGround(34.625, -120.625, COAST), "land");
const out = build([
cell(34.625, -121.125, 55),
cell(34.625, -120.875, 52),
cell(34.625, -120.625, 48), // ashore, on Vandenberg
]);
assert.deepEqual(out.suppressed, []);
assert.ok(out.field);
assert.equal(at(out.field, 34.625, -121.125), 55);
});
it("leaves weak marine echo alone, because drizzle offshore is real", () => {
const weak = AP_MIN_DBZ - 5;
const out = build([cell(35.625, -121.875, weak), cell(35.875, -121.875, weak), cell(33.875, -117.375, 45)]);
assert.deepEqual(out.suppressed, []);
assert.ok(out.field);
assert.equal(at(out.field, 35.625, -121.875), weak);
});
it("refuses to delete a blob larger than one radar's clutter ring", () => {
// AP_MAX_CELLS is a cap on how much weather one bad night can remove, and it
// is a judgement rather than a measurement — so it is asserted, not assumed.
const big: RadarCell[] = [];
for (let i = 0; i <= AP_MAX_CELLS; i++) {
big.push(cell(33.375 + Math.floor(i / 6) * 0.25, -121.875 + (i % 6) * 0.25, 45));
}
const out = build(big);
assert.deepEqual(out.suppressed, [], "a system this size is not clutter");
});
it("does not mistake Nevada for the Pacific", () => {
// The extended board's eastern third is land the California trace has never
// described. Tonight's Great Basin storm sits at 41.375,-117.125 at 53 dBZ.
assert.equal(classifyGround(41.375, -117.125, COAST), "unmapped");
const out = build([cell(41.375, -117.125, 53)]);
assert.deepEqual(out.suppressed, []);
assert.ok(out.field);
assert.equal(at(out.field, 41.375, -117.125), 53);
});
it("puts the shore where the shore is, at every latitude", () => {
// The first version of the land test took the westernmost coastline VERTEX
// within half a degree, which on a diagonal coast is wrong by the diagonal:
// between 35.1 N and 36.1 N the shore moves 1.2 degrees of longitude, and
// the band rule declared the Big Sur clutter inland.
assert.equal(classifyGround(35.625, -121.125, COAST), "land");
assert.equal(classifyGround(35.375, -121.125, COAST), "sea");
assert.equal(classifyGround(35.875, -121.375, COAST), "land");
assert.equal(classifyGround(35.625, -121.375, COAST), "sea");
});
});
// ---- Coverage holes -------------------------------------------------------
describe("a radar that is off the air", () => {
it("marks its cells unknown rather than dry", () => {
const inoperable: RadarStation[] = [
{ id: "KDOWN", lat: 40.0, lon: -120.0, type: "WSR-88D", operability: "RDA - Inoperable" },
// Far enough away that its 230 km reach cannot cover the cell under test.
{ id: "KFAR", lat: 33.0, lon: -117.0, type: "WSR-88D", operability: "RDA - On-line" },
];
const out = build([cell(33.125, -117.125, 45)], inoperable);
const field = out.field;
assert.ok(field);
assert.equal(at(field, 40.125, -120.125), null, "a cell only a dead radar covers is unknown");
assert.equal(at(field, 33.125, -116.875), RADAR_DRY_DBZ, "a cell a live radar covers is dry");
assert.equal(field.stationsDown, 1);
assert.equal(field.stations, 2);
});
it("does not count a maintenance work order as an outage", () => {
// The trap. Six of sixteen stations carry one of these right now and all six
// are transmitting; treating them as down blanks the southern half of the
// state on the night this was written.
assert.equal(stationIsDown("RDA - Maintenance Action Mandatory"), false);
assert.equal(stationIsDown("RDA - Maintenance Action Required"), false);
assert.equal(stationIsDown("RDA - On-line"), false);
assert.equal(stationIsDown("RDA - Inoperable"), true);
assert.equal(stationIsDown("RDA - Off-line"), true);
// Unknown is not down. A status feed that hiccups must not grey the board.
assert.equal(stationIsDown(""), false);
assert.equal(stationIsDown(null), false);
assert.equal(stationIsDown(undefined), false);
const out = build([cell(33.875, -117.375, 45)]);
assert.ok(out.field);
assert.equal(out.field.stationsDown, 0, "not one of the twelve real stations is down");
assert.equal(at(out.field, 33.875, -117.625), RADAR_DRY_DBZ, "Orange County is dry, not unknown");
});
});
// ---- Promotion ------------------------------------------------------------
describe("promotion", () => {
it("is decided from statewide coverage and not from cell count", () => {
const cells = [cell(33.875, -117.375, 45)];
const below = build(cells, STATIONS, RADAR_MIN_WET_FRACTION / 2);
assert.equal(below.field, null);
assert.equal(below.quiet, "below-threshold");
const above = build(cells, STATIONS, RADAR_MIN_WET_FRACTION * 2);
assert.ok(above.field, "the same one cell, above the coverage floor, is drawn");
});
it("draws nothing when the gate has taken everything that was there", () => {
const out = build([cell(35.625, -121.375, 47.5)], STATIONS, 0.02);
assert.equal(out.field, null);
assert.equal(out.quiet, "nothing-survived");
assert.equal(out.suppressed.length, 1, "…and says what it took");
});
it("gives a lattice that is the product's own grid, clipped to the board", () => {
const out = build([cell(33.875, -117.375, 45)]);
const field = out.field;
assert.ok(field);
// 32.5-42.05 N and -124.5 to -114.0 W on a quarter-degree grid whose centres
// sit on the half-cell. Every one of these is derived, not configured, so a
// board that moves again moves this with it.
assert.equal(field.rows, 38);
assert.equal(field.cols, 42);
assert.equal(field.minLat, 32.625);
assert.equal(field.minLng, -124.375);
assert.equal(field.dbz.length, 38 * 42);
assert.ok(field.rows * field.cols * 4 < 8 * 1024, "the whole raster is under 8 KB as RGBA");
});
});
// ---- The client gate ------------------------------------------------------
describe("promoteRadar", () => {
const body = (over: Partial<RadarBody>): RadarBody => ({
source: "cloud1",
fetchedAt: "2026-08-23T04:00:21Z",
field: null,
ttlSeconds: 300,
...over,
});
const NOW = Date.parse("2026-08-23T04:05:00Z");
it("survives a null, an undefined and a body full of nonsense", () => {
for (const input of [null, undefined, 42, "rain", { source: "cloud1" }]) {
const out = promoteRadar(input as unknown as RadarBody, NOW);
assert.equal(out.field, null);
assert.notEqual(out.message, "");
}
});
it("says which kind of empty it is", () => {
const never = promoteRadar(body({ fetchedAt: new Date(0).toISOString() }), NOW);
assert.equal(never.ageMs, null);
assert.match(never.message, /fact about this box/);
const quiet = promoteRadar(body({}), NOW);
assert.equal(quiet.ageMs, 4 * 60_000 + 39_000);
assert.match(quiet.message, /Nothing is falling/);
assert.match(quiet.message, /minutes old/);
});
it("refuses a lattice whose dbz length disagrees with its own shape", () => {
// Not a field with a problem — a field that would be drawn rotated, every
// row after the first offset by the difference.
const out = promoteRadar(
body({
field: {
minLat: 32.625, minLng: -124.375, cellLat: 0.25, cellLng: 0.25,
rows: 38, cols: 42, dbz: [45, null], observedAt: "x",
wetFraction: 0.02, stations: 16, stationsDown: 0,
},
}),
NOW,
);
assert.equal(out.field, null);
});
it("carries a promoted lattice through unchanged, and counts what is in it", () => {
const built = build([cell(33.875, -117.375, 61.5), cell(34.125, -117.375, 22)]);
assert.ok(built.field);
const out = promoteRadar(body({ field: built.field }), NOW);
assert.ok(out.field);
assert.equal(out.field.dbz.length, built.field.dbz.length);
assert.match(out.message, /61\.5 dBZ/);
assert.match(out.message, /2 cells of 1596/);
assert.match(out.message, /All 12 radars reporting/);
assert.ok(RADAR_RAIN_DBZ === 20);
});
it("has an empty promotion that is a sentence and not a blank", () => {
const empty = emptyRadarPromotion();
assert.equal(empty.source, "none");
assert.equal(Date.parse(empty.fetchedAt), 0);
assert.ok(empty.message.length > 40);
});
});
+479
View File
@@ -0,0 +1,479 @@
/**
* The vessel gate: three AIS sentinels, one berth, and the position that must
* never exist.
*
* Every assertion in this file is about a defect that **typechecks, throws
* nothing and renders a perfectly plausible harbour**. That is why they are
* here rather than left to a picture: a fleet of ships all facing due north
* looks like a design choice, a hull three kilometres out to sea looks like a
* hull three kilometres out to sea, and a ship cutting the corner of a
* breakwater looks like a ship. None of the three is visible in a still frame
* and all three are wrong.
*
* The load-bearing one is `cog 358.7 survives`. `cog % 360` is the obvious
* normalisation, it is what anybody would write, and it silently converts the
* "not available" sentinel — exactly 360.0 — into a course of zero, due north.
* Real course over ground reaches 358.7 in the store behind this feed, so a
* range check cannot separate them either: the value, and only the value, can.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
AIS_COURSE_UNAVAILABLE,
AIS_HEADING_UNAVAILABLE,
AIS_SOG_UNAVAILABLE_KN,
KNOTS_TO_MPS,
VESSEL_MAKING_WAY_MPS,
aisCourse,
aisHeading,
aisSpeedMps,
berthAnchors,
isMakingWay,
metresBetween,
modelHarbour,
promoteVessels,
reckonVessel,
resolveBearing,
vesselStatus,
vesselSummary,
type BerthAnchor,
type VesselBounds,
} from "../../server/vessels.ts";
import type { Port } from "../../engine/types.ts";
import type { VesselsBody, WireVessel } from "../../server/wire.ts";
// ---- Fixtures --------------------------------------------------------------
const SAN_PEDRO: VesselBounds = {
minLat: 33.55,
maxLat: 33.85,
minLng: -118.4,
maxLng: -118.0,
};
/**
* One berth on Pier 400, bearing 118° — a hull lying alongside it points
* east-south-east. Hand-typed like everything else on these boards; the point of
* it here is that the number is *authored* and therefore known before any ship
* arrives.
*/
const BERTH: BerthAnchor = { id: "uslax-p400-a", lat: 33.72, lng: -118.24, bearing: 118 };
function wire(overrides: Partial<WireVessel> = {}): WireVessel {
return {
id: "w-1",
kind: "container",
lat: 33.72,
lon: -118.24,
speed: 0,
course: null,
heading: null,
navStatus: 5,
length: 300,
beam: 45,
ageSeconds: 0,
...overrides,
};
}
function body(vessels: WireVessel[], source: VesselsBody["source"] = "cloud1"): VesselsBody {
return {
source,
fetchedAt: "2026-08-22T02:00:00.000Z",
vessels,
intervalSeconds: 900,
ttlSeconds: 900,
};
}
// ---- The three sentinels ---------------------------------------------------
describe("the AIS sentinels, none of which is ever NULL", () => {
it("rejects sog 102.3, which is 'not available' and 52.6 m/s", () => {
assert.equal(aisSpeedMps(AIS_SOG_UNAVAILABLE_KN), null);
assert.equal(aisSpeedMps(102.3), null);
// Sixty seconds of it is 3.2 km — eight SoCal scene units — which is the
// whole reason this is a rejection and not a clamp.
assert.ok(AIS_SOG_UNAVAILABLE_KN * KNOTS_TO_MPS * 60 > 3_000);
});
it("keeps a real speed, including the zero half the fleet reports", () => {
assert.equal(aisSpeedMps(0), 0);
assert.ok(Math.abs((aisSpeedMps(12) ?? 0) - 12 * KNOTS_TO_MPS) < 1e-9);
// Zero is an answer and not an absence: 554 of 1,138 fixes in the store are
// exactly 0.0, and conflating them with "unknown" would stop every moored
// ship in the product being drawn.
assert.notEqual(aisSpeedMps(0), null);
});
it("rejects heading 511, which 40% of fixes carry", () => {
assert.equal(aisHeading(AIS_HEADING_UNAVAILABLE), null);
assert.equal(aisHeading(511), null);
assert.equal(aisHeading(0), 0);
assert.equal(aisHeading(359.9), 359.9);
});
it("rejects cog exactly 360.0", () => {
assert.equal(aisCourse(AIS_COURSE_UNAVAILABLE), null);
assert.equal(aisCourse(360), null);
assert.equal(aisCourse(360.0), null);
});
it("ACCEPTS cog 358.7 — the assertion this whole file exists for", () => {
// Real course over ground reaches 358.7, and 355.0, 355.4, 355.7, 356.3,
// 356.9 and 357.0 all occur in the same store. A naive range check near
// north, or a `% 360`, eats every one of them or turns the sentinel into a
// course. Neither may happen.
assert.equal(aisCourse(358.7), 358.7);
for (const cog of [355.0, 355.4, 355.7, 356.3, 356.9, 357.0, 358.7, 359.99]) {
assert.equal(aisCourse(cog), cog, `real course ${cog} was eaten by the gate`);
}
// And the trap itself, stated so nobody reintroduces it: `% 360` maps the
// sentinel onto a perfectly good course.
assert.equal(AIS_COURSE_UNAVAILABLE % 360, 0);
assert.notEqual(aisCourse(AIS_COURSE_UNAVAILABLE), 0);
});
it("survives garbage without throwing", () => {
for (const bad of [null, undefined, Number.NaN, Infinity, -1]) {
assert.equal(aisSpeedMps(bad as number), null);
assert.equal(aisHeading(bad as number), null);
assert.equal(aisCourse(bad as number), null);
}
});
});
// ---- Motion is gated on speed, never on nav_status -------------------------
describe("speed gates the motion; nav_status only labels it", () => {
it("treats nav_status 0 at 0.2 kn as stopped", () => {
// 83 of 197 vessels reporting "under way using engine" are under half a
// knot. The status is a word on a card; the speed is the fact.
const speed = aisSpeedMps(0.2);
assert.notEqual(speed, null);
assert.equal(isMakingWay(speed), false);
assert.equal(vesselStatus(0), "under-way");
const promotion = promoteVessels(
body([wire({ lat: 33.7, lon: -118.2, speed: 0.2 * KNOTS_TO_MPS, navStatus: 0, course: 90 })]),
SAN_PEDRO,
[],
);
const drawn = promotion.drawn[0];
assert.ok(drawn, "the hull was dropped rather than drawn stopped");
assert.equal(drawn.status, "under-way", "the label must survive");
assert.equal(drawn.speed, 0, "the motion must not");
assert.equal(promotion.makingWay, 0);
});
it("lets a hull at 6 kn make way", () => {
const promotion = promoteVessels(
body([wire({ lat: 33.7, lon: -118.2, speed: 6 * KNOTS_TO_MPS, navStatus: 0, course: 210 })]),
SAN_PEDRO,
[],
);
assert.equal(promotion.makingWay, 1);
assert.ok((promotion.drawn[0]?.speed ?? 0) >= VESSEL_MAKING_WAY_MPS);
});
it("refuses a wire speed that is the sentinel in metres per second", () => {
// The upstream half of this feed is in another repo on another box and does
// not exist yet, so "already stripped" is a promise nobody can keep today.
const promotion = promoteVessels(
body([wire({ speed: AIS_SOG_UNAVAILABLE_KN * KNOTS_TO_MPS })]),
SAN_PEDRO,
[BERTH],
);
assert.equal(promotion.drawn.length, 0);
assert.equal(promotion.suppressed, 1);
});
});
// ---- Orientation comes from the berth --------------------------------------
describe("a berthed hull points the way its quay does", () => {
it("gives a stopped hull with no heading and no cog its berth's bearing", () => {
// 21 of 150 stopped vessels have neither heading nor cog, and only 75 of the
// 150 have a heading at all. This is the case the layer must not be spun by.
const promotion = promoteVessels(
body([wire({ lat: BERTH.lat, lon: BERTH.lng, heading: null, course: null, speed: 0 })]),
SAN_PEDRO,
[BERTH],
);
const drawn = promotion.drawn[0];
assert.ok(drawn, "a hull with no orientation from the wire was dropped");
assert.equal(drawn.bearing, BERTH.bearing);
assert.equal(drawn.berthId, BERTH.id);
assert.equal(promotion.alongside, 1);
});
it("lets the wire nudge a berthed hull, and never swing it", () => {
const nudged = resolveBearing({ heading: 121, course: null, speedMps: 0, berthBearing: 118 });
assert.equal(nudged, 121);
// A heading 180 degrees off the quay is an AIS unit that is wrong about
// which end is the bow, not a ship moored backwards. The concrete wins.
const absurd = resolveBearing({ heading: 298, course: null, speedMps: 0, berthBearing: 118 });
assert.ok(absurd !== null && Math.abs(absurd - 118) <= 3, `berth bearing was swung to ${absurd}`);
});
it("uses the course for a hull making way, and the heading for one at anchor", () => {
assert.equal(resolveBearing({ heading: 30, course: 210, speedMps: 5, berthBearing: null }), 210);
assert.equal(resolveBearing({ heading: 30, course: 210, speedMps: 0, berthBearing: null }), 30);
});
it("withholds a hull nothing will orient, rather than inventing an angle", () => {
const promotion = promoteVessels(
body([wire({ lat: 33.6, lon: -118.1, heading: null, course: null, speed: 0 })]),
SAN_PEDRO,
[BERTH],
);
assert.equal(promotion.drawn.length, 0);
assert.equal(promotion.withoutOrientation, 1);
assert.equal(promotion.suppressed, 0, "an unoriented hull is not the same as an unreadable one");
assert.match(vesselSummary(promotion), /which way/);
});
it("does not reach across the harbour for a berth", () => {
// A berth is one authored point and the reach is 400 m; a hull a kilometre
// away is not lying on it.
assert.ok(metresBetween(BERTH.lat, BERTH.lng, 33.73, -118.24) > 400);
const promotion = promoteVessels(
body([wire({ lat: 33.73, lon: -118.24, heading: 44, course: null, speed: 0 })]),
SAN_PEDRO,
[BERTH],
);
assert.equal(promotion.drawn[0]?.bearing, 44, "the wire heading should stand off the berth");
assert.equal(promotion.drawn[0]?.berthId, undefined);
});
});
// ---- Dead reckoning, and the position that must not exist ------------------
describe("motion is along the reported course and never between two fixes", () => {
/**
* Two fixes fifteen minutes apart, on a hull that turned.
*
* `A` reports a course of 090 — due east — and eight knots. Fifteen minutes
* later it is reported at `B`, which is to the *south* east, because it came
* round the breakwater in between. The chord from A to B is therefore a line
* nothing sailed, and every point on it except the ends is a place the ship
* never was.
*/
const A = { lat: 33.72, lng: -118.28, speed: 8 * KNOTS_TO_MPS, course: 90 };
const B = { lat: 33.69, lng: -118.2 };
it("advances along the course at the speed", () => {
const after = reckonVessel(A, 450);
const metres = metresBetween(A.lat, A.lng, after.lat, after.lng);
assert.ok(
Math.abs(metres - A.speed * 450) < 1,
`advanced ${metres.toFixed(1)} m instead of ${(A.speed * 450).toFixed(1)}`,
);
// Due east means the latitude does not move.
assert.ok(Math.abs(after.lat - A.lat) < 1e-9, "a course of 090 changed the latitude");
assert.ok(after.lng > A.lng, "a course of 090 went west");
});
it("does not arrive at the second fix, which is what an interpolator does", () => {
// The crispest statement of the rule. An interpolator hands back exactly `B`
// at the end of the interval; a dead-reckoner hands back wherever the
// reported course took the ship, which here is five kilometres away because
// the ship turned and the course did not say so until the next fix.
const atInterval = reckonVessel(A, 900);
const missBy = metresBetween(atInterval.lat, atInterval.lng, B.lat, B.lng);
assert.ok(missBy > 3_000, `reckoning landed ${missBy.toFixed(0)} m from the second fix`);
});
it("never lands on the chord between two fixes", () => {
// The geometric statement of "no interpolation": walk the interval and
// assert every reckoned point stays clear of the segment A->B. It cannot be
// otherwise, because `reckonVessel` is handed one fix and has no second
// point to reach toward — but that is the property being pinned.
//
// From 150 s rather than from zero, because the first fix *is* an endpoint
// of the chord and the ship genuinely was there: a clearance test that
// started at t=0 would be asserting the ship was never at its own reported
// position.
let closest = Infinity;
for (let t = 150; t <= 900; t += 30) {
const at = reckonVessel(A, t);
closest = Math.min(closest, metresToSegment(at, A, B));
}
assert.ok(
closest > 200,
`a reckoned position came within ${closest.toFixed(0)} m of the chord between two fixes`,
);
});
it("stops rather than sailing on for ever once the next fix is overdue", () => {
const atLimit = reckonVessel(A, 900);
const wayPast = reckonVessel(A, 4_000);
assert.deepEqual(wayPast, atLimit);
});
it("does not move a hull with no course, whatever its speed says", () => {
const still = reckonVessel({ lat: 33.72, lng: -118.28, speed: 6, course: null }, 600);
assert.deepEqual(still, { lat: 33.72, lng: -118.28 });
});
it("does not move a stopped hull", () => {
const still = reckonVessel({ lat: 33.72, lng: -118.28, speed: 0, course: 90 }, 600);
assert.deepEqual(still, { lat: 33.72, lng: -118.28 });
});
});
// ---- The empty state, designed first ---------------------------------------
describe("the harbour with nothing in it says which kind of nothing it is", () => {
it("distinguishes an unconfigured feed from an empty board", () => {
const unconfigured = promoteVessels(null, SAN_PEDRO, []);
assert.equal(unconfigured.source, "none");
assert.match(vesselSummary(unconfigured), /No vessel feed is configured/i);
const answered = promoteVessels(body([]), SAN_PEDRO, []);
assert.equal(answered.source, "cloud1");
assert.match(vesselSummary(answered), /answered/i);
});
it("counts what it withheld rather than going blank", () => {
const promotion = promoteVessels(
body([
wire({ id: "off", lat: 30.0, lon: -118.2, heading: 10 }),
wire({ id: "bad", lat: Number.NaN, heading: 10 }),
wire({ id: "blind", lat: 33.6, lon: -118.1, heading: null, course: null }),
]),
SAN_PEDRO,
[],
);
assert.equal(promotion.drawn.length, 0);
assert.equal(promotion.offBoard, 1);
assert.equal(promotion.suppressed, 1);
assert.equal(promotion.withoutOrientation, 1);
const summary = vesselSummary(promotion);
assert.match(summary, /outside the frame/);
assert.match(summary, /unreadable/);
});
it("never labels a hull laden or in ballast", () => {
const promotion = promoteVessels(
body([wire({ lat: BERTH.lat, lon: BERTH.lng })]),
SAN_PEDRO,
[BERTH],
);
// The owner asked "whether they are empty or full". The honest answer is a
// port figure — 348,691 of 460,467 boxes left Los Angeles empty in July
// 2026 — and it is never attached to a hull, because `vessels` carries no
// draught column and the static AIS message is absent for most ships.
assert.match(vesselSummary(promotion), /not a ship one/);
assert.equal("draught" in (promotion.drawn[0] ?? {}), false);
});
});
// ---- The modelled harbour --------------------------------------------------
const PORT: Port = {
id: "USLAX",
name: "Port of Los Angeles",
lat: 33.73,
lng: -118.26,
harborType: "CB",
channel: [
[33.705, -118.26],
[33.72, -118.255],
[33.74, -118.25],
],
berths: [
{ id: "a", lat: 33.735, lng: -118.262, bearing: 118, maxLength: 400 },
{ id: "b", lat: 33.737, lng: -118.259, bearing: 118, maxLength: 400 },
{ id: "c", lat: 33.739, lng: -118.256, bearing: 118, maxLength: 340 },
{ id: "d", lat: 33.741, lng: -118.253, bearing: 296, maxLength: 120 },
],
};
describe("the modelled harbour, which is what runs this round", () => {
it("is deterministic — two people see the same ships", () => {
const a = modelHarbour([PORT], { seed: 115, atMs: 1_000_000 });
const b = modelHarbour([PORT], { seed: 115, atMs: 1_000_000 });
assert.deepEqual(a, b);
const other = modelHarbour([PORT], { seed: 116, atMs: 1_000_000 });
assert.notDeepEqual(other.vessels.map((v) => v.id), []);
});
it("carries no name, no MMSI, no callsign and no destination", () => {
// The store has CSCL INDIAN OCEAN and EVER LOVELY in it right now, and
// hardcoding them would be the fire layer's twenty-two orange marks in a
// nicer costume. Identity arrives with a licence entry or not at all.
for (const vessel of modelHarbour([PORT], { seed: 115 }).vessels) {
for (const forbidden of ["name", "mmsi", "callsign", "destination", "draught", "laden"]) {
assert.equal(forbidden in vessel, false, `a modelled vessel carried ${forbidden}`);
}
}
});
it("says it is modelled, and the panel says so too", () => {
const modelled = modelHarbour([PORT], { seed: 115 });
assert.equal(modelled.source, "modelled");
assert.equal(modelled.intervalSeconds, 900);
const promotion = promoteVessels(modelled, SAN_PEDRO, berthAnchors([PORT]));
assert.match(vesselSummary(promotion), /Modelled/);
assert.match(vesselSummary(promotion), /no names and no MMSIs/);
});
it("volunteers no heading, so the berths have to do the work", () => {
const modelled = modelHarbour([PORT], { seed: 115 });
assert.ok(modelled.vessels.length > 0);
for (const vessel of modelled.vessels) assert.equal(vessel.heading, null);
const promotion = promoteVessels(modelled, SAN_PEDRO, berthAnchors([PORT]));
assert.ok(promotion.alongside > 0, "no modelled hull found its berth");
for (const drawn of promotion.drawn) {
if (!drawn.berthId) continue;
const berth = PORT.berths?.find((b) => b.id === drawn.berthId);
assert.equal(drawn.bearing, berth?.bearing);
}
});
it("puts a handful under way on the channel and the rest alongside", () => {
const promotion = promoteVessels(
modelHarbour([PORT], { seed: 115, underWayPerPort: 3 }),
SAN_PEDRO,
berthAnchors([PORT]),
);
assert.equal(promotion.makingWay, 3);
assert.ok(promotion.alongside >= 2, "the quays came out empty");
// Every moving hull has a course, or the layer could not reckon it and would
// not draw a wake — which is the one thing that reads at board scale.
for (const drawn of promotion.drawn) {
if (drawn.speed > 0) assert.notEqual(drawn.course, null);
}
});
it("draws nothing at all for a board with no ports", () => {
const modelled = modelHarbour(undefined, { seed: 115 });
assert.deepEqual(modelled.vessels, []);
const promotion = promoteVessels(modelled, SAN_PEDRO, []);
assert.deepEqual(promotion.drawn, []);
});
});
// ---- Geometry helper -------------------------------------------------------
/** Metres from a point to the segment `a`-`b`, in the flat local approximation. */
function metresToSegment(
p: { lat: number; lng: number },
a: { lat: number; lng: number },
b: { lat: number; lng: number },
): number {
const scale = Math.cos((a.lat * Math.PI) / 180);
const px = (p.lng - a.lng) * scale;
const py = p.lat - a.lat;
const bx = (b.lng - a.lng) * scale;
const by = b.lat - a.lat;
const denominator = bx * bx + by * by;
const t = denominator > 0 ? Math.max(0, Math.min(1, (px * bx + py * by) / denominator)) : 0;
const dx = px - bx * t;
const dy = py - by * t;
return Math.hypot(dx, dy) * 111_320;
}
+182
View File
@@ -27,10 +27,14 @@ import { createTeraClient } from "../../adapters/http.ts";
import { createDeviceSource, createNullDeviceSource } from "../../devices/adapter.ts";
import { initialDeviceState, type DeviceDeclaration, type DeviceState } from "../../devices/types.ts";
import type {
BirdsBody,
DeviceCommandBody,
DeviceCommandResultBody,
DevicesBody,
HealthBody,
PortsBody,
RadarBody,
VesselsBody,
} from "../../server/wire.ts";
Object.defineProperty(globalThis, "window", {
@@ -128,6 +132,184 @@ describe("the bodies are JSON, and stay JSON", () => {
});
});
describe("the four bodies this build added", () => {
/**
* The empty answer for each one, round-tripped.
*
* Every one of these feeds spends most of its life empty and **that is the
* case that has to be right**: California is under rain a mean 0.596% of the
* time, birds are aloft about ten hours in twenty-four by construction, and
* no deployment has an AIS licence. So the assertions below are all about the
* empty body — that it survives JSON, that it carries a fetch age, and that
* emptiness arrives with a reason rather than as a bare `[]`.
*/
it("round-trips an empty vessels body, with the sampling interval on it", () => {
const body: VesselsBody = {
source: "none",
fetchedAt: new Date(0).toISOString(),
vessels: [],
intervalSeconds: 900,
ttlSeconds: 60,
};
assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
// Not decoration. Upstream listens for thirty seconds every fifteen minutes,
// so a hull under way has moved about five kilometres between two fixes: a
// client may dead-reckon along the reported course, and may never draw a
// point on the chord between two samples.
assert.equal(body.intervalSeconds, 900);
});
it("keeps every AIS sentinel expressible as absence rather than as a number", () => {
// The three sentinels are valid numbers and none of them is ever NULL, so a
// null-check catches nothing and the gate upstream is what strips them. What
// the wire has to provide is somewhere for "absent" to go once it has.
const body: VesselsBody = {
source: "modelled",
fetchedAt: new Date(0).toISOString(),
vessels: [
{
id: "hull-1",
kind: "container",
lat: 33.74,
lon: -118.26,
speed: 0,
course: null,
heading: null,
navStatus: 5,
length: null,
beam: null,
ageSeconds: 120,
},
],
intervalSeconds: 900,
ttlSeconds: 60,
};
assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
const [hull] = body.vessels;
assert.ok(hull);
assert.equal(hull.course, null, "an unknown course must be null, never 360");
assert.equal(hull.heading, null, "an unknown heading must be null, never 511");
// And no identity. Names arrive with a licensed feed or not at all.
for (const forbidden of ["name", "mmsi", "callsign", "destination"]) {
assert.ok(!(forbidden in hull), `WireVessel grew a ${forbidden}`);
}
});
it("dates a port's throughput to a month and never to now", () => {
const body: PortsBody = {
source: "none",
fetchedAt: new Date(0).toISOString(),
throughput: [
{
portId: "USLAX",
asOf: "2026-07",
loadedExport: 111_776,
emptyExport: 348_691,
loadedImport: 499_552,
emptyImport: 446,
},
],
rates: [
{ id: "FBX01", lane: "China / East Asia to North America West Coast", usdPerFeu: 7_491 },
{ id: "FBX02", lane: "North America West Coast to China / East Asia", usdPerFeu: 347 },
],
ttlSeconds: 3_600,
};
assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
const [row] = body.throughput;
assert.ok(row);
assert.match(row.asOf, /^\d{4}-\d{2}$/, "throughput must be dated to its month");
// The headline the card leads with, restated as arithmetic so a transcription
// error in the figures fails here rather than on a page.
const exported = row.loadedExport + row.emptyExport;
assert.equal(exported, 460_467);
assert.ok(row.emptyExport / exported > 0.75, "the empty share is the story");
// No timestamp on a rate. Freightos publishes none, our `observed_at` is our
// own read clock, and a card that renders it as "as of" is lying.
for (const rate of body.rates) {
assert.ok(!("observedAt" in rate), "a freight rate acquired a false timestamp");
}
});
it("lets the radar draw a hole as a hole", () => {
const body: RadarBody = {
source: "none",
fetchedAt: new Date(0).toISOString(),
field: {
minLat: 32.5,
minLng: -124.5,
cellLat: 0.25,
cellLng: 0.25,
rows: 2,
cols: 2,
// Dry, dry, raining, and *unknown* — a cell inside the coverage radius
// of a radar whose RDA is down. `null` and `0` are different claims and
// this is the type that keeps them apart.
dbz: [0, 0, 47.5, null],
observedAt: new Date(0).toISOString(),
wetFraction: 0.25,
stations: 16,
stationsDown: 1,
},
ttlSeconds: 300,
};
const round = JSON.parse(JSON.stringify(body)) as RadarBody;
assert.deepEqual(round, body);
assert.equal(round.field?.dbz[3], null, "an unknown cell survived as anything but null");
assert.equal(round.field?.dbz.length, (round.field?.rows ?? 0) * (round.field?.cols ?? 0));
});
it("never lets an empty sky be a bare empty array", () => {
const body: BirdsBody = {
source: "none",
fetchedAt: new Date(0).toISOString(),
observedAt: null,
counties: [],
statewide: {
crossed: 393_290,
peakAloft: 1_501_193,
peakAt: "2026-08-22T06:20:00Z",
meanAltitude: 726,
heading: "south-east",
},
quiet: {
reason: "daylight",
message:
"Nothing is aloft. BirdCast measures migration only after dark. Last night " +
"393,290 birds crossed California heading south-east, peaking at 1,501,193 " +
"aloft at 23:20 PDT, at a mean 726 metres.",
},
ttlSeconds: 600,
};
assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
// The empty state is the layer for most visitors, so an empty set without a
// reason beside it is the defect rather than the normal case.
assert.equal(body.counties.length, 0);
assert.ok(body.quiet !== null, "an empty sky with no reason is indistinguishable from a dead feed");
// And the statewide headline is the state row, never a sum over counties:
// summing all 58 gives 2,360,086 against an authoritative 393,290.
assert.equal(body.statewide?.crossed, 393_290);
});
it("reads the three new source ids the same defensive way as `devices`", async () => {
// A browser meeting a server one version behind sees `undefined` for all
// three and must conclude the box serves none of them.
const health = {
ok: true,
service: "tera-api",
version: "0.1.0",
uptimeSeconds: 1,
sources: { weather: "none", flights: "sim", satellites: "none", markers: "none", devices: "none" },
auth: { mode: "none", entryUrl: null },
regions: [],
degraded: [],
} as unknown as HealthBody;
assert.equal(health.sources.vessels, undefined);
assert.equal(health.sources.radar, undefined);
assert.equal(health.sources.birds, undefined);
});
});
describe("what the browser learns from /health", () => {
it("reports the device source and the demotions to the interface", async () => {
const access = await resolveAccess(
+62 -1
View File
@@ -16,7 +16,7 @@
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
@@ -102,6 +102,67 @@ test("nothing reachable from the barrel imports three.js", () => {
);
});
/**
* The render layers this build added, named so that adding one cannot quietly
* put three.js on the package surface.
*
* Four new engine modules — the port kit, the vessel layer, the reflectivity
* sheet and the migration field — are *render* layers in the same sense
* `interiors/devices.ts` and `engine/officeExterior.ts` are, and are therefore
* conspicuously absent from `src/index.ts`. Their **gates** are not: a gate is a
* pure function over plain data, it is testable with no GL context, and it is
* where every honest decision about a feed is taken. The asymmetry is the whole
* shape of this repo, and it is written down here rather than left to be
* rediscovered by whichever workstream lands last.
*
* Listed as names rather than inferred, so this test fails loudly with the
* offending path printed rather than by the closure quietly growing.
*/
const RENDER_LAYERS = [
"src/engine/ports.ts",
"src/engine/vessels.ts",
"src/engine/precip.ts",
"src/engine/migration.ts",
];
/** The pure gates behind them. Every one of these may be exported; none may import three. */
const PURE_GATES = [
"src/server/vessels.ts",
"src/server/radar.ts",
"src/server/birds.ts",
];
test("the render layers stay off the package surface", () => {
const reached = new Set([...closure(ENTRY).keys()].map((f) => path.relative(ROOT, f)));
const offenders = RENDER_LAYERS.filter((file) => reached.has(file));
assert.deepEqual(
offenders,
[],
"a render layer reached the barrel. The port kit, the vessels, the radar " +
"sheet and the migration field all build meshes; their gates are the half " +
"that belongs on the public surface.",
);
});
test("the pure gates behind those layers import no renderer", () => {
const offenders: string[] = [];
for (const file of PURE_GATES) {
const full = path.join(ROOT, file);
if (!existsSync(full)) continue;
const source = readFileSync(full, "utf8");
if (/from\s+["']three(?:\/|["'])/.test(source) || /import\s*\(\s*["']three/.test(source)) {
offenders.push(file);
}
}
assert.deepEqual(
offenders,
[],
"a gate imported three.js. Everything a gate asserts must be assertable " +
"without a GL context — the argument fires.ts:16-23 makes, applied to the " +
"three feeds this build added.",
);
});
test("nothing reachable from the barrel takes a bare dependency at all", () => {
const offenders: string[] = [];
for (const [file] of closure(ENTRY)) {
+241
View File
@@ -0,0 +1,241 @@
/**
* The four seams three other workstreams compile against, asserted from both
* ends.
*
* `SceneOptions` grew four factory slots in one commit — `ports`, `vessels`,
* `precip`, `migration` — and each of them is built, ticked, lit and disposed by
* `createScene`. Every one of those five touch points is a line somebody can
* delete without breaking a compile, and the layer would then simply never
* appear: no error, no failing type, just a board with nothing on it, which is
* exactly the picture a quiet day is supposed to produce. That is the failure
* this file exists to catch.
*
* ## Why this reads source rather than calling `createScene`
*
* The same reason `sceneWiring.test.ts` gives and no other: `createScene` awaits
* a terrain `Worker` and takes a live `Stage`, so the only place it runs is a
* browser. Pretending otherwise with a mock `Stage` would test the mock. What is
* asserted here instead is *structural* and is the half a browser test is
* slowest to tell you about — the factory is called, it is called once, and it
* is called only when it was supplied. `scripts/ui-smoke.mjs` is the brace to
* this belt.
*
* The type-level half is real, though, and it is the first block below: an
* object literal is assigned to `SceneOptions` and the layer shapes are
* satisfied by hand-written stubs. If a signature moves, `tsc` fails here.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import type {
MigrationLayerFactory,
PortLayer,
PortLayerFactory,
PrecipLayerFactory,
SceneOptions,
VesselLayerFactory,
} from "../../engine/scene.ts";
import CALIFORNIA from "../../cities/california.ts";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const SCENE = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
const MAIN = readFileSync(path.join(ROOT, "src/main.ts"), "utf8");
const INDEX = readFileSync(path.join(ROOT, "index.html"), "utf8");
// ---- The compile-time half -------------------------------------------------
//
// Nothing here runs anything. Its whole job is to fail `npx tsc --noEmit` the
// day one of these signatures changes under a workstream that was told it would
// not, which is cheaper than three agents discovering it separately.
const group = { name: "stub" } as unknown as PortLayer["group"];
const port: PortLayerFactory = () => ({ group, setLighting() {}, dispose() {} });
const vessels: VesselLayerFactory = () => ({
group,
setVessels() {},
setLighting() {},
tick() {},
dispose() {},
});
const precip: PrecipLayerFactory = () => ({
group,
setField() {},
setLighting() {},
tick() {},
dispose() {},
});
const migration: MigrationLayerFactory = () => ({
group,
setField() {},
setLighting() {},
setSolarElevation() {},
tick() {},
dispose() {},
});
const _options: SceneOptions = {
city: CALIFORNIA,
ports: port,
vessels,
precip,
migration,
};
// Named so an unused-locals rule cannot delete the assertion above.
void _options;
/** Each factory, and the setter on the handle that must reach its layer. */
const SEAMS: readonly (readonly [string, string, string])[] = [
["ports", "portLayer", ""],
["vessels", "vesselLayer", "setVessels"],
["precip", "precipLayer", "setPrecip"],
["migration", "migrationLayer", "setMigration"],
];
test("SceneOptions carries all four factory slots", () => {
for (const [slot] of SEAMS) {
assert.match(
SCENE,
new RegExp(`\\n\\s{2}${slot}\\?:\\s*\\w+LayerFactory;`),
`SceneOptions lost the ${slot} slot`,
);
}
});
test("createScene calls each factory exactly once, and only when it was supplied", () => {
for (const [slot] of SEAMS) {
const calls = [...SCENE.matchAll(new RegExp(`options\\.${slot}\\(`, "g"))].length;
assert.equal(
calls,
1,
`createScene calls options.${slot}() ${calls} times. Once, mirroring the ` +
"fire layer: a layer built twice is two of everything on one board.",
);
// The guard and the call in one expression, so absence cannot allocate.
// `options.fires ? options.fires(world, { span: boardSpan }) : null` is the
// shape being held to, and the span is what lets a layer size itself from
// the board rather than from a constant.
assert.match(
SCENE,
new RegExp(
`options\\.${slot}\\s*\\n?\\s*\\?\\s*options\\.${slot}\\(world,\\s*\\{\\s*span:\\s*boardSpan\\s*\\}\\)`,
),
`options.${slot} must be built exactly the way options.fires is: guarded, ` +
"handed the world under construction, and sized by the board span.",
);
}
});
test("a layer that was not supplied contributes nothing at all", () => {
for (const [, local] of SEAMS) {
// Declared `| null` and added to the scene only inside the truthy branch.
// The empty state for every one of these feeds is the common case — a mean
// 0.596% of California is under rain and birds are absent fourteen hours a
// day — so "not visited at all" is the requirement, not "draws nothing".
assert.match(
SCENE,
new RegExp(`const ${local}: \\w+Layer \\| null = options\\.`),
`${local} must be nullable: absent has to cost no geometry and no draw call`,
);
assert.match(
SCENE,
new RegExp(`if \\(${local}\\) \\{[\\s\\S]{0,200}?scene\\.add\\(${local}\\.group\\);`),
`${local} is added to the scene outside its own null guard`,
);
}
});
test("every supplied layer is lit, ticked and disposed with the board", () => {
for (const [slot, local, setter] of SEAMS) {
assert.ok(
SCENE.includes(`${local}?.setLighting(state)`),
`${local} never receives a rig. CONTRACT §4: Atmosphere computes, a scene ` +
"applies, nothing writes back — a layer left out of setLighting is a " +
"layer lit for whatever hour it happened to be built at.",
);
assert.ok(
SCENE.includes(`${local}?.dispose()`),
`${local} is never disposed; a board switch would orphan its buffers`,
);
if (slot !== "ports") {
assert.ok(
SCENE.includes(`${local}?.tick(dt)`),
`${local} declares tick() and never gets one`,
);
}
if (setter !== "") {
assert.match(
SCENE,
new RegExp(`${setter}:\\s*\\([\\w\\s,]*\\)\\s*=>\\s*${local}\\?\\.`),
`SceneHandle.${setter} must reach ${local} and must be a no-op without it`,
);
}
}
});
test("the port kit is static geometry and says so by having no tick", () => {
// A deliberate asymmetry worth asserting rather than leaving to be noticed: a
// quay does not move, so `PortLayer` has no `tick` and `createScene` must not
// invent one. Vessels move, rain moves, birds move; stone does not.
assert.ok(
!SCENE.includes("portLayer?.tick("),
"the port kit acquired a per-frame tick. Nothing in it moves.",
);
});
// ---- The other end of the seam --------------------------------------------
//
// `scene.ts` can be perfect and the product still draw nothing, because the
// factories are optional and `main.ts` is the only thing that supplies them. A
// deleted line there is a silent, compiling, type-correct board with no ships on
// it — the same failure this file exists to catch, one file further out.
test("main.ts supplies every factory slot the scene declares", () => {
for (const [slot] of SEAMS) {
assert.match(
MAIN,
new RegExp(`\\b${slot}:\\s`),
`main.ts never passes SceneOptions.${slot}, so the layer is never built`,
);
}
});
test("main.ts feeds the three layers that take a feed", () => {
// Each setter, and the gate whose answer it must be handed. The pairing is the
// assertion: a `setVessels` fed from anything but a promotion would be a hull
// that skipped the sentinel strip, and a `setPrecip` fed from a body rather
// than from `promoteRadar(...).field` would be a raster nobody clipped.
const FEEDS: readonly (readonly [string, string])[] = [
["setVessels", "promoteVessels("],
["setPrecip", "promoteRadar("],
["setMigration", "promoteBirds("],
];
for (const [setter, gate] of FEEDS) {
assert.ok(MAIN.includes(setter + "("), `main.ts never calls ${setter}`);
assert.ok(MAIN.includes(gate), `main.ts never calls ${gate}), so ${setter} has no source`);
}
});
test("every one of the three feeds has a sentence, and a place to print it", () => {
/**
* The quiet day is the common day — 0.47% of California under rain, nothing
* aloft for fourteen hours, and no AIS licence read — so an empty layer with
* no caption beside it is what a stranger actually sees. Each gate owns a
* sentence that is never blank; these are the three elements they are written
* into, and a panel section that lost its paragraph would take the explanation
* with it and leave a board that looks broken.
*/
for (const id of ["sea-note", "radar-note", "birds-note"]) {
assert.ok(INDEX.includes(`id="${id}"`), `index.html has no #${id} to write into`);
assert.ok(MAIN.includes(`"${id}"`), `main.ts never writes #${id}`);
}
for (const gate of ["vesselSummary(", "promoteRadar(", "promoteBirds("]) {
assert.ok(MAIN.includes(gate), `main.ts never asks ${gate}) for its sentence`);
}
});
+52
View File
@@ -644,6 +644,58 @@ test("the city scene is handed the same environment rig the office is", () => {
);
});
test("a camera step never reaches the light, and so never reaches the environment", () => {
/*
* The seam this asserts, and why it is worth a source test.
*
* `Scene.setLighting` ends in `options.environment?.apply(scene, state, "city")`,
* and that rig rebuilds its PMREM cubemap whenever the rig's *colours* move.
* The camera's orbit `change` handler fires a few dozen times in one drag. So
* long as those two are joined, every feature that makes a colour depend on
* where the camera is standing is a frame-rate regression waiting to be
* written, and the tempting fix — coarsening `environmentKey` — hides the
* instance and leaves the mechanism.
*
* They are separate now: the camera moves two fog distances through
* `setAerialFog`, which cannot carry a colour because `AerialFog` has no
* colour in it. This test fails if somebody merges them back.
*/
const scene = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
const from = MAIN.indexOf("function applyCameraFog");
assert.ok(from > 0, "the camera's fog path has been renamed or deleted");
const body = MAIN.slice(from, MAIN.indexOf("\n}", from));
assert.ok(
!/setLighting\(/.test(body),
"the camera path is applying a whole rig again. `setLighting` fans out across " +
"six layers and the PMREM environment; a camera step must move fog distances " +
"and nothing else.",
);
assert.ok(
/city\.setAerialFog\(\s*atmosphere\.aerial\(/.test(body),
"the camera path must ask `Atmosphere` for the fog — CONTRACT §4 keeps it the " +
"sole light owner, so the app may not work a distance out for itself",
);
assert.ok(
/controls\.addEventListener\("change", onCameraMoved\)/.test(MAIN) &&
/applyCameraFog\(\)/.test(MAIN),
"the orbit `change` event must still drive the fog; without it a chase camera " +
"on the state board flies through the haze the whole feature exists to draw",
);
const setter = scene.indexOf("setAerialFog: (");
assert.ok(setter > 0, "SceneHandle.setAerialFog has been renamed or deleted");
const setterBody = scene.slice(setter, scene.indexOf("\n },", setter));
for (const forbidden of ["environment", "applyLighting", "setLighting"]) {
assert.ok(
!setterBody.includes(forbidden),
`setAerialFog reached \`${forbidden}\`. It may touch the three things that draw ` +
"fog and nothing else — the whole value of the split is that a camera step " +
"cannot re-enter the lighting path.",
);
}
});
test("the aircraft pick reaches the card, and the card reaches an anonymous visitor", () => {
const scene = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
assert.ok(
+33 -5
View File
@@ -121,20 +121,48 @@ describe("California board — geometry that only a picture used to catch", () =
});
it("stands the ranges up far enough to be seen from the state camera", () => {
/**
* Relief measured against the board **span**, which is a change from the
* board's north-south extent and is worth being explicit about, because
* lowering a threshold and changing its denominator in the same commit is
* exactly what a weakened test looks like.
*
* The span is the number `scene.ts` actually frames on — `boardSpan` is
* `max(width, height)` and every camera limit, the fog and `chapterFraming`
* divide by it — so it is the denominator that decides how big a mountain
* looks. On the board that stopped at 38.05 the two differed a lot (428
* across against 319 tall) and this assertion was quietly measuring against
* the smaller one, which flattered it by a third. On the whole state they
* are both 554 and the distinction stops mattering; it is corrected here so
* that the next board to change shape is measured against the right thing.
*
* The bar is 7%, and it is calibrated against the two boards that already
* look right rather than chosen. Measured today:
*
* Southern California 29.4 u of 393 = 7.5%
* the whole state 41.2 u of 554 = 7.4%
* the Bay Area 48.7 u of 1003 = 4.9%
*
* The state board sits on Southern California's number, which is the
* calibration that matters — the two are meant to read as one landscape at
* two zooms. It got there by the exaggeration going 13 to 15 when the bounds
* grew, not by this number moving to meet it: at 13 the extended board is
* 6.4% and this assertion fails, which is the failure doing its job.
*/
const world = builtWorld(CALIFORNIA_CITY);
const { bounds } = CALIFORNIA_CITY;
const boardUnits = (bounds.maxLat - bounds.minLat) * CALIFORNIA_CITY.latScale;
const [westX, northZ] = world.project(bounds.maxLat, bounds.minLng);
const [eastX, southZ] = world.project(bounds.minLat, bounds.maxLng);
const boardUnits = Math.max(Math.abs(eastX - westX), Math.abs(southZ - northZ));
let peak = 0;
for (const metres of world.lattice().height) if (metres > peak) peak = metres;
const peakUnits = world.metres(peak);
assert.ok(peak > 4_000, `the highest ground is only ${Math.round(peak)} m`);
// 8% of the board's own height. Southern California's San Gabriels clear
// this comfortably; the old 2.25 exaggeration put this board at 0.6%.
assert.ok(
peakUnits / boardUnits > 0.08,
`relief is ${((peakUnits / boardUnits) * 100).toFixed(1)}% of the board — flat`,
peakUnits / boardUnits > 0.07,
`relief is ${((peakUnits / boardUnits) * 100).toFixed(1)}% of the board span — flat`,
);
});
+183
View File
@@ -0,0 +1,183 @@
/**
* The state board is the whole state, and the cell that paid for it.
*
* This board used to stop at 38.05 N. The minimap beside it draws the whole of
* California from the same pack, so a single frame 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. That is the
* defect this file guards, and the reason it is a pack test and not a picture
* is that a picture is what it took to notice.
*
* ## The three claims, and why each one needs an assertion
*
* 1. **The bounds reach the corners.** Easy to state, easy to half-do: an
* extension that moved `maxLat` and forgot `minLng` gives a state with an
* Oregon border and no Cape Mendocino, which reads as a different place.
* 2. **The land was authored, not merely permitted.** Growing `bounds` costs
* nothing and draws nothing — the polygon decides where the ground is, and
* a board whose bounds reach 42 N over a coastline that stops at 38.13 is a
* board with two hundred kilometres of open ocean where the North Coast is.
* Twenty vertices north of 40 N is the cheapest arithmetic statement of
* "somebody traced this".
* 3. **The cell was coarsened to pay for it.** This is the load-bearing one.
* At the old 0.022 x 0.027 the extended board takes the lattice from 84,924
* points to 168,813 — 2.02x — and the terrain with it, against a mobile
* budget with 70,000 triangles spare and two more layers landing on the same
* board in the same round. The 1.42x coarsening is the entire reason the
* extension fits, and it is one edit away from being silently reverted by
* somebody who thinks a finer lattice is always better.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import CALIFORNIA, { CORRIDOR_LAND } from "../../cities/california.ts";
import { World, computeField } from "../../engine/world.ts";
/** Metres in a degree of latitude. The same constant `World` uses. */
const M_PER_DEGREE = 111_320;
describe("the board reaches the whole state", () => {
it("contains the corners the old bounds cut off", () => {
const { bounds } = CALIFORNIA;
// Crescent City is at 41.75 N; the Oregon line is at 42.00. A board that
// stops short of 41.9 has cut off Del Norte County and the redwoods.
assert.ok(bounds.maxLat > 41.9, `maxLat is ${bounds.maxLat}`);
// Cape Mendocino is the westernmost ground in California at -124.41, and it
// is the corner that makes the northern silhouette read as this state.
assert.ok(bounds.minLng < -124.3, `minLng is ${bounds.minLng}`);
// And the three edges that were already right stayed right.
assert.ok(bounds.minLat <= 32.55, `minLat is ${bounds.minLat}`);
assert.ok(bounds.maxLng >= -114.0, `maxLng is ${bounds.maxLng}`);
});
it("centres scene space on the middle of the bounds", () => {
// Everything sized from the origin — the satellite dome, the shadow box, the
// star field — is centred with it, so a centre left where it was when the
// board was smaller makes all three too small on the far side by the offset.
const { bounds, center } = CALIFORNIA;
const midLat = (bounds.minLat + bounds.maxLat) / 2;
const midLng = (bounds.minLng + bounds.maxLng) / 2;
assert.ok(Math.abs(center.lat - midLat) < 0.2, `centre is ${center.lat}, middle is ${midLat}`);
assert.ok(Math.abs(center.lng - midLng) < 0.4, `centre is ${center.lng}, middle is ${midLng}`);
});
it("has a traced North Coast rather than a wider empty ocean", () => {
const north = CORRIDOR_LAND.filter(([lat]) => lat > 40.0);
assert.ok(
north.length >= 20,
`only ${north.length} vertices north of 40 N — the added land is a shelf, not a coast`,
);
// The cape itself, because it is the one vertex whose absence changes the
// silhouette rather than the detail: without it the North Coast is a
// straight line from Trinidad to Shelter Cove.
assert.ok(
CORRIDOR_LAND.some(([lat, lng]) => lat > 40.3 && lat < 40.6 && lng < -124.35),
"Cape Mendocino is not in the trace",
);
});
it("puts real ground under the new land, north and north-east", () => {
const world = new World(CALIFORNIA);
for (const [name, lat, lng] of [
["Crescent City", 41.75, -124.18],
["Eureka", 40.8, -124.16],
["Redding", 40.58, -122.39],
["Sacramento", 38.58, -121.49],
["Mount Shasta", 41.41, -122.19],
["Alturas, in Modoc", 41.49, -120.54],
] as [string, number, number][]) {
assert.equal(world.isLand(lat, lng), true, `${name} is not on the board`);
}
// And the two edges that are new: Oregon is not California, and neither is
// the Great Basin east of the 120th meridian.
assert.equal(world.isLand(42.3, -122.5), false, "Oregon is on the board");
assert.equal(world.isLand(41.0, -119.5), false, "Nevada is on the board");
});
it("stands Shasta and Lassen up as the two things that make the north the north", () => {
const world = new World(CALIFORNIA);
world.lattice();
assert.ok(
world.elevationAt(41.409, -122.194) > 3_800,
`Shasta is only ${Math.round(world.elevationAt(41.409, -122.194))} m`,
);
assert.ok(
world.elevationAt(40.488, -121.505) > 2_600,
`Lassen is only ${Math.round(world.elevationAt(40.488, -121.505))} m`,
);
// The gap between them is as much of the picture as the peaks. Hat Creek
// country sits around 1,000 m and must not be filled in by either skirt.
assert.ok(world.elevationAt(40.95, -121.5) < 2_200, "the Cascade gap has been filled in");
// And the Sacramento Valley is a floor, not a range: farmland at tens of
// metres, which is what makes the ranges either side of it read as ranges.
for (const [lat, lng] of [[39.5, -121.95], [39.0, -121.75], [40.2, -122.1]] as const) {
const floor = world.elevationAt(lat, lng);
assert.ok(floor < 300, `the Sacramento Valley at ${lat}N is ${Math.round(floor)} m`);
assert.ok(floor > 3, `the Sacramento Valley at ${lat}N is at the beach colour`);
}
});
});
describe("the cell is what paid for the extension", () => {
it("holds the ground cell between 3.3 and 3.7 kilometres", () => {
const metres = CALIFORNIA.cellLat * M_PER_DEGREE;
assert.ok(
metres > 3_300 && metres < 3_700,
`the cell is ${Math.round(metres)} m. Below 3,300 the lattice doubles and the ` +
"terrain goes through the mobile budget; above 3,700 the Sierra stops " +
"reading as a range.",
);
// Longitude is squashed by cos(centre latitude), so an equal-area cell has to
// be 1/cos as wide as it is tall. Both axes were multiplied by the same 1.42.
const squash = Math.cos((CALIFORNIA.center.lat * Math.PI) / 180);
const ratio = CALIFORNIA.cellLng / CALIFORNIA.cellLat;
assert.ok(
Math.abs(ratio - 1 / squash) < 0.06,
`the cell is ${ratio.toFixed(3)} as wide as it is tall; 1/cos(${CALIFORNIA.center.lat}) ` +
`is ${(1 / squash).toFixed(3)}, so the ground cell is not square`,
);
});
it("holds the lattice where it was on a board a third bigger", () => {
const field = computeField(new World(CALIFORNIA));
const points = (field.latSteps + 1) * (field.lngSteps + 1);
// 84,924 was the measured figure on the board that stopped at 38.05, and the
// whole argument of the coarsening is that this number does not move. A 5%
// band, because it is a rounding of two axis lengths and not a target.
assert.ok(
points > 80_000 && points < 89_200,
`the lattice is ${points} points against 84,924 on the smaller board`,
);
});
it("keeps the cell finer in the frame than the board two revisions ago", () => {
/**
* The claim the coarsening rests on, asserted rather than argued.
*
* What the eye sees is not the cell in metres — it is the cell as a fraction
* of the board, because the camera retreats to frame whatever it is given.
* The corridor board before it grew east was 0.020° on 284 units, which is
* 0.0041 of a span; the board that stopped at 38.05 was 0.022° on 428, or
* 0.0030. This board must land between them, which means the cell got 42%
* coarser on the earth and *finer* in the frame than the board two revisions
* ago.
*/
const world = new World(CALIFORNIA);
const [westX, northZ] = world.project(CALIFORNIA.bounds.maxLat, CALIFORNIA.bounds.minLng);
const [eastX, southZ] = world.project(CALIFORNIA.bounds.minLat, CALIFORNIA.bounds.maxLng);
const span = Math.max(Math.abs(eastX - westX), Math.abs(southZ - northZ));
const cellUnits = CALIFORNIA.cellLat * CALIFORNIA.latScale;
const fraction = cellUnits / span;
assert.ok(
fraction < 0.0041,
`the cell is ${fraction.toFixed(4)} of the board span, coarser in frame than ` +
"the 0.0041 of the board before the state's eastern edge arrived",
);
assert.ok(
fraction > 0.0025,
`the cell is ${fraction.toFixed(4)} of the board span, which is finer than the ` +
"board has ever needed and is being paid for in terrain triangles",
);
});
});
+378
View File
@@ -0,0 +1,378 @@
/**
* San Pedro Bay as the SoCal pack authors it — the assertions that keep a quay
* on land and a breakwater the right length.
*
* Every coordinate in `socal.ts` is hand-traced by house rule (ARCHITECTURE
* §3.2), which means it is eyeball-accurate and there is no authority to check
* it against. What there *is* is a set of relationships that have to hold, and a
* hand-traced number that breaks one of them is a typo rather than a judgement
* call. This file is those relationships:
*
* - **A quay is on land.** Every vertex of every quay polygon lies inside a
* landmass. This is the assertion the whole re-trace of Terminal Island exists
* to satisfy: with the old six-point hexagon a quay could be on the water or
* buried in the fill, and nothing would have said so.
* - **A berth is on its quay.** Within 200 m of a quay edge, which at 391 m to
* the scene unit is half a scene unit — close enough that a hull placed there
* is alongside rather than parked in the yard or moored in the fairway.
* - **The breakwater is thirteen kilometres.** Between twelve and fifteen, and
* it comes out 13.06 against a real federal breakwater of 13.07.
* - **Nothing is placed from `ports.sqlite`.** All seven rows of that table sit
* on an exact arc-minute grid; a coordinate here that lands on one is a
* coordinate somebody copied out of it, and it is up to 1,852 m from the water
* it claims. This is the cheapest possible guard against the single most
* likely way this data goes wrong later.
* - **The bridges still land.** Re-tracing Terminal Island moved every shoreline
* the Vincent Thomas and the Long Beach Gateway touch, and a bridge whose
* abutment ends up over open water fails silently — it just looks slightly
* wrong from one angle.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { metresBetween, yardCorners } from "../../engine/ports.ts";
import SOCAL_CITY, {
LONG_BEACH,
LOS_ANGELES,
PORTS,
TERMINAL_ISLAND,
VINCENT_THOMAS,
LONG_BEACH_GATEWAY,
} from "../../cities/socal.ts";
import { World } from "../../engine/world.ts";
import type { LatLng, Quay } from "../../engine/types.ts";
const world = new World(SOCAL_CITY);
function onLand(point: LatLng): boolean {
return world.pointInAny(point[0], point[1], SOCAL_CITY.landmasses);
}
/** Shortest distance from a point to a polygon's boundary, in metres. */
function metresToEdge(point: LatLng, polygon: readonly LatLng[]): number {
let best = Infinity;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const from = polygon[j];
const to = polygon[i];
if (!from || !to) continue;
const length = metresBetween(from, to);
if (length === 0) {
best = Math.min(best, metresBetween(point, from));
continue;
}
// Project onto the segment in a local metres frame; at this size the earth
// is flat enough that the error is centimetres.
const scaleLng = Math.cos((point[0] * Math.PI) / 180);
const ax = (from[1] - point[1]) * scaleLng;
const ay = from[0] - point[0];
const bx = (to[1] - point[1]) * scaleLng;
const by = to[0] - point[0];
const dx = bx - ax;
const dy = by - ay;
const square = dx * dx + dy * dy;
const t = square === 0 ? 0 : Math.max(0, Math.min(1, -(ax * dx + ay * dy) / square));
const nearest = Math.hypot(ax + t * dx, ay + t * dy);
best = Math.min(best, nearest * 111_320);
}
return best;
}
const quays: { port: string; quay: Quay }[] = PORTS.flatMap((port) =>
(port.quays ?? []).map((quay) => ({ port: port.id, quay })),
);
describe("the pack declares two ports and the engine can find them", () => {
it("hangs them off the city", () => {
assert.equal(SOCAL_CITY.ports, PORTS);
assert.deepEqual(
PORTS.map((port) => port.id),
["USLAX", "USLGB"],
);
});
it("keeps every port record JSON-serialisable", () => {
// A pack is posted to the terrain Worker as a structured clone. One method
// on one port record and the whole board stops booting.
assert.doesNotThrow(() => structuredClone(PORTS));
assert.deepEqual(JSON.parse(JSON.stringify(PORTS)), JSON.parse(JSON.stringify(PORTS)));
});
});
describe("nothing is placed from ports.sqlite", () => {
it("puts no port anchor on an exact arc-minute", () => {
// Every row of the upstream table has lat*60 and lon*60 whole. One arc-minute
// here is 1,852 m of latitude — 4.7 scene units — from the water.
for (const port of PORTS) {
const latMinutes = port.lat * 60;
const lngMinutes = port.lng * 60;
const onGrid =
Math.abs(latMinutes - Math.round(latMinutes)) < 1e-6 &&
Math.abs(lngMinutes - Math.round(lngMinutes)) < 1e-6;
assert.equal(onGrid, false, `${port.id} sits on the arc-minute grid — that is a WPI row`);
}
});
it("puts no quay vertex, berth or crane rail on an exact arc-minute either", () => {
const suspects: LatLng[] = [
...quays.flatMap(({ quay }) => quay.polygon),
...PORTS.flatMap((port) => (port.berths ?? []).map((berth): LatLng => [berth.lat, berth.lng])),
...PORTS.flatMap((port) => (port.cranes ?? []).flatMap((crane) => [crane.from, crane.to])),
];
assert.ok(suspects.length > 30);
for (const [lat, lng] of suspects) {
const onGrid =
Math.abs(lat * 60 - Math.round(lat * 60)) < 1e-6 &&
Math.abs(lng * 60 - Math.round(lng * 60)) < 1e-6;
assert.equal(onGrid, false, `${lat}, ${lng} is on the arc-minute grid`);
}
});
});
describe("quays are on land and berths are on quays", () => {
it("puts every quay vertex inside a landmass", () => {
assert.ok(quays.length >= 6);
for (const { port, quay } of quays) {
for (const vertex of quay.polygon) {
assert.ok(
onLand(vertex),
`${port}/${quay.id} vertex ${vertex.join(", ")} is on open water`,
);
}
}
});
it("puts every berth anchor within 200 m of its quay", () => {
const byId = new Map(quays.map(({ quay }) => [quay.id, quay]));
let checked = 0;
for (const port of PORTS) {
for (const berth of port.berths ?? []) {
const quay = berth.quayId ? byId.get(berth.quayId) : undefined;
assert.ok(quay, `${berth.id} names quay ${berth.quayId}, which does not exist`);
const distance = metresToEdge([berth.lat, berth.lng], quay.polygon);
assert.ok(distance < 200, `${berth.id} is ${Math.round(distance)} m from its quay`);
checked += 1;
}
}
assert.equal(checked, 15, "San Pedro Bay is authored with fifteen berths");
});
it("puts every berth just off the wall rather than on top of it", () => {
// A hull whose anchor is inside the quay polygon is a hull parked in the
// yard. The berth is the water beside the wall, not the wall.
for (const port of PORTS) {
for (const berth of port.berths ?? []) {
assert.equal(
onLand([berth.lat, berth.lng]),
false,
`${berth.id} is inside the landmass — it should be alongside, not ashore`,
);
}
}
});
it("puts every yard corner on land", () => {
for (const port of PORTS) {
for (const yard of port.yards ?? []) {
for (const corner of yardCorners(yard)) {
assert.ok(
onLand(corner),
`${port.id}/${yard.id} corner ${corner.map((n) => n.toFixed(4)).join(", ")} is on water`,
);
}
}
}
});
it("puts every crane rail on the quay it serves", () => {
for (const port of PORTS) {
for (const crane of port.cranes ?? []) {
for (const end of [crane.from, crane.to]) {
assert.ok(onLand(end), `${port.id}/${crane.id} rail end ${end.join(", ")} is on water`);
}
}
}
});
});
describe("the breakwater", () => {
it("is between twelve and fifteen kilometres, in three arms", () => {
const arms = LOS_ANGELES.breakwater ?? [];
assert.equal(arms.length, 3, "San Pedro, Middle and Long Beach");
let total = 0;
for (const arm of arms) {
for (let i = 1; i < arm.length; i += 1) total += metresBetween(arm[i - 1]!, arm[i]!);
}
assert.ok(total > 12_000 && total < 15_000, `${Math.round(total)} m`);
});
it("leaves Angels Gate and Queens Gate open", () => {
const arms = LOS_ANGELES.breakwater ?? [];
const angels = metresBetween(arms[0]!.at(-1)!, arms[1]![0]!);
const queens = metresBetween(arms[1]!.at(-1)!, arms[2]![0]!);
// Both real gates are between five hundred and a thousand metres wide, and a
// harbour whose arms meet is a lagoon.
assert.ok(angels > 400 && angels < 1_200, `Angels Gate ${Math.round(angels)} m`);
assert.ok(queens > 400 && queens < 1_200, `Queens Gate ${Math.round(queens)} m`);
});
it("lies in open water for its whole length", () => {
for (const arm of LOS_ANGELES.breakwater ?? []) {
for (const point of arm) {
assert.equal(onLand(point), false, `breakwater point ${point.join(", ")} is inland`);
}
}
});
it("belongs to the coastal-breakwater harbour and is drawn once", () => {
// `harbor_type` is CB for both San Pedro ports and CN for Oakland. The arms
// are one federal structure: declaring them on both would double the
// geometry for an identical picture, and giving them to a CN harbour would
// be inventing the largest object on its waterfront.
for (const port of PORTS) {
if (port.breakwater) assert.equal(port.harborType, "CB", `${port.id} is not a CB harbour`);
}
assert.equal(LONG_BEACH.breakwater, undefined);
});
});
describe("the dredged channels stay in the water", () => {
it("keeps every channel vertex off both landmasses", () => {
for (const port of PORTS) {
for (const point of port.channel ?? []) {
assert.equal(onLand(point), false, `${port.id} channel point ${point.join(", ")} is inland`);
}
}
});
});
describe("Terminal Island still carries the two bridges", () => {
const island = TERMINAL_ISLAND;
it("lands the Vincent Thomas on the island and San Pedro on the mainland", () => {
const path = VINCENT_THOMAS.path;
const mainland = path[0]!;
const islandEnd = path.at(-1)!;
assert.equal(world.pointInPolygon(islandEnd[0], islandEnd[1], island), true);
assert.equal(world.pointInPolygon(mainland[0], mainland[1], island), false);
assert.equal(onLand(mainland), true);
});
it("lands the Long Beach Gateway on the island and Long Beach on the mainland", () => {
const path = LONG_BEACH_GATEWAY.path;
const islandEnd = path[0]!;
const mainland = path.at(-1)!;
assert.equal(world.pointInPolygon(islandEnd[0], islandEnd[1], island), true);
assert.equal(world.pointInPolygon(mainland[0], mainland[1], island), false);
});
it("carries the comb of slips rather than a hexagon", () => {
// The point of the re-trace. Six points cannot express a basin; this outline
// has the West Basin, the East Basin, Fish Harbor and the Pier 400 causeway
// in it, and every one of them shows up as a reversal in the north-south
// walk along the north shore.
assert.ok(island.length >= 24, `${island.length} points`);
const north = island.filter(([lat]) => lat > 33.755);
let reversals = 0;
for (let i = 2; i < north.length; i += 1) {
const a = north[i - 2]![0];
const b = north[i - 1]![0];
const c = north[i]![0];
if (Math.sign(b - a) !== Math.sign(c - b)) reversals += 1;
}
assert.ok(reversals >= 3, `the north shore has ${reversals} basin walls cut into it`);
});
it("does not overlap the mainland", () => {
// The Main Channel and the Back Channel are the two pieces of water this
// board cannot afford to lose: an island fused to the shore has no harbour
// in it at all.
const mainland = SOCAL_CITY.landmasses[0]!;
for (const point of island) {
assert.equal(
world.pointInPolygon(point[0], point[1], mainland),
false,
`${point.join(", ")} is inside the mainland`,
);
}
});
it("keeps the port off the district lattice", () => {
// The Harbour chapter's whole failure was that Terminal Island sat inside
// the San Pedro and Long Beach district polygons, so the busiest container
// terminal in the hemisphere came out as generic industrial blocks. No
// district may claim a quay.
for (const { port, quay } of quays) {
for (const vertex of quay.polygon) {
for (const district of SOCAL_CITY.districts) {
assert.equal(
world.pointInPolygon(vertex[0], vertex[1], district.polygon),
false,
`${port}/${quay.id} is inside district ${district.id}; blocks.ts will build on it`,
);
}
}
}
});
});
describe("the empty-box figures are the ones that were measured", () => {
it("leads Los Angeles with July 2026 and says which month it is", () => {
const throughput = LOS_ANGELES.throughput;
assert.ok(throughput);
assert.equal(throughput.asOf, "2026-07");
const exported = throughput.loadedExport + throughput.emptyExport;
assert.equal(exported, 460_467);
assert.equal(throughput.emptyExport, 348_691);
const share = throughput.emptyExport / exported;
assert.ok(Math.abs(share - 0.757) < 0.001, `${(share * 100).toFixed(1)}%`);
});
it("draws that share in the yards rather than only writing it in a caption", () => {
for (const yard of LOS_ANGELES.yards ?? []) {
// The rail yard is the deliberate exception; see the next assertion.
if (yard.id === "rail-yard") continue;
assert.equal(yard.emptyShare, 0.757);
}
for (const yard of LONG_BEACH.yards ?? []) assert.equal(yard.emptyShare, 0.765);
});
it("leaves the one yard nobody counts without a share, rather than guessing one", () => {
// `emptyShare` absent means unknown, and `yardAtlas` paints an unknown yard
// in one flat colour. That difference is visible on the board, which is the
// point: a measured number and an unmeasured one must not look alike.
const rail = (LOS_ANGELES.yards ?? []).find((yard) => yard.id === "rail-yard");
assert.ok(rail);
assert.equal(rail.emptyShare, undefined);
assert.equal("emptyShare" in rail, false);
});
it("carries the freight pair that explains it, and no timestamp on it", () => {
const rates = LOS_ANGELES.rates ?? [];
assert.deepEqual(
rates.map((rate) => [rate.id, rate.usdPerFeu]),
[
["FBX01", 7_491],
["FBX02", 347],
],
);
// `observed_at` upstream is our own read clock; Freightos publishes none.
for (const rate of rates) assert.equal("asOf" in rate, false);
});
it("gives Long Beach no half-written throughput record", () => {
// Its export split is known — 341,806 empty against 104,843 loaded — and its
// import halves were never read. `PortThroughput` requires all four, and two
// real numbers beside two invented ones is the failure the fire layer nearly
// shipped. No record beats half a record.
assert.equal(LONG_BEACH.throughput, undefined);
});
it("puts the split in the Harbour chapter, which is the card the page shows", () => {
const chapter = SOCAL_CITY.chapters.find((one) => one.id === "harbour");
assert.ok(chapter);
assert.match(chapter.description, /348,691 of 460,467/);
assert.match(chapter.description, /\$7,491/);
assert.match(chapter.description, /\$347/);
});
});
+457
View File
@@ -0,0 +1,457 @@
/**
* Aerial perspective, and the guard that stops it moving San Francisco.
*
* The state board has no haze in it. Every distance in this engine is a
* fraction of `boardSpan`, and on a board 1,063 km across the clear-day fog
* plane lands 1,200 km out — so nothing on it is ever in front of anything
* else. Converted to metres the three boards disagree by a factor of eight and
* San Francisco is the one that is physically right: 86 km at 94.34 m to the
* unit.
*
* ## The change that was proposed, and why it is not the change that landed
*
* The obvious fix is to state fog in physical metres and stop. It does not
* survive the arithmetic and this file is where that is written down as a test
* rather than as a comment. 86 km is 0.91 spans on the Bay Area board and
* **0.085 spans on the extended state board**, where the camera orbits out to
* 1,108 units. A literal metre fog puts California behind a wall 47 units from
* the lens and there is no pose on that board from which the state is visible.
*
* What landed instead is a fraction: how much of the reach a board was authored
* for the air at this altitude actually supports. It saturates at 1 at any
* whole-board pose, which is what leaves the Bay Area untouched **by
* construction rather than by measurement** — and the first test below is that
* claim, made structural, because "we checked and it did not move" is a
* property that decays the moment somebody edits the curve.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
aerialFog,
aerialReach,
createAtmosphere,
observe,
PACIFIC_MARINE_LAYER,
} from "../../engine/atmosphere.ts";
import type { AerialView, Environment } from "../../engine/atmosphere.ts";
import type { LightingState } from "../../engine/types.ts";
import CALIFORNIA from "../../cities/california.ts";
import SF from "../../cities/sf.ts";
import SOCAL from "../../cities/socal.ts";
import type { City } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
/**
* The board span and the exchange rate, from the pack itself.
*
* `boardSpan` is `max(width, height)` in scene units, computed exactly the way
* `createScene` computes it — projected through the same `World`, so a change to
* the projection cannot make this file agree with a scene that has moved on.
*/
function boardOf(city: City): { span: number; metresPerUnit: number } {
const world = new World(city);
const [westX, northZ] = world.project(city.bounds.maxLat, city.bounds.minLng);
const [eastX, southZ] = world.project(city.bounds.minLat, city.bounds.maxLng);
return {
span: Math.max(Math.abs(eastX - westX), Math.abs(southZ - northZ)),
metresPerUnit: world.metresPerUnit,
};
}
/** `cityDaylight`'s clear pair: the ceiling this function may never exceed. */
function ceilingOf(span: number): { near: number; far: number } {
return { near: span * 0.91, far: span * 2 };
}
/**
* The pair a **deployed** board actually gets, which is not `cityDaylight`'s.
*
* `main.ts` hands `createAtmosphere` its own wider clear pair — `span * 1.15`
* and `span * 3.9` — because `scene.ts` lets the orbit reach two board spans so
* a visitor can get above the satellite dome, and at the narrower pair the board
* sat at half fog from that pose. Asserting only against `cityDaylight`'s
* numbers would test a formula no board on this page uses, and would have missed
* the one real regression this change nearly shipped: the Bay Area's opening
* pose saturates at 6.7 km against the narrow ceiling and at 12.4 km against the
* wide one, and its camera is 11.3 km up.
*/
function deployedCeilingOf(span: number): { near: number; far: number } {
return { near: span * 1.15, far: span * 3.9 };
}
/** The camera's distance to its target, in metres, at a pack's chapter. */
function chapterStandoffM(city: City, index: number): number {
const chapter = city.chapters[index];
assert.ok(chapter, `${city.id} has no chapter ${index}`);
// Horizontal, so `metresPerUnit` and not `unitsToMetres` — only the board's
// height is exaggerated, and dividing a stand-off by the exaggeration reports
// every pose as many times closer than it is.
const world = new World(city);
return Math.hypot(chapter.focus.distance, chapter.focus.height) * world.metresPerUnit;
}
/** The camera's height above its target, in metres, at a pack's chapter. */
function chapterAltitudeM(city: City, index: number): number {
const chapter = city.chapters[index];
assert.ok(chapter, `${city.id} has no chapter ${index}`);
const world = new World(city);
// The pose's `height` is scene units above the ground under the target, and
// the vertical exaggeration divides back out — the same conversion
// `SceneHandle.cameraAltitudeMetres` does.
return world.unitsToMetres(chapter.focus.height);
}
describe("San Francisco does not move", () => {
it("renders its resting chapter with exactly the fog it had before", () => {
const { span, metresPerUnit } = boardOf(SF);
const ceiling = ceilingOf(span);
const altitudeMetres = chapterAltitudeM(SF, 0);
const fog = aerialFog({ ceiling, metresPerUnit, altitudeMetres });
// Within 2%, and it is in fact within 0%: the Bay Area's opening pose is
// 430 units up, which at 94.34 m to the unit is 40.6 km, and the curve
// saturates by 6.7 km. The tolerance is there so a future adjustment to the
// scale height is allowed to be an adjustment rather than a regression.
assert.ok(
Math.abs(fog.near - ceiling.near) / ceiling.near < 0.02,
`SF fog near moved from ${ceiling.near.toFixed(1)} to ${fog.near.toFixed(1)}`,
);
assert.ok(
Math.abs(fog.far - ceiling.far) / ceiling.far < 0.02,
`SF fog far moved from ${ceiling.far.toFixed(1)} to ${fog.far.toFixed(1)}`,
);
});
it("saturates every board's whole-board pose against the DEPLOYED ceiling", () => {
/**
* The guard that matters, and the one the narrower ceiling does not give.
*
* Every marketing still on lumbridgecorp.com is shot from a board's opening
* chapter, and the promise of aerial perspective is that none of them is a
* different photograph. Measured at the poses the packs actually carry:
*
* california 67.6 km up, 1,551 km stand-off, 4,146 km authored reach
* socal 17.0 km up, 109 km stand-off, 597 km authored reach
* sf 11.3 km up, 71 km stand-off, 369 km authored reach
*
* The first two clear it on the air term alone. The Bay Area does not — 86 km
* of clear-day visibility times e^(11.3/8.5) is 324 km against 369 needed —
* and it is the stand-off clearance that carries it. That is exactly the
* regression this assertion exists to catch: with the clearance at 2.4 the
* Bay Area renders at 88% of its authored reach and the far corner of the
* board picks up haze it has never had.
*/
for (const city of [CALIFORNIA, SOCAL, SF]) {
const { span, metresPerUnit } = boardOf(city);
const ceiling = deployedCeilingOf(span);
const fog = aerialFog({
ceiling,
metresPerUnit,
altitudeMetres: chapterAltitudeM(city, 0),
standoffMetres: chapterStandoffM(city, 0),
});
assert.equal(
fog.far,
ceiling.far,
`${city.id}'s opening pose renders at ${((fog.far / ceiling.far) * 100).toFixed(1)}% ` +
"of the reach it was authored with. Every still shot from this board just moved.",
);
assert.equal(fog.near, ceiling.near, `${city.id}'s fog near moved at the opening pose`);
}
});
it("leaves every board's whole-board pose where it was", () => {
// Not only San Francisco. The wide pose is the frame every marketing still
// on lumbridgecorp.com is shot from, on all three boards, and the promise of
// this change is that none of them is a different photograph.
for (const city of [CALIFORNIA, SOCAL, SF]) {
const { span, metresPerUnit } = boardOf(city);
const ceiling = ceilingOf(span);
const fog = aerialFog({
ceiling,
metresPerUnit,
altitudeMetres: chapterAltitudeM(city, 0),
});
assert.equal(
fog.far,
ceiling.far,
`${city.id}'s opening pose is no longer at the clear-day ceiling`,
);
}
});
});
describe("the state board gets the aerial perspective it has none of", () => {
const { span, metresPerUnit } = boardOf(CALIFORNIA);
const ceiling = ceilingOf(span);
it("closes the fog right in at four kilometres", () => {
const fog = aerialFog({ ceiling, metresPerUnit, altitudeMetres: 4_000 });
const spans = fog.near / span;
assert.ok(
spans < 0.2,
`at 4 km the fog still starts ${spans.toFixed(3)} spans out, which on a ` +
"1,063 km board is 200 km and is the whole defect this exists to fix",
);
// And it is haze, not a wall: the near plane must stay well inside the far.
assert.ok(fog.near < fog.far * 0.6, "the fog closed to a single plane");
});
it("is back at the authored ceiling by fifty kilometres", () => {
const fog = aerialFog({ ceiling, metresPerUnit, altitudeMetres: 50_000 });
assert.ok(
Math.abs(fog.near - span * 0.91) / (span * 0.91) < 0.05,
`at 50 km the fog near is ${(fog.near / span).toFixed(3)} spans, not 0.91`,
);
});
it("rises with altitude and never falls", () => {
let last = -1;
for (const h of [0, 500, 1_000, 2_000, 4_000, 8_500, 17_000, 30_000, 60_000, 200_000]) {
const { far } = aerialFog({ ceiling, metresPerUnit, altitudeMetres: h });
assert.ok(far >= last, `fog far fell from ${last.toFixed(1)} to ${far.toFixed(1)} at ${h} m`);
last = far;
}
});
});
describe("the shape of the curve", () => {
it("is exactly today's behaviour when nobody says where the camera is", () => {
// The whole compatibility story in one assertion. Every caller that has not
// been taught about altitude — every test, every offline boot, every future
// renderer — passes `null` and gets the pack's own pair back untouched.
const ceiling = { near: 210, far: 460 };
const fog = aerialFog({ ceiling, metresPerUnit: 94.34, altitudeMetres: null });
assert.deepEqual(fog, ceiling);
assert.equal(aerialReach(null, 1_000_000), 1);
for (const bad of [Number.NaN, Number.POSITIVE_INFINITY]) {
assert.equal(aerialReach(bad, 1_000_000), 1, `altitude ${bad} must be treated as unknown`);
}
});
it("never exceeds the ceiling, however high the camera goes", () => {
// The ceiling is the contract. This function may only ever pull the fog in,
// so no board can be made to render further than the pose it was tuned at —
// which is what makes it safe to apply to three boards at once.
const ceiling = { near: 210, far: 460 };
for (const h of [0, 1e4, 1e6, 1e9]) {
const fog = aerialFog({ ceiling, metresPerUnit: 94.34, altitudeMetres: h });
assert.ok(fog.near <= ceiling.near + 1e-9, `near exceeded the ceiling at ${h} m`);
assert.ok(fog.far <= ceiling.far + 1e-9, `far exceeded the ceiling at ${h} m`);
}
});
it("never collapses the fog to a wall at ground level", () => {
// The failure mode of an aerial-perspective term has to be haze. A camera
// put at zero altitude by a controller bug must not render a board that is
// one flat colour.
const { span, metresPerUnit } = boardOf(CALIFORNIA);
const fog = aerialFog({ ceiling: ceilingOf(span), metresPerUnit, altitudeMetres: 0 });
assert.ok(fog.far * metresPerUnit > 50_000, `on the ground you can only see ${fog.far} units`);
});
it("never fogs the thing the camera is looking at", () => {
// The clearance, stated as the property it buys rather than as its own
// number: with `main.ts`'s 1.15/3.9 ratio the near plane lands at 1.77
// stand-offs, so the subject of any shot is outside the fog entirely and the
// haze begins somewhere behind it. A camera 223 km from a mountain on a
// 1,063 km board is a map being read, not an observer who cannot see.
const { span, metresPerUnit } = boardOf(CALIFORNIA);
const standoffMetres = 500_000;
const fog = aerialFog({
ceiling: deployedCeilingOf(span),
metresPerUnit,
// Low enough that the air term cannot be what is being measured.
altitudeMetres: 1_000,
standoffMetres,
});
assert.ok(
fog.near * metresPerUnit > standoffMetres,
`the fog starts at ${((fog.near * metresPerUnit) / 1000).toFixed(0)} km with the ` +
`subject at ${(standoffMetres / 1000).toFixed(0)} km — the shot is inside its own fog`,
);
// But it is aerial perspective and not a clear day: something three
// stand-offs away has to be visibly hazed.
assert.ok(fog.far * metresPerUnit < standoffMetres * 8, "the fog is out past anything on the board");
});
it("puts one scale height at a factor of e", () => {
// The one physical claim in the module, asserted as physics rather than as a
// number somebody liked: air thins as exp(-h/H), so a camera one scale
// height up sees e times as far. Taken well below saturation so the clamp
// is not what is being measured.
// A million metres of authored reach, chosen so neither clamp is in play:
// 86 km of it at the ground and 234 km at one scale height, both strictly
// between the 2% floor and the ceiling of 1.
const reachLow = aerialReach(0, 1_000_000);
const reachHigh = aerialReach(8_500, 1_000_000);
assert.ok(
Math.abs(reachHigh / reachLow - Math.E) < 0.001,
`one scale height gave ${(reachHigh / reachLow).toFixed(4)}x, not e`,
);
});
});
/**
* The seam that keeps a camera out of the environment map.
*
* ## What this is guarding, in one paragraph
*
* `environmentRig.ts` decides whether to re-render and re-convolve the sky's
* PMREM cubemap by fingerprinting the rig's **colours** — `sky.top`,
* `sky.horizon`, the hemisphere pair, the ambient and the sun. Separately,
* `interiors/daylight.ts` pins the sky's horizon stop to the fog colour on
* purpose, because that is what makes the horizon a horizon instead of the seam
* where a dome meets a haze. Put those two facts beside a fog that follows the
* camera and there is a live wire: the first camera-dependent term that reaches
* a *colour* puts a cubemap rebuild on every orbit step, and the symptom is a
* board that halves its frame rate while drawing exactly the same triangles.
*
* Aerial perspective does not do that — `aerialReach` scales three distances and
* touches nothing else — and this is that claim made structural rather than
* remembered. The fix it forecloses is the tempting one: coarsening the
* fingerprint until the rebuild stops hides one instance and leaves the
* mechanism armed for the next feature that varies a colour.
*/
describe("the camera moves the fog and nothing else", () => {
/** The deployed Bay Area rig: marine layer on, so obscuration is in play. */
function bayArea() {
const { span, metresPerUnit } = boardOf(SF);
return createAtmosphere({
lng: SF.center.lng,
metresPerUnit,
clearFog: deployedCeilingOf(span),
minVisibilityM: span * metresPerUnit * 1.6,
marineLayer: PACIFIC_MARINE_LAYER,
});
}
/** The deployed state board: no marine layer, and the board with real haze. */
function state() {
const { span, metresPerUnit } = boardOf(CALIFORNIA);
return createAtmosphere({
lng: CALIFORNIA.center.lng,
metresPerUnit,
clearFog: deployedCeilingOf(span),
minVisibilityM: span * metresPerUnit * 1.6,
marineLayer: null,
});
}
/**
* Everything a `LightingState` carries except the two numbers a camera is
* allowed to move. Compared as JSON so a field added later is compared too,
* which is the point — a guard that has to be updated to notice a new field is
* not a guard.
*/
function everythingButTheDistances(l: LightingState): string {
return JSON.stringify({
sun: l.sun,
hemisphere: l.hemisphere,
ambient: l.ambient,
sky: l.sky,
fogColor: l.fog?.color ?? null,
moon: l.moon ?? null,
});
}
/**
* Four hours that exercise every branch that could plausibly acquire a camera
* term: full day, the golden hour, **dusk** — where the horizon pin is
* load-bearing and the seam shows — and astronomical night, where the moon is
* the key light and the fog is floored at a lifted blue.
*/
const HOURS = [
["noon", "2026-08-22T20:00:00Z"],
["golden hour", "2026-08-23T02:00:00Z"],
["dusk", "2026-08-23T02:30:00Z"],
["night", "2026-08-23T09:00:00Z"],
] as const;
/** A chase camera on the deck, a mid-board pose, and above the whole board. */
const VIEWS: readonly (readonly [string, AerialView])[] = [
["ground", { altitudeMetres: 0, standoffMetres: 500 }],
["crow", { altitudeMetres: 1_200, standoffMetres: 4_000 }],
["low", { altitudeMetres: 4_000, standoffMetres: 20_000 }],
["high", { altitudeMetres: 40_000, standoffMetres: 200_000 }],
["unknown", { altitudeMetres: null }],
];
it("leaves every colour in the rig bit-identical however far the camera climbs", () => {
for (const [label, atmosphere] of [
["bay-area", bayArea()],
["california", state()],
] as const) {
const city = label === "bay-area" ? SF : CALIFORNIA;
for (const [hour, iso] of HOURS) {
const env: Environment = observe(
city.center.lat,
city.center.lng,
new Date(iso),
null,
);
const reference = everythingButTheDistances(atmosphere.apply(env, VIEWS[0]![1]));
for (const [pose, view] of VIEWS) {
assert.equal(
everythingButTheDistances(atmosphere.apply(env, view)),
reference,
`${label} at ${hour}: the "${pose}" camera changed something that is not a fog ` +
"distance. Every field compared here is fingerprinted by " +
"`environmentRig.ts`, so a camera-dependent colour rebuilds and re-convolves " +
"the PMREM cubemap on every orbit step. Move the term out of the camera path " +
"rather than coarsening `environmentKey`.",
);
}
}
}
});
it("does move the fog distances, or there is no feature here", () => {
// The other half of the assertion above, and it has to be stated or the
// first one passes perfectly on a rig that ignores the camera entirely.
const atmosphere = state();
const env = observe(
CALIFORNIA.center.lat,
CALIFORNIA.center.lng,
new Date("2026-08-22T20:00:00Z"),
null,
);
const low = atmosphere.apply(env, { altitudeMetres: 1_200, standoffMetres: 4_000 }).fog;
const high = atmosphere.apply(env, { altitudeMetres: 40_000, standoffMetres: 200_000 }).fog;
assert.ok(low && high);
assert.ok(
high.far > low.far * 4,
`the state board sees ${low.far.toFixed(0)} units at 1.2 km and ` +
`${high.far.toFixed(0)} at 40 km — aerial perspective has stopped working`,
);
});
it("hands the camera path exactly the distances the clock path would write", () => {
/*
* `Atmosphere.aerial` is what a camera step calls now, and `apply` is what
* the clock still calls. Two derivations of one fog is how a drag and a
* clock tick start disagreeing about the weather, so `aerial` is defined as
* a slice of `apply` and this is that definition held in place.
*/
for (const [label, atmosphere] of [
["bay-area", bayArea()],
["california", state()],
] as const) {
const city = label === "bay-area" ? SF : CALIFORNIA;
for (const [hour, iso] of HOURS) {
const env = observe(city.center.lat, city.center.lng, new Date(iso), null);
for (const [pose, view] of VIEWS) {
const full = atmosphere.apply(env, view).fog;
assert.ok(full, `${label} lost its fog at ${hour}`);
assert.deepEqual(
atmosphere.aerial(env, view),
{ near: full.near, far: full.far },
`${label} at ${hour}, "${pose}": the camera path and the clock path disagree`,
);
}
}
}
});
});
+345
View File
@@ -0,0 +1,345 @@
/**
* The migration field, held to the claims it must not make.
*
* **It is one `THREE.Points`, whatever the sky is doing.** Fifty-eight counties,
* one draw call, zero triangles — the `nightlights.ts` arrangement that puts San
* Francisco's 12,038 street lamps on the board for the cost of one cloud. The
* alternative that keeps suggesting itself is the articulated crow, and it is
* 4,390 triangles across 33 meshes: **33 draw calls per bird**, against a
* whole-board budget of 650. Forty of them would be 1,320.
*
* **No mote position is ever computed from two granules.** BirdCast is a
* forecast raster aggregated to county-nights. There is no track and no
* individual, so joining consecutive samples into a trajectory would be
* inventing the ten minutes in between — the same lie `Vessel` refuses between
* AIS fixes. A mote is spawned once with its county's reported heading and
* ground speed, integrated forward on its own, and respawned from whatever the
* newest granule says when its life runs out. The assertion below is that a mote
* alive across a granule change keeps moving on the velocity it was born with.
*
* **The state row never reaches it.** `US-CA` has NULL coordinates and 793,141
* birds aloft against the largest county's 82,549, and it looks exactly like the
* other fifty-eight rows.
*
* **It constructs no light.** CONTRACT.md §4.
*
* The world is the real california board — 1,919 m to the unit — because a 1:1
* fake would pass every drift assertion here while the shipped layer moved motes
* two thousand times too far.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import CALIFORNIA_CITY from "../../cities/california.ts";
import {
allocateMotes,
createMigrationLayer,
discRadiusKm,
MIGRATION_ALTITUDE_UNITS_PER_METRE,
MIGRATION_MAX_POINTS,
MIGRATION_MOTES_PER_COUNTY,
} from "../../engine/migration.ts";
import type { MigrationLayerFactory } from "../../engine/scene.ts";
import type { MigrationCounty, MigrationField } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
const world = new World(CALIFORNIA_CITY);
const CALIFORNIA_SPAN = 553.9;
/** The seam `scene.ts` constructs through. Asserted at compile time. */
const _factory: MigrationLayerFactory = (w, o) => createMigrationLayer(w, o);
void _factory;
function county(over: Partial<MigrationCounty> = {}): MigrationCounty {
return {
id: "US-CA-019",
name: "Fresno County",
lat: 36.761006,
lng: -119.655019,
areaKm2: 15569,
aloft: 82549,
altitude: 333,
direction: 140,
speed: 6.9,
...over,
};
}
/** All 58 counties, at the areas and the busiest-night densities they really have. */
function fullState(): MigrationCounty[] {
const out: MigrationCounty[] = [];
for (let i = 0; i < 58; i++) {
out.push(
county({
id: `US-CA-${String(i * 2 + 1).padStart(3, "0")}`,
lat: 33 + (i % 20) * 0.4,
lng: -122 + Math.floor(i / 20) * 2,
// The real spread: 601 km² (San Francisco) to 52,073 (San Bernardino).
areaKm2: 601 + (i / 57) * (52073 - 601),
// Every county busy, which is the worst case for the point count.
aloft: 400_000,
}),
);
}
return out;
}
function field(counties: MigrationCounty[], over: Partial<MigrationField> = {}): MigrationField {
return {
counties,
observedAt: "2026-08-23T03:20:00Z",
statewide: null,
quiet: null,
...over,
};
}
function clouds(root: THREE.Object3D): THREE.Points[] {
const found: THREE.Points[] = [];
root.traverse((node) => {
if ((node as THREE.Points).isPoints) found.push(node as THREE.Points);
});
return found;
}
// ---- Cost -----------------------------------------------------------------
describe("the migration field's cost", () => {
it("is one Points and no triangles, for any number of counties", () => {
for (const counties of [[county()], fullState()]) {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field(counties));
const points = clouds(layer.group);
assert.equal(points.length, 1, `${counties.length} counties must still be one cloud`);
assert.equal(layer.group.children.length, 1);
let meshes = 0;
layer.group.traverse((node) => {
if ((node as THREE.Mesh).isMesh) meshes += 1;
});
assert.equal(meshes, 0, "not one triangle anywhere in it");
layer.dispose();
}
});
it("draws at most 700 points with every county in California busy", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field(fullState()));
assert.ok(layer.activeCount() <= MIGRATION_MAX_POINTS, `${layer.activeCount()} points`);
assert.equal(layer.activeCount(), 58 * MIGRATION_MOTES_PER_COUNTY);
const cloud = clouds(layer.group)[0] as THREE.Points;
assert.equal(cloud.geometry.drawRange.count, layer.activeCount());
// The buffer is allocated once at the ceiling and never grows.
assert.equal(cloud.geometry.getAttribute("position").count, MIGRATION_MAX_POINTS);
layer.dispose();
});
it("holds the allocation to the buffer even if the feed sends more counties", () => {
const counts = allocateMotes([...fullState(), ...fullState()]);
assert.ok(counts.reduce((a, b) => a + b, 0) <= MIGRATION_MAX_POINTS);
// …and drops the quietest rather than the last, so the busiest county always
// draws whatever order the wire happened to use.
const mixed = allocateMotes([county({ aloft: 10 }), county({ aloft: 400_000 })]);
assert.ok((mixed[1] as number) > (mixed[0] as number));
});
it("draws nothing at all for a quiet sky", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setSolarElevation(-20);
layer.setField(field([], { quiet: { reason: "daylight", message: "…" } }));
assert.equal(layer.activeCount(), 0);
assert.equal((clouds(layer.group)[0] as THREE.Points).visible, false);
layer.dispose();
});
it("retires every mote when the feed goes away, rather than leaving them drifting", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field([county()]));
layer.setSolarElevation(-20);
const before = layer.mote(0);
assert.ok(before);
layer.setField(null);
assert.equal(layer.activeCount(), 0);
// A different night, over a county four hundred kilometres away. Nothing
// may survive from the granule the feed stopped claiming.
layer.setField(field([county({ lat: 33.0, lng: -116.0, direction: 320, speed: 35 })]));
const after = layer.mote(0);
assert.ok(after);
const [x, z] = world.project(33.0, -116.0);
const radius = discRadiusKm(15569) / (world.metresPerUnit / 1000);
assert.ok(Math.hypot(after.x - x, after.z - z) <= radius + 1e-3);
layer.dispose();
});
it("survives a null, an undefined and a field full of nonsense", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(null);
layer.setField(undefined as unknown as MigrationField);
layer.setField({ counties: [null, { id: "x" }] } as unknown as MigrationField);
layer.tick(0.016);
assert.equal(layer.activeCount(), 0);
layer.dispose();
});
});
// ---- The claim it must not make -------------------------------------------
describe("a mote's position", () => {
it("is never computed by interpolating between two consecutive granules", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
// Granule one: Fresno, flying south-east at 6.9 m/s.
layer.setField(field([county()]));
layer.setSolarElevation(-20);
const before = layer.mote(0);
assert.ok(before);
// Granule two, ten minutes later: a county 400 km away, flying the opposite
// way at five times the speed. If anything blended the two, this is where it
// would show.
layer.setField(field([county({ lat: 33.0, lng: -116.0, direction: 320, speed: 35 })]));
const after = layer.mote(0);
assert.ok(after);
assert.deepEqual(
[after.x, after.z, after.vx, after.vz],
[before.x, before.z, before.vx, before.vz],
"a live mote must not move because a new granule arrived",
);
layer.tick(1);
const stepped = layer.mote(0);
assert.ok(stepped);
// Exactly its own velocity for exactly one second, and nothing else.
assert.ok(Math.abs(stepped.x - (before.x + before.vx)) < 1e-6);
assert.ok(Math.abs(stepped.z - (before.z + before.vz)) < 1e-6);
layer.dispose();
});
it("takes the newest granule only when it is born again", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field([county()]));
layer.setSolarElevation(-20);
const fresno = layer.mote(0);
assert.ok(fresno);
layer.setField(field([county({ lat: 33.0, lng: -116.0, direction: 320, speed: 35 })]));
// Past every mote's life, which is 900 seconds give or take a third.
layer.tick(2_000);
const reborn = layer.mote(0);
assert.ok(reborn);
const [x, z] = world.project(33.0, -116.0);
const radius = discRadiusKm(15569) / (world.metresPerUnit / 1000);
assert.ok(Math.hypot(reborn.x - x, reborn.z - z) <= radius + 1e-3, "inside the new county's disc");
assert.ok(reborn.vx < 0, "…and flying north-west, the way the new granule says");
layer.dispose();
});
it("drifts at the reported ground speed and no faster", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field([county({ direction: 90, speed: 10 })]));
layer.setSolarElevation(-20);
const start = layer.mote(0);
assert.ok(start);
layer.tick(60);
const moved = layer.mote(0);
assert.ok(moved);
// Due east at 10 m/s for a minute is 600 m, which at 1,919 m to the unit is
// 0.313 units — a slow drift, deliberately not exaggerated.
const metres = Math.hypot(moved.x - start.x, moved.z - start.z) * world.metresPerUnit;
assert.ok(Math.abs(metres - 600) < 1, `${metres.toFixed(0)} m in sixty seconds`);
assert.ok(moved.x > start.x, "east is +X");
assert.ok(Math.abs(moved.z - start.z) < 1e-6, "…and due east is not north or south");
});
it("flies toward the reported bearing, on a board where north is -Z", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field([county({ direction: 0, speed: 10 })]));
layer.setSolarElevation(-20);
const start = layer.mote(0);
layer.tick(60);
const moved = layer.mote(0);
assert.ok(start && moved);
// A field drifting north-west when the feed says south-east is the one bug
// here nobody would see, because a cloud of dots has no other way to be wrong.
assert.ok(moved.z < start.z, "heading 0 must go north, which is -Z");
layer.dispose();
});
});
// ---- The disc -------------------------------------------------------------
describe("where the motes are", () => {
it("scatters them inside a disc of the county's true area", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
// San Bernardino: 52,073 km², a 129 km disc.
const big = county({ areaKm2: 52073, lat: 34.84, lng: -116.18 });
layer.setField(field([big]));
layer.setSolarElevation(-20);
const [cx, cz] = world.project(big.lat, big.lng);
const radiusUnits = (discRadiusKm(big.areaKm2) * 1000) / world.metresPerUnit;
assert.ok(radiusUnits > 60 && radiusUnits < 75, `${radiusUnits.toFixed(1)} units`);
let far = 0;
for (let i = 0; i < layer.activeCount(); i++) {
const mote = layer.mote(i);
assert.ok(mote);
const d = Math.hypot(mote.x - cx, mote.z - cz);
assert.ok(d <= radiusUnits * 1.02, `${d.toFixed(1)} units from the internal point`);
if (d > radiusUnits * 0.5) far += 1;
}
// Uniform over the disc, not clustered at the point: more than half the area
// is outside half the radius, so most motes should be.
assert.ok(far > layer.activeCount() * 0.5, "the scatter fills the disc rather than the middle");
layer.dispose();
});
it("puts them above the ground they are over, not above sea level", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
// Inyo County: the internal point is up against the White Mountains, and
// `height_mean_m` is above ground level. At true scale 826 m would be 0.43
// units and inside the hill.
const inyo = county({ lat: 36.56216, lng: -117.404209, areaKm2: 26488, altitude: 826 });
layer.setField(field([inyo]));
layer.setSolarElevation(-20);
for (let i = 0; i < layer.activeCount(); i++) {
const mote = layer.mote(i);
assert.ok(mote);
assert.ok(mote.y > 0);
}
const mote = layer.mote(0);
assert.ok(mote);
const lift = 826 * MIGRATION_ALTITUDE_UNITS_PER_METRE;
assert.ok(lift > 8 && lift < 9, "826 m is 8.3 units on the aircraft seam");
assert.ok(mote.y >= lift, "…measured up from the terrain under it");
layer.dispose();
});
});
// ---- Night ----------------------------------------------------------------
describe("the field and the light rig", () => {
it("constructs no light of any kind", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field([county()]));
layer.group.traverse((node) => {
assert.ok(!(node as THREE.Light).isLight, `${node.name || node.type} is a light`);
});
layer.dispose();
});
it("is gone in daylight, whatever the feed sent", () => {
const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(field([county()]));
const cloud = clouds(layer.group)[0] as THREE.Points;
layer.setSolarElevation(-20);
assert.equal(cloud.visible, true);
layer.setSolarElevation(12);
assert.equal(cloud.visible, false, "BirdCast does not measure by day");
layer.setSolarElevation(-8);
assert.equal(cloud.visible, true);
layer.dispose();
});
});
+365
View File
@@ -0,0 +1,365 @@
/**
* The port kit, held to the four things a screenshot cannot check.
*
* **It is four draw calls, and it stays four when a second port arrives.** That
* is the whole design in one assertion. `socal mobile` measures 140 draw calls
* against a cap of 170 — thirty spare for this feature and every future one —
* and the obvious shape for a kit like this, a `Group` per port with a mesh per
* surface, lands at fifteen for two ports and fifty for a board with six. So the
* merge-across-ports property is asserted here rather than assumed: **four ports
* must produce exactly the same mesh count as one.**
*
* **No crane is a `Group`.** Fifty-six gantries at five boxes each is 280
* matrices in one `InstancedMesh` or 280 draw calls, and this repo has already
* made the second mistake twice — a suspension bridge at ~34 draw calls, and
* twelve identical asphalt freeways that could never merge because a fresh
* material was allocated per ribbon. A test is the only thing that keeps the
* first answer once somebody wants a crane to be pickable.
*
* **Every geometry carries position, normal AND uv, indexed.** `airports.ts:43`
* records the scar: `mergeGeometries` returns `null` for a bucket whose
* attribute sets disagree, and the bucket vanishes with no error and no missing
* pixels to notice — it is simply not there. The `Batch` warns; this asserts it
* never had to.
*
* **It constructs no light.** CONTRACT §4 gives `Atmosphere` sole ownership of
* the rig, and a working container terminal under high-mast floods is one of the
* more tempting exceptions in the product — `fires.ts` records the last time
* somebody nearly took it. `setLighting` reaches for a material's `emissive`
* instead, which is a property of a surface and not a light in the scene.
*
* The world below is a real board projection rather than a tidy 1:1 fake —
* Southern California's `latScale: 285` and `verticalExaggeration: 3.4`, which
* puts one scene unit at 390.6 m. A 1:1 fake would pass while every crane was
* four hundred times too tall.
*
* There is no `document` in a node test, so `yardAtlas` correctly returns
* `null` and the yard material falls back to a flat colour. The geometry — which
* is what this file is about — is identical either way, because `groundQuad`
* writes uv whether or not anything samples it.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
createPortLayer,
createPorts,
craneStations,
metresBetween,
pathLengthMetres,
yardCorners,
PORT_PALETTE,
type PortLayer,
} from "../../engine/ports.ts";
import type { PortLayerFactory, PortLayer as ScenePortLayer } from "../../engine/scene.ts";
import { LOS_ANGELES, LONG_BEACH, PORTS } from "../../cities/socal.ts";
import type { LightingState, Port } from "../../engine/types.ts";
import type { World } from "../../engine/world.ts";
/** `socal.ts`: centre 33.82 / -118.05, `latScale: 285`, exaggeration 3.4. */
function socalWorld(ground: (lat: number, lng: number) => number = () => 0): World {
const centre = { lat: 33.82, lng: -118.05 };
const latScale = 285;
const lngScale = latScale * Math.cos((centre.lat * Math.PI) / 180);
const metresPerUnit = 111_320 / latScale;
return {
project(lat: number, lng: number): [number, number] {
return [(lng - centre.lng) * lngScale, -(lat - centre.lat) * latScale];
},
groundAt: ground,
metresPerUnit,
metres(value: number): number {
return (value / metresPerUnit) * 3.4;
},
} as unknown as World;
}
/**
* `createPortLayer` must be a `PortLayerFactory` once its ports are closed over,
* and the module's own `PortLayer` must be the one `scene.ts` declares.
*
* Asserted at compile time, which is the only place it can be. Both are
* type-only imports, so nothing about `scene.ts` is pulled into this test at
* runtime and `engine/ports.ts` still imports nothing from it — that is the
* whole point of the arrangement.
*/
const _factory: PortLayerFactory = (world, options) => createPortLayer(world, PORTS, options);
const _sameShape: (layer: PortLayer) => ScenePortLayer = (layer) => layer;
void _factory;
void _sameShape;
function meshes(root: THREE.Object3D): THREE.Mesh[] {
const found: THREE.Mesh[] = [];
root.traverse((object) => {
if (object instanceof THREE.Mesh) found.push(object);
});
return found;
}
const NOON: LightingState = {
sun: { direction: [0.1, 0.94, 0.32], color: 0xfff3e2, intensity: 2.4 },
hemisphere: { sky: 0x8db2d4, ground: 0xa9a291, intensity: 0.6 },
ambient: { color: 0xffffff, intensity: 0.2 },
sky: { top: 0x8db2d4, horizon: 0xe6ded0 },
fog: { color: 0xe6ded0, near: 200, far: 900 },
};
/**
* Full night, and note what it does with `sun`: the key light is **thirty
* degrees up**, because `atmosphere.ts` hands the key over to the moon after
* dark and floors the direction besides. A layer that reads night off
* `sun.direction[1]` passes a naive fixture and then stays dark on the board on
* every moonlit night. This fixture exists to fail that implementation.
*/
const MIDNIGHT: LightingState = {
...NOON,
sun: { direction: [0.1, 0.5, 0.32], color: 0x2a3550, intensity: 1.15 },
sky: { top: 0x05070f, horizon: 0x121a2c },
};
/** Civil twilight: the sky has blue in it and the floods are half up. */
const DUSK: LightingState = {
...NOON,
sun: { direction: [0.1, 0.02, 0.32], color: 0xffb27a, intensity: 0.4 },
sky: { top: 0x101a3a, horizon: 0x3b4a68 },
};
describe("the port kit is four draw calls and stays four", () => {
it("returns at most six meshes for the whole board", () => {
const group = createPorts(socalWorld(), PORTS);
const drawn = meshes(group);
assert.ok(
drawn.length <= 6,
`a port board must fit in six meshes; got ${drawn.length}: ${drawn.map((m) => m.name).join(", ")}`,
);
// And it is not accidentally empty: San Pedro Bay has stone, yards, water
// and cranes, so all four buckets must be present.
const names = new Set(drawn.map((mesh) => mesh.name));
for (const bucket of ["ports:stone", "ports:yard", "ports:channel", "ports:cranes"]) {
assert.ok(names.has(bucket), `missing bucket ${bucket}`);
}
});
it("costs the same number of meshes for four ports as for one", () => {
const world = socalWorld();
const one = meshes(createPorts(world, [LOS_ANGELES])).length;
const four: Port[] = [
LOS_ANGELES,
LONG_BEACH,
{ ...LOS_ANGELES, id: "USLAX-B" },
{ ...LONG_BEACH, id: "USLGB-B" },
];
const many = meshes(createPorts(world, four));
assert.equal(
many.length,
one,
`four ports drew ${many.length} meshes against one port's ${one} — the buckets stopped merging across ports`,
);
});
it("draws nothing at all for a board with no port", () => {
const group = createPorts(socalWorld(), []);
assert.equal(meshes(group).length, 0);
assert.equal(group.children.length, 0);
});
});
describe("every gantry on the board is one InstancedMesh", () => {
it("has no Group anywhere in the layer", () => {
const group = createPorts(socalWorld(), PORTS);
const groups: string[] = [];
group.traverse((object) => {
if (object !== group && object instanceof THREE.Group) groups.push(object.name || "(unnamed)");
});
assert.deepEqual(groups, [], `a crane became a Group: ${groups.join(", ")}`);
});
it("puts five boxes per gantry in a single instanced mesh", () => {
const group = createPorts(socalWorld(), PORTS);
const cranes = meshes(group).filter((mesh) => mesh.name === "ports:cranes");
assert.equal(cranes.length, 1, "there must be exactly one crane mesh for the whole board");
const mesh = cranes[0];
assert.ok(mesh instanceof THREE.InstancedMesh);
const gantries = PORTS.flatMap((port) => port.cranes ?? []).reduce(
(total, row) => total + row.count,
0,
);
assert.equal(gantries, 56, "San Pedro Bay is authored with fifty-six gantries");
assert.equal((mesh as THREE.InstancedMesh).count, gantries * 5);
});
it("raises exactly the booms the pack asked for, from the far end of the rail", () => {
// Pier T East is the quiet frontage: five of its six booms are up.
const row = (LONG_BEACH.cranes ?? []).find((crane) => crane.id === "pier-t-east");
assert.ok(row);
const stations = craneStations(row);
assert.equal(stations.length, 6);
assert.equal(stations.filter((station) => station.idle).length, 5);
// Clustered at one end, never scattered — a random pattern of raised booms
// reads as a fault rather than as a berth with nothing alongside.
assert.equal(stations[0]?.idle, false);
assert.ok(stations.slice(1).every((station) => station.idle));
});
it("puts a raised boom higher than a lowered one, through the exaggerated axis", () => {
const world = socalWorld();
const working = createPorts(world, [
{ ...LOS_ANGELES, cranes: [{ ...(LOS_ANGELES.cranes ?? [])[0]!, idleFraction: 0 }] },
]);
const idle = createPorts(world, [
{ ...LOS_ANGELES, cranes: [{ ...(LOS_ANGELES.cranes ?? [])[0]!, idleFraction: 1 }] },
]);
const topOf = (group: THREE.Object3D) => {
const mesh = meshes(group).find((m) => m.name === "ports:cranes") as THREE.InstancedMesh;
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const scale = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
let highest = -Infinity;
for (let i = 0; i < mesh.count; i += 1) {
mesh.getMatrixAt(i, matrix);
matrix.decompose(position, quaternion, scale);
highest = Math.max(highest, position.y + scale.x / 2);
}
return highest;
};
assert.ok(
topOf(idle) > topOf(working) * 1.4,
"a raised boom must reach well above a lowered one; the vertical axis is exaggerated and the horizontal is not, so the boom has to be composed rather than rotated",
);
});
});
describe("no bucket is silently dropped", () => {
it("gives every geometry position, normal, uv and an index", () => {
const group = createPorts(socalWorld(), PORTS);
for (const mesh of meshes(group)) {
const geometry = mesh.geometry;
for (const attribute of ["position", "normal", "uv"]) {
assert.ok(
geometry.getAttribute(attribute),
`${mesh.name} has no ${attribute} — mergeGeometries drops a bucket whose attribute sets disagree, in silence`,
);
}
assert.ok(geometry.getIndex(), `${mesh.name} is not indexed`);
}
});
it("never warns that a bucket failed to merge", () => {
const warnings: unknown[][] = [];
const original = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
createPorts(socalWorld(), PORTS);
} finally {
console.warn = original;
}
assert.deepEqual(warnings, []);
});
});
describe("the layer owns no light", () => {
it("constructs no THREE.Light anywhere in the subtree", () => {
const layer = createPortLayer(socalWorld(), PORTS, { span: 393 });
layer.setLighting(MIDNIGHT);
const lights: string[] = [];
layer.group.traverse((object) => {
if (object instanceof THREE.Light) lights.push(object.type);
});
assert.deepEqual(lights, [], `CONTRACT §4: Atmosphere owns the rig. Found ${lights.join(", ")}`);
layer.dispose();
});
it("brings the yard up at night and puts it away by day, through emissive", () => {
const layer = createPortLayer(socalWorld(), PORTS, { span: 393 });
const yardMaterial = () => {
const mesh = meshes(layer.group).find((m) => m.name === "ports:yard");
return mesh?.material as THREE.MeshLambertMaterial;
};
layer.setLighting(NOON);
assert.equal(yardMaterial().emissiveIntensity, 0);
layer.setLighting(MIDNIGHT);
assert.ok(yardMaterial().emissiveIntensity > 0.5, "a moonlit night is still night");
layer.setLighting(DUSK);
const dusk = yardMaterial().emissiveIntensity;
assert.ok(dusk > 0.1 && dusk < 0.5, `twilight should be partway up, got ${dusk}`);
layer.setLighting(NOON);
assert.equal(yardMaterial().emissiveIntensity, 0);
layer.dispose();
});
it("disposes its geometry and clears the group", () => {
const layer = createPortLayer(socalWorld(), PORTS, { span: 393 });
assert.ok(layer.group.children.length > 0);
layer.dispose();
assert.equal(layer.group.children.length, 0);
});
});
describe("the kit is sized in metres against this board", () => {
it("makes a gantry taller than a container ship is long", () => {
const world = socalWorld();
// The claim the module comment is built on: at 390.6 m per unit and 3.4x
// exaggeration a 130 m gantry stands 1.13 units while a 400 m ship is 1.02
// units long. If that ever stops being true the crane stops being the hero.
const craneUnits = world.metres(130);
const shipUnits = 400 / world.metresPerUnit;
assert.ok(craneUnits > shipUnits, `${craneUnits.toFixed(3)} vs ${shipUnits.toFixed(3)}`);
// And the arithmetic that makes a container paint rather than instances.
assert.ok(12.2 / world.metresPerUnit < 0.04);
});
it("keeps the whole board's stone, yards and water within twenty thousand triangles", () => {
// The socal mobile budget has 134,404 triangles spare. The whole port kit is
// allowed 20,000 of them, and it is nowhere near that: this is the number
// that stops a later "just a few more boxes" landing without anybody noticing.
const group = createPorts(socalWorld(), PORTS);
let triangles = 0;
for (const mesh of meshes(group)) {
const index = mesh.geometry.getIndex();
const per = index ? index.count / 3 : mesh.geometry.getAttribute("position").count / 3;
triangles += per * (mesh instanceof THREE.InstancedMesh ? mesh.count : 1);
}
assert.ok(triangles < 20_000, `port kit is ${triangles} triangles`);
});
it("uses a palette that stays off the Vincent Thomas green", () => {
// `0x3f7d55` is already the most saturated object in the Harbour frame. Real
// container red and Maersk blue put two more loud hues beside it and the
// whole harbour reads as a toy. Every box colour here is under half the
// chroma of the bridge.
const chroma = (hex: number) => {
const colour = new THREE.Color(hex);
const max = Math.max(colour.r, colour.g, colour.b);
const min = Math.min(colour.r, colour.g, colour.b);
return max === 0 ? 0 : (max - min) / max;
};
const bridge = chroma(0x3f7d55);
for (const key of ["boxLoadedA", "boxLoadedB", "boxEmpty", "boxUnknown", "stone"] as const) {
assert.ok(
chroma(PORT_PALETTE[key]) < bridge,
`${key} is more saturated than the Vincent Thomas`,
);
}
});
});
describe("the authoring helpers agree with the renderer", () => {
it("derives a yard's corners the way the quad is built", () => {
const yard = { lat: 33.75, lng: -118.25, length: 1000, width: 400, bearing: 0 };
const corners = yardCorners(yard);
assert.equal(corners.length, 4);
// 1000 m along a bearing of zero is 1000 m of latitude.
assert.ok(Math.abs(metresBetween(corners[0]!, corners[1]!) - 1000) < 2);
assert.ok(Math.abs(metresBetween(corners[1]!, corners[2]!) - 400) < 2);
});
it("measures the federal breakwater at thirteen kilometres", () => {
const total = (LOS_ANGELES.breakwater ?? []).reduce(
(sum, arm) => sum + pathLengthMetres(arm),
0,
);
assert.ok(total > 12_000 && total < 15_000, `${Math.round(total)} m`);
});
});
+344
View File
@@ -0,0 +1,344 @@
/**
* The reflectivity sheet, held to what a picture cannot show.
*
* **It is two triangles and one mesh, whatever the weather is doing.** That is
* the entire cost argument for the layer, and it holds because `echo_cells`
* turned out to be a regular lattice so the field is a texture and the
* geometry is a quad. A version built from per-cell geometry would look
* identical in a screenshot and cost 1,596 quads.
*
* **It contributes nothing at all to a quiet frame.** Not an invisible mesh, not
* 1,596 transparent texels: no scene child. California is under rain a mean
* 0.596% of the time, so this is the layer's ordinary state and it has to cost
* nothing.
*
* **It is never built for a board too fine to carry the cell.** A 0.25-degree
* cell is 27.8 km 4.3 texels tall on the SoCal board and 3.4 on the Bay Area
* one. Four enormous squares over Los Angeles is a lie about resolution told in
* a medium that reads as truthful, and the refusal is a pure function so it can
* be asserted rather than screenshotted.
*
* **It constructs no light.** CONTRACT.md §4 gives `Atmosphere` sole ownership
* of the rig; the build spec's grep catches the letter, and walking the subtree
* catches the spirit.
*
* The world below is a real board projection california's `latScale: 58`,
* 1,919 m to the unit, `verticalExaggeration: 15` because a 1:1 fake would
* pass every one of these while the shipped sheet was buried in the Sierra.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import CALIFORNIA_CITY from "../../cities/california.ts";
import SOCAL_CITY from "../../cities/socal.ts";
import SF_CITY from "../../cities/sf.ts";
import {
alphaForDbz,
boardCarriesRaster,
createPrecipLayer,
paint,
precipFactoryFor,
PRECIP_ALTITUDE_M,
} from "../../engine/precip.ts";
import { radarRampRgb } from "../../assets/radarRamp.ts";
import { buildRadarField, RADAR_DRY_DBZ, RADAR_RAIN_DBZ, type RadarCell } from "../../server/radar.ts";
import type { PrecipLayerFactory } from "../../engine/scene.ts";
import type { RadarField } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
/** The real board. Its heightfield is what puts the Sierra at 39.5 units. */
const world = new World(CALIFORNIA_CITY);
/** `scene.ts`'s own derivation: the larger projected extent of `city.bounds`. */
const CALIFORNIA_SPAN = 553.9;
/**
* `createPrecipLayer` must be a `PrecipLayerFactory` the one seam `scene.ts`
* constructs through. A type-only import, so nothing about `scene.ts` is pulled
* in at runtime and `engine/precip.ts` still imports nothing from it. Without
* this line the first time anyone found out the two had drifted would be the
* moment somebody wired them.
*/
const _factory: PrecipLayerFactory = (w, o) => createPrecipLayer(w, o);
void _factory;
function fieldWith(cells: RadarCell[], wetFraction = 0.02): RadarField {
const built = buildRadarField({
cells,
stations: [{ id: "KHNX", lat: 36.31416, lon: -119.63213, type: "WSR-88D", operability: "RDA - On-line" }],
bounds: CALIFORNIA_CITY.bounds,
coast: CALIFORNIA_CITY.landmasses,
observedAt: "2026-08-23T03:55Z",
wetFraction,
});
assert.ok(built.field, "the fixture must promote or the test asserts nothing");
return built.field;
}
const RAINY = fieldWith([
{ lat: 36.375, lon: -119.625, dbz: 55 },
{ lat: 36.125, lon: -119.625, dbz: 34 },
{ lat: 36.375, lon: -119.375, dbz: 22 },
]);
function meshes(root: THREE.Object3D): THREE.Mesh[] {
const found: THREE.Mesh[] = [];
root.traverse((node) => {
if ((node as THREE.Mesh).isMesh) found.push(node as THREE.Mesh);
});
return found;
}
function triangles(mesh: THREE.Mesh): number {
const geometry = mesh.geometry;
const index = geometry.getIndex();
const count = index !== null ? index.count : (geometry.getAttribute("position")?.count ?? 0);
return count / 3;
}
// ---- Cost -----------------------------------------------------------------
describe("the reflectivity sheet's cost", () => {
it("is exactly one mesh and two triangles with a whole state raining", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(RAINY);
const drawn = meshes(layer.group);
assert.equal(drawn.length, 1);
assert.equal(triangles(drawn[0] as THREE.Mesh), 2);
assert.equal(layer.group.children.length, 1);
assert.equal(layer.drawing(), true);
assert.equal(layer.wetTexels(), 3);
layer.dispose();
});
it("costs one draw call and not the two a double-sided transparent quad costs", () => {
// three.js renders `transparent` + `DoubleSide` in two passes by default —
// back faces then front — so that a closed transparent solid composites
// correctly. Measured on the shipped board with `renderer.info.render.calls`
// before this line existed: 11 calls with no sky layers, **13** with the
// sheet alone. A single flat quad cannot overlap itself, so the second pass
// buys nothing and spends one of the forty-five draws this round has.
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(RAINY);
const material = (meshes(layer.group)[0] as THREE.Mesh).material as THREE.MeshBasicMaterial;
assert.equal(material.side, THREE.DoubleSide, "visible from under the cloud base too");
assert.equal(material.forceSinglePass, true, "…in one pass");
assert.equal(material.premultipliedAlpha, true);
assert.equal(material.depthWrite, false);
layer.dispose();
});
it("contributes zero scene children when nothing is promoted", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
assert.equal(layer.group.children.length, 0, "before any field at all");
layer.setField(RAINY);
assert.equal(layer.group.children.length, 1);
layer.setField(null);
assert.equal(layer.group.children.length, 0, "an invisible mesh is still a mesh");
assert.equal(layer.drawing(), false);
assert.equal(layer.wetTexels(), 0);
layer.dispose();
});
it("holds the whole statewide raster in under eight kilobytes", () => {
const painted = paint(RAINY);
assert.equal(painted.bytes.length, RAINY.rows * RAINY.cols * 4);
assert.ok(painted.bytes.length < 8 * 1024, `${painted.bytes.length} bytes`);
});
it("survives a null, an undefined and a field full of nonsense", () => {
// The consumer is a render loop, so a throw here is a black page.
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(null);
layer.setField(undefined as unknown as RadarField);
layer.setField({ ...RAINY, dbz: [1, 2] } as unknown as RadarField);
layer.setField({ ...RAINY, rows: 0 });
layer.setField({ ...RAINY, cellLat: 0 });
layer.tick(0.016);
assert.equal(layer.group.children.length, 0);
layer.dispose();
});
});
// ---- Which boards ---------------------------------------------------------
describe("which boards carry a raster", () => {
it("is not constructed for the socal or sf packs", () => {
assert.equal(precipFactoryFor(SOCAL_CITY.bounds), null);
assert.equal(precipFactoryFor(SF_CITY.bounds), null);
assert.notEqual(precipFactoryFor(CALIFORNIA_CITY.bounds), null);
});
it("says why, in cells rather than in board names", () => {
// The rule is about the cell, so it survives a board being re-cut — which
// happened to california this very round.
assert.equal(boardCarriesRaster(CALIFORNIA_CITY.bounds), true);
assert.equal(boardCarriesRaster(SOCAL_CITY.bounds), false);
assert.equal(boardCarriesRaster(SF_CITY.bounds), false);
// SoCal is 4.3 cells tall and 6.6 wide at a quarter degree.
const socalRows = (SOCAL_CITY.bounds.maxLat - SOCAL_CITY.bounds.minLat) / 0.25;
assert.ok(socalRows < 5, `socal is ${socalRows.toFixed(1)} cells tall`);
// …and the same board would carry a raster at a tenth of a degree.
assert.equal(boardCarriesRaster(SOCAL_CITY.bounds, 0.08), true);
});
});
// ---- The picture ----------------------------------------------------------
describe("what a texel says", () => {
it("draws unknown as a mark and dry as nothing at all", () => {
// `null` is "nobody is looking there", and a hole drawn as clear sky is a
// claim nobody made. `RADAR_DRY_DBZ` is "a working radar saw under 20".
const field: RadarField = { ...RAINY, dbz: [null, RADAR_DRY_DBZ, 45], rows: 1, cols: 3 };
const { bytes, wet } = paint(field);
assert.equal(wet, 1);
assert.ok((bytes[3] as number) > 0, "unknown is visible");
assert.ok((bytes[3] as number) < 40, "…but faint");
assert.equal(bytes[7], 0, "dry is fully transparent");
assert.ok((bytes[11] as number) > 100, "rain is not");
});
it("climbs steeply off the rain threshold", () => {
assert.equal(alphaForDbz(RADAR_RAIN_DBZ - 0.5), 0);
assert.ok(alphaForDbz(RADAR_RAIN_DBZ) > 0.25, "a cell that has just crossed is already a mark");
assert.ok(alphaForDbz(30) > alphaForDbz(RADAR_RAIN_DBZ) * 1.5);
assert.ok(alphaForDbz(55) > 0.9);
assert.ok(alphaForDbz(70) <= 1);
assert.equal(alphaForDbz(Number.NaN), 0);
});
it("uses the NWS ramp, so 25 dBZ is green and 50 is red", () => {
const { bytes } = paint({ ...RAINY, dbz: [25, 50], rows: 1, cols: 2 });
assert.ok((bytes[1] as number) > (bytes[0] as number), "25 dBZ is green");
assert.ok((bytes[4] as number) > 200 && (bytes[5] as number) < 60, "50 dBZ is red");
});
it("writes premultiplied bytes, so a rain edge fades out rather than to black", () => {
// 1,596 texels stretched across a 554-unit board is a very long linear
// interpolation between a coloured texel and a transparent one. With
// straight alpha the *colour* walks to black on the way, and the first frame
// this layer ever produced had a dark halo round every echo and a hard dark
// line where the lattice is clipped at the board edge. Invisible in a test
// that only checks hue; visible in the first screenshot.
const { bytes } = paint({ ...RAINY, dbz: [RADAR_RAIN_DBZ], rows: 1, cols: 1 });
const alpha = (bytes[3] as number) / 255;
const [r, g, b] = [0, 1, 2].map((i) => bytes[i] as number);
const straight = radarRampRgb(RADAR_RAIN_DBZ);
assert.ok(alpha > 0 && alpha < 1, "the threshold texel is partly transparent");
for (const [was, now] of [[straight[0], r], [straight[1], g], [straight[2], b]]) {
assert.ok(Math.abs((now as number) - (was as number) * alpha) <= 1, `${now} vs ${was} * ${alpha}`);
}
});
});
// ---- Where it sits --------------------------------------------------------
describe("where the sheet sits", () => {
it("sits at the cloud base and lets the Sierra rise through it", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(RAINY);
const sheet = meshes(layer.group)[0] as THREE.Mesh;
// Measured, not assumed: the highest ground on this board is 39.47 units, at
// 36.60,-118.30, because `verticalExaggeration` is 15. The sheet is at 10.6.
// That is deliberate — see the header. A sheet high enough to clear the
// crest is 47 units up, and 47 units of lift under a camera at fifty degrees
// draws the rain seventy-five kilometres from where it fell.
const crest = world.groundAt(36.6, -118.3);
assert.ok(crest > 38 && crest < 41, `the crest measures ${crest.toFixed(2)} units`);
assert.equal(sheet.position.y, world.metres(PRECIP_ALTITUDE_M));
assert.ok(sheet.position.y < crest, "the Sierra rises through the rain, as it should");
// …but it is well clear of the Central Valley floor, which is what it is
// actually a sheet over: 1,350 m at 15x is ten units above a valley at 0.6.
const valley = world.groundAt(36.7, -119.8);
assert.ok(sheet.position.y > valley + 8, `the valley floor is ${valley.toFixed(2)} units`);
// …and far under the aircraft, which fly at 0.01 units to the metre.
assert.ok(sheet.position.y < 10_000 * 0.01);
layer.dispose();
});
it("covers the lattice's own footprint, half a cell outside the outer centres", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(RAINY);
const sheet = meshes(layer.group)[0] as THREE.Mesh;
const [westX] = world.project(RAINY.minLat, RAINY.minLng - RAINY.cellLng / 2);
const [eastX] = world.project(RAINY.minLat, RAINY.minLng + (RAINY.cols - 0.5) * RAINY.cellLng);
// Half a cell is 14 km — invisible, and wrong.
assert.ok(Math.abs(sheet.scale.x - Math.abs(eastX - westX)) < 1e-6);
layer.dispose();
});
it("crossfades a new scan in rather than cutting to it", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN, crossfadeSeconds: 10 });
layer.setField(RAINY);
const wetter = fieldWith([
{ lat: 36.375, lon: -119.625, dbz: 65 },
{ lat: 36.125, lon: -119.625, dbz: 34 },
{ lat: 36.375, lon: -119.375, dbz: 22 },
]);
layer.setField(wetter);
const sheet = meshes(layer.group)[0] as THREE.Mesh;
const map = (sheet.material as THREE.MeshBasicMaterial).map as THREE.DataTexture;
const before = (map.image.data as Uint8Array).slice();
layer.tick(1);
const after = map.image.data as Uint8Array;
assert.notDeepEqual([...after], [...before], "a tick during the fade must move the pixels");
layer.tick(20);
layer.tick(1);
const settled = (map.image.data as Uint8Array).slice();
layer.tick(1);
assert.deepEqual([...(map.image.data as Uint8Array)], [...settled], "…and stop when it lands");
layer.dispose();
});
});
// ---- The rig --------------------------------------------------------------
describe("the sheet and the light rig", () => {
it("constructs no light of any kind", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(RAINY);
layer.group.traverse((node) => {
assert.ok(!(node as THREE.Light).isLight, `${node.name || node.type} is a light`);
});
layer.dispose();
});
it("dims after dark without disappearing, and reads the sky rather than the fill", () => {
const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
layer.setField(RAINY);
const material = (meshes(layer.group)[0] as THREE.Mesh).material as THREE.MeshBasicMaterial;
// The two rigs are the ones measured off the shipped `atmosphere.ts` at
// 20:00Z and 04:35Z — including `hemisphere.intensity`, which is HIGHER at
// night (1.33) than at noon (0.95) because the fill compensates a moonlit
// scene. The first draft of this layer read that as a day/night signal and
// ran the sheet at full strength in the dark; the fixture carries the real
// numbers so that cannot come back.
layer.setLighting({
sky: { top: 0x77a1cb, horizon: 0xe1ebf1 },
hemisphere: { sky: 0xe6f2fb, ground: 0x74786a, intensity: 0.95 },
} as never);
const day = material.opacity;
layer.setLighting({
sky: { top: 0x0d1730, horizon: 0x232f4e },
hemisphere: { sky: 0x374d88, ground: 0x1f2740, intensity: 1.33 },
} as never);
const night = material.opacity;
assert.ok(night < day, `rain at night is darker (${night} vs ${day})`);
assert.ok(night > 0.3, "…but a data overlay that vanishes after sunset is a defect");
assert.ok(day > 0.9, "…and it is at full strength in daylight");
// An interior rig has no sky at all. It must not throw and must not go dark.
layer.setLighting({ hemisphere: { sky: 0xe6f2fb, ground: 0x74786a, intensity: 1 } } as never);
assert.ok(material.opacity > 0.3);
layer.dispose();
});
});
+410
View File
@@ -0,0 +1,410 @@
/**
* The vessel layer, counted rather than looked at.
*
* Three of the four things this file pins are *shapes of the scene graph*, and
* they are here because the picture cannot see them. A harbour drawn from two
* hundred `THREE.Mesh`es and a harbour drawn from one `InstancedMesh` are the
* same photograph and a different frame budget the SoCal mobile cell has
* thirty draw calls spare for the whole of ports and ships and this repo has
* been bitten by exactly that twice already: a suspension bridge at ~34 draws
* and twelve identical freeways that could never merge because each allocated
* its own material.
*
* The fourth is the empty state, which is most days. A layer that draws nothing
* must be a layer nothing *visits*: zero children, not one cheap mesh with
* `count = 0` still walked by the renderer.
*
* The board below is synthetic and small, because none of these facts are about
* California. Scale against real packs is `vehicle/vesselScale.test.ts`'s job.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
KELVIN_HALF_ANGLE_DEG,
VESSEL_HULL_CAPACITY,
WAKE_CAPACITY,
bearingRotation,
createVesselLayer,
hullGeometry,
metresAcross,
wakeLengthMetres,
} from "../../engine/vessels.ts";
import type { City, LightingState, Vessel } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
// ---- A board -------------------------------------------------------------
const BOARD: City = {
id: "test-harbour",
name: "Test Harbour",
center: { lat: 33.72, lng: -118.24 },
bounds: { minLat: 33.5, maxLat: 33.95, minLng: -118.5, maxLng: -118.0 },
latScale: 285,
verticalExaggeration: 3.4,
cellLat: 0.02,
cellLng: 0.024,
coastFalloff: 0.02,
landmasses: [],
parks: [],
inlandWater: [],
hills: [],
districts: [],
landmarks: [],
bridges: [],
roads: [],
chapters: [],
};
/** No `ready()`: nothing in this layer samples the heightfield. Ships float. */
function board(): World {
return new World(BOARD);
}
function moored(id: string, overrides: Partial<Vessel> = {}): Vessel {
return {
id,
kind: "container",
lat: 33.72,
lng: -118.24,
bearing: 118,
length: 300,
beam: 45,
speed: 0,
course: null,
status: "moored",
berthId: "a",
...overrides,
};
}
function underWay(id: string, overrides: Partial<Vessel> = {}): Vessel {
return moored(id, {
speed: 6.2,
course: 210,
bearing: 210,
status: "under-way",
berthId: undefined,
...overrides,
});
}
function instanced(group: THREE.Object3D): THREE.InstancedMesh[] {
return group.children.filter((c): c is THREE.InstancedMesh => (c as THREE.InstancedMesh).isInstancedMesh);
}
function lines(group: THREE.Object3D): THREE.LineSegments[] {
return group.children.filter((c): c is THREE.LineSegments => (c as THREE.LineSegments).isLineSegments);
}
const DAYLIGHT: LightingState = {
sun: { direction: [0.3, 0.8, 0.5], color: 0xfff4e2, intensity: 2.1 },
hemisphere: { sky: 0x8fb6d8, ground: 0x9d9482, intensity: 0.6 },
ambient: { color: 0xffffff, intensity: 0.2 },
sky: { top: 0x2f6fb0, horizon: 0xbcd6e8 },
fog: { color: 0xbcd6e8, near: 100, far: 900 },
};
// ---- One mesh, one line ---------------------------------------------------
describe("the whole board's ships are two draw calls", () => {
it("draws 1 vessel and 200 vessels from exactly one InstancedMesh", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("one")]);
assert.equal(instanced(layer.group).length, 1);
const fleet = Array.from({ length: 200 }, (_, i) =>
underWay(`v-${i}`, { lat: 33.6 + i * 0.001, lng: -118.3 + i * 0.0005 }),
);
layer.setVessels(fleet);
assert.equal(
instanced(layer.group).length,
1,
"two hundred ships must not be two hundred meshes",
);
assert.equal(layer.hullCount(), VESSEL_HULL_CAPACITY, "the instance cap should bind, not grow");
layer.dispose();
});
it("draws every wake on the board from exactly one LineSegments", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("one")]);
assert.equal(lines(layer.group).length, 1);
layer.setVessels(
Array.from({ length: 200 }, (_, i) => underWay(`v-${i}`, { lat: 33.6 + i * 0.001 })),
);
assert.equal(lines(layer.group).length, 1, "one wake buffer, whatever the traffic");
assert.equal(layer.wakeCount(), WAKE_CAPACITY, "the wake cap should bind");
layer.dispose();
});
it("is one hull geometry of about forty triangles", () => {
const geometry = hullGeometry("generic");
const triangles = geometry.getAttribute("position").count / 3;
assert.equal(triangles, 40, `the hull is ${triangles} triangles`);
// Position, normal AND uv, or a future merge silently drops the bucket —
// `airports.ts:43` records that exact scar.
for (const attribute of ["position", "normal", "uv"]) {
assert.ok(geometry.getAttribute(attribute), `the hull has no ${attribute}`);
}
geometry.dispose();
});
it("is wound outward, which a picture found and a sign fixed", () => {
// The first photograph of this hull had its deck wound downward: back-face
// culled, lit from inside, and reading as "the ships came out a bit dark"
// rather than as a hole in the ship. `computeVertexNormals` takes its answer
// from the winding, so this is the only thing standing between a sign error
// and a fleet of hollow boxes.
const geometry = hullGeometry("generic");
const position = geometry.getAttribute("position");
const normal = geometry.getAttribute("normal");
// The divergence theorem: for a closed surface wound outward, the sum of
// r . n over the faces is three times the enclosed volume, and it is
// positive. Flip any face and the sum drops by twice that face's share.
let flux = 0;
let checked = 0;
for (let t = 0; t < position.count; t += 3) {
const a = new THREE.Vector3().fromBufferAttribute(position, t);
const b = new THREE.Vector3().fromBufferAttribute(position, t + 1);
const c = new THREE.Vector3().fromBufferAttribute(position, t + 2);
const face = new THREE.Vector3()
.subVectors(b, a)
.cross(new THREE.Vector3().subVectors(c, a))
.multiplyScalar(0.5);
flux += a.clone().add(b).add(c).divideScalar(3).dot(face);
const n = new THREE.Vector3().fromBufferAttribute(normal, t);
// The two faces whose orientation is unambiguous by inspection: the
// funnel's cap is the highest thing on the ship and the bottom plating is
// the lowest.
if (a.y === b.y && b.y === c.y) {
if (a.y > 1.8) {
assert.ok(n.y > 0.9, "the funnel cap points down");
checked += 1;
}
if (a.y === 0) {
assert.ok(n.y < -0.9, "the bottom plating points up");
checked += 1;
}
}
}
assert.ok(flux > 0, `the hull encloses ${(flux / 3).toFixed(3)} of signed volume`);
assert.ok(checked >= 4, "the horizontal-face check found nothing to check");
geometry.dispose();
});
});
// ---- The buffer is allocated once -----------------------------------------
describe("the wake buffer", () => {
it("is allocated at construction and does not resize across 100 ticks", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("a"), underWay("b", { lat: 33.7, course: 30, bearing: 30 })]);
const line = lines(layer.group)[0];
assert.ok(line);
const positions = line.geometry.getAttribute("position") as THREE.BufferAttribute;
const colors = line.geometry.getAttribute("color") as THREE.BufferAttribute;
const positionLength = positions.array.length;
const colorLength = colors.array.length;
const positionArray = positions.array;
for (let i = 0; i < 100; i++) {
layer.tick(1 / 60);
const now = line.geometry.getAttribute("position") as THREE.BufferAttribute;
assert.equal(now.array.length, positionLength, `the wake buffer resized on tick ${i}`);
assert.equal(now.array, positionArray, `the wake buffer was reallocated on tick ${i}`);
assert.equal(
(line.geometry.getAttribute("color") as THREE.BufferAttribute).array.length,
colorLength,
);
}
// And it is `setDrawRange` that decides how much of it is read.
assert.ok(line.geometry.drawRange.count > 0);
assert.ok(line.geometry.drawRange.count <= positionLength / 3);
layer.dispose();
});
it("holds two rails per wake at the Kelvin half-angle", () => {
// 19.47 degrees regardless of speed. It is a real constant, not a tuned one,
// and using it means the picture is right for a reason.
assert.ok(Math.abs(KELVIN_HALF_ANGLE_DEG - (Math.asin(1 / 3) * 180) / Math.PI) < 0.01);
const world = board();
const layer = createVesselLayer(world, { span: 400 });
layer.setVessels([underWay("a", { course: 0, bearing: 0 })]);
const line = lines(layer.group)[0];
assert.ok(line);
const positions = line.geometry.getAttribute("position") as THREE.BufferAttribute;
const count = line.geometry.drawRange.count;
// Steaming due north, so the wake trails south (+z) and opens in x. The
// widest pair of vertices should sit at the tail, at tan(19.47) of its
// length either side.
let widest = 0;
let deepest = 0;
const shipZ = world.project(33.72, -118.24)[1];
for (let i = 0; i < count; i++) {
widest = Math.max(widest, Math.abs(positions.getX(i)));
deepest = Math.max(deepest, positions.getZ(i) - shipZ);
}
const expected = deepest * Math.tan((KELVIN_HALF_ANGLE_DEG * Math.PI) / 180);
assert.ok(
Math.abs(widest - expected) < expected * 0.25 + metresAcross(world, 45),
`wake half-width ${widest.toFixed(3)} against ${expected.toFixed(3)} at the Kelvin angle`,
);
layer.dispose();
});
});
// ---- A wake is a claim about motion ---------------------------------------
describe("a wake is speed through water, so a moored ship has none", () => {
it("draws no wake behind a berthed hull", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([moored("a"), moored("b", { lat: 33.73 })]);
assert.equal(layer.hullCount(), 2);
assert.equal(layer.wakeCount(), 0);
assert.equal(lines(layer.group).length, 0, "an empty wake buffer must not be visited");
layer.dispose();
});
it("scales the wake with speed and hull length, and stops at a standstill", () => {
assert.equal(wakeLengthMetres(0, 400), 0);
assert.equal(wakeLengthMetres(0.1, 400), 0);
// A 400 m ship at twelve knots: about 1.5 km, which is 3.84 units on SoCal
// and the length at which a moving vessel reads from the whole-board pose.
const full = wakeLengthMetres(6.17, 400);
assert.ok(Math.abs(full - 1_500) < 20, `${full.toFixed(0)} m of wake behind a ULCV`);
// A tug is not a container ship with a shorter name.
assert.ok(wakeLengthMetres(6.17, 30) < 150);
// Half speed, half wake.
assert.ok(Math.abs(wakeLengthMetres(3.085, 400) - full / 2) < 1);
});
it("makes the wake longer than the hull it trails, which is the design claim", () => {
const world = board();
const hull = metresAcross(world, 400);
const wake = metresAcross(world, wakeLengthMetres(6.17, 400));
assert.ok(Math.abs(hull - 1.024) < 0.01, `a 400 m hull is ${hull.toFixed(3)} units`);
assert.ok(wake / hull > 3.5, `the wake is only ${(wake / hull).toFixed(1)}x the hull`);
});
});
// ---- The empty state ------------------------------------------------------
describe("the harbour with no ships in it", () => {
it("contributes zero scene children for an empty list", () => {
const layer = createVesselLayer(board(), { span: 400 });
assert.equal(layer.group.children.length, 0, "a layer nothing has answered must be empty");
layer.setVessels([]);
assert.equal(layer.group.children.length, 0, "the feed answered and this board is empty");
assert.equal(layer.hullCount(), 0);
assert.equal(layer.wakeCount(), 0);
layer.dispose();
});
it("treats null and [] as the same picture", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("a")]);
assert.equal(layer.group.children.length, 2);
layer.setVessels(null);
assert.equal(layer.group.children.length, 0);
layer.dispose();
});
it("survives a tick, a lighting change and a dispose with nothing in it", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setLighting(DAYLIGHT);
layer.tick(1 / 60);
assert.equal(layer.group.children.length, 0);
layer.dispose();
assert.equal(layer.group.children.length, 0);
});
});
// ---- Orientation and motion, through the scene graph ----------------------
describe("where a hull is drawn and which way it faces", () => {
it("turns the bow to the bearing, with north at -z", () => {
// A sign error here sails the whole fleet backwards and is completely
// plausible in a still frame, which is why it is asserted rather than seen.
assert.ok(Math.abs(bearingRotation(0)) < 1e-12);
assert.ok(Math.abs(bearingRotation(90) + Math.PI / 2) < 1e-12);
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([moored("a", { bearing: 90 })]);
const mesh = instanced(layer.group)[0];
assert.ok(mesh);
const matrix = new THREE.Matrix4();
mesh.getMatrixAt(0, matrix);
// `decompose`, not `setFromRotationMatrix`: the instance matrix carries a
// deliberately non-uniform scale — beam, depth, length — and reading a
// quaternion straight off it folds the ship's proportions into its heading.
const rotation = new THREE.Quaternion();
matrix.decompose(new THREE.Vector3(), rotation, new THREE.Vector3());
const bow = new THREE.Vector3(0, 0, -1).applyQuaternion(rotation);
// Bearing 090 is due east, which on this board is +x.
assert.ok(bow.x > 0.99, `the bow points ${bow.x.toFixed(3)} east`);
assert.ok(Math.abs(bow.z) < 0.01);
layer.dispose();
});
it("advances a hull under way along its course, and only along it", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("a", { course: 90, bearing: 90, speed: 6 })]);
const start = layer.positionOf("a");
assert.ok(start);
for (let i = 0; i < 60; i++) layer.tick(1);
const after = layer.positionOf("a");
assert.ok(after);
// Due east for a minute at 6 m/s: 360 m, which is 0.92 units on this board.
assert.ok(after.x - start.x > 0.8, `moved ${(after.x - start.x).toFixed(3)} units east`);
assert.ok(Math.abs(after.z - start.z) < 0.01, "a course of 090 changed the latitude");
layer.dispose();
});
it("leaves a moored hull exactly where the feed put it, for ever", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([moored("a")]);
const start = layer.positionOf("a");
assert.ok(start);
for (let i = 0; i < 600; i++) layer.tick(1);
assert.deepEqual(layer.positionOf("a")?.toArray(), start.toArray());
layer.dispose();
});
it("stops reckoning once the next fix is overdue", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("a", { course: 90, bearing: 90, speed: 6 })]);
for (let i = 0; i < 900; i++) layer.tick(1);
const atLimit = layer.positionOf("a");
for (let i = 0; i < 3_600; i++) layer.tick(1);
assert.deepEqual(layer.positionOf("a")?.toArray(), atLimit?.toArray());
layer.dispose();
});
});
// ---- Lighting -------------------------------------------------------------
describe("the layer owns no light", () => {
it("adds no THREE.Light, at any hour", () => {
const layer = createVesselLayer(board(), { span: 400 });
layer.setVessels([underWay("a"), moored("b")]);
layer.setLighting(DAYLIGHT);
layer.setLighting({ ...DAYLIGHT, sun: { direction: [0, -0.2, 1], color: 0x223355, intensity: 0.1 } });
let lights = 0;
layer.group.traverse((object) => {
if ((object as THREE.Light).isLight) lights += 1;
});
assert.equal(lights, 0, "CONTRACT §4 gives the rig to atmosphere.ts and to nothing else");
layer.dispose();
});
});
+177
View File
@@ -0,0 +1,177 @@
/**
* Ships are drawn at true size, on both boards, and this is the file that says
* so out loud.
*
* ### Why this needs an assertion at all
*
* "Make the ships a bit bigger so you can see them" is a reasonable-sounding
* change that somebody will propose, and there is a precedent in this repo that
* appears to license it: `aircraftGeometry.ts` holds an aeroplane at a fixed
* 0.42 units on every board, which over Southern California is about four times
* life size, and nobody has ever noticed. That oversizing is correct **because an
* aeroplane in flight is alone in the sky with nothing to be wrong against.**
*
* A ship is never alone. It is alongside a quay, under a gantry crane and inside
* a breakwater, all of which `ports.ts` draws at true scale. A hull scaled up to
* be legible is a hull visibly longer than the berth it is lying in, and the
* error is unmissable in exactly the frame the feature exists for. So the
* inversion is deliberate, it is the opposite of the neighbouring convention,
* and a bare number in a mesh builder would not survive a reviewer who
* remembered the aircraft rule.
*
* The legibility oversizing would have bought is bought by the wake instead
* see the last case here, which is the whole design in one ratio.
*
* ### The numbers
*
* They come from the packs rather than from a comment. SoCal declares
* `latScale: 285`, so a unit is 111,320 / 285 = 390.6 m and a 400 m ULCV is
* **1.024 units**. The Bay declares `latScale: 1180`, a unit is 94.3 m, and the
* same ship is **4.24 units** four times bigger for the same steel, which is
* the thing about board scale that is hardest to hold in your head and the
* reason the tolerance below is tight.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { SOCAL_CITY } from "../../cities/socal.ts";
import { SAN_FRANCISCO_CITY } from "../../cities/sf.ts";
import {
WAKE_FULL_SPEED_MPS,
hullDraughtMetres,
hullFreeboardMetres,
hullLengthUnits,
metresAcross,
wakeLengthMetres,
} from "../../engine/vessels.ts";
import { DEFAULT_HULL, beamFor } from "../../server/vessels.ts";
import type { Vessel } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
/** No `ready()`: every number here is projection, and projection is the constructor. */
const socal = new World(SOCAL_CITY);
const bay = new World(SAN_FRANCISCO_CITY);
const ULCV: Pick<Vessel, "kind" | "length" | "beam" | "draught"> = {
kind: "container",
length: 400,
beam: 61,
};
describe("a ship is the size a ship is", () => {
it("draws a 400 m hull as 1.024 units on the Southern California board", () => {
const units = hullLengthUnits(socal, ULCV);
assert.ok(
Math.abs(units - 1.024) < 0.01,
`a 400 m ULCV came out ${units.toFixed(4)} units on SoCal, not 1.024`,
);
// And the board's own scale is where that came from, not a constant.
assert.ok(Math.abs(socal.metresPerUnit - 390.6) < 0.2);
});
it("draws the same hull as 4.24 units on the Bay Area board", () => {
const units = hullLengthUnits(bay, ULCV);
assert.ok(
Math.abs(units - 4.24) < 0.05,
`a 400 m ULCV came out ${units.toFixed(4)} units on the Bay, not 4.24`,
);
assert.ok(Math.abs(bay.metresPerUnit - 94.3) < 0.2);
});
it("is 4.14x bigger on the Bay for the same steel", () => {
const ratio = hullLengthUnits(bay, ULCV) / hullLengthUnits(socal, ULCV);
assert.ok(Math.abs(ratio - socal.metresPerUnit / bay.metresPerUnit) < 1e-9);
assert.ok(ratio > 4 && ratio < 4.3, `boards differ by ${ratio.toFixed(2)}x`);
});
it("does NOT hold a hull at a fixed map-symbol size, the way an aeroplane is held", () => {
// `aircraftGeometry.ts`'s AIRLINER glyph is a flat 0.42 units on every
// board. If a hull were ever given the same treatment these two would be
// equal, and they must never be.
assert.notEqual(hullLengthUnits(socal, ULCV), hullLengthUnits(bay, ULCV));
});
it("scales a tug and a ULCV from one geometry, thirteen times apart", () => {
const tug = { kind: "tug" as const, length: 30, beam: 11 };
const ratio = hullLengthUnits(socal, ULCV) / hullLengthUnits(socal, tug);
assert.ok(Math.abs(ratio - 400 / 30) < 1e-9, "the same solid at 13.3x, not two solids");
// 30 m is 0.077 units on SoCal: a tug alone is genuinely invisible from the
// board pose, which is why it is drawn with a wake or not noticed at all.
assert.ok(hullLengthUnits(socal, tug) < 0.08);
});
it("does not run a plan measurement through the vertical exaggeration", () => {
// `world.metres()` multiplies by `verticalExaggeration` — 3.4 on SoCal —
// because it is for heights. Running a 400 m length through it would make
// the ship 3.5 units long and overhang its berth by two ship lengths. The
// separation between the two functions is the assertion.
assert.ok(socal.metres(400) > 3.4);
assert.ok(Math.abs(socal.metres(400) / metresAcross(socal, 400) - 3.4) < 1e-9);
});
});
describe("the vertical, which is the axis the board does exaggerate", () => {
it("stands a laden box ship about a third of its own length tall", () => {
// Draught plus freeboard, through `world.metres`, is what makes a hull read
// as a solid rather than as a decal — and it is exaggerated on purpose,
// exactly as the 130 m gantry crane beside it is.
const depth = hullDraughtMetres(ULCV) + hullFreeboardMetres(ULCV);
assert.ok(depth > 40 && depth < 50, `${depth.toFixed(1)} m of hull, keel to deck`);
const units = socal.metres(depth);
assert.ok(units > 0.3 && units < 0.45, `${units.toFixed(3)} units tall on SoCal`);
});
it("authors draught rather than observing it, and never as a cargo claim", () => {
// `vessels` in the store has no draught column at all. This is a hull
// dimension, it is derived from the length, and an authored value overrides
// it — but nothing here is ever an observation and no hull is ever labelled
// laden or in ballast. "Empty or full" is answered at the port.
assert.ok(Math.abs(hullDraughtMetres(ULCV) - 14.4) < 0.1);
assert.equal(hullDraughtMetres({ ...ULCV, draught: 12 }), 12);
const tug = hullDraughtMetres({ kind: "tug", length: 30 });
assert.ok(tug > 3 && tug < 5, `a harbour tug drawing ${tug.toFixed(1)} m`);
});
});
describe("the wake is what is actually legible", () => {
it("is 3.75 hull lengths at speed, and 3.84 units on SoCal", () => {
const metres = wakeLengthMetres(WAKE_FULL_SPEED_MPS, 400);
assert.ok(Math.abs(metres - 1_500) < 20, `${metres.toFixed(0)} m of wake`);
const units = metresAcross(socal, metres);
assert.ok(Math.abs(units - 3.84) < 0.06, `${units.toFixed(3)} units of wake on SoCal`);
});
it("is 3.7x more legible than the hull it trails, which is the design", () => {
// At the whole-board pose a 1.02-unit hull is about four pixels and is
// invisible; a 3.84-unit wake is about fourteen and reads. That ratio is why
// the wake is the primary object in this layer and the hull is the thing at
// the sharp end of it.
const hull = hullLengthUnits(socal, ULCV);
const wake = metresAcross(socal, wakeLengthMetres(WAKE_FULL_SPEED_MPS, 400));
assert.ok(Math.abs(wake / hull - 3.75) < 0.01, `the wake is ${(wake / hull).toFixed(2)}x`);
});
it("is 15.9 units on the Bay, where everything is four times bigger", () => {
const units = metresAcross(bay, wakeLengthMetres(WAKE_FULL_SPEED_MPS, 400));
assert.ok(Math.abs(units - 15.9) < 0.2, `${units.toFixed(2)} units of wake on the Bay`);
});
});
describe("the display defaults for a source that sent no dimensions", () => {
it("gives a Panamax-plus box ship its real 400 x 61", () => {
// The ratios are display defaults, stated as ratios because that is what
// they are. The one worth checking is the box ship, which is the hull the
// whole San Pedro complex is shaped around.
assert.ok(Math.abs(beamFor("container", 400) - 61) < 1.5);
assert.ok(Math.abs(beamFor("tug", 30) - 10.7) < 1);
});
it("keeps every default hull a plausible ship rather than a placeholder", () => {
for (const [kind, hull] of Object.entries(DEFAULT_HULL)) {
const ratio = hull.length / hull.beam;
assert.ok(ratio > 2 && ratio < 9, `${kind} is ${ratio.toFixed(1)} long to a beam`);
assert.ok(hull.length > 10 && hull.length < 450, `${kind} is ${hull.length} m long`);
}
});
});