1
0

The same bug the camera had, still live in the default light rig

Three loose ends turned into four, because one of them was not a stale comment.

`cityDaylight()` closed its fog at 210/460 — numbers tuned when San Francisco's
230-unit board was the only one. Every caller who supplies an `Atmosphere`
overrides them, which is why this survived: the demo does, so nothing looked
wrong. A self-hoster who renders a city with no clock and no weather gets the
default path, and on the Bay Area's 1003 units that fog closes well inside the
city. It now scales to the board exactly as the camera limits do, with the old
constants preserved as the default for a caller holding a palette but no world.

The three that really were comments: socal.ts justified its `latScale` by
citing the 340-unit orbit cap and the 460-unit fog as things the engine
insisted on, and the engine stopped insisting when both became board-relative —
285 is now a choice the pack makes because its lot sizes and hill radii were
authored against it. CONTRACT.md and ARCHITECTURE.md still measured the
retention argument against a 336,864-point San Francisco that has been the
whole Bay Area for some time. And server/package.json listed five routes where
there are six.

None of these changed behaviour except the first. All of them would have gone
on quietly disagreeing with the pages that now cite them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Karti Tripathi
2026-08-05 04:34:00 -07:00
parent 924be42f19
commit fe43077ff8
6 changed files with 40 additions and 18 deletions
+12 -6
View File
@@ -3088,12 +3088,18 @@ export const SOCAL_CITY: City = {
/**
* 391 m to the scene unit, against San Francisco's 94.
*
* Not a stylistic choice. `scene.ts` caps the orbit at 340 units from the
* target and `cityDaylight` closes its fog at 460, so however large a city is
* on the ground it has to end up roughly the same size in scene units or it
* cannot be looked at. The board this produces is 393 by 308 units — a little
* wider and deeper than San Francisco's 331 by 230, which is about as far as
* the camera will stretch.
* The board this produces is 393 by 308 units, against the Bay Area's 1003 by
* 830 — a metro four times the area rendered on a third of the board, which is
* why one instance here is a city block rather than a building.
*
* This used to be forced rather than chosen: `scene.ts` capped the orbit at
* 340 units and `cityDaylight` closed its fog at 460, both tuned when San
* Francisco was the only city, so however large a place was on the ground it
* had to come out roughly San Francisco's size in scene units or it could not
* be looked at. Both now derive from the board, so a pack is free to pick its
* own scale. This one keeps 285 because the numbers below — lot size, hill
* radii, bridge spans — were all authored against it, not because the engine
* still insists.
*/
latScale: 285,
+17 -5
View File
@@ -13,8 +13,8 @@
* that ought to be here — is the city itself: which layers go in the scene,
* where a chapter puts the camera, and what a pick means. An office builds the
* same two pieces with its own answers and swaps in on the same `Stage`, which
* keeps this city alive and paused rather than rebuilding its ~1.0 s
* heightfield on the way back. See CONTRACT.md §1.
* keeps this city alive and paused rather than rebuilding its heightfield — for
* the Bay Area, about 2.3 s — on the way back. See CONTRACT.md §1.
*/
import * as THREE from "three";
@@ -113,7 +113,7 @@ export function createScene(canvas: HTMLCanvasElement, options: SceneOptions): S
shadowExtent: boardSpan * 0.75,
shadowFar: boardSpan * 2.2,
});
kit.applyLighting(options.lighting ?? cityDaylight(pal));
kit.applyLighting(options.lighting ?? cityDaylight(pal, boardSpan));
scene.add(createWater(world));
scene.add(createShorePlates(world));
@@ -254,12 +254,24 @@ export function createScene(canvas: HTMLCanvasElement, options: SceneOptions): S
* It exists so the engine renders with no server, no clock and no config, which
* is the acceptance test the whole repo is held to.
*/
export function cityDaylight(palette: ScenePalette): LightingState {
export function cityDaylight(palette: ScenePalette, boardSpan = 230): LightingState {
return {
sun: { direction: [-0.632, 0.717, 0.295], color: 0xfff3e0, intensity: 2.1 },
hemisphere: { sky: 0xdcecf7, ground: 0x6b6f5e, intensity: 1.05 },
ambient: { color: 0xffffff, intensity: 0.32 },
sky: { top: palette.skyTop, horizon: palette.skyHorizon },
fog: { color: palette.skyHorizon, near: 210, far: 460 },
// Scaled to the board for the same reason the camera is, and it was the same
// bug: 210/460 were tuned when San Francisco's 230-unit board was the only
// one, and on the Bay Area's 1003 units they close the fog well inside the
// city. The camera sits about 0.6 spans out on a whole-board view, so a fog
// that starts nearer than that is behind the viewer's own shoulder.
//
// The default keeps the original numbers exactly, for a caller that has a
// palette but no world.
fog: {
color: palette.skyHorizon,
near: boardSpan * 0.91,
far: boardSpan * 2,
},
};
}
+3 -2
View File
@@ -6,8 +6,9 @@
* The renderer and the loop live in `Stage`; the camera, controls, flights and
* picking live in a `SceneKit`; what is left here is the office itself. Swap it
* in with `stage.setScene(office)` and the city is *paused, not disposed* —
* rebuilding SF's 336,864-point heightfield on the way back out costs about a
* second, which is the measurement CONTRACT.md §1 is built on.
* rebuilding the Bay Area's 0.53M-point heightfield on the way back out costs
* about two and a half seconds, which is the measurement CONTRACT.md §1 is
* built on.
*
* Whoever builds one of these disposes it. `Stage` disposes nothing it did not
* create, and the city handle's `dispose()` does not reach in here.