1
0

fix: a marker is a true-metre object, not a scene-unit one

`markers.ts` sized a pin in scene units — 1.6 of lift, a 1.1 stem, a 0.16
head — against the Bay Area board, where a unit is 94.3 m and those are a
151 m lift under a 104 m pin. Correct, and correct for exactly one board.

`cities/unify.ts` put San Francisco's districts on a board that is
1,919 m to the unit and `main.ts` draws markers on any board carrying
metro detail, so the same constants became **a 3.07 km lift under a
2.1 km pin with a 614 m sphere on top**, standing over downtown San
Francisco. Photographed at a 2.5 km stand-off they are the most prominent
objects in the frame by an order of magnitude.

Nothing failed. Not a type, not a test, not a budget — twenty triangles
do not move a triangle count. It was found by taking a photograph and
looking at it, which is the third time this exact class has been found in
this engine: `structures.ts`'s 4.7 km freeway ribbon, which `23e4bb8` had
to withdraw, and the aeroplane glyph, which the same commit gave a
true-metre size.

The metre values are exactly what the old constants measured on the Bay
Area board, so that board does not move by a pixel, and the merged board
at the same true stand-off over the same city now draws the same pin at
the same apparent size.

`markerScale.test.ts` asserts the property rather than the numbers: two
boards twenty times apart in scale draw the same marker at the same size
on the ground. Verified to fail against the old constants — 15.1 m
against 307.1 m, and a pin topping out at 71 m against 1,439 m — rather
than assumed to. Its first draft measured the widest bounding sphere in
the layer and got 187 m, which is the *stem* carrying the vertical
exaggeration it is supposed to carry; it asks for the head by geometry
type instead, and says so.

