/** * The draw-call reclaim in `engine/structures.ts`. * * These are budget tests, and they are here because the budget is the reason * anything else in this build can be made to look better. The city measured 616 * draw calls against a cap of 650 while the office spent 8% of its triangle * allowance: indoors quality is nearly free, outdoors it is not, and every call * this module gives back is one the exterior Model X and the aircraft get to * spend. `scripts/performance-budget.mjs` is the real gate, but it needs a * built bundle, a browser and eleven seconds a cell — these run in * milliseconds and fail on the line that caused the regression. * * Two invariants, and they are the two ways this file has gone wrong before: * * 1. **A material is per colour, not per call site.** `roadRibbon` used to * close over `new THREE.MeshLambertMaterial({ color })`, so twelve * identical asphalt decks were twelve materials — and two meshes that do * not share a material can never be merged, whatever else you do. * 2. **Geometry is merged per bucket.** A suspension bridge used to arrive as * about thirty-four meshes of one colour. * * There is a third thing the tests below quietly guard, and it is the one that * fails silently: `mergeGeometries` returns `null` when the attribute sets * disagree, so a ribbon without UVs sitting in a bucket beside a tube that has * them loses the whole bucket. Asserting on merged vertex counts is what catches * that, because a dropped bucket looks exactly like a very efficient one. */ import assert from "node:assert/strict"; import test from "node:test"; import * as THREE from "three"; import { createBridge, createBridges, createRoads } from "../../engine/structures.ts"; import type { Bridge, City, Road } from "../../engine/types.ts"; import type { World } from "../../engine/world.ts"; /** * The smallest thing `structures.ts` will accept: San Francisco's projection, * ground at sea level, and no heightfield. * * A real `World` builds one, which is 0.53M lattice points and a couple of * seconds — none of which any assertion here depends on. The *scale* does * matter and used to be a tidy 20 units per degree with metres straight * through: `bridges.ts` sizes its members against `metresPerUnit` and compares * span lengths against tower heights, so a world whose projection and whose * `metres()` disagree gives a bridge nothing real to be checked against. */ const LAT_SCALE = 1180; const CENTRE = { lat: 37.7749, lng: -122.4194 }; const METRES_PER_UNIT = 111_320 / LAT_SCALE; function flatWorld(city: Partial): World { const lngScale = LAT_SCALE * Math.cos((CENTRE.lat * Math.PI) / 180); return { city: { roads: [], bridges: [], inlandWater: [], ...city } as unknown as City, project(lat: number, lng: number): [number, number] { return [(lng - CENTRE.lng) * lngScale, -(lat - CENTRE.lat) * LAT_SCALE]; }, groundAt(): number { return 0; }, metres(value: number): number { return (value / METRES_PER_UNIT) * 3.6; }, metresPerUnit: METRES_PER_UNIT, } as unknown as World; } const GOLDEN_GATE: Bridge = { name: "golden-gate", path: [ [37.806, -122.4756], [37.8199, -122.4783], [37.8324, -122.4796], ], towers: [ [37.8104, -122.4767], [37.8249, -122.4787], ], deckHeight: 67, towerHeight: 227, sag: 0.45, color: 0xc0553b, }; /** Everything painted the bridge's own colour, which is everything but the road. */ function structureOf(root: THREE.Object3D): THREE.Mesh | undefined { return meshes(root).find((mesh) => mesh.name !== "bridge:roadway"); } function meshes(root: THREE.Object3D): THREE.Mesh[] { const found: THREE.Mesh[] = []; root.traverse((object) => { if (object instanceof THREE.Mesh) found.push(object); }); return found; } function materialsIn(root: THREE.Object3D): Set { const set = new Set(); for (const mesh of meshes(root)) { if (Array.isArray(mesh.material)) for (const material of mesh.material) set.add(material); else set.add(mesh.material); } return set; } // ---- Bridges --------------------------------------------------------------- test("a suspension bridge is two draw calls: structure and roadway", () => { const bridge = createBridge(flatWorld({}), GOLDEN_GATE); // The spec's number was six for a bridge painted one colour throughout, and // it is two now for a reason worth stating: the deck of a bridge is a road, // and painting it International Orange with the towers is most of why the // Golden Gate used to read as a red line. Everything structural is still one // material — anything above two is a part that fell out of a bucket. const distinct = materialsIn(bridge); assert.equal(distinct.size, 2, `the bridge holds ${distinct.size} materials, not two`); assert.equal(meshes(bridge).length, 2, "the bridge did not merge into two meshes"); assert.ok(structureOf(bridge), "nothing in the bridge is painted the bridge's colour"); }); test("merging kept every part of the bridge", () => { const bridge = createBridge(flatWorld({}), GOLDEN_GATE); const merged = structureOf(bridge); assert.ok(merged); // The arithmetic, because a bucket that failed to merge comes out as one // *part* of a bridge and otherwise looks entirely healthy. Two towers are six // frusta, two fenders and five struts each — 13 boxes, 24 vertices apiece — // the deck box is four strips over about fifty stations, and the cables and // their hangers are the rest. Two thousand is well under the floor. const vertices = merged.geometry.getAttribute("position").count; assert.ok(vertices > 2_000, `the bridge merged down to ${vertices} vertices`); // The merge only happens because every part carries the same attributes. for (const name of ["position", "normal", "uv"]) { assert.ok(merged.geometry.getAttribute(name), `the merged bridge has no ${name}`); } assert.ok(merged.geometry.getIndex(), "the merged bridge lost its index"); // A 227 m tower is the tallest thing on the board; it has to cast. assert.equal(merged.castShadow, true); }); test("the bridge is still shaped like a bridge after the merge", () => { const world = flatWorld({}); const bridge = createBridge(world, GOLDEN_GATE); const merged = structureOf(bridge); assert.ok(merged); merged.geometry.computeBoundingBox(); const box = merged.geometry.boundingBox; assert.ok(box); // Towers to 8.66 units — 227 m at 94 m per unit and 3.6× exaggeration — with // the deck and its cables below. Baking the transforms into the geometry is // where a merge goes wrong: a part that lost its translation collapses onto // the origin and the box stops matching. const top = world.metres(227); assert.ok(Math.abs(box.max.y - top) < 0.05, `the towers top out at ${box.max.y.toFixed(2)}`); // Tower feet and pier footings go under the surface on purpose; nothing // should be a whole tower's worth of them. assert.ok(box.min.y > -1, `something sank to ${box.min.y.toFixed(2)}`); assert.ok(box.max.z - box.min.z > 20, "the bridge has no span"); }); test("two bridges are three draw calls, not sixty-eight", () => { const second: Bridge = { ...GOLDEN_GATE, name: "bay-bridge", color: 0x9aa6ad }; const group = createBridges(flatWorld({ bridges: [GOLDEN_GATE, second] })); // Two structures — different colours, so genuinely two materials — and one // roadway, because both decks are the same asphalt and one batch covers the // whole board. That batch still cannot outlive the build: `createScene() // .dispose()` walks the scene disposing every material it finds, and a cache // that survived would hand the next board a disposed one. assert.equal(meshes(group).length, 3); assert.equal(materialsIn(group).size, 3); const names = meshes(group).map((mesh) => mesh.name).sort(); assert.deepEqual(names, ["bay-bridge", "bridge:roadway", "golden-gate"]); }); // ---- Roads ----------------------------------------------------------------- test("identical roads share one material and one mesh", () => { const street: Road = { kind: "street", width: 0.1, path: [ [37.7, -122.4], [37.75, -122.42], [37.8, -122.45], ], }; const group = createRoads(flatWorld({ roads: [street, street, street] })); // Three streets, one colour: one draw call. Before the cache this was three // materials and three meshes, and it scaled with the pack. assert.equal(materialsIn(group).size, 1); assert.equal(meshes(group).length, 1); const merged = meshes(group)[0]; assert.ok(merged); // All three really are in there — three drapes of the same path. const vertices = merged.geometry.getAttribute("position").count; assert.ok(vertices > 100, `three roads merged to ${vertices} vertices`); assert.ok(merged.geometry.getAttribute("uv"), "the road deck lost the UVs merging depends on"); }); test("a freeway carries its markings as texture, not as a second ribbon", () => { const freeway: Road = { kind: "freeway", width: 0.14, path: [ [37.7, -122.4], [37.9, -122.45], ], }; const group = createRoads(flatWorld({ roads: [freeway] })); // One ribbon, one call. The median stroke used to be a second draped ribbon // in a second colour; verge, shoulders, edge lines and median now live in the // surface texture, which costs no triangles and reads as a road rather than // as a line on a map. assert.equal(meshes(group).length, 1); assert.equal(materialsIn(group).size, 1); }); test("a road is drawn wider than its carriageway, and streets less so", () => { const shape = (kind: Road["kind"]): number => { const road: Road = { kind, width: 0.2, path: [ [37.7, -122.4], [37.9, -122.4], ], }; const mesh = meshes(createRoads(flatWorld({ roads: [road] })))[0]; assert.ok(mesh); mesh.geometry.computeBoundingBox(); const box = mesh.geometry.boundingBox; assert.ok(box); return box.max.x - box.min.x; }; // The widening is the graded right-of-way the texture paints, and a freeway // gets more of it than a boulevard does. Both are wider than the authored // 0.2, which is the carriageway alone. const street = shape("street"); const freeway = shape("freeway"); assert.ok(street > 0.2 && street < 0.35, `a street came out ${street.toFixed(3)} wide`); assert.ok(freeway > street, "a freeway is no wider than a street"); });