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:
@@ -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.5–2.2 — written to assemble a Salesforce-shaped site from 40 m
|
||||||
|
lots — on a 400 m lot is a 600–880 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
@@ -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 600–880 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.85–1.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,
|
||||||
|
|||||||
@@ -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.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`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user