1
0

SoCal, the whole bay, a moon, and gates that actually run

Six agents in parallel, and the two city packs independently reported the same
blocker: `focusRegions` and `coarseFactor` existed on the `City` type and
nothing implemented them. Uniform lattices would have been 2.9M points for
Southern California and 3.7M for the expanded bay. Both packs were unloadable
as written.

`buildAxis` is the answer, and it is honest about its limits: refinement is per
axis, not per rectangle, so a focus region sharpens its whole row *and* its
whole column. Two regions at opposite corners refine nearly everything between
them. Measured, not guessed — the bay went 0.53M points with one region and
1.64M with three, for detail nobody is looking at from a board this wide. One
region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s.

Then three things that were only ever right because San Francisco was the only
city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a
230-unit board; the bay is 1003 units across and the camera physically could
not retreat far enough to frame it. Fog distances were scene units pinned to
the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest
weather, which over ninety-four kilometres of bay correctly hides three
quarters of it — the night view was a black rectangle for a completely
reasonable reason. All three now derive from the board.

The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a
physical ratio of one to four hundred thousand. What is being reproduced is
what a moonlit night looks like on a screen in a lit room.

The CI gate caught itself, which is the part worth keeping. Port 8431 was
already held by a server from an earlier session, so the boot check polled a
healthy stranger while the process it started died on EADDRINUSE. It now
refuses to run rather than pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Karti Tripathi
2026-08-05 03:13:32 -07:00
parent 8bcb391455
commit 44c5a79424
25 changed files with 9246 additions and 220 deletions
+40 -6
View File
@@ -19,6 +19,7 @@
import * as THREE from "three";
import { createBlocks, createLandmarks } from "./blocks.ts";
import { createNightLights, type NightLights } from "./nightlights.ts";
import { createFlightLayer, type FlightLayer } from "./flights.ts";
import { createMarkerLayer, type MarkerLayer } from "./markers.ts";
import { createSceneKit, type Pose } from "./scenekit.ts";
@@ -63,6 +64,12 @@ export interface SceneHandle {
stageScene: StageScene;
/** Applies a rig computed elsewhere. The scene never works one out itself. */
setLighting(state: LightingState): void;
/**
* Solar elevation in degrees, for the layers that need the sun's position
* rather than the rig it implies. `LightingState` deliberately carries no
* elevation, so the number has to arrive separately.
*/
setSolarElevation(degrees: number): void;
flyTo(chapterId: string): void;
current(): string;
onChapterChange(fn: (id: string) => void): void;
@@ -78,16 +85,33 @@ export function createScene(canvas: HTMLCanvasElement, options: SceneOptions): S
const stage = createStage(canvas);
const scene = new THREE.Scene();
/**
* Every camera limit is derived from how big this city's board actually is.
*
* These were constants tuned for San Francisco — `maxDistance: 340`,
* `far: 900`, a 170-unit shadow box. That silently made board size a fixed
* property of the engine rather than of a city: expanding the pack from San
* Francisco to the whole Bay Area took the board from 230 units across to
* 1003, and the camera physically could not retreat far enough to frame it.
* You got a close-up of the peninsula with everything else off-screen, and
* nothing in the types said why.
*
* A city pack now chooses its own `latScale` freely and the camera follows.
*/
const [westX, northZ] = world.project(city.bounds.maxLat, city.bounds.minLng);
const [eastX, southZ] = world.project(city.bounds.minLat, city.bounds.maxLng);
const boardSpan = Math.max(Math.abs(eastX - westX), Math.abs(southZ - northZ));
const kit = createSceneKit({
scene,
dom: stage.renderer.domElement,
fov: 42,
near: 0.1,
far: 900,
minDistance: 12,
maxDistance: 340,
shadowExtent: 170,
shadowFar: 520,
far: boardSpan * 3,
minDistance: Math.max(4, boardSpan * 0.02),
maxDistance: boardSpan * 1.5,
shadowExtent: boardSpan * 0.75,
shadowFar: boardSpan * 2.2,
});
kit.applyLighting(options.lighting ?? cityDaylight(pal));
@@ -95,10 +119,18 @@ export function createScene(canvas: HTMLCanvasElement, options: SceneOptions): S
scene.add(createShorePlates(world));
scene.add(createTerrain(world));
scene.add(createRoads(world));
scene.add(createBlocks(world));
const blocks = createBlocks(world);
scene.add(blocks);
scene.add(createLandmarks(world));
scene.add(createBridges(world));
/**
* The city switching itself on after sunset. Built after `blocks` because it
* patches the material that `createBlocks` made — order is load-bearing.
*/
const nightLights: NightLights = createNightLights({ world, blocks });
scene.add(nightLights.group);
const markerLayer: MarkerLayer = createMarkerLayer(world, options.markerPalette ?? {});
scene.add(markerLayer.group);
@@ -176,6 +208,7 @@ export function createScene(canvas: HTMLCanvasElement, options: SceneOptions): S
dispose() {
options.flights?.dispose?.();
flightLayer?.dispose();
nightLights.dispose();
markerLayer.dispose();
kit.dispose();
scene.traverse((obj) => {
@@ -195,6 +228,7 @@ export function createScene(canvas: HTMLCanvasElement, options: SceneOptions): S
stage,
stageScene,
setLighting: (state) => kit.applyLighting(state),
setSolarElevation: (degrees) => nightLights.setSolarElevation(degrees),
flyTo,
current: () => currentChapter,
onChapterChange(fn) {