Also filed: an unidentified 700 m slab on the Financial District at the
same pose, with the reproduction, six things it is ruled out as, and why
three runtime probes timed out. It is not this bug and it is still there.
This commit is contained in:
2026-08-24 21:40:36 -07:00
parent ee9ef7d2ec
commit bb7b68dfca
3 changed files with 264 additions and 5 deletions
+44
View File
@@ -991,3 +991,47 @@ Two smaller things, both disclosed by their own tracks and neither blocking:
The detail map's slope correction correctly declines to add grain on top of it,
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.
## An unidentified 700 m slab stands on downtown San Francisco
Found 2026-08-24 by photographing the merged board at a close pose, not by any
test. **Reproduce it exactly:**
```
node scripts/look.mjs slab --url "/" --lat 37.7897 --lng -122.3972 \
--standoff 2500 --at 2026-08-24T18:30:00Z --wait 11000
```
A pale blue-grey slab, roughly 700 m square with visible thickness, rotated onto
San Francisco's street grid, sitting on the Financial District and covering the
buildings under it. It is the most prominent object in that frame. The same pose
on `?city=sf` does not show it, so it is specific to the merged board.
**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.
+50 -5
View File
@@ -31,8 +31,39 @@ export interface MarkerLayer {
* a mid-rise; towers will still occlude, which is honest a pin behind the
* skyline should look like it is behind the skyline.
*/
const PIN_LIFT = 1.6;
const PIN_HEIGHT = 1.1;
/**
* A pin, in **true metres** because the boards are no longer one scale.
*
* These were 1.6 and 1.1 *scene units*, and on the board they were authored for
* that is 151 m of lift and a 104 m pin: tall, deliberately, because a pin has
* to clear a mid-rise. San Francisco is 94.3 m to the unit and every number in
* this file was chosen against that.
*
* `cities/unify.ts` put San Francisco's districts on a board that is **1,919 m
* to the unit**, and `main.ts` draws markers on any board carrying metro detail,
* so the same constants became a 3.07 km lift under a 2.1 km pin with a 614 m
* sphere on top of it twenty times over, sitting on downtown like a moon.
* Photographed at a 2.5 km stand-off it is the most prominent object in the
* frame by an order of magnitude.
*
* This is the third time this exact failure has been found in this engine, which
* is the reason for the length of this comment. `structures.ts`'s freeway
* corridor is a 4.7 km ribbon for the same reason and `23e4bb8` had to withdraw
* it; `engine/aircraftGeometry.ts` was given "a true-metre size" in the same
* commit. **A constant in scene units is a constant that assumes one board.**
* The engine has one board with two decades of scale in it now, and true metres
* are the only units that mean the same thing on all of it.
*
* The metre values are exactly what the old constants measured on the Bay Area
* board, so that board's appearance does not move by a pixel and the merged
* board, at the same true stand-off over the same city, now draws the same pin
* at the same apparent size, which is the property that was wanted all along.
*/
const PIN_LIFT_M = 151;
const PIN_HEIGHT_M = 104;
const PIN_HEAD_RADIUS_M = 15.1;
const PIN_STEM_RADIUS_M = 4.2;
const GHOST_RADIUS_M = 14.1;
export function createMarkerLayer(world: World, palette: MarkerPalette): MarkerLayer {
const group = new THREE.Group();
@@ -44,10 +75,24 @@ export function createMarkerLayer(world: World, palette: MarkerPalette): MarkerL
// One shared geometry per shape; colour varies per instance material, which
// is cheap enough at the scale markers live at (hundreds, not tens of
// thousands — that is what `blocks` is for).
const stemGeo = new THREE.CylinderGeometry(0.045, 0.045, PIN_HEIGHT, 6);
/*
* Every dimension below is a true metre put into this board's units. A pin is
* *lifted* by the exaggeration the same way the ground under it is it has to
* clear a skyline that is itself exaggerated but it is not *stretched* by it,
* so height uses `world.metres` and the two radii do not.
*/
const u = (m: number) => m / world.metresPerUnit;
const PIN_LIFT = world.metres(PIN_LIFT_M);
const PIN_HEIGHT = world.metres(PIN_HEIGHT_M);
const stemGeo = new THREE.CylinderGeometry(
u(PIN_STEM_RADIUS_M),
u(PIN_STEM_RADIUS_M),
PIN_HEIGHT,
6,
);
stemGeo.translate(0, PIN_HEIGHT / 2, 0);
const headGeo = new THREE.SphereGeometry(0.16, 12, 10);
const ghostGeo = new THREE.OctahedronGeometry(0.15);
const headGeo = new THREE.SphereGeometry(u(PIN_HEAD_RADIUS_M), 12, 10);
const ghostGeo = new THREE.OctahedronGeometry(u(GHOST_RADIUS_M));
const materials = new Map<string, THREE.Material>();
const materialFor = (key: string, located: boolean): THREE.Material => {
+170
View File
@@ -0,0 +1,170 @@
/**
* A marker is a true-metre object, and every board has to agree about how big
* it is.
*
* This exists because it did not, and the way it failed is the way this engine
* keeps failing. `markers.ts` sized a pin in **scene units** 1.6 of lift, a
* 1.1 stem, a 0.16 head numbers chosen against the Bay Area board, where a
* unit is 94.3 m and they come out as a 151 m lift under a 104 m pin. Correct,
* and correct for exactly one board.
*
* `cities/unify.ts` then put San Francisco's districts onto a board that is
* **1,919 m to the unit**, and `main.ts` draws markers on any board carrying
* metro detail. The same constants became a 3.07 km lift under a 2.1 km pin
* with a 614 m sphere on it, standing on downtown San Francisco. Nothing
* failed: not a type, not a test, not a budget twenty triangles do not move a
* triangle count. It was found by taking a photograph and looking at it.
*
* So the assertion here is not "the head is 0.16". It is the property that
* makes a photograph unnecessary: **two boards twenty times apart in scale draw
* the same marker at the same size on the ground.** Any future constant that
* forgets what a scene unit is worth breaks this, whatever number it picks.
*
* The same class has now been found three times the freeway corridor's 4.7 km
* ribbon, the aeroplane glyph, and this. If it is found a fourth time, the
* lesson is not another test; it is that scene-unit constants need a type.
*/
import assert from "node:assert/strict";
import test from "node:test";
import * as THREE from "three";
import { setReconcile } from "../../cities/reconcile.ts";
import { createMarkerLayer } from "../../engine/markers.ts";
import type { City, Marker, MarkerPalette } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
setReconcile(false);
/**
* The same square of California at two scales.
*
* `latScale` is the whole of the difference: it is what `World` divides 111,320
* by to get `metresPerUnit`, so 1180 is the Bay Area's 94.3 m to the unit and 58
* is the merged board's 1,919 m. Same ground, same markers, same everything
* else which is what makes a difference in the result attributable.
*/
function board(latScale: number, verticalExaggeration: number): City {
return {
id: `scale-${latScale}`,
name: "Scale Board",
center: { lat: 37.78, lng: -122.42 },
bounds: { minLat: 37.7, maxLat: 37.86, minLng: -122.5, maxLng: -122.34 },
latScale,
verticalExaggeration,
cellLat: 0.01,
cellLng: 0.01,
coastFalloff: 0.02,
landmasses: [
[
[37.71, -122.49],
[37.85, -122.49],
[37.85, -122.35],
[37.71, -122.35],
],
],
parks: [],
inlandWater: [],
districts: [],
landmarks: [],
bridges: [],
roads: [],
chapters: [],
hills: [],
};
}
const PALETTE: MarkerPalette = { applied: 0x4c8dd8 };
const MARKERS: Marker[] = [
{ id: "here", label: "Here", lat: 37.78, lng: -122.42, colorKey: "applied" },
{
id: "nowhere",
label: "Nowhere",
lat: 37.79,
lng: -122.41,
colorKey: "applied",
located: false,
},
];
/** Every mesh in the layer, with its radius in **true metres**. */
async function pinMetres(latScale: number, verticalExaggeration = 3.6) {
const world = new World(board(latScale, verticalExaggeration));
assert.equal(await world.ready(), true, `board at latScale ${latScale} failed to build`);
const layer = createMarkerLayer(world, PALETTE);
layer.setMarkers(MARKERS);
/*
* The **head**, by geometry type, and not "the largest bounding sphere".
*
* The first draft took the widest bounding sphere in the layer and it measured
* 187 m which is not a bug in the pin, it is the *stem*: a cylinder 104 true
* metres tall, drawn through a 3.6x exaggeration, has a bounding sphere half
* its own drawn height. A test that reaches for "the biggest thing" gets the
* one dimension that is *supposed* to carry the exaggeration, and then reports
* the pin as implausible for doing exactly what it should. Ask for the head.
*/
let headRadius = 0;
let top = -Infinity;
layer.group.traverse((node) => {
if (!(node instanceof THREE.Mesh)) return;
node.updateWorldMatrix(true, false);
const geo = node.geometry as THREE.SphereGeometry;
if (geo.type === "SphereGeometry") {
const radius = (geo.parameters?.radius ?? 0) * node.matrixWorld.getMaxScaleOnAxis();
headRadius = Math.max(headRadius, radius * world.metresPerUnit);
}
const at = new THREE.Vector3().setFromMatrixPosition(node.matrixWorld);
if (at.y > top) top = at.y;
});
assert.ok(headRadius > 0, "the layer built no pin head; the fixture is wrong");
return {
/** The head's radius, in metres of real ground. */
headMetres: headRadius,
/** How far the highest piece floats, in true metres above sea level. */
topMetres: world.unitsToMetres(top),
};
}
test("a marker is the same size on the ground whatever the board's scale is", async () => {
// 1180 is the Bay Area's own `latScale` (94.3 m to the unit); 58 is the merged
// California board's (1,919 m). Twenty times apart, which is the whole point.
const metro = await pinMetres(1180);
const state = await pinMetres(58);
assert.ok(
Math.abs(metro.headMetres - state.headMetres) < 0.5,
`a marker is ${metro.headMetres.toFixed(1)} m across on a 94 m/unit board and ` +
`${state.headMetres.toFixed(1)} m on a 1,919 m/unit one. A pin is a true-metre ` +
`object; a constant in scene units is a constant that assumes one board.`,
);
// And it is a *plausible* size, not merely a consistent one — a test that only
// compared the two would pass just as well on two boards drawing a moon.
assert.ok(
metro.headMetres > 5 && metro.headMetres < 60,
`a marker head measures ${metro.headMetres.toFixed(1)} m; that is not a pin`,
);
});
test("a pin floats the same true height over both boards", async () => {
const metro = await pinMetres(1180);
const state = await pinMetres(58);
/*
* Height goes through `world.metres`, so it carries the board's vertical
* exaggeration on purpose: a pin has to clear a skyline that is itself
* exaggerated. Both fixtures declare the same exaggeration, so what is left is
* the horizontal scale and that must not reach the answer at all.
*/
assert.ok(
Math.abs(metro.topMetres - state.topMetres) < 2,
`a pin tops out at ${metro.topMetres.toFixed(0)} m on one board and ` +
`${state.topMetres.toFixed(0)} m on the other`,
);
assert.ok(
metro.topMetres > 80 && metro.topMetres < 500,
`a pin tops out at ${metro.topMetres.toFixed(0)} m, which clears no skyline or every one`,
);
});