1
0

California gets roads, traffic, and a car to follow

This commit is contained in:
2026-08-11 18:24:53 -07:00
parent 9c9e78f6f9
commit fe58290728
43 changed files with 5593 additions and 68 deletions
+61 -4
View File
@@ -28,7 +28,7 @@
*/
import * as THREE from "three";
import { createBlocks, createLandmarks } from "./blocks.ts";
import { createBlocks, createLandmarks, type BuildingReservation } from "./blocks.ts";
import { createNightLights, type NightLights } from "./nightlights.ts";
import { createFlightLayer, type FlightLayer } from "./flights.ts";
import { createCloudLayer, type CloudLayer } from "./clouds.ts";
@@ -41,7 +41,17 @@ import {
type SatelliteLayer,
} from "./satellites.ts";
import { createSceneKit, type Pose } from "./scenekit.ts";
import {
createRoadTrafficLayer,
type RoadTrafficLayer,
type RoadTrafficOptions,
type VehicleCameraMode,
} from "./roadTraffic.ts";
import type { Stage, StageScene } from "./stage.ts";
import type {
VehicleActionSnapshot,
VehicleControllerState,
} from "../transport/vehicleController.ts";
import { createBridges, createRoads } from "./structures.ts";
import { createShorePlates, createTerrain, createWater, paletteFor } from "./terrain.ts";
import type {
@@ -58,6 +68,17 @@ import { World, type FieldProgress } from "./world.ts";
export interface SceneOptions {
city: City;
markerPalette?: MarkerPalette;
/**
* Markers available at construction time.
*
* Building glyphs in this first set reserve their footprints in the
* anonymous block scatter. Later `setMarkers()` calls remain cheap and do
* not rebuild a city, so callers should put stable destinations here and use
* updates for genuinely live marker feeds.
*/
markers?: Marker[];
/** Optional deterministic road traffic for a state/corridor-scale board. */
roadTraffic?: RoadTrafficOptions;
flights?: FlightSource;
/**
* Element sets to propagate, if this deployment has any.
@@ -148,6 +169,12 @@ export interface SceneHandle {
flyTo(chapterId: string): void;
current(): string;
onChapterChange(fn: (id: string) => void): void;
/** Device-neutral input for the corridor hero; a no-op on boards without one. */
setVehicleActions(actions: Partial<VehicleActionSnapshot>): void;
/** Current playable corridor state, or null on a city-scale board. */
vehicleState(): Readonly<VehicleControllerState> | null;
setVehicleCamera(mode: VehicleCameraMode): void;
vehicleCamera(): VehicleCameraMode | null;
setMarkers(markers: Marker[]): void;
/** Take this city off the stage and release everything it built. */
dispose(): void;
@@ -283,9 +310,19 @@ export async function createScene(
scene.add(createShorePlates(world));
scene.add(createTerrain(world));
scene.add(createRoads(world));
const blocks = createBlocks(world);
const buildingReservations: BuildingReservation[] = [];
for (const marker of options.markers ?? []) {
const glyph = marker.glyph;
if (marker.located === false || glyph?.kind !== "building") continue;
const [x, z] = world.project(marker.lat, marker.lng);
// Five metres of breathing room keeps an anonymous wall from sitting
// exactly on the authored façade after both reservation circles touch.
const radius = (Math.hypot(glyph.width, glyph.depth) / 2 + 5) / world.metresPerUnit;
buildingReservations.push({ x, z, radius });
}
const blocks = createBlocks(world, buildingReservations);
scene.add(blocks);
scene.add(createLandmarks(world));
scene.add(createLandmarks(world, buildingReservations));
scene.add(createBridges(world));
/**
@@ -300,8 +337,14 @@ export async function createScene(
scene.add(clouds.group);
const markerLayer: MarkerLayer = createMarkerLayer(world, options.markerPalette ?? {});
markerLayer.setMarkers(options.markers ?? []);
scene.add(markerLayer.group);
const roadTraffic: RoadTrafficLayer | null = options.roadTraffic
? createRoadTrafficLayer(world, kit.camera, kit.controls, options.roadTraffic)
: null;
if (roadTraffic) scene.add(roadTraffic.group);
let flightLayer: FlightLayer | null = null;
let flightTimer = 0;
if (options.flights) {
@@ -365,7 +408,15 @@ export async function createScene(
function flyTo(chapterId: string) {
const ch = chapterById[chapterId];
if (!ch) return;
kit.flyTo(chapterPose(ch));
const route = options.roadTraffic?.pack.routes.find((candidate) => candidate.id === chapterId);
if (route && roadTraffic) {
roadTraffic.setRoute(route.id);
roadTraffic.setFollowing(true);
} else {
roadTraffic?.setFollowing(false);
roadTraffic?.setVehicleActions({});
kit.flyTo(chapterPose(ch));
}
if (currentChapter !== chapterId) {
currentChapter = chapterId;
for (const fn of chapterListeners) fn(chapterId);
@@ -396,6 +447,7 @@ export async function createScene(
onExit: () => kit.resetPick(),
tick(dt) {
kit.tick(dt);
roadTraffic?.tick(dt);
clouds.tick(dt);
if (options.flights && flightLayer) {
flightTimer -= dt;
@@ -444,6 +496,7 @@ export async function createScene(
clouds.dispose();
nightLights.dispose();
markerLayer.dispose();
roadTraffic?.dispose();
kit.dispose();
scene.traverse((obj) => {
const mesh = obj as THREE.Mesh;
@@ -481,6 +534,10 @@ export async function createScene(
onChapterChange(fn) {
chapterListeners.push(fn);
},
setVehicleActions: (actions) => roadTraffic?.setVehicleActions(actions),
vehicleState: () => roadTraffic?.hero() ?? null,
setVehicleCamera: (mode) => roadTraffic?.setCameraMode(mode),
vehicleCamera: () => roadTraffic?.cameraMode() ?? null,
setMarkers(markers) {
markerLayer.setMarkers(markers);
},