Two independent reviewers found the same defect in `a2a04e0`, which is
the only reason it is being fixed rather than shipped.
`DetailRange.r` is `reach` from the lattice sweep: the half-diagonal of
the district's lat/lng bounding box plus a lot. That is a **plan**
radius, and it is a good one — measured over all 59,166 instances, every
lot is inside its own district's `r` of the centre with metres to spare.
It was then used as the radius of a sphere pinned at `y = 0`, which is a
different claim, and one this board does not support: a lot's local y is
the ground under it times `verticalExaggeration`, and that is 15 on the
merged board. **39 of 97 district ranges had lots outside their own cull
sphere**, the worst by 4.78 units — 9.2 km of scene height, in the San
Gabriel foothills. Swept over 32,400 poses built the way `scene.ts`
builds them, 61 dropped lots that project strictly inside the NDC cube.
Hillside buildings vanishing while you look at them.
So the range carries `y0`/`y1` — walked once per district at build, over
lots that were just pushed — and the test is `Frustum.intersectsBox`
instead of `intersectsSphere`. Same six plane tests, and exact for the
volume being described rather than right in x/z and wrong in y.
Measured, SF 2.5 km: 215,598 → 216,392 triangles. The fix *adds* 794,
which is the shape of a correct answer here — it stops discarding
geometry that was on screen. The whole-board pose is unchanged and draws
are unchanged at 437.
The doc comments that asserted the invariant this violated are rewritten
rather than deleted; they were the part most likely to survive a fix and
mislead. Also corrected there: the pack declares 99 detail districts but
ten emit no lot and carry no range, so the quoted reach statistics are
over 89, not 99 — the file and the hand-off report disagreed.
`blocksDetail.test.ts` gains a peak board, because the existing fixture
cannot express this: it exaggerates by 2 with a 200 m hill, so every
district's height is a rounding error next to its plan radius and a
y=0 volume contains it by accident. The new board is one small district
on a 3,000 m peak at 15x — lots at y 36.09-39.28 on a 2.70-unit
footprint. Verified to fail against the old volume and pass against the
box, rather than assumed to.
`createBlocks` returns one `InstancedMesh` for every anonymous building on
the board and three culls per *object*, so its bounding sphere contains
California and nothing about it is ever rejected. Standing 2.5 km over
Los Angeles the merged board packed 48,081 detail lots and 2,839 base
lots — **480,810 triangles of buildings alone against a 400,000 cap**, two
thirds of them behind the camera or off the sides. No budget cell stands
there, which is why nothing had measured it. It was a live breach.
`DetailRange` now carries the district's `reach` — already computed to
size the lattice sweep and thrown away — and a range is recorded for
*every* district that emitted a lot, base ones included. A range is packed
when it is within reach and its sphere is in frustum, which drops the base
set at any close pose: it used to be a fixed prefix of the buffer and was
therefore drawn wherever the camera stood.
Measured at 2.5 km of stand-off, reach only against reach and frustum:
San Francisco 3,546 detail + 2,839 base becomes 1,987 + 0; Los Angeles
48,081 + 2,839 becomes 15,067 + 0. **For zero extra draw calls** — this is
the one place where culling is free, because `WebGLIndexedBufferRenderer`
returns on `primcount === 0`, so a repack to nothing is cheaper than one
draw and six chunk meshes would have cost five draws to buy the same
28,390 triangles.
The pad is 2% of the stand-off rather than a distance, because it covers
one frame of lag and a frame is a long way at 400 km and nothing at 2.5.
Measured, a fixed 4 km pad packs 8,079 lots at 2.5 km over Los Angeles for
nothing; 2% packs what no pad packs, and is worth 966 lots at 7.7 km.
Two hazards that come with a moving packing, both handled: `count` starts
at 0 rather than at `baseCount`, so `hasDetail` asks the store whether it
exists instead of subtracting (the old expression would be negative); and
`InstancedMesh.boundingSphere` is nulled on every repack, because
`Frustum.intersectsObject` computes it once over whatever `count` held and
never again — a sphere computed over San Francisco would have hidden Los
Angeles for the rest of the session.
`updateBlocksDetail` with no frustum behaves exactly as it did, which is
what every board that is not the merged one gets, and what the one
up-front call at scene build gets so the opening frame is not a district
short. It does not fix the 110 km pose over Los Angeles, where all 42
in-reach districts genuinely are in frustum: closing that needs a coarser
`DETAIL_LOT_METRES` at long stand-off, which is separate work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>