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
+176 -35
View File
@@ -32,8 +32,16 @@ import type { Pose } from "./engine/scenekit.ts";
import { createStage, deviceProfile } from "./engine/stage.ts";
import { daylightPhase } from "./engine/solar.ts";
import type { City, Marker, MarkerPalette, View } from "./engine/types.ts";
import CALIFORNIA from "./cities/california.ts";
import SAN_FRANCISCO from "./cities/sf.ts";
import SOCAL from "./cities/socal.ts";
import CALIFORNIA_TRANSPORT from "./transport/california.ts";
import {
mergeVehicleActions,
sampleStandardGamepad,
type GamepadButtonState,
} from "./input/vehicle.ts";
import type { VehicleActionSnapshot } from "./transport/vehicleController.ts";
import {
createTeraClient,
describeLiveness,
@@ -78,6 +86,7 @@ import type { Godmode, GodmodeHouseLights, GodmodePlace } from "./tools/index.ts
import type { PoseEditor } from "./tools/poseEditor.ts";
const CITIES: { id: string; label: string; city: City }[] = [
{ id: "california", label: "California", city: CALIFORNIA },
{ id: "sf", label: "Bay Area", city: SAN_FRANCISCO },
{ id: "socal", label: "SoCal", city: SOCAL },
];
@@ -131,7 +140,7 @@ const stage = createStage(canvas);
const tera = createTeraClient({ fetch: authFetch });
let city: SceneHandle | null = null;
let cityId = "sf";
let cityId = "california";
/**
* The city the user last *asked* for, which is not the same as the one that is
* mounted or even the one that is being built.
@@ -145,13 +154,13 @@ let cityId = "sf";
* to be against the intention, and the intention is recorded synchronously in
* the click handler.
*/
let wantedCity = "sf";
let wantedCity = "california";
let office: OfficeScene | null = null;
let inside = false;
let markers: Marker[] = SAMPLE_MARKERS;
/**
* The buildings you can walk into, as pins on the city.
* The buildings you can walk into, as procedural glyphs on the city.
*
* This is the one thing that makes Tera and Spaces feel like one product rather
* than two views sharing a bundle. Both packs carry a real `site` — it is what
@@ -163,9 +172,10 @@ let markers: Marker[] = SAMPLE_MARKERS;
* lazy chunk worth tens of kilobytes and the city wants these the instant the
* board appears, long before anybody opens a door. See `offices/sites.ts`.
*
* `colorKey` is opaque to the engine, as every `Pin.colorKey` is — `SAMPLE_PALETTE`
* `colorKey` is opaque to the engine, as every `Pin.colorKey` is — the palette
* resolves it, and giving these their own key is what lets a door look different
* from a company.
* from a company. `glyph` is equally literal: dimensions and a silhouette, with
* no office semantics in the renderer.
*/
const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
id: `office:${entry.id}`,
@@ -177,6 +187,7 @@ const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
// Hand-typed from the street grid, like every other coordinate here. Not a
// placeholder, so it is drawn as a real address.
located: true,
...(entry.site.exterior ? { glyph: entry.site.exterior } : {}),
}));
/**
@@ -677,8 +688,14 @@ async function mountCity(id: string) {
// unconditionally and all of it is over San Francisco, so the SoCal board's
// entire sky projected ~590 km off the world and rendered as nothing at all.
const routes = sampleRoutesFor(entry.city);
// The public traffic API is region-oriented and intentionally capped around
// one metro. A state-wide request would either be rejected or become a data
// vacuum, so California keeps the honest deterministic sky while its two
// detailed boards continue to use live ADS-B when available.
const traffic =
access.can.liveEnvironment && access.feeds?.flights ? tera.flights(region, routes) : null;
id !== "california" && access.can.liveEnvironment && access.feeds?.flights
? tera.flights(region, routes)
: null;
cityFlights = traffic;
/**
@@ -717,9 +734,42 @@ async function mountCity(id: string) {
dial.setExtra(trafficDial?.extra() ?? 0);
trafficDial = dial;
/**
* Stable destinations are handed to the scene at construction time, not a
* frame later. A building glyph needs that head start so `blocks.ts` can
* reserve its footprint before the anonymous one-draw-call skyline is
* emitted; otherwise both buildings occupy the same address and the useful
* one is usually hidden inside the random one.
*
* A door belongs to the board it stands on. This used to be gated on a
* hard-coded `id === "sf"`; using the board's own bounds is what keeps Mateo
* Court on the Southland board and off the Bay Area one. Sample companies
* stay SF-only because that sample feed is about one city and always was.
*/
const bounds = entry.city.bounds;
const doors = OFFICE_MARKERS.filter(
(m) =>
m.lat >= bounds.minLat &&
m.lat <= bounds.maxLat &&
m.lng >= bounds.minLng &&
m.lng <= bounds.maxLng,
);
const initialMarkers = id === "sf" ? [...markers, ...doors] : doors;
const handle = await createScene(stage, {
city: entry.city,
markerPalette: palette,
markers: initialMarkers,
...(id === "california"
? {
roadTraffic: {
pack: CALIFORNIA_TRANSPORT,
routeId: "la-sf-us-101",
count: 14,
seed: 115,
},
}
: {}),
flights: dial.source,
...(catalogue ? { satellites: catalogue } : {}),
/**
@@ -818,28 +868,6 @@ async function mountCity(id: string) {
// not a decoration. LA gets its own weather, not San Francisco's fog.
marineLayer: id === "sf" ? PACIFIC_MARINE_LAYER : null,
});
/**
* A door belongs to the board it stands on.
*
* The gate here used to be `id === "sf"`, which was correct for exactly as
* long as every office was in the Bay Area. It stopped being correct the
* moment one was not: a hard-coded city id would have kept the Los Angeles
* building off the Los Angeles board and pinned it to San Francisco's.
*
* So the test is the board's own bounds, which is the same question asked
* honestly — a pin for a building outside the rectangle being drawn is a pin
* in the wrong place, whichever city that happens to be. The sample company
* markers stay SF-only; they are sample data about one city and always were.
*/
const bounds = entry.city.bounds;
const doors = OFFICE_MARKERS.filter(
(m) =>
m.lat >= bounds.minLat &&
m.lat <= bounds.maxLat &&
m.lng >= bounds.minLng &&
m.lng <= bounds.maxLng,
);
city.setMarkers(id === "sf" ? [...markers, ...doors] : doors);
city.onChapterChange(() => renderLegend());
/**
@@ -889,7 +917,7 @@ async function mountCity(id: string) {
},
});
showPlan();
minimap.setMarkers(id === "sf" ? [...markers, ...doors] : doors);
minimap.setMarkers(initialMarkers);
// The instruments, for the one visitor in a deployment who has them. The pose
// editor holds a `World`, a camera and a controls, so it belongs to the board
@@ -1304,6 +1332,7 @@ const shortcutsCard = document.querySelector<HTMLElement>("#shortcuts");
const helpButton = document.querySelector<HTMLButtonElement>("#help");
const planToggle = document.querySelector<HTMLButtonElement>("#plan-toggle");
const credits = document.querySelector<HTMLElement>("#credits");
const driveControls = document.querySelector<HTMLElement>("#drive-controls");
function showDetail(text: string | null) {
const card = document.querySelector<HTMLElement>("#detail");
@@ -1316,7 +1345,7 @@ function showDetail(text: string | null) {
}
/**
* The two-button strip above the legend: cities outside, buildings inside.
* The board strip above the legend: world scales outside, buildings inside.
*
* One control that answers "which of these am I in", pointed at whichever list
* is currently the answer. A second, separate office strip was the obvious
@@ -1332,8 +1361,8 @@ function renderCityPicker() {
for (const entry of entries) {
const b = document.createElement("button");
// `aria-pressed` rather than a class, because that is what these are: two
// buttons of which exactly one is on. The stylesheet keys off the attribute
// `aria-pressed` rather than a class, because these buttons choose exactly
// one active board. The stylesheet keys off the attribute
// so the visual state and the announced state cannot drift apart.
b.className = "city";
b.type = "button";
@@ -1449,6 +1478,7 @@ function renderLegend() {
minimap?.setChapters(city.chapters, city.current());
officePlan?.setActiveView(office?.current() ?? null);
renderOfficeBadge();
if (driveControls) driveControls.hidden = !routeDriveIsActive();
}
/**
@@ -1814,8 +1844,6 @@ panelToggle?.addEventListener("click", () => {
applyPanel();
});
planToggle?.addEventListener("click", () => togglePlan());
/**
* The scrim behind the phone's panel sheet. It is `display: none` above 600px,
* so this listener is only ever reachable where the sheet exists.
@@ -1855,6 +1883,98 @@ shortcutsCard?.addEventListener("click", (event) => {
if (event.target === shortcutsCard) closeShortcuts();
});
/** Held keyboard state translated into the same snapshot a gamepad/touch UI uses. */
const heldDriveKeys = new Set<string>();
function routeDriveIsActive(): boolean {
const state = !inside ? city?.vehicleState() : null;
return state !== null && state !== undefined && city?.current() === state.routeId;
}
function publishVehicleActions(
supplement: Partial<VehicleActionSnapshot> = {},
): boolean {
if (!routeDriveIsActive() || !city) return false;
const left = heldDriveKeys.has("a");
const right = heldDriveKeys.has("d");
city.setVehicleActions(
mergeVehicleActions(
{
throttle: heldDriveKeys.has("w") ? 1 : 0,
brake: heldDriveKeys.has("s") ? 1 : 0,
steering: (right ? 1 : 0) - (left ? 1 : 0),
handbrake: heldDriveKeys.has(" "),
},
supplement,
),
);
return true;
}
function toggleVehicleCamera(): boolean {
if (!routeDriveIsActive() || !city) return false;
city.setVehicleCamera(city.vehicleCamera() === "driver" ? "chase" : "driver");
return true;
}
for (const button of driveControls?.querySelectorAll<HTMLButtonElement>("[data-drive-key]") ?? []) {
const key = button.dataset.driveKey;
if (key === undefined) continue;
const release = (event: PointerEvent) => {
heldDriveKeys.delete(key);
button.setAttribute("aria-pressed", "false");
publishVehicleActions();
event.preventDefault();
};
button.addEventListener("pointerdown", (event) => {
button.setPointerCapture(event.pointerId);
heldDriveKeys.add(key);
button.setAttribute("aria-pressed", "true");
publishVehicleActions();
event.preventDefault();
});
button.addEventListener("pointerup", release);
button.addEventListener("pointercancel", release);
button.addEventListener("lostpointercapture", release);
}
driveControls?.querySelector<HTMLButtonElement>("[data-drive-action='assist']")
?.addEventListener("click", () => publishVehicleActions({ modeRequest: "assisted" }));
driveControls?.querySelector<HTMLButtonElement>("[data-drive-action='reset']")
?.addEventListener("click", () => publishVehicleActions({ reset: true }));
driveControls?.querySelector<HTMLButtonElement>("[data-drive-action='camera']")
?.addEventListener("click", () => toggleVehicleCamera());
window.addEventListener("keyup", (event) => {
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
if (!heldDriveKeys.delete(key)) return;
if (publishVehicleActions()) event.preventDefault();
});
window.addEventListener("blur", () => {
heldDriveKeys.clear();
publishVehicleActions();
});
let gamepadButtons: GamepadButtonState = { assist: false, reset: false };
function pollDriveGamepad() {
try {
const pad = navigator.getGamepads?.().find((candidate) => candidate !== null);
if (pad && routeDriveIsActive()) {
const sample = sampleStandardGamepad(pad, gamepadButtons);
gamepadButtons = sample.buttons;
publishVehicleActions(sample.actions);
} else {
gamepadButtons = { assist: false, reset: false };
}
} catch {
// Some privacy-hardened browsers expose the method but throw until a pad
// has produced a trusted event. Keyboard/touch remain fully functional.
}
requestAnimationFrame(pollDriveGamepad);
}
requestAnimationFrame(pollDriveGamepad);
/**
* Keyboard access to everything the mouse can reach.
*
@@ -1902,6 +2022,25 @@ window.addEventListener("keydown", (event) => {
return;
}
const lower = event.key.toLowerCase();
if (lower === "w" || lower === "a" || lower === "s" || lower === "d" || event.key === " ") {
heldDriveKeys.add(event.key === " " ? " " : lower);
if (publishVehicleActions()) {
event.preventDefault();
return;
}
}
if (lower === "p" && publishVehicleActions({ modeRequest: "assisted" })) {
event.preventDefault();
return;
}
if (lower === "r" && publishVehicleActions({ reset: true })) {
event.preventDefault();
return;
}
if (lower === "c" && toggleVehicleCamera()) {
event.preventDefault();
return;
}
if (lower === "m") {
togglePlan();
return;
@@ -2369,7 +2508,9 @@ async function boot() {
*/
const wanted = new URLSearchParams(location.search).get("city");
const first = CITIES.find((c) => c.id === wanted) ?? CITIES[0];
await building(`Building ${first?.label ?? "the city"}`, () => mountCity(first?.id ?? "sf"));
await building(`Building ${first?.label ?? "the city"}`, () =>
mountCity(first?.id ?? "california"),
);
// The instruments, after the first board, because the panel reads a live
// stage and there is not one before this line.