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:
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user