/** * SFO, and the airport kit under it. * * Every defect this file guards against typechecked, threw nothing, and cost * nothing in the performance budget. Each one was found by rendering the board * and looking at it, and each assertion below is the cheapest arithmetic * statement of what the picture showed: * * 1. **A field that was not there.** `terrain.ts` builds its relief mesh at * `world.metres(e) + 0.012` — a bias of its own — so an airport plate laid * a hundredth of a unit above `groundAt` is laid *under the ground*. SFO * rendered as four runways floating on bare terrain with a magenta sliver * of fill visible only where it overhung the bay. Nothing warned. * 2. **Taxiways wound face-down.** A strip whose two rails are emitted * right-before-left has reversed winding, `computeVertexNormals` points * every normal at the ground, and a one-sided material draws nothing. Six * taxiways were simply absent. * 3. **Every building mirrored.** `rotateY` takes the angle whose sine and * cosine are the *scene-space* along-vector, so the bearing is * `atan2(x, z)` and not `atan2(x, −z)`. With the negation SFO's terminal * horseshoe was built on 153.5° instead of 26.5° — not rotated, reflected — * and the aircraft on stand, which never went through that function, parked * in a tidy row beside nothing. * 4. **Two control towers.** The pack already carried a labelled `SFO Control * Tower` landmark, which is what puts SFO on the minimap, and the airport * declared one as well. They stood four hundred metres apart. * * The geography assertions are a different kind. A runway on the wrong bearing * is not a bug in any code — it is a number somebody typed — and the only thing * that catches it is stating the published figure next to the authored one. */ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import * as THREE from "three"; import SF_CITY, { AIRPORTS, SFO, SJC, PENINSULA } from "../../cities/sf.ts"; import { createAirports, parallelTaxiway, runwayThresholds, type Airport, type Runway, } from "../../engine/airports.ts"; import type { LatLng } from "../../engine/types.ts"; import { World } from "../../engine/world.ts"; const METRES_PER_DEGREE_LAT = 111_320; const DEG = Math.PI / 180; /** Metres between two coordinates, flat-earth, which is right at this scale. */ function metresBetween(a: LatLng, b: LatLng): number { const north = (a[0] - b[0]) * METRES_PER_DEGREE_LAT; const east = (a[1] - b[1]) * METRES_PER_DEGREE_LAT * Math.cos(a[0] * DEG); return Math.hypot(north, east); } /** True bearing from `a` to `b`, degrees clockwise from north. */ function bearing(a: LatLng, b: LatLng): number { const north = (b[0] - a[0]) * METRES_PER_DEGREE_LAT; const east = (b[1] - a[1]) * METRES_PER_DEGREE_LAT * Math.cos(a[0] * DEG); return ((Math.atan2(east, north) / DEG) + 360) % 360; } /** Perpendicular distance in metres from a runway's centreline to a point. */ function offsetFromCentreline(runway: Runway, point: LatLng): number { const north = (point[0] - runway.lat) * METRES_PER_DEGREE_LAT; const east = (point[1] - runway.lng) * METRES_PER_DEGREE_LAT * Math.cos(runway.lat * DEG); // The runway's right-hand normal: its heading turned a quarter clockwise. const rightEast = Math.cos(runway.heading * DEG); const rightNorth = -Math.sin(runway.heading * DEG); return east * rightEast + north * rightNorth; } function runwayById(airport: Airport, id: string): Runway { const runway = airport.runways.find((candidate) => candidate.id === id); assert.ok(runway, `${airport.id} has no runway ${id}`); return runway; } /** * San Francisco's real projection with flat ground, which is what SFO stands * on: the field is bay fill and `elevationAt` returns exactly zero across all * of it. A tidy 1:1 world would hide every scale mistake in the kit. */ function bayWorld(): World { const world = new World(SF_CITY); return Object.assign(Object.create(Object.getPrototypeOf(world) as object), world, { groundAt: () => 0, elevationSampled: () => 0, }) as World; } describe("SFO — the numbers a picture cannot check", () => { it("lays both crossing pairs on their true bearings, not their painted ones", () => { // A designator is magnetic and rounded to ten degrees; San Francisco's // declination is ~13.5° east. Building from "28" or "01" puts the whole // airport thirteen degrees out. assert.equal(runwayById(SFO, "10L/28R").heading, 118.6); assert.equal(runwayById(SFO, "10R/28L").heading, 118.6); assert.equal(runwayById(SFO, "1L/19R").heading, 26.5); assert.equal(runwayById(SFO, "1R/19L").heading, 26.5); }); it("crosses the two pairs at very close to a right angle", () => { const crossing = Math.abs( runwayById(SFO, "10L/28R").heading - runwayById(SFO, "1L/19R").heading, ); // 92.1°. An airfield whose runways cross at seventy degrees is a different // airport, and reads as a generic one. assert.ok( Math.abs(crossing - 90) <= 4, `SFO's pairs cross at ${crossing.toFixed(1)}°, which is not SFO`, ); }); it("holds each runway to its published length", () => { // Threshold to threshold, metres, from the published feet. for (const [id, feet] of [ ["10L/28R", 11_870], ["10R/28L", 11_381], ["1L/19R", 7_650], ["1R/19L", 8_650], ] as const) { const runway = runwayById(SFO, id); const metres = feet * 0.3048; assert.ok( Math.abs(runway.length - metres) < 12, `${id} is ${runway.length} m against ${metres.toFixed(0)} m`, ); // And the derived thresholds agree with the declared length, which is what // makes `runwayThresholds` safe for anything downstream to build on. const { low, high } = runwayThresholds(runway); assert.ok(Math.abs(metresBetween(low, high) - runway.length) < 2); assert.ok(Math.abs(bearing(low, high) - runway.heading) < 0.1); } }); it("keeps the two famous parallel separations", () => { // 750 ft between the 28s: the closest parallel pair in the United States // used for simultaneous approaches, and the reason every SFO arrival in low // cloud is a single-file arrival. 700 ft between the 01s. const tenRight = offsetFromCentreline(runwayById(SFO, "10L/28R"), [ runwayById(SFO, "10R/28L").lat, runwayById(SFO, "10R/28L").lng, ]); assert.ok( Math.abs(tenRight - 750 * 0.3048) < 8, `10L/28R to 10R/28L is ${tenRight.toFixed(0)} m, not 229 m`, ); const oneRight = offsetFromCentreline(runwayById(SFO, "1L/19R"), [ runwayById(SFO, "1R/19L").lat, runwayById(SFO, "1R/19L").lng, ]); assert.ok( Math.abs(oneRight - 700 * 0.3048) < 8, `1L/19R to 1R/19L is ${oneRight.toFixed(0)} m, not 213 m`, ); // Sign matters as much as magnitude: 10R is south-south-west of 10L and 1R // is east-south-east of 1L. A mirrored pair is a plausible airport in the // wrong place. assert.ok(tenRight > 0 && oneRight > 0, "both right-hand runways are on the right"); }); it("builds the 28 thresholds out onto the mud, and not past it", () => { const world = new World(SF_CITY); for (const id of ["10L/28R", "10R/28L"] as const) { const { high } = runwayThresholds(runwayById(SFO, id)); assert.equal(world.isLand(high[0], high[1]), true, `${id}'s east threshold is in the bay`); // Within 250 m of the traced bay edge. `PENINSULA` was drawn with "SFO's // bay edge — the runways are built out onto the mud" on the vertex, and // this is the assertion that the runways now honour it rather than // stopping half a mile short. let nearest = Infinity; for (const vertex of PENINSULA) nearest = Math.min(nearest, metresBetween(high, vertex)); assert.ok(nearest < 900, `${id}'s east threshold is ${nearest.toFixed(0)} m from any shore vertex`); } }); }); describe("every airport on the Bay Area board sits somewhere legal", () => { const world = new World(SF_CITY); /** Every authored coordinate an airport puts on the ground. */ function coordinates(airport: Airport): LatLng[] { const out: LatLng[] = [[airport.lat, airport.lng]]; for (const runway of airport.runways) { const { low, high } = runwayThresholds(runway); out.push(low, high, [runway.lat, runway.lng]); } for (const point of airport.field ?? []) out.push(point); for (const apron of airport.aprons ?? []) out.push(...apron.polygon); for (const taxiway of airport.taxiways ?? []) out.push(...taxiway.path); for (const terminal of airport.terminals ?? []) out.push([terminal.lat, terminal.lng]); return out; } for (const airport of AIRPORTS) { it(`${airport.id} is entirely on land`, () => { for (const [lat, lng] of coordinates(airport)) { assert.equal(world.isLand(lat, lng), true, `${airport.id}: ${lat},${lng} is water`); } }); it(`${airport.id} has no district or park scattered over it`, () => { // `blocks.ts` skips no lot for an airport — it has never heard of one — so // the only thing keeping houses off a runway is the district polygon // stopping short of it. `millbrae-burlingame` says so in a comment; this // is the assertion that makes the comment true, and it covers the whole // graded plate rather than only its corners. const field = airport.field; assert.ok(field, `${airport.id} declares no field outline`); for (let lat = 37.34; lat < 37.65; lat += 0.0006) { for (let lng = -122.4; lng < -121.9; lng += 0.0006) { if (!world.pointInPolygon(lat, lng, field)) continue; for (const district of SF_CITY.districts) { assert.equal( world.pointInPolygon(lat, lng, district.polygon), false, `${airport.id}: district ${district.id} reaches ${lat.toFixed(4)},${lng.toFixed(4)}`, ); } assert.equal(world.inPark(lat, lng), false, `${airport.id}: park over ${lat},${lng}`); } } }); } it("draws exactly one control tower at SFO", () => { // The pack's landmark is the tower, because `label: true` is what puts SFO // on the minimap and `minimap.ts` reads `city.landmarks`, not the airport. const towers = SF_CITY.landmarks.filter((landmark) => /SFO/.test(landmark.name)); assert.equal(towers.length, 1); assert.equal(SFO.tower, undefined, "SFO declares a second tower on top of its landmark"); // And it stands with the terminals rather than out on the field. const tower = towers[0]!; let nearest = Infinity; for (const terminal of SFO.terminals ?? []) { nearest = Math.min(nearest, metresBetween([tower.lat, tower.lng], [terminal.lat, terminal.lng])); } assert.ok(nearest < 400, `the tower is ${nearest.toFixed(0)} m from the nearest terminal`); }); }); describe("the pack stays data, and stays derivable", () => { it("keeps SFO's parallel taxiways aligned with the runways they parallel", () => { // The coordinates are typed rather than computed, because a city pack must // not import three.js. This is what stops them drifting: each one still has // to be what `parallelTaxiway` would produce for the runway it belongs to. const spec: Array<[string, string, 1 | -1, number, number]> = [ ["A", "10L/28R", -1, 165, 120], ["B", "10R/28L", 1, 165, 120], ["F", "1L/19R", -1, 110, 100], ["Z", "1R/19L", 1, 165, 100], ]; for (const [id, runwayId, side, offset, trim] of spec) { const authored = (SFO.taxiways ?? []).find((taxiway) => taxiway.id === id); assert.ok(authored, `SFO has no taxiway ${id}`); const derived = parallelTaxiway(runwayById(SFO, runwayId), side, offset, trim).path; assert.equal(authored.path.length, derived.length); authored.path.forEach((point, index) => { const want = derived[index]!; // Five decimal places of latitude is about a metre, which is the // rounding in the pack and nothing else. assert.ok( metresBetween(point, want) < 2, `taxiway ${id} point ${index} is ${metresBetween(point, want).toFixed(1)} m off`, ); }); } }); it("survives the JSON round trip a served pack would take", () => { // CONTRACT.md §2: a pack hand-written as a module and one arriving over HTTP // have to be literally the same thing. An `undefined`-valued key is the way // that quietly stops being true, and `City` is posted to the terrain worker // as a structured clone besides. for (const airport of AIRPORTS) { assert.deepEqual(JSON.parse(JSON.stringify(airport)), airport); assert.doesNotThrow(() => structuredClone(airport)); } }); }); describe("the airport kit's geometry", () => { const world = bayWorld(); const group = createAirports(world, AIRPORTS); /** Every mesh in the group, with its triangle count. */ function meshes(): Array<{ name: string; triangles: number; mesh: THREE.Mesh }> { const out: Array<{ name: string; triangles: number; mesh: THREE.Mesh }> = []; group.traverse((object) => { const mesh = object as THREE.Mesh & { isMesh?: boolean; isInstancedMesh?: boolean; count?: number }; if (!mesh.isMesh) return; const geometry = mesh.geometry; const indices = geometry.index?.count ?? geometry.getAttribute("position").count; const instances = mesh.isInstancedMesh ? (mesh.count ?? 1) : 1; out.push({ name: mesh.name, triangles: (indices / 3) * instances, mesh }); }); return out; } it("costs a handful of draw calls no matter how many airports a board has", () => { // Buckets are shared **across** airports rather than per airport, so the // count is a function of how many kinds of surface an airport has and not of // how many airports there are. Two fields here; SoCal will have six. assert.ok(meshes().length <= 12, `${meshes().length} draw calls for two airports`); }); it("stays far inside its triangle allowance", () => { const total = meshes().reduce((sum, entry) => sum + entry.triangles, 0); // The allowance for this round was 35,000 on the Bay Area cell. Runways are // quads and the only thing here that costs anything is the aircraft on // stand, which are worth it. assert.ok(total < 6_000, `the Bay Area's airports are ${total} triangles`); const runways = meshes().find((entry) => entry.name === "airports:runway"); assert.ok(runways); assert.equal(runways.triangles, AIRPORTS.reduce((n, a) => n + a.runways.length, 0) * 2); }); it("faces every paved surface at the sky", () => { // The taxiways were built right-rail-first and every triangle's normal // pointed at the ground, so a one-sided material drew nothing at all: no // warning, no black stripe, just no taxiways. This is that regression. for (const { name, mesh } of meshes()) { if (!/field|apron|taxiway|runway|markings/.test(name)) continue; const normals = mesh.geometry.getAttribute("normal"); assert.ok(normals, `${name} has no normals and cannot merge`); for (let i = 0; i < normals.count; i += 1) { assert.ok(normals.getY(i) > 0.9, `${name} vertex ${i} faces ${normals.getY(i).toFixed(2)}`); } } }); it("lays the field above the bias terrain.ts gives its own mesh", () => { // `terrain.ts` pushes its relief to `world.metres(e) + 0.012`. An airport at // `groundAt` plus a hundredth is an airport under the ground. const field = meshes().find((entry) => entry.name === "airports:field"); assert.ok(field); field.mesh.geometry.computeBoundingBox(); const y = field.mesh.geometry.boundingBox!.min.y; assert.ok(y > 0.012, `the field plate sits at ${y}, under terrain's own 0.012 bias`); // And the paint is above the concrete, which is above the field. const order = ["airports:field", "airports:apron", "airports:taxiway", "airports:runway", "airports:markings"]; let previous = -Infinity; for (const name of order) { const entry = meshes().find((candidate) => candidate.name === name); assert.ok(entry, `${name} was not built`); entry.mesh.geometry.computeBoundingBox(); const min = entry.mesh.geometry.boundingBox!.min.y; assert.ok(min > previous, `${name} is not above the layer below it`); previous = min; } }); it("turns a terminal to its heading rather than mirroring it", () => { // `rotateY` wants `atan2(x, z)` of the scene-space along-vector. With the // sign of z flipped a building is not rotated but *reflected*, and SFO's // horseshoe came out on 153.5° instead of 26.5°. The test builds one // terminal on a known bearing and measures the box that comes back. const single: Airport = { id: "TEST", name: "one shed", lat: 37.6189, lng: -122.375, elevation: 0, runways: [], terminals: [ // Long and thin on purpose: the two furthest vertices of a box are its // diagonal corners, so a stubby shed measures a couple of degrees off // its own axis for reasons that have nothing to do with the bug. { id: "shed", lat: 37.6189, lng: -122.375, length: 2400, width: 18, height: 4, heading: 26.5 }, ], }; const built = createAirports(world, [single]); const shed = [...built.children].find((child) => child.name === "airports:terminal") as THREE.Mesh; assert.ok(shed, "no terminal was built"); const positions = shed.geometry.getAttribute("position"); // The two vertices furthest apart lie on the long axis, so the bearing // between them is the building's. let best = -1; let a = new THREE.Vector3(); let b = new THREE.Vector3(); const p = new THREE.Vector3(); const q = new THREE.Vector3(); for (let i = 0; i < positions.count; i += 1) { p.fromBufferAttribute(positions, i); for (let j = i + 1; j < positions.count; j += 1) { q.fromBufferAttribute(positions, j); const d = p.distanceTo(q); if (d > best) { best = d; a = p.clone(); b = q.clone(); } } } // Scene space runs x east and z south, so north is −z. const along = b.clone().sub(a); const measured = ((Math.atan2(along.x, -along.z) / DEG) + 360) % 360; // The long axis is a line, not a ray, so either end is correct. const error = Math.min(Math.abs(measured - 26.5), Math.abs(measured - 206.5)); // The mirror this catches is 127° wrong, so a degree of slack for the // diagonal costs the test nothing. assert.ok(error < 1.5, `the shed was built on ${measured.toFixed(1)}° rather than 26.5°`); }); it("parks aircraft against the terminals that declared gates", () => { const stands = meshes().find((entry) => entry.name === "airports:stands"); assert.ok(stands, "nothing is parked at either airport"); const expected = AIRPORTS.flatMap((airport) => airport.terminals ?? []).reduce( (sum, terminal) => sum + (terminal.gates?.count ?? 0), 0, ); const instanced = stands.mesh as THREE.InstancedMesh; assert.equal(instanced.count, expected); // Each one within a wingspan or two of the building it belongs to, which is // the check that caught the mirrored terminals: the stands were exactly // where they should be and the buildings were not. const position = new THREE.Vector3(); const matrix = new THREE.Matrix4(); for (let i = 0; i < instanced.count; i += 1) { instanced.getMatrixAt(i, matrix); position.setFromMatrixPosition(matrix); let nearest = Infinity; for (const airport of AIRPORTS) { for (const terminal of airport.terminals ?? []) { if (!terminal.gates) continue; const [x, z] = world.project(terminal.lat, terminal.lng); nearest = Math.min(nearest, Math.hypot(position.x - x, position.z - z)); } } // Half the longest terminal plus the stand depth, in scene units. assert.ok(nearest < 4.2, `stand ${i} is ${nearest.toFixed(2)} units from any terminal`); } }); it("draws nothing at all for a board with no airports", () => { const empty = createAirports(world, []); assert.equal(empty.children.length, 0); assert.equal(empty.name, "airports"); }); it("is unbothered by a Node run with no canvas", () => { // `markingsAtlas` needs a 2D context and there is none here. The paint has // to fall back to a flat colour rather than throwing, because these tests // and the office's server-side pack checks both run without a DOM. const markings = meshes().find((entry) => entry.name === "airports:markings"); assert.ok(markings); const material = markings.mesh.material as THREE.MeshBasicMaterial; assert.equal(material.map, null); assert.equal(material.toneMapped, false); }); }); describe("SJC", () => { it("puts both parallels on one bearing 700 ft apart", () => { assert.equal(SJC.runways.length, 2); for (const runway of SJC.runways) assert.equal(runway.heading, 131.5); const separation = offsetFromCentreline(SJC.runways[0]!, [ SJC.runways[1]!.lat, SJC.runways[1]!.lng, ]); assert.ok(Math.abs(separation - 700 * 0.3048) < 8, `SJC's parallels are ${separation.toFixed(0)} m apart`); }); it("keeps its plate off the freeway this pack draws beside it", () => { // `BAYSHORE_101` passes about 240 m north-east of runway 12L here, so the // graded plate runs further to the south-west than to the north-east. A // symmetric one had US-101 drawn across the middle of the airport. const world = new World(SF_CITY); for (const [lat, lng] of SF_CITY.roads.flatMap((road) => road.path)) { if (lat < 37.34 || lat > 37.38) continue; assert.equal( world.pointInPolygon(lat, lng, SJC.field!), false, `a road vertex at ${lat},${lng} is inside SJC's plate`, ); } }); });