/** * Lot size follows the camera. * * `DETAIL_LOT_TIERS` used to be one number, 160 m, chosen as a compromise for a * board that was looked at from one distance. The merged board is not: at 2.5 km * over San Francisco it drew 1,987 buildings with 184,000 triangles of the * budget unspent, and at 7.7 km over Los Angeles it drew 40,000 and measured * 621,866 triangles against a 400,000 cap. Both are the same fault, and the fix * is that a detail district is built at several lot sizes and the board picks * the finest one whose *visible* lots fit a budget. * * Four things hold that honest, and none of them is a number out of the merged * pack — the board is synthetic for the reason `seaAndTerrain.test.ts` gives: * * - the ladder is a ladder, so a smaller budget never draws more; * - the budget is obeyed, and where it cannot be — the coarsest rung is over * it — the mesh still has room, which is the allocation's whole claim; * - **the buildings actually get smaller**, which is the product claim and * the one a count alone does not make: a board that drew the same lots and * merely hid some of them would pass the first two; * - it is still one mesh, one geometry and one material, because the board it * runs on has twenty-two spare draw calls out of four hundred and sixty and * a second instance set would spend one of them. */ import assert from "node:assert/strict"; import test from "node:test"; import * as THREE from "three"; import { setReconcile } from "../../cities/reconcile.ts"; import { createBlocks, detailLotMetres, LOT_BUDGET, NEIGHBOURHOOD_LOT_METRES, updateBlocksDetail, } from "../../engine/blocks.ts"; import type { City, District } from "../../engine/types.ts"; import { World } from "../../engine/world.ts"; setReconcile(false); /** A square district, `half` degrees to a side, centred on (lat, lng). */ function district(id: string, lat: number, lng: number, half: number, detail?: true): District { return { id, name: id, polygon: [ [lat - half, lng - half], [lat + half, lng - half], [lat + half, lng + half], [lat - half, lng + half], ], gridAngle: 0, minHeight: 20, maxHeight: 120, towerChance: 0.05, palette: "downtown", ...(detail === true ? { detail } : {}), }; } /** * One base district and two metro ones of deliberately different sizes. * * The size difference is the fixture's whole point, because it is the merged * board's own shape: `TIER_DISTRICT_LOTS` refuses a fine rung to a district * that would be enormous at it, so `downtown` — two kilometres across, like the * Financial District — gets the whole ladder and `sprawl` gets only its coarse * end, exactly as West Covina does. A fixture with one district size would * never exercise the coarse-fill that lets the two be drawn in the same frame. */ const CITY: City = { id: "ladder-board", name: "Ladder Board", center: { lat: 37, lng: -122 }, bounds: { minLat: 36, maxLat: 38, minLng: -123, maxLng: -121 }, latScale: 100, verticalExaggeration: 2, cellLat: 0.05, cellLng: 0.05, coastFalloff: 0.02, landmasses: [ [ [36.1, -122.9], [37.9, -122.9], [37.9, -121.1], [36.1, -121.1], ], ], parks: [], inlandWater: [], districts: [ district("statewide", 37, -122, 0.08), district("downtown", 37.4, -122.4, 0.012, true), district("sprawl", 37.5, -121.6, 0.05, true), ], landmarks: [], bridges: [], roads: [], chapters: [], hills: [{ name: "swell", lat: 37, lng: -122, elevation: 200, radius: 0.5 }], }; async function built(): Promise { const world = new World(CITY); assert.equal(await world.ready(), true, "the synthetic board failed to build a heightfield"); return world; } /** The smallest building footprint currently packed, in scene units. */ function smallestFootprint(mesh: THREE.InstancedMesh): number { const m = mesh.instanceMatrix.array as Float32Array; let smallest = Infinity; for (let i = 0; i < mesh.count; i++) { const at = i * 16; const width = Math.hypot(m[at]!, m[at + 1]!, m[at + 2]!); if (width < smallest) smallest = width; } return smallest; } /** Everything in reach, no frustum, at one lot budget. */ function packAt(world: World, mesh: THREE.InstancedMesh, budget: number): number { const [x, z] = world.project(37, -122); updateBlocksDetail(mesh, x, z, 1_000, undefined, 0, budget); return mesh.count; } test("a smaller lot budget never draws more of the city", async () => { const world = await built(); const blocks = createBlocks(world); const capacity = (blocks.instanceMatrix.array.length / 16) | 0; /* * A sweep rather than two points, because the claim is that this is a ladder * and not a switch: several rungs have to be reachable, in order, or the * mechanism is a boolean with extra steps. */ const budgets = [LOT_BUDGET, 6_000, 4_000, 3_000, 2_000, 100]; const counts = budgets.map((b) => packAt(world, blocks, b)); for (let i = 1; i < counts.length; i += 1) { assert.ok( counts[i]! <= counts[i - 1]!, `budget ${budgets[i]} drew ${counts[i]} against ${counts[i - 1]} at ${budgets[i - 1]}`, ); } assert.ok( new Set(counts).size >= 3, `the ladder has one rung in practice: ${[...new Set(counts)].join(", ")}`, ); assert.ok( counts[0]! > counts[counts.length - 1]! * 2, `the ladder spans nothing: ${counts[0]} at the top and ${counts[counts.length - 1]} at the bottom`, ); for (const count of counts) { assert.ok(count <= capacity, `${count} instances is past the end of a ${capacity} buffer`); } }); test("the budget is a budget, and the mesh has room when it cannot be met", async () => { const world = await built(); const blocks = createBlocks(world); const capacity = (blocks.instanceMatrix.array.length / 16) | 0; /* * Above the coarsest rung's own total there is nothing left to give, so the * board draws it and goes over — which is legal and is why the capacity is * `max(budget, the coarsest rung)` rather than the budget. What is *not* * legal is a rung that fits being drawn past the budget. */ const floor = packAt(world, blocks, 1); for (const budget of [2_000, 4_000, 8_000, LOT_BUDGET]) { const count = packAt(world, blocks, budget); assert.ok( count <= Math.max(budget, floor), `budget ${budget} packed ${count}, past both it and the ${floor}-lot floor`, ); assert.ok(count <= capacity, `${count} instances is past the end of a ${capacity} buffer`); } }); test("the buildings themselves get smaller, which is the whole claim", async () => { const world = await built(); const blocks = createBlocks(world); packAt(world, blocks, 100); const coarse = smallestFootprint(blocks); packAt(world, blocks, LOT_BUDGET); const fine = smallestFootprint(blocks); /* * A count can fall for two reasons and only one of them is this one: a board * that kept 160 m lots and merely stopped drawing some of them would pass * every assertion above. So read the instance matrices and measure a * building. The rungs are at least 1.25x apart in lot and the ladder spans * 40 m to 400 m, so half is a wide margin around a real effect. */ assert.ok( fine < coarse * 0.5, `the smallest building is ${fine.toFixed(4)} units at a full budget and ${coarse.toFixed(4)} at none`, ); assert.equal(detailLotMetres(0) < detailLotMetres(5), true, "the ladder is not finest-first"); }); test("every rung is drawn from the same mesh, geometry and material", async () => { const world = await built(); const blocks = createBlocks(world); const geometry = blocks.geometry; const material = blocks.material; /* * The board this ships on has 438 draw calls against a 460 cap on the desktop * profile and 434 against 455 on mobile. `mesh.count` is the level of detail * precisely because a second `InstancedMesh` — a fine tier and a coarse tier * cross-fading, say — would be non-empty at the same time as the first and * cost one of the twenty-two that are left. So: the ladder may move `count` * and rewrite the attribute buffers, and may not acquire an object. */ for (const budget of [LOT_BUDGET, 3_000, 100, LOT_BUDGET]) { packAt(world, blocks, budget); assert.equal(blocks.geometry, geometry, "a rung swapped the geometry out"); assert.equal(blocks.material, material, "a rung swapped the material out"); assert.equal(blocks.boundingSphere, null, "a stale sphere would hide the city it was not built at"); } }); /** * The largest building footprint currently packed, in scene units. * * Same extract as `smallestFootprint`: the first three entries of each 4×4 * instance matrix are the X-axis of the building, whose length is its width. * Depth is the Z-axis at offsets 8, 9, 10. */ function largestFootprintM(mesh: THREE.InstancedMesh, metresPerUnit: number): number { const m = mesh.instanceMatrix.array as Float32Array; let largest = 0; for (let i = 0; i < mesh.count; i++) { const at = i * 16; const width = Math.hypot(m[at]!, m[at + 1]!, m[at + 2]!); const depth = Math.hypot(m[at + 8]!, m[at + 9]!, m[at + 10]!); const span = Math.max(width, depth) * metresPerUnit; if (span > largest) largest = span; } return largest; } test("a coarse lot is a parcel, not a 700 m building", async () => { const world = await built(); const blocks = createBlocks(world); /* * Budget 100 forces the coarsest rung — 400 m lots on this fixture, the * same rung the merged board picked over FiDi when the 700 m slab was * photographed. Without the cap, tower fill of 1.5–2.2 on a 400 m lot is * a 600–880 m cube. With it, nothing on the board is wider than a block. */ packAt(world, blocks, 100); assert.ok(blocks.count > 0, "the coarsest rung packed nothing"); const widest = largestFootprintM(blocks, world.metresPerUnit); assert.ok( widest <= NEIGHBOURHOOD_LOT_METRES + 1, `a ${widest.toFixed(0)} m building on a coarse lot is the FiDi slab`, ); });