1
0

fix: a coarse lot is a parcel, not a 700 m building

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.
This commit is contained in:
2026-08-24 22:02:47 -07:00
parent 3f0581686f
commit 11d54c48ef
3 changed files with 77 additions and 42 deletions
+17 -36
View File
@@ -992,46 +992,27 @@ Two smaller things, both disclosed by their own tracks and neither blocking:
which is why it neither helps nor hurts. The fix is finer cells there, which is which is why it neither helps nor hurts. The fix is finer cells there, which is
terrain level of detail — the piece that still does not exist. terrain level of detail — the piece that still does not exist.
## An unidentified 700 m slab stands on downtown San Francisco ## ~~An unidentified 700 m slab stands on downtown San Francisco~~ — identified and capped
Found 2026-08-24 by photographing the merged board at a close pose, not by any Found 2026-08-24 by photograph; named 2026-08-24 by reading instance matrices
test. **Reproduce it exactly:** on the merged board at the same pose, not by walking the scene graph.
It was never a mystery mesh. `DETAIL_LOT_TIERS`'s coarsest rung is 400 m, and
tower fill of 1.52.2 — written to assemble a Salesforce-shaped site from 40 m
lots — on a 400 m lot is a 600880 m cube, rotated with `gridAngle`, downtown
palette. The same pose on `?city=sf` uses 40 m lots and does not show it.
The fill is now capped at `NEIGHBOURHOOD_LOT_METRES` (260 m). Fine rungs are
unchanged. `blocksLotLadder.test.ts` holds the cap.
The reproduction still photographs the pose:
``` ```
node scripts/look.mjs slab --url "/" --lat 37.7897 --lng -122.3972 \ node scripts/look.mjs slab --url "/" --lat 37.7897 --lng -122.3972 \
--standoff 2500 --at 2026-08-24T18:30:00Z --wait 11000 --standoff 2500 --at 2026-08-24T18:30:00Z --wait 11000
``` ```
A pale blue-grey slab, roughly 700 m square with visible thickness, rotated onto What the original list correctly ruled out (marker, office glyph, landmark,
San Francisco's street grid, sitting on the Financial District and covering the corridor, port, airport, office-exterior) stays ruled out. The glyph's window
buildings under it. It is the most prominent object in that frame. The same pose pane thickness (`0.012` / `0.028` scene units → 23 m / 54 m on this board) is
on `?city=sf` does not show it, so it is specific to the merged board. a separate scene-unit leftover and is not this slab.
**What it is not**, each ruled out by reading the code rather than by guessing:
- Not a marker. The giant marker spheres in the first photograph of this pose
were a separate defect, fixed in this same round (scene-unit pin constants on a
1,919 m board); with that fixed every sphere is correctly sized and **the slab
is unchanged**.
- Not an office door glyph. `buildingGlyph.ts` is metre-correct (`1 /
world.metresPerUnit` horizontally, `world.metres` vertically) and the three
office sites are 48x42, 54x30 and 36x26 m.
- Not a landmark. San Francisco's largest authored `footprint` is 0.0011° — a
200 m box, and the pack's landmarks render correctly on their own board.
- Not the freeway corridor. `overviewLayers` withdraws it inside
`DETAIL_STANDOFF_M`, and no corridor ribbon is in the frame.
- Not a port or an airport plate. The merged pack carries two ports, both in San
Pedro Bay, and SFO is 13 km south of a frame about 3 km across.
- Not `office-exterior`. That is the Model X apron and it lives in the office
scene at 1 unit = 1 m.
**How to find it**, since three attempts at a runtime probe timed out walking the
scene graph: `__teraCamera` deliberately does not expose the scene, so add a
temporary hook beside `lots()` in `main.ts` that traverses
`record.handle.stageScene.scene` and **filters inside `page.evaluate`** — the
traverse itself is fine, but returning every geometry in the board serialises
megabytes and hangs the call. Filter to `boundingBox` width above 250 m in board
metres and return fifteen rows. That names it in one run.
It is a visual defect on the default board at a pose a visitor reaches by
descending, and it should be fixed before the next round of imagery.
+15 -5
View File
@@ -246,7 +246,7 @@ export function detailLotMetres(tier: number): number {
* 260 m is comfortably above Southern California's 164 and far below * 260 m is comfortably above Southern California's 164 and far below
* California's 806, so neither of the detailed boards changes at all. * California's 806, so neither of the detailed boards changes at all.
*/ */
const NEIGHBOURHOOD_LOT_METRES = 260; export const NEIGHBOURHOOD_LOT_METRES = 260;
/** /**
* Whether the anonymous city goes into the shadow map, given the lot size and * Whether the anonymous city goes into the shadow map, given the lot size and
@@ -525,10 +525,20 @@ export function createBlocks(
// Towers take several lots. A 260 m tower on one 40 m lot is a 25:1 // Towers take several lots. A 260 m tower on one 40 m lot is a 25:1
// needle, and downtown came out looking like a bed of nails; real // needle, and downtown came out looking like a bed of nails; real
// towers assemble their sites, and Salesforce Tower is about 5:1. // towers assemble their sites, and Salesforce Tower is about 5:1.
const fill = isTower ? 1.5 + rand() * 0.7 : 0.78 + rand() * 0.18; let fill = isTower ? 1.5 + rand() * 0.7 : 0.78 + rand() * 0.18;
/*
const width = lot * fill; * Those multipliers assemble a tower site from a neighbourhood lot.
const depth = lot * fill * (0.85 + rand() * 0.3); * On a 400 m lot they draw a 600880 m cube — photographed at FiDi
* from 2.5 km as the unidentified 700 m slab, rotated onto the
* street grid, covering the buildings under it. A coarse lot is a
* parcel: the building sits on it, it is not the building. Fine
* rungs (40 / 80 m) are unchanged, because the cap is larger than
* the fill they already use. Depth is capped too: the 0.851.15
* stretch after fill would otherwise put a 260 m cap at 300 m.
*/
const cap = NEIGHBOURHOOD_LOT_METRES / world.metresPerUnit;
const width = Math.min(lot * fill, cap);
const depth = Math.min(lot * fill * (0.85 + rand() * 0.3), cap);
const rotation = angle + (rand() - 0.5) * 0.03; const rotation = angle + (rand() - 0.5) * 0.03;
const color = new THREE.Color( const color = new THREE.Color(
palette[Math.floor(rand() * palette.length)] ?? 0xd9d3c6, palette[Math.floor(rand() * palette.length)] ?? 0xd9d3c6,
+45 -1
View File
@@ -28,7 +28,13 @@ import test from "node:test";
import * as THREE from "three"; import * as THREE from "three";
import { setReconcile } from "../../cities/reconcile.ts"; import { setReconcile } from "../../cities/reconcile.ts";
import { createBlocks, detailLotMetres, LOT_BUDGET, updateBlocksDetail } from "../../engine/blocks.ts"; import {
createBlocks,
detailLotMetres,
LOT_BUDGET,
NEIGHBOURHOOD_LOT_METRES,
updateBlocksDetail,
} from "../../engine/blocks.ts";
import type { City, District } from "../../engine/types.ts"; import type { City, District } from "../../engine/types.ts";
import { World } from "../../engine/world.ts"; import { World } from "../../engine/world.ts";
@@ -219,3 +225,41 @@ test("every rung is drawn from the same mesh, geometry and material", async () =
assert.equal(blocks.boundingSphere, null, "a stale sphere would hide the city it was not built at"); 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.52.2 on a 400 m lot is
* a 600880 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`,
);
});