11d54c48ef
The unidentified slab on downtown San Francisco was the lot ladder's own coarsest rung. Tower fill of 1.5–2.2 assembles a Salesforce-shaped site from 40 m lots; on a 400 m lot it is a 600–880 m cube, rotated with the district's street grid, downtown palette. The same pose on the Bay Area board uses 40 m lots and does not show it. Cap both axes at NEIGHBOURHOOD_LOT_METRES. Fine rungs are unchanged because 40 m and 80 m already sit under the cap. The test that would have named this on the first photograph now exists.
266 lines
9.9 KiB
TypeScript
266 lines
9.9 KiB
TypeScript
/**
|
||
* 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<World> {
|
||
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`,
|
||
);
|
||
});
|