fix: withdraw the corridor glyph close in, and give the aeroplane a true-metre size
Two scale lies on the merged board, both of the same kind and one of them exactly the bug `LOT` had: a constant denominated in **scene units**, which means a different real size on every board. **The corridor is 4.7 km wide and it is deliberate.** `createFreewayWorld` draws US-101 and I-5 as an atlas glyph because DRIVE mode has to be able to drive down it on a board where a real freeway is a fifth of a pixel — `main.ts` says so where it sets the corridor altitude. On three separate boards that was invisible, because the only board with a corridor was the one you never came close on. `cities/unify.ts` put cities on that board, and the first photograph of San Francisco from 9 km is two black ribbons four and a half kilometres wide lying across the city, wider than the peninsula at the Golden Gate. `overviewLayers` is the mirror of `detailLayers`: withdrawn when the camera comes close, and empty on every board that is not the merged one. Drive mode is exempt, for the obvious reason. **It withdraws the glyph rather than replacing it, and that is a half-measure with the honest half named.** A true-width corridor is the real answer and it is not one commit: the roadside props are laid out against the slab, so a 44 m ribbon with today's props puts 600 m oaks beside a two-lane road; and drive mode's camera cannot see a true-scale car at all — the near plane is 0.1 units, 192 m on this board, against a car 0.0026 units long. Until both are done, a missing road reads as "not modelled yet" and a 4.7 km slab reads as broken. **And with the slabs gone, the aeroplanes were the size of neighbourhoods.** `AIRLINER_LENGTH` is 0.42 *scene units*. `aircraftGeometry.ts` states what that means without flinching — 40 m over San Francisco, 164 m over the Southland, the same 0.42 on both — and on the statewide board at 1,919.3 m to the unit it is **806 metres**. The focus-distance clamp was working correctly and pinning the glyph to scale 1; scale 1 was the problem. `flightBaseScale` gives the glyph a true-metre size, 60 m, and **floors it at 1 so no board's aeroplanes ever grow**: | board | authored | after | | --- | --- | --- | | sf | 40 m | 40 m — untouched | | socal | 164 m | 60 m | | california / one | 806 m | 60 m | San Francisco ships exactly as it is; the two boards where the authored size was 2.7x and 13.5x life are corrected downward. A fix that only ever removes an exaggeration cannot surprise a board that did not have one. The screen-space floor is unchanged and still lifts the glyph to a legible size wherever it is far away — what changes is only the size it collapses *to* when a real aeroplane would be legible on its own. `glyphScale` takes the floor as a fourth argument defaulting to 1, so the twenty assertions pinning its two-argument behaviour are untouched. **A latent throw fixed on the way.** `applyDetailLod` reads `controlMode` to exempt drive mode, and the LOD is applied once up front — before the line `controlMode` used to be declared on. A `let` is in its temporal dead zone until its declaration *executes*, so that ordering made the merged board throw a `ReferenceError` on the first board it drew. TypeScript cannot catch it: the binding is in scope for the whole function and only the runtime knows the order. All twelve budget cells pass and every board's draw calls fell: bay-area 204 -> 199, socal 206 -> 203, california 373 -> 371, california-one 420 -> 415. 1,705 tests pass. Next, and measured rather than guessed: landmarks go through `world.metres`, which multiplies by `verticalExaggeration`. Los Angeles City Hall at 760 m draws 11,399 m tall on the merged board (15x) and Salesforce Tower draws 1,883 m on San Francisco's own (5.8x). That is the grey cube standing over downtown in the photograph, and it is one board, one exaggeration — an owner's call before it is a code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+53
-5
@@ -1053,6 +1053,43 @@ const GLYPH_MAX_SCALE = 52;
|
|||||||
*/
|
*/
|
||||||
const GLYPH_FOCUS_HEADROOM = 3;
|
const GLYPH_FOCUS_HEADROOM = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long an airliner actually is, in metres.
|
||||||
|
*
|
||||||
|
* **`AIRLINER_LENGTH` is 0.42 *scene units*, and that is the same bug `LOT` had
|
||||||
|
* in `blocks.ts`.** A constant denominated in scene units means a different real
|
||||||
|
* size on every board: `aircraftGeometry.ts` states it plainly — 40 m over San
|
||||||
|
* Francisco, 164 m over the Southland, the same 0.42 on both — and on the
|
||||||
|
* statewide board at 1,919.3 m to the unit it is **806 metres**. That was
|
||||||
|
* invisible while the only board you ever came close on was a metro board.
|
||||||
|
* `cities/unify.ts` put cities on the state board, and the first photograph of
|
||||||
|
* San Francisco from 9 km has aeroplanes the size of neighbourhoods lying over
|
||||||
|
* it.
|
||||||
|
*
|
||||||
|
* 60 m is a mid-size airliner — an A320 is 38, a 777 is 74 — and the exact
|
||||||
|
* number matters much less than the units, because the screen-space floor above
|
||||||
|
* still lifts the glyph to a legible size wherever it is far away. What this
|
||||||
|
* changes is only the size it collapses *to* when the camera is close enough
|
||||||
|
* for a real one to be legible on its own.
|
||||||
|
*
|
||||||
|
* **Floored at 1 in `flightBaseScale`, so no board's aeroplanes ever grow.** On
|
||||||
|
* San Francisco the authored 0.42 units is already 39.6 m and the floor keeps it
|
||||||
|
* exactly as it ships; on the Southland and the statewide board, where the
|
||||||
|
* authored size is 2.7x and 13.5x life, it is corrected downward. A fix that
|
||||||
|
* only ever removes an exaggeration cannot surprise a board that did not have
|
||||||
|
* one.
|
||||||
|
*/
|
||||||
|
export const AIRLINER_TRUE_METRES = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The multiplier that makes the authored glyph true size on this board, never
|
||||||
|
* larger than the glyph as drawn.
|
||||||
|
*/
|
||||||
|
export function flightBaseScale(metresPerUnit: number): number {
|
||||||
|
if (!Number.isFinite(metresPerUnit) || metresPerUnit <= 0) return 1;
|
||||||
|
return Math.min(1, AIRLINER_TRUE_METRES / metresPerUnit / AIRLINER_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The radius of the sphere a pointer actually has to hit, in glyph lengths.
|
* The radius of the sphere a pointer actually has to hit, in glyph lengths.
|
||||||
*
|
*
|
||||||
@@ -1459,6 +1496,11 @@ export function createFlightLayer(world: World): FlightLayer {
|
|||||||
* what an office sky, a chase camera and every test with no controls get.
|
* what an office sky, a chase camera and every test with no controls get.
|
||||||
*/
|
*/
|
||||||
let focusDistance: number | null = null;
|
let focusDistance: number | null = null;
|
||||||
|
/*
|
||||||
|
* Computed once: the board's scale does not change under it, and this is read
|
||||||
|
* per aircraft per frame.
|
||||||
|
*/
|
||||||
|
const baseScale = flightBaseScale(world.metresPerUnit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One material per altitude band, built on demand.
|
* One material per altitude band, built on demand.
|
||||||
@@ -1953,7 +1995,12 @@ export function createFlightLayer(world: World): FlightLayer {
|
|||||||
*/
|
*/
|
||||||
if (viewer !== null) {
|
if (viewer !== null) {
|
||||||
track.mesh.scale.setScalar(
|
track.mesh.scale.setScalar(
|
||||||
glyphScale(viewer.position.distanceTo(track.head), viewer.fov, focusDistance ?? undefined),
|
glyphScale(
|
||||||
|
viewer.position.distanceTo(track.head),
|
||||||
|
viewer.fov,
|
||||||
|
focusDistance ?? undefined,
|
||||||
|
baseScale,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// A heading of 0 is north, and north is -z, so an aircraft whose nose is
|
// A heading of 0 is north, and north is -z, so an aircraft whose nose is
|
||||||
@@ -2165,9 +2212,10 @@ export function glyphScale(
|
|||||||
distance: number,
|
distance: number,
|
||||||
fovDegrees: number,
|
fovDegrees: number,
|
||||||
focusDistance?: number,
|
focusDistance?: number,
|
||||||
|
minScale = 1,
|
||||||
): number {
|
): number {
|
||||||
if (!Number.isFinite(distance) || !Number.isFinite(fovDegrees)) return 1;
|
if (!Number.isFinite(distance) || !Number.isFinite(fovDegrees)) return minScale;
|
||||||
if (distance <= 0 || fovDegrees <= 0 || fovDegrees >= 180) return 1;
|
if (distance <= 0 || fovDegrees <= 0 || fovDegrees >= 180) return minScale;
|
||||||
const legible = legibleScale(distance, fovDegrees);
|
const legible = legibleScale(distance, fovDegrees);
|
||||||
/*
|
/*
|
||||||
* The third argument is optional and omitting it must reproduce the previous
|
* The third argument is optional and omitting it must reproduce the previous
|
||||||
@@ -2182,10 +2230,10 @@ export function glyphScale(
|
|||||||
focusDistance !== undefined && Number.isFinite(focusDistance) && focusDistance > 0
|
focusDistance !== undefined && Number.isFinite(focusDistance) && focusDistance > 0
|
||||||
? Math.min(
|
? Math.min(
|
||||||
GLYPH_MAX_SCALE,
|
GLYPH_MAX_SCALE,
|
||||||
Math.max(1, legibleScale(focusDistance, fovDegrees) * GLYPH_FOCUS_HEADROOM),
|
Math.max(minScale, legibleScale(focusDistance, fovDegrees) * GLYPH_FOCUS_HEADROOM),
|
||||||
)
|
)
|
||||||
: GLYPH_MAX_SCALE;
|
: GLYPH_MAX_SCALE;
|
||||||
return Math.min(ceiling, Math.max(1, legible));
|
return Math.min(ceiling, Math.max(minScale, legible));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+53
-3
@@ -851,9 +851,10 @@ export async function createScene(
|
|||||||
scene.add(createWater(world));
|
scene.add(createWater(world));
|
||||||
scene.add(createShorePlates(world));
|
scene.add(createShorePlates(world));
|
||||||
scene.add(createTerrain(world));
|
scene.add(createTerrain(world));
|
||||||
scene.add(options.roadTraffic
|
const corridorGroup = options.roadTraffic
|
||||||
? createFreewayWorld(world, options.roadTraffic.pack)
|
? createFreewayWorld(world, options.roadTraffic.pack)
|
||||||
: createRoads(world));
|
: createRoads(world);
|
||||||
|
scene.add(corridorGroup);
|
||||||
const buildingReservations: BuildingReservation[] = [];
|
const buildingReservations: BuildingReservation[] = [];
|
||||||
for (const marker of options.markers ?? []) {
|
for (const marker of options.markers ?? []) {
|
||||||
const glyph = marker.glyph;
|
const glyph = marker.glyph;
|
||||||
@@ -948,7 +949,52 @@ export async function createScene(
|
|||||||
* Empty on every other board, because `hasDetail` is false there and
|
* Empty on every other board, because `hasDetail` is false there and
|
||||||
* `applyDetailLod` returns before reading this.
|
* `applyDetailLod` returns before reading this.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Declared **here**, above the detail LOD, and not beside its listeners.
|
||||||
|
*
|
||||||
|
* `applyDetailLod` reads it to exempt drive mode from the corridor withdrawal,
|
||||||
|
* and the LOD is applied once up front — before the line this used to sit on.
|
||||||
|
* A `let` is in its temporal dead zone until its declaration *executes*, so
|
||||||
|
* the earlier position made the merged board throw a `ReferenceError` on the
|
||||||
|
* first board it ever drew. TypeScript does not catch that: the binding is in
|
||||||
|
* scope for the whole function and only the runtime knows the order.
|
||||||
|
*/
|
||||||
|
let controlMode: CityControlMode = "overview";
|
||||||
|
|
||||||
const detailLayers: THREE.Object3D[] = [];
|
const detailLayers: THREE.Object3D[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layers that belong to the wide view and are **withdrawn** when the camera
|
||||||
|
* comes in close — the mirror of `detailLayers`, and empty on every board that
|
||||||
|
* is not the merged one.
|
||||||
|
*
|
||||||
|
* The corridor is the whole of it. `createFreewayWorld` draws US-101 and I-5
|
||||||
|
* as a deliberate atlas glyph about 4.7 km across, because DRIVE mode has to
|
||||||
|
* be able to drive down it on a board where a real freeway is a fifth of a
|
||||||
|
* pixel — `main.ts` says so where it sets the corridor's altitude. On the
|
||||||
|
* three separate boards that was invisible, because the only board with a
|
||||||
|
* corridor was the one you never came close on.
|
||||||
|
*
|
||||||
|
* `cities/unify.ts` put cities on that board, and the first photograph of San
|
||||||
|
* Francisco from 9 km showed what that means: two black ribbons four and a
|
||||||
|
* half kilometres wide lying across the city, wider than the peninsula is at
|
||||||
|
* the Golden Gate. It is the single most dominant thing in every close frame
|
||||||
|
* of the merged board.
|
||||||
|
*
|
||||||
|
* **This withdraws the glyph rather than replacing it**, and that is a
|
||||||
|
* deliberate half-measure with the honest half named. A true-width corridor is
|
||||||
|
* the real answer and it is not one commit: the roadside props are laid out
|
||||||
|
* against the slab (`structures.ts` sets their setback from it), so a 44 m
|
||||||
|
* ribbon with today's props puts 600 m oaks beside a two-lane road; and drive
|
||||||
|
* mode's camera cannot see a true-scale car at all — the near plane is 0.1
|
||||||
|
* units, which is 192 m on this board, against a car 0.0026 units long. Until
|
||||||
|
* both are done, a missing road reads as "not modelled yet" and a 4.7 km slab
|
||||||
|
* reads as broken, and the first is the better of the two.
|
||||||
|
*
|
||||||
|
* Drive mode is exempt, for the obvious reason: it is the mode that needs a
|
||||||
|
* road to be on.
|
||||||
|
*/
|
||||||
|
const overviewLayers: THREE.Object3D[] = [];
|
||||||
let detailShown = true;
|
let detailShown = true;
|
||||||
function applyDetailLod(): void {
|
function applyDetailLod(): void {
|
||||||
if (!hasDetail) return;
|
if (!hasDetail) return;
|
||||||
@@ -975,6 +1021,10 @@ export async function createScene(
|
|||||||
detailShown = want;
|
detailShown = want;
|
||||||
for (const child of detailLandmarks) child.visible = want;
|
for (const child of detailLandmarks) child.visible = want;
|
||||||
for (const layer of detailLayers) layer.visible = want;
|
for (const layer of detailLayers) layer.visible = want;
|
||||||
|
// Withdrawn close in, except when the mode being used is the one that needs
|
||||||
|
// a road under it.
|
||||||
|
const driving = kit.controls.enabled === false || controlMode === "drive";
|
||||||
|
for (const layer of overviewLayers) layer.visible = !want || driving;
|
||||||
}
|
}
|
||||||
// Applied once up front so the opening frame is already correct rather than
|
// Applied once up front so the opening frame is already correct rather than
|
||||||
// correct one tick later, which is a frame the capture harness can catch.
|
// correct one tick later, which is a frame the capture harness can catch.
|
||||||
@@ -1045,6 +1095,7 @@ export async function createScene(
|
|||||||
*/
|
*/
|
||||||
if (hasDetail) {
|
if (hasDetail) {
|
||||||
detailLayers.push(bridgeGroup, airportGroup);
|
detailLayers.push(bridgeGroup, airportGroup);
|
||||||
|
overviewLayers.push(corridorGroup);
|
||||||
if (portLayer) detailLayers.push(portLayer.group);
|
if (portLayer) detailLayers.push(portLayer.group);
|
||||||
if (vesselLayer) detailLayers.push(vesselLayer.group);
|
if (vesselLayer) detailLayers.push(vesselLayer.group);
|
||||||
detailShown = true;
|
detailShown = true;
|
||||||
@@ -1174,7 +1225,6 @@ export async function createScene(
|
|||||||
|
|
||||||
let currentChapter = first.id;
|
let currentChapter = first.id;
|
||||||
const chapterListeners: ((id: string) => void)[] = [];
|
const chapterListeners: ((id: string) => void)[] = [];
|
||||||
let controlMode: CityControlMode = "overview";
|
|
||||||
const controlModeListeners: ((mode: CityControlMode) => void)[] = [];
|
const controlModeListeners: ((mode: CityControlMode) => void)[] = [];
|
||||||
|
|
||||||
function applyControlMode(requested: CityControlMode): CityControlMode {
|
function applyControlMode(requested: CityControlMode): CityControlMode {
|
||||||
|
|||||||
Reference in New Issue
Block a user