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
+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();
});
});