1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/main.ts
T
karti eeedfd7d6d feat: one California is the product, not a flag
Behind `?one=1` the merged board was something a visitor could not find
and did not get: the site still landed on the Bay Area, the left column
still offered two other boards, and clicking one still tore the board
down and built another. The board existed and the product did not change.

A bare URL is now one California. `?city=sf` and `?city=socal` still get
the dedicated boards, unchanged and on purpose — twenty-nine capture
guards and the `bay-area` and `socal` budget cells aim at those boards by
name and assert the `data-board` they land on, and a metro id falling
back to the merged board would not degrade them, it would make them
silently measure something else. `?one=0` forces the old arrangement, and
both `california` budget cells now pass it: their caps are the plain
state board's 440,000, and without it they would measure the merged board
that `california-one` already covers.

Also `CALIFORNIA.md`: what a single cohesive California is still missing,
measured rather than listed. Every district on the board by the latitude
of its centre is 1 / 23 / 26 / 1 / 2 / 54 across 32-37 N and **zero above
38 N** — 107 districts, 103 of them in two clusters four hundred
kilometres apart, and four hundred and fifty kilometres of northern
California with not one building on it. Eight airports, all Bay or LA.
Two ports, both in San Pedro Bay. Two freeways of twelve vertices each
across a thousand kilometres of state.

The scale is not the problem and the file says so first: the board is
32.50-42.05 N and -124.50 to -114.00 W at 1,919 m to the unit, every
feature on its real coordinate, nothing schematic. It is not too small.
It is empty.
2026-08-24 16:57:18 -07:00

6431 lines
269 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The demo: two cities under a real sun and moon, and two studios you can step
* into.
*
* It ships **no real company data**. The markers are fabricated — see
* `src/adapters/sample.ts`, which says so loudly — because real positions are
* geocoded and real pipeline status is private, and neither belongs in this
* repo. When a Tera API is present the same markers arrive from it instead, and
* the UI says which of the two it is showing.
*
* It also runs with no server at all: the sun and moon are computed locally,
* the traffic is simulated, the office is a data file, the studio hardware is a
* fixed-step simulator running in this tab. Clone it and it works.
*
* ### What this file is, after the interface moved out
*
* This file used to hold about forty imperative `element.hidden = condition`
* decisions with the condition and the write on the same line, spread through a
* 655-line "Chrome" section, and none of it was testable without a WebGL
* context. That is now two modules: `ui/chromeState.ts` decides and `ui/mount.ts`
* writes, and what is left here is the one call between them —
* `chrome.apply(chromeState(chromeInputs()))` — plus the thing this file is
* actually for, which is **assembly**: it owns the clock, the access tier, which
* board is mounted, which building is open, and which of the delivered modules
* is handed to which scene.
*
* The rule that keeps it that way: nothing below this line may write to a DOM
* node that `mount.ts` owns. Where you want a piece of chrome to change, add a
* field to `ChromeInputs` and call `renderChrome()`.
*/
import {
collapsedFog,
createAtmosphere,
dipFog,
FOG_DIP_IN_SECONDS,
FOG_DIP_OUT_SECONDS,
observe,
PACIFIC_MARINE_LAYER,
type AerialFog,
type Atmosphere,
type WeatherObservation,
} from "./engine/atmosphere.ts";
import {
createBoardCache,
prefetchTarget,
residentCapacity,
type BoardCache,
type PrefetchTier,
} from "./engine/boards.ts";
import {
activeRung,
buildLadder,
handover,
HANDOVER_STANDOFF_M,
placesRows,
REGION_LABELS,
type LadderRung,
} from "./engine/ladder.ts";
import { officeDaylight, smokeCaption, withHouseLights } from "./interiors/daylight.ts";
import { createScene, type SceneHandle } from "./engine/scene.ts";
import { createEnvironmentRig } from "./engine/environmentRig.ts";
import {
aircraftDetail,
regionOf,
SimulatedFlights,
withTrafficDial,
type TrafficDial,
} from "./engine/flights.ts";
import { SatelliteCatalogue, type SatelliteElements } from "./engine/satellites.ts";
import type { Pose } from "./engine/scenekit.ts";
import { createStage, deviceProfile } from "./engine/stage.ts";
import { daylightPhase } from "./engine/solar.ts";
import type { Aircraft, City, Marker, MarkerPalette, Port, View } from "./engine/types.ts";
import CALIFORNIA from "./cities/california.ts";
import { UNIFIED_SOURCES, unifiedCalifornia } from "./cities/unify.ts";
import { reconciledCity } from "./cities/reconcile.ts";
import SAN_FRANCISCO from "./cities/sf.ts";
import SOCAL from "./cities/socal.ts";
import CALIFORNIA_TRANSPORT from "./transport/california.ts";
import {
aircraftActionsFromPlay,
cameraRelativePlanar,
crowActionsFromPlay,
groundActorActionsFromPlay,
PlayInputRouter,
sampleStandardPlayGamepad,
vehicleActionsFromPlay,
type StandardPlayGamepadButtons,
} from "./input/play.ts";
import {
createTeraClient,
type FireWatch,
type PresenceWatch,
mergedTraffic,
type TrafficSource,
type WeatherWatch,
} from "./adapters/http.ts";
import { promote, type FireBounds } from "./server/fires.ts";
import type { BirdsBody, FiresBody, RadarBody, VesselsBody } from "./server/wire.ts";
import { createFireLayer } from "./engine/fires.ts";
import { createPortLayer } from "./engine/ports.ts";
import { createVesselLayer } from "./engine/vessels.ts";
import { boardCarriesRaster, precipFactoryFor } from "./engine/precip.ts";
import { createMigrationLayer } from "./engine/migration.ts";
import {
berthAnchors,
modelHarbour,
promoteVessels,
vesselSummary,
type BerthAnchor,
type VesselBounds,
} from "./server/vessels.ts";
import { promoteRadar } from "./server/radar.ts";
import { promoteBirds } from "./server/birds.ts";
import { mountFirePanel, type FirePanelHandle } from "./ui/firePanel.ts";
import {
SAMPLE_MARKERS,
SAMPLE_PALETTE,
SAMPLE_PRESENCE_PALETTE,
samplePresenceForOfficeAt,
sampleRoutesFor,
} from "./adapters/sample.ts";
import { OFFICE_SITES, type ShippedOfficeId } from "./offices/sites.ts";
import { authFetch } from "./session.ts";
import { capabilitiesFor, resolveAccess, type Access } from "./access.ts";
import { randomId } from "./ids.ts";
import { createMinimap, type Minimap } from "./engine/minimap.ts";
import {
createJourney,
journeyReducer,
loadJourneySession,
saveJourneySession,
type JourneyCity,
type JourneyEvent,
type JourneyState,
} from "./journey/index.ts";
import {
actorKindForPresence,
createProfileEditor,
createDefaultLocalProfile,
createWebcamCapture,
createWebcamFaceConsent,
createWebcamFacePanel,
createWebcamFaceTexture,
loadLocalProfile,
resolveHumanoidAppearance,
saveLocalProfile,
type LocalProfile,
type ProfileEditor,
type WebcamCaptureController,
type WebcamFacePanel,
type WebcamFaceTextureAdapter,
} from "./profile/index.ts";
import type { ActorIdentity } from "./actors/controller.ts";
import { SRGBColorSpace, Vector3, VideoTexture } from "three";
import {
CALIFORNIA_AIR_ROUTE,
createAircraftPoseSnapshot,
} from "./aircraft/index.ts";
import {
cityControlMode,
createControlModeState,
transitionControlMode,
type ControlMode,
} from "./play/controlMode.ts";
import {
createOfficeScreenPanel,
type MediaSurfaceDescriptor,
type OfficeScreenPanel,
type OfficeScreenRemoteStatus,
} from "./media/index.ts";
import type { RemoteMediaState, RemoteOfficeMedia } from "./media/remoteMedia.ts";
/**
* The interface, as two modules and one call.
*
* `chromeState` is pure — no DOM, no THREE, no clock — and `mountChrome` is the
* only thing in the product that writes to the page. Both are value imports and
* both belong in the entry chunk: the chrome is on screen before anything else
* is, including the boot card's own successor.
*/
import {
chromeState,
seedPanelOpen,
seedPlanOpen,
type ChromeDetail,
type ChromeInputs,
type ChromeLayout,
} from "./ui/chromeState.ts";
import { mountChrome, type ChromeHandle } from "./ui/mount.ts";
import { controlForKey, edgeForKey } from "./ui/shortcuts.ts";
import { browserStorage, hasSeenOnboarding } from "./ui/onboarding.ts";
import { playHudKindFor, type AircraftDetailInput, type PlayTelemetry } from "./ui/hud.ts";
/**
* Three type-only imports and not one value among them, which is what keeps the
* office and the instruments out of the entry chunk.
*
* `import type` is erased before Rollup sees it, so none of these files is an
* edge in the module graph. The office arrives through `loadOffice()`, the tools
* through the `await import()` in `boot()`. Turning any of them into a value
* import silently undoes the split and nothing fails — the bundle just gets big
* again. The device and vehicle simulators travel with the office for the same
* reason: renderer-free, but with nothing to say until you are in a studio.
*/
import type { OfficeScene } from "./interiors/officeScene.ts";
import type { Office, Presence } from "./interiors/types.ts";
import type { RobotOperationsDefinition } from "./interiors/robotOperations.ts";
import type { MaterialQuality, MaterialRegistry } from "./assets/materials.ts";
import type { DeviceSource } from "./devices/adapter.ts";
import type { DeviceCommand, DeviceDeclaration, DeviceState } from "./devices/types.ts";
import type { SimulatedVehicleTelemetry } from "./transport/vehicleTelemetry.ts";
// Type-only for the reason above, and it matters more here than it looks:
// `officeMinimap.ts` imports the asset registry as a *value*, to read prop
// footprints, so a value import of it here would drag the furniture catalogue
// back into the entry chunk and quietly undo the split this block exists to
// protect. It arrives with the office, in `loadOffice()`, because it is only
// ever drawn once you are standing in one.
import type { OfficeMinimap } from "./engine/officeMinimap.ts";
import type { Godmode, GodmodeHouseLights, GodmodePlace } from "./tools/index.ts";
import type { PoseEditor } from "./tools/poseEditor.ts";
import type {
EntityPoseSnapshot,
InterestCell,
RealtimeClient,
ServerRealtimeMessage,
} from "./realtime/index.ts";
import type { PresenceIndicator } from "./realtime/presenceIndicator.ts";
/**
* One full California, as **one pack** — and now the default.
*
* The first attempt at this nested each metro's built scene inside the state
* board's under a similarity transform. The transform was right and the result
* was not: two independently built terrains occupied the same ground and
* interpenetrated. `cities/unify.ts` is the answer that has no seam to stitch —
* one `City` carrying the state's landform and both metros' detail, so the
* engine builds one world, one lattice and one terrain mesh. See that file for
* the measured lattice cost and for the two merges that are not a union.
*
* ## Why it is no longer behind a flag
*
* Because a flag is a second product, and the whole point was to stop having
* three. Shipped behind `?one=1` the merged board was something a visitor could
* not find and did not get: `tera.lumbridgecorp.com` still landed on the Bay
* Area, the left column still offered two other boards, and clicking one still
* tore the board down and built another. The board existed and the product did
* not change.
*
* ## The three ways this resolves, and why the escape hatches are shaped so
*
* Default — no `?city=`, or `?city=california` — is **one California**.
*
* `?city=sf` and `?city=socal` still get the dedicated boards, unchanged. That
* is deliberate and it is not hedging: twenty-nine capture guards in
* `scripts/brand-assets` and the `bay-area` and `socal` cells of
* `scripts/performance-budget.mjs` aim at those boards by name and assert the
* `data-board` they land on. Making a metro id fall back to the merged board
* would not degrade those, it would make them silently measure and photograph
* something else — which is the exact failure the budget harness's own comment
* says the `data-board` assertion exists to catch. A named board stays a named
* board.
*
* `?one=0` forces the three-board product whatever the city is. One consumer
* needs it — the `california` budget cell, which is written against the *plain*
* state board's 440,000 cap and would otherwise measure the merged board that
* `california-one` already covers — and it is the honest way to say "the old
* arrangement" without spelling a board id.
*/
const ONE_PARAM = new URLSearchParams(location.search).get("one");
const CITY_PARAM = new URLSearchParams(location.search).get("city");
const ONE_CALIFORNIA =
ONE_PARAM === "1" ||
(ONE_PARAM !== "0" && CITY_PARAM !== "sf" && CITY_PARAM !== "socal");
/**
* The boards on offer — three, or **one**.
*
* With `?one=1` this is a single entry, and that is the whole of what makes the
* merged board a *board* rather than a nicer statewide tier on a three-board
* product. Everything downstream that decides "is this somewhere else" reads
* this array:
*
* - `LADDER` is built from it, so every rung's `board` is `california` and
* `goToPlace`'s `rung.board !== cityId` switch never fires. Clicking FiDi
* moves the camera instead of tearing the board down and building another.
* - `PREFETCH_TIERS` and the handover rule get one tier, so there is no
* handover: nothing to promote to, nothing to demote from, and no cut to
* conceal with a fog dip.
* - The office return links and `?city=` fall back to the one board that
* exists rather than to a board the visitor was never on.
*
* The metro packs are still imported and still merged — `unifiedCalifornia()`
* reads both — they simply stop being *destinations*. Without the flag nothing
* here moves, which is what keeps the twenty-nine capture guards and every cell
* of `scripts/performance-budget.mjs` aiming at the boards they were written
* against.
*/
const CITIES: { id: string; label: string; city: City }[] = ONE_CALIFORNIA
? [{ id: "california", label: "California", city: unifiedCalifornia().city }]
: [
{ id: "california", label: "California", city: CALIFORNIA },
{ id: "sf", label: "Bay Area", city: SAN_FRANCISCO },
{ id: "socal", label: "SoCal", city: SOCAL },
];
/**
* The board a bare URL lands on. **The detailed one, not the state tier.**
*
* ## Why this is not `CITIES[0]`
*
* It used to be, implicitly, in three places — a `wantedCity` initialiser, the
* office's return-link builder and the boot fallback — and the board it named
* was the 1,919 m-per-unit state tier. So the first thing every visitor saw was
* the coarsest representation the product owns: seventeen districts over
* 1063x930 km, no city legible, and a left column whose first offer is a list of
* somewhere else to go. The detailed boards carry 52 and 47 districts at 94 and
* 391 m per unit, and they are what the engine is actually for.
*
* With the free-camera handover on, the state tier has stopped being a
* destination and become **what you get when you pull back** — which is the role
* it is good at, since it is the only board that draws 97.4% of California.
* Landing on the Bay and zooming out to the state is the same map at two
* scales. Landing on the state and hunting for a city is three products.
*
* ## Why the constant rather than reordering `CITIES`
*
* Because `CITIES` order is load-bearing elsewhere: twenty-nine capture guards
* in `scripts/brand-assets` and every cell in `scripts/performance-budget.mjs`
* aim with an explicit `?city=`, and the budget harness binds each scene to the
* `data-board` it asserts precisely because an unknown `?city=` falls back to
* the first entry rather than failing. Reordering the array to change a landing
* page would silently re-point that fallback and every consumer that reads the
* array positionally. A named constant changes the one thing intended.
*
* `?city=california` still goes straight to the state board, and nothing about
* the deep link changed.
*/
/*
* With one board there is nothing to choose: the argument above is entirely
* about which of three products a bare URL lands on, and `?one=1` is the answer
* that there are not three. The landing *pose* is a separate question and still
* belongs to the chapter list — `california-overview` frames the state, and the
* metro rungs restored in `cities/unify.ts` are how you get down from it.
*/
const DEFAULT_CITY_ID = ONE_CALIFORNIA ? "california" : "sf";
/** The corridor's scale doors, and the office each detailed board arrives near. */
const CALIFORNIA_DESTINATIONS = new Map<string, { cityId: string; officeId: string }>([
["los-angeles", { cityId: "socal", officeId: "mateo-court" }],
["san-francisco", { cityId: "sf", officeId: "lumbridge-hq" }],
]);
/**
* The three boards' twenty-six authored chapters as one ordered descent.
*
* Built once, at module load, from the same packs `CITIES` holds — so the ladder
* cannot disagree with the boards about what a chapter is. `PLACES` is the same
* twenty-four rungs in the order the left column prints them; see
* `engine/ladder.ts` for why the printed list is grouped by region and the array
* is not.
*
* Neither of these replaces `city.chapters`. The per-board chapter list still
* exists, still renders into `#chapters`, and still carries the ids, the order,
* the short labels and the `data-view` attributes that twenty-nine capture
* guards in `scripts/brand-assets` aim at. The ladder is a **view** over that
* data and nothing more.
*/
/**
* The ladder is built from the **reconciled** packs, because `World` is.
*
* `world.ts:116` is `this.city = reconciledCity(city)`, so every number the
* engine draws with — `lngScale`, `verticalExaggeration`, road widths — is the
* reconciled one. The ladder reads `focus` to derive each rung's stand-off, and
* a ladder built from the raw packs is a ladder measuring a world nobody is
* looking at. With every rule off that is the same object by identity and this
* line is a no-op; with a rule on it is the difference between the rail agreeing
* with the camera and quietly disagreeing with it.
*
* Deliberately calling `reconciledCity` here rather than reading it off a
* `World`: the ladder is built at module load, before any board is mounted, and
* it must be — `PLACES` is the navigation and it exists before the first frame.
*/
const LADDER = buildLadder(
CITIES.map((entry) => ({ id: entry.id, city: reconciledCity(entry.city) })),
);
const PLACES = placesRows(LADDER);
const JOURNEY_SESSION_KEY = "tera:journey:v1";
const restoredJourney = loadJourneySession(sessionStorage, JOURNEY_SESSION_KEY);
let journey: JourneyState = restoredJourney.status === "loaded"
? restoredJourney.state
: createJourney({
actor: {
id: "anonymous",
kind: "crow",
signedIn: false,
profile: { displayName: "Guest" },
},
});
const LOCAL_PROFILE_KEY = "tera:profile:v2";
let localProfile: LocalProfile | null = null;
function humanoidAppearance() {
return localProfile ? resolveHumanoidAppearance(localProfile.appearance) : null;
}
function signedInActorIdentity(profile = localProfile): ActorIdentity {
const appearance = profile ? resolveHumanoidAppearance(profile.appearance) : null;
return {
id: access.subject ?? "anonymous",
displayName: profile?.displayName ?? access.subject ?? "Guest",
authenticated: access.subject !== null,
profile: {
appearance: appearance
? {
skinTone: appearance.skinTone,
primaryColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
}
: { primaryColor: "#151a20", accentColor: "#f2b134" },
},
};
}
function dispatchJourney(event: JourneyEvent): boolean {
const next = journeyReducer(journey, event);
if (next === journey) return false;
journey = next;
saveJourneySession(sessionStorage, JOURNEY_SESSION_KEY, journey);
return true;
}
function journeyToCity(city: JourneyCity): void {
if (journey.location.scale === "office") dispatchJourney({ type: "leave-office" });
if (journey.location.scale !== "california") dispatchJourney({ type: "return-to-california" });
dispatchJourney({ type: "navigate-to-city", city });
}
/**
* The buildings this page can walk into.
*
* Three, deliberately unalike — a tower above Transbay, a hangar at Alameda
* Point, an Arts District courtyard — because the thing worth showing is that
* one engine and one format render all three, and that `OfficeSite` is what
* makes them feel like different places rather than the same room with
* different furniture.
*
* The loaders stay lazy: a pack eagerly imported would put its furniture in the
* entry bundle for every visitor who never opens that door.
*/
const OFFICE_LOADERS: Readonly<Record<ShippedOfficeId, () => Promise<{ default: Office }>>> = {
"lumbridge-hq": () => import("./offices/lumbridge-hq.ts"),
"frontier-valley": () => import("./offices/frontier-valley.ts"),
"mateo-court": () => import("./offices/mateo-court.ts"),
};
const OFFICE_OPERATION_LOADERS: Readonly<Partial<Record<
ShippedOfficeId,
() => Promise<{ default: RobotOperationsDefinition }>
>>> = {
"lumbridge-hq": async () => ({
default: (await import("./offices/operations/lumbridge-hq.ts")).LUMBRIDGE_HQ_ROBOT_OPERATIONS,
}),
"mateo-court": async () => ({
default: (await import("./offices/operations/mateo-court.ts")).MATEO_COURT_ROBOT_OPERATIONS,
}),
};
const OFFICES = OFFICE_SITES.map((entry) => ({
...entry,
label: entry.name,
load: OFFICE_LOADERS[entry.id],
loadOperations: OFFICE_OPERATION_LOADERS[entry.id],
}));
const canvas = document.querySelector<HTMLCanvasElement>("#scene");
if (!canvas) throw new Error("#scene canvas missing");
/**
* One renderer, one loop, for as long as this page is open.
*
* Built here rather than inside `createScene` because a `WebGLRenderer` is a
* property of the *canvas* and not of the city drawn on it. When each city built
* its own, every switch abandoned a renderer on the one GL context this page
* has — and `WebGLRenderer.dispose()` frees no texture at all. `stage.ts` has
* the measurement and the reading of three's source it comes from.
*
* Nothing disposes this: it outlives every city and every office on the page,
* the same arrangement as `officeMaterials` below.
*/
const stage = createStage(canvas);
/**
* One environment map for the page, beside the one renderer, for the same
* reason there is one renderer.
*
* A `PMREMGenerator` compiles three shader programs and each cached environment
* is a half-float render target; both are properties of the GL context, not of
* the board drawn on it. Building a rig inside `createScene` would allocate a
* fresh chain per city and orphan the previous one on every switch, which is
* precisely the arithmetic `stage.ts` records for the renderer itself. So it is
* built here, handed to both scenes, and each scene calls `release()` on its way
* out so the rig's ledger does not retain a floor plate nobody can see.
*
* Nothing about it is eager: it allocates on the first `apply()`, and the two
* kinds — a sky for the city, a room for an office — are cached separately, so a
* page that never opens a door never builds the office blur chain.
*/
const environment = createEnvironmentRig(stage.renderer);
// `authFetch` so the private-office pack (`/api/v1/offices/:id`, which answers
// 404 rather than 403 to anyone who may not see it) is requested as the signed-in
// viewer. On a `password`-mode or open deployment it is an ordinary fetch.
const tera = createTeraClient({ fetch: authFetch });
/**
* Everything a board owns, bundled so a board can be **kept**.
*
* `mountCity` used to tear sixteen module-level variables down before it built
* the next board, which is why a switch was a full-screen card: the outgoing
* board was already gone, so there was nothing to draw for the whole build.
* Measured, that is 65-78% of every pause — 1,711 / 1,079 / 997 ms of cover
* against only 604 / 372 / 222 ms of blocked main thread, with 68 / 45 / 49
* frames drawn into the gap. Retention converts a modal into a live picture
* that freezes briefly, and it costs no triangles, because `Stage` retains and
* pauses by contract and a paused scene is zero draw calls.
*
* The split between what is in here and what is *not* is the whole design:
*
* - **In here** is everything expensive and everything derived from the pack:
* the scene handle, the plan view, the atmosphere, the board span, the
* harbour model, the traffic dial. All of it survives a switch away and
* comes back instantly.
* - **Not in here** is everything that is a *subscription*: the weather watch,
* the fire watch, the fire panel, the camera's fog listener and the pose
* editor. Those belong to the board that is on screen, not to the board that
* exists — a `/weather` poll for a city nobody is looking at is a request
* nobody needs, and a fire panel mounted per resident board would put two
* copies of it in a piece of chrome this file does not own. They are stopped
* on the way out of `presentBoard` and started again on the way in.
*
* The module-level `city`, `atmosphere`, `minimap` and the rest below are now
* the **visible** board's, projected out of the record by `adoptBoard`. That is
* deliberate rather than lazy: it keeps eighty-five `city.` call sites, six
* render functions and the frame pump reading exactly what they read before, so
* retention is a change to the lifetime of a board and not to the shape of this
* file.
*/
interface MountedBoard {
id: string;
entry: { id: string; label: string; city: City };
handle: SceneHandle;
minimap: Minimap | null;
atmosphere: Atmosphere;
/** The board's largest edge in scene units. The fog dip's collapse is a fraction of it. */
span: number;
/** Live traffic when the deployment has it; `null` means the simulator. */
flights: TrafficSource | null;
trafficDial: TrafficDial;
/** The rectangle a fire poll is clipped to, or `null` on a board that draws none. */
fireBounds: FireBounds | null;
carriesSky: boolean;
vesselsBody: VesselsBody | null;
vesselBounds: VesselBounds | null;
vesselBerths: readonly BerthAnchor[];
vesselPorts: readonly Port[];
harbourAtMs: number;
/** The markers this board was built with, for the plan view on the way back. */
markers: Marker[];
/** False until the first time it has been on screen; see `presentBoard`. */
arrived: boolean;
}
/**
* The resident boards, least recently shown first.
*
* California is pinned because it is the cheapest (5.86 MB of GPU buffers
* against the Bay Area's 14.12) and the root of every descent. The ceiling is
* three on a desktop — every board this build has — and two on a handheld,
* because `deviceProfile()` changes only the pixel ratio and the shadow map, so
* a phone carries exactly the same resident geometry as a laptop against a much
* smaller budget. `sf` and `socal` bounds do not intersect and California
* contains both, so two is provably enough to describe any camera position.
*/
const boards: BoardCache<MountedBoard> = createBoardCache<MountedBoard>({
pinned: ["california"],
capacity: residentCapacity(deviceProfile().handheld),
});
/**
* The board on screen, or `null` before the first one and between a dispose and
* the next swap. The record behind the `city` handle below.
*/
let visibleBoard: MountedBoard | null = null;
let city: SceneHandle | null = null;
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.
*
* `building()` defers its work by two animation frames so the boot card can
* paint, and a frame under load is not 16 ms — measured at 250 ms on a software
* rasteriser. Clicking SoCal and then changing your mind inside that window
* used to hit `if (id === cityId) return` against a `cityId` the deferred
* `mountCity` had not written yet, so the second click was discarded as
* redundant and you arrived at the city you had just cancelled. The guard has
* to be against the intention, and the intention is recorded synchronously in
* the click handler.
*/
let wantedCity = DEFAULT_CITY_ID;
let office: OfficeScene | null = null;
/** The pack used by `office`; kept separate from the currently selected door. */
let builtOfficeId: string | null = null;
let inside = false;
let controlModeState = createControlModeState();
const playInput = new PlayInputRouter();
let markers: Marker[] = SAMPLE_MARKERS;
let realtimeClient: RealtimeClient | null = null;
let presenceIndicator: PresenceIndicator | null = null;
let stopRealtimeSubscription: (() => void) | null = null;
let realtimeOperation = 0;
let lastRealtimePublishAt = 0;
let realtimePageActive = true;
/** Page-scoped wire identities; auth subject never enters the spatial protocol. */
const realtimeActorId = randomId();
const realtimeVehicleId = randomId();
const realtimeAircraftId = randomId();
/**
* The buildings you can walk into, as procedural glyphs on the city.
*
* The one thing that makes Tera and Spaces read as one product rather than two
* views sharing a bundle: both packs carry a real `site`, and until these
* existed that coordinate was known to the lighting and to nothing else.
*
* `OFFICE_SITES` rather than the packs, deliberately — a pack is a lazy chunk
* and the city wants these the instant the board appears. `colorKey` and `glyph`
* stay opaque to the engine, so a door looks different from a company without
* the renderer knowing what either means.
*/
const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
id: `office:${entry.id}`,
label: entry.name,
colorKey: "office",
blurb: `${entry.entryCopy} · ${entry.status}`,
lat: entry.site.lat,
lng: entry.site.lng,
// 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 } : {}),
}));
/**
* Whether a marker is a door rather than a company.
*
* The id prefix is the whole test, and it is deliberately something no marker
* off the wire can collide with: `markers/gate.ts` serves rows from a synced
* database and none of them are namespaced this way.
*/
function officeIdOf(marker: Marker): string | null {
return marker.id.startsWith("office:") ? marker.id.slice("office:".length) : null;
}
/**
* Whatever the pointer is over, so a click knows what it clicked.
*
* The engine reports picks by hover rather than by click — that is what drives
* the detail card — so the click handler has no argument of its own and reads
* this instead. `null` whenever the pointer is over open ground, which is what
* makes a click on the terrain do nothing.
*/
let hoveredMarker: Marker | null = null;
/**
* The colour a door is drawn in, which no marker feed knows about.
*
* Merged in **here** rather than added to `SAMPLE_PALETTE`, because it is not a
* sample of anything: a deployment that replaces the whole marker feed with its
* own palette must still get doors it can see. Amber, so a door reads as this
* application's navigation rather than as another marker hue.
*/
const OFFICE_PALETTE: MarkerPalette = { office: 0xf5b53f };
let palette: MarkerPalette = { ...SAMPLE_PALETTE, ...OFFICE_PALETTE };
let liveData = false;
/**
* What this visitor may do. Resolved once in `boot()`; every gate below reads
* `access.can.*` and nothing else.
*
* The pre-boot value is the **closed** one, deliberately. A handler that
* somehow fires before `resolveAccess()` has settled — a keystroke on a slow
* connection, a click on a control that is in the document from first paint —
* should offer a visitor less than they are entitled to and never more. The
* rule that produces this value, and the SSO bug that once produced it wrongly,
* are written out in `src/access.ts`.
*/
let access: Access = {
tier: "anon",
subject: null,
signInUrl: null,
can: capabilitiesFor("anon"),
feeds: null,
};
let atmosphere: ReturnType<typeof createAtmosphere> | null = null;
let minimap: Minimap | null = null;
/**
* The sky over the city currently on screen, polled while it is on screen.
*
* One watch at a time and it belongs to the board, not to the page. Stopping it
* at the top of `mountCity` is the whole of the cancel-on-switch rule: a
* `/weather` request for San Francisco that lands after the user has moved to
* SoCal would otherwise put the marine layer over Long Beach, and it is a
* request in flight for most of the second in which somebody clicks.
*/
let weatherWatch: WeatherWatch | null = null;
/**
* The live traffic source, when there is one, kept so the corner label can ask
* it whether the aircraft on screen were observed. `null` means the simulator,
* which is never live and does not need asking.
*/
let cityFlights: TrafficSource | null = null;
/**
* What is burning, polled while a board that draws fire is on screen.
*
* A board watch like the weather's, and stopped in the same place for a
* different reason. The body is not per-region — one statewide answer, one
* cache key, every viewer — so a late arrival cannot describe the wrong place
* the way a stale `/weather` can. What it can do is arrive for a board that has
* been torn down, and `promote()` needs *bounds* to say anything at all.
*
* `null` on the Bay Area board and on every keyless clone. See `firesFor`.
*/
let fireWatch: FireWatch | null = null;
/**
* Stops this board's camera listening to the fog, or `null` between boards.
*
* The rig follows the clock at about one hertz, which was fine while every term
* in it was a function of time. Aerial perspective is a function of where the
* camera is, and a camera can cross a board in a second — so a fog recomputed
* only on the clock steps four times during a chapter flight and pops each
* time. `OrbitControls` fires `change` on every update that actually moved
* something, including damping and including `setPose`, which is exactly the
* event this needs and is already the event the minimap redraws on.
*/
let cameraFogWatch: (() => void) | null = null;
/** The panel that says what the board is showing, and what it is not. */
let firePanel: FirePanelHandle | null = null;
/** The board's own rectangle, held so a poll landing later can be clipped to it. */
let fireBounds: FireBounds | null = null;
/**
* The last body, kept for the whole page rather than for the board.
*
* This is the one place the "not per-region" property of `/fires` pays a
* visible dividend. The same statewide body answers for every board, so a
* visitor who switches from California to the Southland can be shown the
* correct — and correctly *empty* — answer in the same frame as the switch,
* re-clipped to the new rectangle, instead of reading "nothing has answered"
* for as long as a fresh request takes. `undefined` means no watch has ever
* settled; `null` means one settled and the feed refused.
*/
let firesBody: FiresBody | null | undefined;
/**
* The harbour on the board, and the three sentences beside the three feeds.
*
* All of it is per-board and all of it is cleared on the way out of `mountCity`,
* because — unlike `firesBody`, which is one statewide answer for every board —
* a harbour is a fact about a rectangle and a raster is clipped to one.
*
* `vesselsBody` is **modelled**, not observed: `modelHarbour` builds it from the
* board's own authored berths and channels, and `promoteVessels` reads it
* through exactly the gate a live AIS body would go through. The day
* `GET /vessels` exists, the only line that changes is where this comes from.
* `harbourAtMs` is the clock that body was built for, so the next refresh is a
* new *fix* rather than a nudge — see `refreshHarbour`.
*/
let vesselsBody: VesselsBody | null = null;
let vesselBounds: VesselBounds | null = null;
let vesselBerths: readonly BerthAnchor[] = [];
let vesselPorts: readonly Port[] = [];
let harbourAtMs = 0;
/** Whether the board on screen is one a statewide instrument can describe. */
let boardCarriesSky = false;
/**
* The two sky bodies, and the one request that fetches them.
*
* `undefined` is "not asked yet", `null` is "asked and the box refused", and a
* body carrying `source: "none"` is "asked and the box serves no such feed" —
* three states with three different sentences, which is the whole reason the
* gates take a nullable body rather than a field. `skyAskedAtMs` throttles the
* refetch to the body's own TTL off the once-a-minute clock, so there is no
* second poller in this file.
*/
let radarBody: RadarBody | null | undefined;
let birdsBody: BirdsBody | null | undefined;
let skyAskedAtMs = 0;
let skyInFlight = false;
/** The three board notes, written by the gates and drawn by `showBoardNotes`. */
let seaNote = "";
let radarNote = "";
let birdsNote = "";
/**
* The satellite element sets, fetched once for the page rather than once per city.
*
* The asymmetry with every other feed here is the physics: an aircraft at
* 10,000 m is visible for tens of kilometres and the two boards are six hundred
* apart, but a satellite at 550 km is above the horizon for a circle two
* thousand kilometres across. So the elements are shared — and the *catalogue*
* is not, because a `SatelliteCatalogue` is built around an observer, and
* reusing one would compute the Southland's sky from San Francisco with nothing
* on screen to say so.
*
* A promise rather than a value, so a second city mounted while the first fetch
* is in the air waits for it instead of starting another. `null` forever on the
* majority of deployments: `TERA_SATELLITES_SOURCE` is off by default.
*/
let satelliteElements: Promise<SatelliteElements[]> | null = null;
/**
* The fabricated-traffic dial for the board on screen, rebuilt with every city.
*
* Held here rather than inside the godmode closure because the panel is mounted
* once and the board is not: a dial captured when the panel opened would keep
* pushing aircraft at a scene that had been disposed two city switches ago.
*/
let trafficDial: TrafficDial | null = null;
/**
* Whether the satellite layer is drawn, remembered across city switches.
*
* A new board builds a new layer, which starts visible, so a god who turned the
* sky off and then changed city would have it come back on — a setting that
* quietly undoes itself is worse than one that is not there.
*/
let satellitesVisible = true;
/**
* The build in progress. Aborting it is what makes a second click on the other
* city cheap: `createScene` drops the heightfield, resolves `null`, and has
* allocated no WebGL context for the abandoned board.
*/
let mounting: AbortController | null = null;
/**
* The build nobody asked for.
*
* A strictly lower-priority second lane. It exists because "one map" is a claim
* about continuity, and continuity is paid for by having the detail already on
* the GPU when the camera gets there: CONTRACT §1.1 keeps three boards resident
* on a desktop and pins California, and until now nothing ever *filled* that
* residency except a visitor waiting through a build.
*
* Held separately from `mounting` rather than reusing it, because the two have
* opposite rights. A foreground build may cancel a background one; a background
* build may never cancel or queue in front of a foreground one, and it must be
* abandonable the instant somebody touches anything. Keeping one controller for
* both would make "abort the build" ambiguous at exactly the moment it matters.
*/
let prefetching: AbortController | null = null;
let godmode: Godmode | null = null;
let poseEditor: PoseEditor | null = null;
/**
* The office, once somebody has asked for it. Both are `null` until the first
* `loadOffice()` because both live in a chunk this page does not fetch until
* then — see `loadOffice` for the arithmetic, and the import block above for
* what keeps them out of the entry chunk.
*
* The registry is one texture set for every office this page ever builds.
* Signing in while standing in the public office is a `dispose()` and a second
* `createOfficeScene` at `"full"` — cheap only if both are handed the same
* registry, because drawing the textures is the expensive part and a registry
* draws them once. Owned here and disposed nowhere: it outlives every scene
* that borrows it, and the page teardown takes the process with it.
*/
let officePack: Office | null = null;
let officeMaterials: MaterialRegistry | null = null;
/**
* Which building the door leads to. Changed by the picker while you are inside.
*
* `officePack` is the pack for *this* id and is rebuilt on a switch rather than
* memoised forever, which is the one-line difference from the arrangement that
* could only ever hold one office.
*/
let officeId: string = OFFICES[0]?.id ?? "lumbridge-hq";
/**
* The office's own sky, when its pack says where it stands.
*
* A second `Atmosphere` rather than the city's, because the two are at different
* scales and in different places: the city's is built with `metresPerUnit` near
* 94 and a fog measured in board spans, and an office runs at 1 m per unit
* fifty metres across. `null` for a pack with no `site`, which keeps the fixed
* interior rig and is a supported state rather than a gap.
*/
let officeAtmosphere: Atmosphere | null = null;
let officeScreenPanel: OfficeScreenPanel | null = null;
let sharedScreen: {
screenId: string;
stream: MediaStream;
video: HTMLVideoElement;
texture: VideoTexture;
remote: RemoteOfficeMedia | null;
} | null = null;
let remoteViewedScreen: {
screenId: string;
officeId: string;
video: HTMLVideoElement;
texture: VideoTexture;
remote: RemoteOfficeMedia;
bound: boolean;
} | null = null;
let remoteMediaOperation = 0;
let remoteViewerRequestScreenId: string | null = null;
/**
* The plan panel's other occupant.
*
* There are two of these because there are two places, and the panel shows the
* one you are standing in. Holding the city plan up while the scene in front of
* it is a floor plate was the bug this pair exists to fix: the single piece of
* chrome whose whole job is "where am I" was answering about another county.
*
* It is built with the office and dies with it, so the office's `Plan`, camera
* and controls — all three of which it holds by reference — cannot outlive the
* scene they came from. Which of the two is *mounted* is a separate question
* from which exist, and `showPlan()` is the only thing that answers it.
*/
let officePlan: OfficeMinimap | null = null;
/**
* Whether the people currently on the floor came from the deployment or from
* `sample.ts`.
*
* Kept because the difference has to be *said*. Every other gap on this page is
* a silence — an empty office, a missing scrubber — and a silence you cannot
* attribute is indistinguishable from a fault; here the failure is the opposite
* and worse, because fabricated people are not silent. Twenty-five invented
* names at real desks is a screenshot somebody will take, and it must not be
* possible to take it without the caption.
*/
let presenceIsSample = false;
/**
* The deployment's own roster, or `null` while the sample one stands in for it.
*
* Held separately from what is on screen because the two are refreshed by
* different things: a real roster arrives from the API on its own timer, and the
* sample one is a function of the clock and has to be recomputed whenever the
* clock moves. Collapsing them would mean either re-rendering a live roster on
* every scrub or freezing the sample one.
*/
let livePresence: Presence[] | null = null;
/**
* The running poll of who is in, or `null` when nobody is standing in the room.
*
* It belongs to the visit and not to the page, which is the same rule
* `weatherWatch` follows one line up and for a sharper reason: a roster is
* requested with a credential and describes people, so a watch left running
* after somebody stepped back out to the city is a page quietly asking about a
* room nobody is looking at. Started by `enterOffice`, stopped by `leaveOffice`
* and by `mountCity`, and there is never more than one.
*/
let presenceWatch: PresenceWatch | null = null;
/**
* `office.lumbridgecorp.com` and `tera.lumbridgecorp.com` are one bundle behind
* two names, and which name you arrived at is the whole difference: one is a
* map with an office in it, the other is an office with a map behind it. The
* city is still built underneath either way — that is what makes "← Back to the
* city" work from the office front door — so this is a policy about where boot
* *stops*, not about what boot builds.
*/
const OFFICE_HOST = location.hostname.split(".")[0] === "office";
const OPENS_IN_OFFICE =
OFFICE_HOST || new URLSearchParams(location.search).get("view") === "office";
/**
* Where the city is, when this page is the office front door.
*
* Leaving the office used to swap the scene and leave the address bar saying
* `office.`, so the URL you would copy and send disagreed with what you were
* looking at. The city door is therefore this hostname with its first label
* swapped — a guess only ever made when that label is literally `office`, which
* is the same comparison that made this the office door. Same registrable
* domain, same static root, same API; a deployment serving one name and not the
* other has half-configured itself, and a 404 is the honest failure for that.
*
* Anything else — a bare domain, `?view=office` on the city door — derives
* nothing and keeps the in-page swap.
*/
function cityDoorUrl(cityWanted?: string): string | null {
if (!OFFICE_HOST) return null;
const url = new URL(location.href);
const labels = url.hostname.split(".");
labels[0] = "tera";
url.hostname = labels.join(".");
url.search = "";
url.hash = "";
// Carried so that stepping out of the office into SoCal lands in SoCal. The
// page load is the cost of telling the truth about where you are; arriving in
// the wrong metro on top of it would not be.
if (cityWanted !== undefined && cityWanted !== DEFAULT_CITY_ID) {
url.searchParams.set("city", cityWanted);
}
return url.href;
}
// ---- The interface's own state --------------------------------------------
/**
* The applier, once the page has one.
*
* `null` for the handful of statements between this module's first line and
* `boot()`, which is why every call site is optional-chained rather than
* asserted: a keystroke that lands in that window should do nothing, not throw.
*/
let chrome: ChromeHandle | null = null;
/**
* Two pieces of chrome are a *user* decision rather than a media query, and the
* distinction matters: a media query that hides the plan below 600px also makes
* `M` do nothing there, which is the width where a plan view is most useful and
* least affordable. So the width only seeds the initial state, and the moment
* someone presses the key or the button the viewport stops having an opinion.
*
* The seeds themselves live in `ui/chromeState.ts` next to the breakpoints they
* read, because that is where the reasoning about what a phone can afford is —
* and because the old inline `window.innerWidth > 600` for the plan was the
* whole of live defect 10, "the minimap disappears entirely" on a phone.
*/
let panelOpen = seedPanelOpen(window.innerWidth);
let planOpen = seedPlanOpen(window.innerWidth);
let panelChosen = false;
let planChosen = false;
/**
* Whether this browser has been offered the first-run coach.
*
* Read **once**, here, before anything mounts: `mountOnboarding` marks the flag
* as it appears rather than as it completes, so asking `localStorage` a second
* time after the coach is up would answer "seen" and take it straight back off
* the screen. The write belongs to the coach; this is the read, and it is the
* only one.
*/
let firstVisit = !hasSeenOnboarding(browserStorage());
/**
* What the detail card is showing: an authored sentence, an observed aircraft,
* or nothing.
*
* A union rather than a string, because the two are not the same kind of thing
* and flattening the aircraft to a sentence is exactly what used to happen —
* `showDetail(\`${m.label} — ${m.blurb}\`)` for everything, including a track
* with five fields and a licence credit on it.
*/
let detail: ChromeDetail | null = null;
// ---- Studio hardware -------------------------------------------------------
/**
* Where the readings for the room you are standing in come from.
*
* One at a time and it belongs to the visit, like `presenceWatch` above it. The
* choice between the deployment's own device route and the fixed-step simulator
* in this tab is made inside `devices/adapter.ts` and deliberately not here —
* that is the one place the anon-first fallback decision lives, and a second
* copy of it in the app is how the two drift apart.
*/
let deviceSource: DeviceSource | null = null;
/** The latest readings, so the panel and the hardware in the room agree. */
let deviceStates: readonly DeviceState[] = [];
/**
* The car outside, as a state machine.
*
* Seeded from the office id, so a studio's Model X has the same charge, the same
* cabin temperature and the same parking jitter on every machine and on every
* reload — a car whose battery was different every time you opened the door
* would read as noise rather than as a vehicle.
*/
let vehicleTelemetry: SimulatedVehicleTelemetry | null = null;
/**
* The clock line, already formatted, because this file owns the clock.
*
* `instantOverride` is the one thing on the page that can make the rendered
* instant disagree with the wall clock, and it lives here — so a formatter
* downstream would have to be handed the override as well as the time, which is
* two facts to keep in step for one string. Written by `updateSun`, which is
* also the only thing that reads the sun.
*/
let clockLabel = "";
// ---- Time -----------------------------------------------------------------
/**
* `null` follows the wall clock. The override exists because the honest answer
* at 2 a.m. is a very dark city — correct, and not what you want to be looking
* at while judging whether the sun is in the right place.
*
* A whole `Date` and not an hour, which is the change the godmode panel forced
* and the right shape anyway. The old scrubber wrote an hour onto *today*, so
* there was no way to ask for the December solstice, and any control that could
* set a date would have had it silently discarded on the next scrub. One
* override, one writer, one type that can carry everything the sun depends on.
*/
let instantOverride: Date | null = null;
/**
* Where the office's house lights take their level from. Godmode's switch, and
* it survives the office being rebuilt because the panel re-asserts it.
*/
let houseLights: GodmodeHouseLights = "sun";
/**
* The sun's height as the *fittings* are told it, which is the real one unless
* somebody has a hand on the switch.
*
* Forcing the level by lying about the elevation rather than opening a second
* path into `luminaires.ts`, and that is the design rather than a shortcut:
* `OfficeScene.setSolarElevation` feeds the fittings and nothing else, while
* the rig outside is computed from the real `env` a line later. So this moves
* the *interior* and leaves the sky, the key light and the fog exactly where the
* clock put them — which is the whole point of a control that lets you look at
* the night office in daylight. ±90° is far outside the 0°–6° ramp in
* `luminaires.ts`, and stays outside it if that band is ever widened.
*/
function houseElevation(actual: number): number {
return houseLights === "sun" ? actual : houseLights === "on" ? -90 : 90;
}
/**
* A fabricated sky, or `null` for whatever the deployment reports.
*
* Kept next to the instant because it is the same kind of thing — a god-only
* lie about the inputs, told to see what the renderer does with it — and it
* takes precedence over the live observation for exactly as long as it is set.
*/
let weatherOverride: WeatherObservation | null = null;
function currentInstant(): Date {
return instantOverride ?? new Date();
}
/**
* What the sky is doing, in the order the answers are trusted.
*
* The override wins because somebody typed it. Otherwise the live observation,
* and `null` — nobody was asked — when there is no watch or it has not landed
* yet. `null` is not "clear": `atmosphere.ts` treats a *reported* clear sky as
* authority that suppresses the modelled marine layer, so handing it an
* invented clear day on every failed poll would permanently kill San
* Francisco's fog on the zero-config box where the local model is all there is.
* See `WeatherFeed` in `adapters/http.ts`, which is careful about the same
* distinction from the other side.
*/
function currentWeather(): WeatherObservation | null {
return weatherOverride ?? weatherWatch?.current().value ?? null;
}
/**
* The office's rig for the instant being rendered, in the building's own frame.
*
* Same clock, same weather and same `Atmosphere` machinery the city runs on —
* which is the entire point. An office that dimmed on a schedule of its own
* would be a second sun, and CONTRACT.md §4 exists to say there is one.
*
* `officeDaylight` does the two things a room needs and a map does not: it turns
* the sun into the building's frame using `site.heading`, so the light comes
* through the windows the pack actually has, and it moves the fog outdoors.
*/
function officeLighting(site: NonNullable<Office["site"]>) {
const env = observe(site.lat, site.lng, currentInstant(), currentWeather());
// `officeAtmosphere` is built alongside the pack; falling back to the fixed
// rig here would be a flicker rather than a fix, so this is only ever called
// where one exists.
const state = officeAtmosphere?.apply(env);
if (!state) return undefined;
/**
* The building's own lights, on top of whatever is left of the sun.
*
* The order matters and is the only subtle thing here: the daylight
* adaptation runs first, because it is about the *sun* — which way the
* building faces and where the weather starts — and the house lights are
* added to the result, because they are about the building. Doing it the
* other way round would rotate the interior lighting by the building's
* heading, which is meaningless: a ceiling does not face a compass point.
*
* `office` may not exist yet — this is called once at construction, before
* there is a scene to ask — in which case the elevation is fed straight to the
* ramp so the first frame is already correct rather than a lit room fading
* down or a dark one fading up.
*/
office?.setSolarElevation(houseElevation(env.sun.elevation));
const house = office?.houseLevel() ?? houseLevelFor(houseElevation(env.sun.elevation));
/**
* And the smoke, which is the third parameter and the only one that comes
* from another building's problem.
*
* It is adapted here, once, for the reason CONTRACT §4 gives: `Atmosphere`
* owns the rig and `daylight.ts` adapts what it returned, so a room may not
* reach into the fire layer and form a second opinion about its own sky. The
* scalar is the drawn set weighted by acreage, distance and wind alignment —
* `smokeLoadAt` in `engine/fires.ts` — and it is zero whenever nothing is
* burning, which is most days and is today on the Southland board.
*
* Costs no geometry and no draw call: it moves the haze colour, the haze
* near-distance and the sun's tint, three numbers `officeDaylight` already
* computed.
*/
return withHouseLights(officeDaylight(state, site, smokeLoadAtSite(site)), house);
}
/**
* The lights-on ramp, for the one moment there is no office to ask.
*
* Duplicating the two bounds from `luminaires.ts` is a smell and is the lesser
* of the two available ones: the alternative is building the office scene with
* a rig computed from a light level it cannot report yet, which means the room
* is visibly wrong for exactly one frame at every entry. Kept in step by being
* four lines long and named after the thing it mirrors.
*/
function houseLevelFor(solarElevationDeg: number): number {
return 1 - Math.min(1, Math.max(0, solarElevationDeg / 6));
}
/**
* The fog alone, for the camera's clock rather than the sun's.
*
* ## The split, and why it is a split rather than a cheaper `setLighting`
*
* Two things move at completely different rates and were being served by one
* call. The **sun** moves on the wall clock, which this file steps once a
* minute. The **camera** moves on a gesture: `change` fires every frame under
* damping, and even throttled to 2% of altitude a single drag gets through a few
* dozen times. This function is the second of those, and it now sends the only
* thing that is actually a fact about the camera — how far you can see from
* where it is standing.
*
* What it used to send was the whole rig, and the whole rig is a fan-out: six
* layer setters, a dirtied `MeshLambertMaterial` in the port yard, a rebuild of
* the vessel wake instances, and the PMREM environment's fingerprint in
* `environmentRig.ts`. Not one of those reads a fog distance. Measured over a
* dolly and a drag on both metro boards, the old path cost 0.14-0.19 ms a step
* and this one costs about 0.02.
*
* ## The trap it also disarms, which is the better reason
*
* `environmentRig.ts` decides whether to re-render and re-convolve the sky
* cubemap by fingerprinting the rig's colours — `sky.horizon` among them — and
* `interiors/daylight.ts` pins that horizon stop to the fog colour so the sky
* dome and the haze meet without a seam. Aerial perspective moves distances and
* no colours, so it does not trip that today; the point of routing the camera
* through a setter that *cannot carry a colour* is that the next altitude-driven
* term cannot trip it either. The alternative fix — coarsening the fingerprint
* until the rebuild stops — hides one instance and leaves the mechanism armed.
*
* `updateSun` still passes the camera's view to `apply`, so a clock tick on a
* board nobody is touching still gets the right fog. `Atmosphere` remains the
* sole owner of both numbers (CONTRACT §4); this file only chooses which of them
* a gesture is allowed to move.
*/
function applyCameraFog(): void {
if (!city || !atmosphere) return;
// A dip owns this board's fog for the ~0.8 s it runs. A camera event landing
// inside that window would snap the haze back open mid-transition, which is
// precisely the seam the dip exists to cover.
if (fogDip !== null && fogDip.record.handle === city) return;
const active = CITIES.find((c) => c.id === cityId)?.city ?? SAN_FRANCISCO;
const env = observe(active.center.lat, active.center.lng, currentInstant(), currentWeather());
city.setAerialFog(
atmosphere.aerial(env, {
altitudeMetres: city.cameraAltitudeMetres(),
standoffMetres: city.cameraStandoffMetres(),
}),
);
}
function updateSun() {
const active = CITIES.find((c) => c.id === cityId)?.city ?? SAN_FRANCISCO;
// The office follows the same clock, and follows it whether or not you are
// standing in it — walking back in to a room lit for an hour ago is the
// failure this avoids.
const site = officePack?.site;
if (office && site && officeAtmosphere) {
const state = officeLighting(site);
if (state) office.setLighting(state);
}
// The roster follows the same clock the sun does. Never over a live answer.
if (livePresence === null) applyPresence();
if (!city || !atmosphere) return;
const env = observe(active.center.lat, active.center.lng, currentInstant(), currentWeather());
/**
* The camera's height is the second argument to the rig now, and the reason
* it comes from the scene rather than from here is that only the scene knows
* this board's metres per unit and its vertical exaggeration.
*
* `atmosphere.ts` turns it into aerial perspective — how far you can see
* through the air you are above — clamped so the clear-day pair below is the
* ceiling and never exceeded. At the whole-board pose it saturates and every
* board renders exactly as it did before, which is why the Bay Area's stills
* are unaffected by construction. Low down it is what puts haze on a range
* eighty kilometres up the valley.
*/
const view = {
altitudeMetres: city.cameraAltitudeMetres(),
standoffMetres: city.cameraStandoffMetres(),
};
city.setLighting(atmosphere.apply(env, view));
// `LightingState` carries a fog pair, so a clock tick inside a dip would undo
// it. Re-asserted rather than suppressed, because the *rest* of the rig — the
// sun, the sky, the environment map — is still exactly what the clock says.
if (fogDip !== null && fogDip.record.handle === city) {
const dip = fogDip;
const t = dip.seconds <= 0 ? 1 : Math.min(1, dip.elapsed / dip.seconds);
city.setAerialFog(dipFog(dip.from, dip.to, t));
}
city.setSolarElevation(env.sun.elevation);
/**
* The board's other three feeds, on the same clock and from the same sun.
*
* `env.sun.elevation` is handed to the migration gate rather than letting it
* ask an ephemeris of its own. A second opinion about where the sun is, taken
* from a clock the scrubber does not own, is how a night board and a night
* layer end up disagreeing — and `LightingState.hemisphere` is not the signal
* either, because `atmosphere.ts` raises the fill after dark and it reads
* *higher* at midnight than at noon.
*
* Both are no-ops on a board without them: `tickHarbour` returns on a board
* with no port, and `setPrecip`/`setMigration` are no-ops without a layer.
*/
tickHarbour(currentInstant().getTime());
if (boardCarriesSky) applySky(env.sun.elevation, Date.now());
/**
* `atmosphere.cloudCover(env)` and **not** `currentWeather()?.cloudCover ?? 0`.
*
* `null` weather is "nobody was asked", which is not an edge case — it is the
* default deployment and the configuration this repo is held to. Falling back
* to zero meant a clean clone's sky was permanently, silently empty.
* `atmosphere` models a sky when nobody has observed one, and an observed
* cover still wins outright when there is one.
*/
city.setCloudCover(atmosphere.cloudCover(env));
city.setWind(currentWeather()?.windKph ?? null, currentWeather()?.windDirDeg ?? null);
// The override itself, not `currentInstant()`. Handing over a resolved date
// would peg the sky to whatever second this ran in, and this runs about once a
// second — so an unscrubbed sky would advance in visible steps while the
// aircraft beside it moved smoothly. `null` means "read the clock yourself".
city.setSkyInstant(instantOverride);
// The plan view follows the same day the map does. It computes its own
// palette from this one number rather than reading the rig, because a rig is
// a set of three.js lights and the minimap has none.
minimap?.setSolarElevation(env.sun.elevation);
const el = env.sun.elevation;
const time = env.time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
const moon = env.moon ? ` · moon ${Math.round(env.moon.illuminated * 100)}%` : "";
// Formatted here rather than in the chrome, because the clock is this file's
// — `instantOverride` is the one thing on the page that can make it disagree
// with the wall clock, and a formatter downstream would have to be told.
clockLabel = `${time} · sun ${el >= 0 ? "+" : ""}${el.toFixed(1)}° · ${daylightPhase(el)}${moon}`;
renderChrome();
}
// ---- Fire -----------------------------------------------------------------
/**
* The boards that draw fire.
*
* Named rather than derived, and deliberately not "every board": the Bay Area
* rectangle has never contained a live incident, so a layer there would be a
* frame-time allowance spent on an empty view every night. California and the
* Southland are the two rectangles the upstream store actually populates.
*/
const FIRE_BOARDS: ReadonlySet<string> = new Set(["california", "socal"]);
function drawsFire(id: string): boolean {
return FIRE_BOARDS.has(id) && access.feeds?.fires === true;
}
/**
* One body, turned into the three things that read it.
*
* **The gate is not here and must never be copied here.** `promote()` in
* `src/server/fires.ts` is the only place that decides what is drawn — the ten
* acres, the eighty per cent, the prescribed burns, and the twenty-one stale
* rows the collector never deletes — and this function's whole job is to hand
* its answer to three consumers unchanged. On today's live store that answer is
* five marks on California and *nothing at all* on the Southland, from the same
* body on the same second, and both are correct.
*
* `null` is a fourth state and it is the one that has to survive: nothing has
* answered. The panel says so out loud rather than showing an all-clear, which
* is the same argument `health.ts` makes for `degraded[]` — a board with no feed
* behind it and a board with nothing burning on it are indistinguishable without
* a timestamp.
*/
function applyFires(body: FiresBody | null): void {
firesBody = body;
if (fireBounds === null) return;
const promotion = promote(body, fireBounds);
city?.setFires(promotion);
firePanel?.apply(body === null ? null : promotion);
// The courtyard the fires are actually over. Cheap — `smokeLoadAt` reads the
// drawn set, which is at most sixty-four rows and is usually none — and only
// ever a repaint of a rig that was going to be recomputed on the next clock
// tick anyway.
if (inside) updateSun();
renderChrome();
}
/**
* How much smoke is in the air at a building, from the fires on the board.
*
* Zero with no fire layer, zero with nothing drawn, and zero on a board that
* does not draw fire at all — so the courtyard is bit-identical to today
* everywhere except under a real fire, which is the property
* `interiors/daylight.ts` was given a third argument for.
*/
function smokeLoadAtSite(site: NonNullable<Office["site"]>): number {
return city?.fireSmokeLoadAt(site.lat, site.lng) ?? 0;
}
/**
* The load below which the building says nothing about smoke.
*
* `smokeLoadAt` is continuous and its floor is a real zero, so "greater than
* zero" looked like the honest test and is not. On today's live board the two
* ten-acre fires in San Bernardino are 101 km from Mateo Court — inside the
* layer's 120 km reach — and contribute a load of about **0.0025**: a quarter of
* one per cent of a haze change, which is nothing anybody can see, under a
* sentence that would have read "Thin smoke". That is a caption describing
* something the picture does not contain, which is the failure this whole round
* is arranged against.
*
* So the **rig** still gets the true scalar — continuous, no pop, and at 0.0025
* indistinguishable from zero — and the **words** wait until there is something
* to say. Calibrated from the layer's own model rather than by eye: a
* 500-acre fire 60 km away lands at 0.14 and a 3,600-acre fire 20 km away at
* 0.60, so this threshold sits an order of magnitude under the smallest fire
* anybody would call smoke and an order of magnitude over the largest one
* nobody would.
*/
const SMOKE_STATED_LOAD = 0.08;
/** The room's smoke disclosure, or `null` on a day the room cannot tell. */
function officeSmokeNote(): string | null {
const site = officePack?.site;
if (site === undefined) return null;
const load = smokeLoadAtSite(site);
return load >= SMOKE_STATED_LOAD ? smokeCaption(load) : null;
}
/**
* Whether the board's fire panel is on screen right now.
*
* Two rules, and the second one is a judgement made from a picture. Outside a
* building the panel is always up on a board that draws fire, because "nothing
* is burning here" is a fact about that board and the whole point is that it is
* *stated* rather than inferred from an absence.
*
* **Inside a building it appears only when the fires are actually in that
* building's sky.** A five-item list of incidents three hundred kilometres away
* pushed "Back to the city" below the fold of the LA office's panel on the first
* frame a visitor sees of the room — a board-level instrument crowding out the
* room's own controls. When the courtyard really does go brown, the list is the
* explanation for what is on screen and it belongs there; `smokeCaption` in the
* office note says the same thing in one sentence either way.
*
* `#fire-section` is this file's element, not `mount.ts`'s. See where it is
* mounted.
*/
function showFireSection(): void {
const section = document.querySelector<HTMLElement>("#fire-section");
if (section === null) return;
const site = officePack?.site;
const wanted =
firePanel !== null &&
(!inside || (site !== undefined && smokeLoadAtSite(site) >= SMOKE_STATED_LOAD));
if (section.hidden === !wanted) return;
section.hidden = !wanted;
}
// ---- The sea, and the sky over it -----------------------------------------
/**
* The seed the harbour is modelled from.
*
* Fixed, and that is the whole point: two visitors on two continents see the
* same ships in the same berths, and a capture script that shoots the Southland
* twice gets the same photograph. Everything downstream of it is a hash of this
* and a stable string — a berth id, a port id — and never `Math.random`.
*/
const HARBOUR_SEED = 115;
/**
* The harbour, rebuilt as a **new fix** rather than nudged along.
*
* `modelHarbour` stands in for a feed this deployment does not have, and the
* feed's shape is what sets the cadence here. Upstream listens for thirty
* seconds every fifteen minutes, so a hull under way has moved about five
* kilometres between two reports; `engine/vessels.ts` is licensed to dead-reckon
* along the reported course for exactly that long and then stops. Rebuilding on
* the same interval is therefore not a smoothing trick — it is the feed's own
* behaviour — and the small jump a moving hull makes when a new body lands is
* the jump a real fix makes. Splining it away would be inventing the positions
* in between, which is the one thing this layer refuses to do.
*
* The clock is the app's instant rather than the wall clock, so a scrubbed sky
* and the harbour under it describe the same moment, and `promoteVessels` is
* handed the same number so the body's age is what it actually is: zero.
*/
function refreshHarbour(atMs: number): void {
if (vesselBounds === null) return;
vesselsBody = modelHarbour(vesselPorts, { seed: HARBOUR_SEED, atMs });
harbourAtMs = atMs;
/**
* Through the gate, never around it.
*
* The modelled body is a `VesselsBody` and it goes through the same
* `promoteVessels` a live AIS body would: the three sentinels are re-checked,
* the hulls are clipped to this board, a moored one takes its bearing from the
* berth it is lying on, and the four suppression counters are what
* `vesselSummary` writes its sentence from. The day `/vessels` exists, the
* only line in this file that changes is the one above.
*/
const promotion = promoteVessels(vesselsBody, vesselBounds, vesselBerths, atMs);
city?.setVessels(promotion.drawn);
seaNote = vesselSummary(promotion);
}
/** A new fix when the declared interval has passed, in either direction. */
function tickHarbour(atMs: number): void {
if (vesselBounds === null) return;
const intervalMs = Math.max(60, vesselsBody?.intervalSeconds ?? 900) * 1000;
// Absolute, because the clock on this page can be dragged backwards.
if (Math.abs(atMs - harbourAtMs) < intervalMs) return;
refreshHarbour(atMs);
}
/**
* Ask for the rain and the birds, at most one request each per TTL.
*
* Gated on `/health`'s `sources`, exactly as the fire watch is: on a deployment
* that serves neither — which is this repo's default and every clone — nothing
* is requested at all and the two gates still have something true to say, so the
* panel reads "no radar feed is configured" rather than an ambiguous silence.
*
* There is no watcher class behind this on purpose. Radar is a five-minute
* composite and BirdCast a ten-minute one; the page already has a once-a-minute
* clock, and a fourth polling ladder in `adapters/http.ts` to re-ask a question
* whose answer changes at most twelve times an hour would be machinery bought
* for nothing.
*/
async function askSky(): Promise<void> {
if (skyInFlight) return;
const wantsRadar = access.feeds?.radar === true;
const wantsBirds = access.feeds?.birds === true;
if (!wantsRadar && !wantsBirds) return;
skyInFlight = true;
skyAskedAtMs = Date.now();
try {
const [radar, birds] = await Promise.all([
wantsRadar ? tera.radar() : Promise.resolve(null),
wantsBirds ? tera.birds() : Promise.resolve(null),
]);
if (wantsRadar) radarBody = radar;
if (wantsBirds) birdsBody = birds;
} finally {
skyInFlight = false;
}
// Straight onto the board rather than at the next minute: a scan that landed
// is the only thing on this page that can change what the sky looks like
// without the clock moving.
updateSun();
}
/**
* Both sky gates, run against whatever has answered.
*
* `nowMs` is the **wall** clock and not the app's instant, because `fetchedAt`
* is a real timestamp and "last scan 5 minutes old" has to stay true when
* somebody scrubs the sky to midnight. `solarElevationDeg` is the opposite: it
* is the elevation the board is *lit* by, because the migration gate is a
* daylight gate and a layer that disagreed with the sky about whether it is
* night is precisely the failure that made the first version of it draw nothing
* at 04:35 while passing its own tests.
*
* Both messages are always full sentences, including — especially — on the day
* nothing is falling and nothing is aloft, which is most days.
*/
function applySky(solarElevationDeg: number, nowMs: number): void {
const radar = promoteRadar(radarBody, nowMs);
const birds = promoteBirds(birdsBody, { nowMs, solarElevationDeg });
city?.setPrecip(radar.field);
city?.setMigration(birds.field);
radarNote = radar.message;
birdsNote = birds.message;
// The next scan, when this one has expired. `askSky` stamps its own clock
// before the request, so this cannot re-enter while one is in the air.
const ttlMs = Math.max(60, radarBody?.ttlSeconds ?? 0, birdsBody?.ttlSeconds ?? 0) * 1000;
if (nowMs - skyAskedAtMs >= ttlMs) void askSky();
}
/**
* The two board notes, drawn the way `showFireSection` draws the fire panel.
*
* `#sea-section` and `#sky-section` are this file's elements rather than
* `mount.ts`'s, the same arrangement `#fire-section` and `#presence-host` have:
* their content is a board's own instrument reading, they are rebuilt per board,
* and they are hidden outright on a board that has no harbour and no statewide
* raster. Hidden inside a building for the reason the fire list is: a room's
* panel belongs to the room.
*/
function showBoardNotes(): void {
const sea = !inside && vesselBounds !== null ? seaNote : "";
writeBoardNote("sea-section", [["sea-note", sea]]);
const sky = !inside && boardCarriesSky;
writeBoardNote("sky-section", [
["radar-note", sky ? radarNote : ""],
["birds-note", sky ? birdsNote : ""],
]);
}
/** One section, its paragraphs, and the rule that an empty one is not shown. */
function writeBoardNote(
sectionId: string,
notes: readonly (readonly [string, string])[],
): void {
const section = document.querySelector<HTMLElement>(`#${sectionId}`);
if (section === null) return;
let any = false;
for (const [id, text] of notes) {
if (text !== "") any = true;
const line = document.querySelector<HTMLElement>(`#${id}`);
if (line === null) continue;
// Compared before it is written, like `mount.ts`'s applier: this runs on
// every `renderChrome` and a `textContent` write is a layout invalidation.
if (line.textContent !== text) line.textContent = text;
if (line.hidden === (text !== "")) line.hidden = text === "";
}
if (section.hidden === !any) return;
section.hidden = !any;
}
// ---- Cities ---------------------------------------------------------------
/**
* Switching boards no longer tears the old one down.
*
* The board on screen stays on screen, fully interactive, for the whole of the
* next board's build; the swap happens afterwards, under a fog dip, and the
* outgoing board is **kept** so coming back to it costs nothing. See
* `MountedBoard` for what is retained and what is not, and `engine/boards.ts`
* for the measurements that say two resident boards are free and three are
* provably enough.
*
* The opaque boot card is now the *first* mount of a session only. A switch
* after that shows a small non-modal pill and leaves the picture alone, which is
* the whole of what the owner can feel.
*/
async function mountCity(id: string): Promise<void> {
const entry = CITIES.find((c) => c.id === id);
if (!entry || !canvas) return;
/**
* Abandon whatever is still building before touching anything else.
*
* Two clicks on the board buttons a second apart used to run two heightfields
* to completion and race to assign `city`; now the first `createScene` sees
* its signal go and resolves `null` without ever allocating a renderer. The
* controller is replaced rather than reused because an aborted signal stays
* aborted, and the new build must not be born cancelled.
*
* It does **not** touch a board that is already resident. An abort cancels a
* build, and a built board belongs to the cache from the moment it lands in
* it.
*/
/*
* The background lane goes first, and the order is the whole guard.
*
* `createScene` awaits a heightfield the terrain worker is building; if the
* prefetch is still holding that worker when a real request arrives, the
* foreground build queues behind work nobody asked for. Aborting it here —
* before `mounting?.abort()`, before anything else — is what makes a visitor's
* click always the most important thing in the process.
*/
cancelPrefetch();
mounting?.abort();
const mount = new AbortController();
mounting = mount;
wantedCity = id;
const resident = boards.get(id);
if (resident !== null) {
// Nothing to build and nothing to wait for: the shader programs, the
// heightfield and every instance buffer are still on the GPU. Measured, a
// second visit to the Bay Area used to cost 1,664 ms against the first
// mount's 1,779 — three refcounts programs per material and deletes at
// zero, so disposal was relinking every shader on the way back.
await presentBoard(resident);
return;
}
showSwitchProgress(entry.label, null);
try {
const record = await buildBoard(entry, mount);
if (record === null) {
pendingPlace = null;
hideSwitchProgress();
return;
}
if (mount.signal.aborted || wantedCity !== id) {
// Somebody changed their mind while this was in the air. It is a complete,
// correct board — but nothing asked for it, and parking an unrequested
// board in a cache with a ceiling would evict one somebody did ask for.
disposeBoard(record);
pendingPlace = null;
hideSwitchProgress();
return;
}
await presentBoard(record);
} finally {
/*
* A finished mount is not a mount in flight.
*
* `mounting` used to be replaced and never cleared, which was harmless while
* the only thing that read it was the next `mountCity` wanting something to
* abort — an already-resolved controller aborts nothing and costs nothing.
* It stopped being harmless the moment a second lane asked the obvious
* question "is a foreground build happening right now?", because the honest
* answer after the first mount of the session was permanently "yes". The
* background lane armed exactly never, and the only symptom was a feature
* that silently did nothing.
*
* Guarded on identity: a build that was superseded must not clear the
* controller belonging to the build that superseded it.
*/
if (mounting === mount) mounting = null;
}
}
/**
* Build a board without touching the one on screen.
*
* Everything in here is either expensive or derived from the pack, which is the
* test for what belongs to a `MountedBoard`. Nothing in here reads `cityId`,
* `city`, `atmosphere` or `minimap` — those are the *visible* board's and the
* visible board is still the outgoing one for the whole of this function. That
* separation is what makes building behind a live picture possible at all, and
* it is the one invariant to keep if this function grows.
*/
async function buildBoard(
entry: { id: string; label: string; city: City },
mount: AbortController,
options: {
/**
* Build without saying so.
*
* The background lane needs this and nothing else does. `onProgress` below
* ends in `bootProgress`, which raises the switch pill — so a board loaded
* ahead of the camera would put "Building The Bay Area… terrain 42%" on
* screen over a visitor who did nothing, which is exactly the "you are
* somewhere else now" chrome that removing the tab strip and the boot card
* was for. A prefetch that announces itself is worse than no prefetch.
*/
quiet?: boolean;
} = {},
): Promise<MountedBoard | null> {
const id = entry.id;
/**
* The sky and the traffic are per-city and are chosen here, before the build,
* because `flights` is fixed at scene construction.
*
* Two gates and only one of them is about the visitor. `can.liveEnvironment`
* is true for everybody — the sky is not a thing an account can grant. What is
* load-bearing is `feeds`, the *deployment*, which stops the ordinary box
* where every source is `none` polling two endpoints forever for a 404.
*
* These used to be gated on the *markers* flag, which is a different feed
* entirely, so a deployment with a real ADS-B receiver and no marker file flew
* the simulator.
*/
/*
* One sky region per detailed source, not one circle around the board.
*
* `regionOf` derives its circle from the board's bounds, which is right for a
* metro board and wrong for the merged one: 402 nautical miles centred on the
* middle of the state, which the API refuses with `400 bad_request, "Nothing
* this deployment serves is near 37.3,-119.25"` — correctly, since what it
* serves is San Francisco and the Southland, five hundred and sixty
* kilometres apart. `region` stays the board's own circle for everything that
* wants one shape (the traffic dial, the sample generator); `skyRegions` is
* what the live feed is actually asked for.
*/
const region = regionOf(entry.city);
const skyRegions =
ONE_CALIFORNIA && entry.id === "california"
? UNIFIED_SOURCES.map((source) => regionOf(source))
: [region];
// The hand-authored corridors for *this* city. `SAMPLE_ROUTES` was passed
// 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 =
carriesMetroDetail(entry.city) && access.can.liveEnvironment && access.feeds?.flights
? skyRegions.length === 1
? tera.flights(region, routes)
: mergedTraffic(skyRegions.map((r) => tera.flights(r, routes)))
: null;
/**
* Started here and awaited below, so the fetch overlaps the heightfield build
* rather than following it. Gated on the deployment for the same reason the
* traffic is: a box with no satellite source answers with an empty catalogue,
* and asking it once per page load for that is a request nobody needs.
*
* Not gated on the visitor. There is no `can.` check because there is nothing
* to grant — the objects in this catalogue broadcast their positions to
* anybody with a radio, and every element set in it is a US Government work.
*/
if (satelliteElements === null && access.feeds?.satellites) {
satelliteElements = tera.satellites();
}
const elements = (await satelliteElements) ?? [];
// Rebuilt per board: the observer is this city's centre. See the note on
// `satelliteElements` for why only the elements are shared.
const catalogue =
elements.length === 0 ? undefined : new SatelliteCatalogue(elements, entry.city.center);
// The build may have been abandoned while that was in the air.
if (mount.signal.aborted) {
traffic?.dispose();
return null;
}
// Wrapped, not replaced: the dial passes the real sky through untouched and
// concatenates fabricated aircraft after it, so it composes with a live ADS-B
// feed as readily as with the simulator. `cityFlights` stays the unwrapped
// source — the corner label asks it whether what is on screen was observed,
// and the answer is about the feed rather than about the dial.
const dial = withTrafficDial(traffic ?? new SimulatedFlights(routes), region);
// Carried across the switch, so a dial somebody set on the last board is still
// set on this one. The dial it copies from is the *visible* board's, which is
// still on screen and still interactive while this build runs.
dial.setExtra(trafficDial?.extra() ?? 0);
/**
* 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,
);
/*
* Bounds **and** metro detail — the doors' rule, plus the feeds' rule.
*
* The sample companies are San Francisco's, so on San Francisco's own board
* they appear and on the Southland's they do not; on a board that contains
* San Francisco *and draws it at metro fidelity* they appear too, which is
* what `cities/unify.ts` needed and what `id === "sf"` silently refused.
*
* Bounds alone is not enough, and the budget said so within one run: the
* coarse statewide board's rectangle contains San Francisco, so it picked up
* forty-four markers it has no business drawing at 1,919 m to the unit and
* went from 373 draw calls to 417. `carriesMetroDetail` is the same predicate
* the live feeds use and it is the same question — is this a board on which a
* building-sized thing means anything.
*/
const sampleMarkers = (carriesMetroDetail(entry.city) ? markers : []).filter(
(m) =>
m.lat >= bounds.minLat &&
m.lat <= bounds.maxLat &&
m.lng >= bounds.minLng &&
m.lng <= bounds.maxLng,
);
const initialMarkers = [...sampleMarkers, ...doors];
/**
* The two sky layers, decided here so the options object below reads as four
* sentences rather than as four nested conditionals.
*
* Two gates each, and they are the pair `drawsFire` uses. The **board** gate
* is `boardCarriesRaster`: both feeds are statewide instruments quantised to a
* quarter of a degree — sixteen WSR-88Ds and fifty-eight counties — and a
* rectangle ninety kilometres across cannot be described by a cell twenty-seven
* kilometres wide. It answers true for California and false for the Southland
* and the Bay, which is the same answer an `id === "california"` would have
* given and is a fact about the geometry rather than about a name.
*
* The **deployment** gate is `access.feeds`, read from `/health`'s `sources`.
* With no projection configured there is no body, no honest caption beyond the
* one the gate already writes, and nothing to draw — so the layer is withheld
* entirely rather than built and left empty, exactly as `fires` is. This repo's
* own default is `none` for both, which is why the panel sentence matters more
* than the layer.
*/
const carriesSky = boardCarriesRaster(entry.city.bounds);
const precipFactory = carriesSky && access.feeds?.radar === true
? precipFactoryFor(entry.city.bounds)
: null;
const drawsMigration = carriesSky && access.feeds?.birds === true;
const handle = await createScene(stage, {
city: entry.city,
// The page's one rig, shared with the office. Handed in rather than built
// per board; see the note where it is constructed.
environment,
actor: {
kind: actorKindForPresence(access.subject !== null, "outdoors"),
identity: access.subject === null
? {
id: "anonymous",
displayName: "Guest",
authenticated: false,
profile: { appearance: { primaryColor: "#11151a", accentColor: "#f2b134" } },
}
: signedInActorIdentity(),
mode: access.subject === null ? "flight" : "ground",
position: access.subject === null ? { y: 1_200 } : { y: 0 },
minFlightAltitude: 20,
maxFlightAltitude: 1_500,
...(access.subject === null
? { camera: { distance: 2.55, height: 1.2, targetHeight: 0.1, lookAhead: 1.3 } }
: {}),
},
...(id === "california"
? { actorAnchor: { lat: 35.5, lng: -119.5 } }
: {}),
markerPalette: palette,
markers: initialMarkers,
...(id === "california"
? {
roadTraffic: {
pack: CALIFORNIA_TRANSPORT,
routeId: "la-sf-us-101",
count: 14,
seed: 115,
},
aircraft: {
route: CALIFORNIA_AIR_ROUTE,
initialPosition: CALIFORNIA_AIR_ROUTE[0],
initialAltitudeM: CALIFORNIA_AIR_ROUTE[0]?.altitudeM ?? 1_350,
initialHeadingDeg: 320,
assistedAltitudeM: 1_500,
// Corridor altitude is an atlas glyph just like the black cars:
// literal metres would put the chase camera inside the coarse hills.
altitudeSceneUnitsPerMetre: 0.01,
visualSceneUnitsPerMetre: 0.22,
camera: { distance: 52, height: 18, lookAhead: 18 },
},
}
: {}),
flights: dial.source,
...(catalogue ? { satellites: catalogue } : {}),
/**
* Fire, on the two boards where fire happens and nowhere else.
*
* Two gates and, unusually here, neither is about the visitor. `drawsFire`
* is about the *map*: the Bay Area rectangle has held zero incidents on
* every day this store has existed, and it is the board already spending
* the largest frame-time allowance in the product, so it does not pay for a
* layer to draw nothing. `feeds.fires` is about the *deployment*: with no
* projection configured there is no body to draw and no honest caption to
* write, and a layer with an empty view is a layer allocated for nothing.
*
* Withheld rather than passed-and-emptied, because `scene.ts` builds no
* group, no material and no draw call when this is absent — see
* `SceneOptions.fires`.
*/
...(drawsFire(id) ? { fires: createFireLayer } : {}),
/**
* The port kit, on the one board that declares a port.
*
* The ports are `city.ports` — plain authored data, closed over here rather
* than passed through `SceneOptions`, because `scene.ts` must not have to
* know what a quay is. Withheld entirely on a board with none, so
* `engine/ports.ts` never enters the graph of a build that draws no port and
* a portless board costs no group, no material and no draw call.
*/
...(entry.city.ports?.length
? { ports: (world, options) => createPortLayer(world, entry.city.ports ?? [], options) }
: {}),
/**
* The hulls, on the same boards as the quays and for the same reason.
*
* Gated on the *ports*, not on a feed: a ship is drawn against a berth, a
* channel and a breakwater, and a hull on a board with none of them is a
* white shape on open water with nothing to say what it is doing. Withheld
* rather than passed-and-emptied, like every layer above it.
*
* What fills it is `refreshHarbour`, below — a modelled body through the
* live gate, never a hull invented at the renderer.
*/
...(entry.city.ports?.length ? { vessels: createVesselLayer } : {}),
/** Reflectivity, on a board a quarter-degree cell can describe. */
...(precipFactory ? { precip: precipFactory } : {}),
/** Tonight's migration, on the same board and under the same argument. */
...(drawsMigration ? { migration: createMigrationLayer } : {}),
/**
* A pin is a hover *and* a click, and an office pin is a door.
*
* `onMarkerPick` fires for both — `scenekit`'s picking calls it on hover
* with the marker and on leave with `null` — so this cannot simply open a
* building on every call or the office would fly open the moment the pointer
* crossed a tower. The hover shows the card; the click is a separate
* listener below, which reads whatever the hover last resolved.
*/
onMarkerPick: (m) => {
hoveredMarker = m;
showDetail(m ? `${m.label}${m.blurb ? `${m.blurb}` : ""}` : null);
},
/**
* The other thing worth pointing at, and the one the owner asked for by
* name: a card per aeroplane, for anybody who lands on this page.
*
* Hover opens it and hover away closes it, which is the same contract the
* marker card has had since picking existed — there is no click handler
* here because there is nothing extra a click could mean. See
* `showAircraftDetail` for where the provenance comes from.
*/
onAircraftPick: (a) => showAircraftDetail(a),
signal: mount.signal,
/**
* Built, not shown. `presentBoard` puts it on the stage once the fog has
* closed over the board the visitor is still looking at — see
* `SceneOptions.present`.
*/
present: false,
// An abandoned build keeps its worker running for a tick or two after the
// abort; its percentages must not land on the card the new city is using.
onProgress: (p) => {
if (options.quiet) return;
if (!mount.signal.aborted) bootProgress(entry.label, p.fraction, p.onMainThread);
},
...(access.subject !== null
? { realtimePeers: { localSceneUnitsPerMetre: entry.city.latScale / 111_320 } }
: {}),
});
if (!handle) {
/**
* Superseded. `scene.dispose()` is what normally cancels the traffic
* source, and there is no scene — so the polling this call started would
* otherwise outlive the board it was started for, and keep a request in
* the air for a city nobody is looking at.
*/
traffic?.dispose();
return null;
}
// A new board builds a new layer, and a new layer starts visible. Reapply
// whatever the panel last said, or the setting silently undoes itself on the
// first board switch.
handle.setSatellitesVisible(satellitesVisible);
if (carriesSky) void askSky();
// Fog distances are scene units, so they have to follow the board — 210/460
// was tuned for a 230-unit San Francisco and fogs out most of a 1000-unit
// Bay Area. They also have to clear the CAMERA, which sits about 0.6 spans
// out on the whole-board view: a fog starting nearer than that is behind the
// viewer's own shoulder, and every pixel in frame is then at full fog.
//
// That last failure used to be catastrophic and is now only bad, and the
// difference is worth recording because the comment used to claim the worse
// version. The night fog colour is derived from the sky, the night sky was
// nearly black, and so a fog plane behind the camera turned the entire map
// off. `atmosphere.ts` now floors the night ground rig and stops the
// obscuration convergence subtracting it again, and the night fog here lands
// around #16203a — aerial perspective that lifts distance rather than a
// blackout. The clearance is still required: a board flattened to one uniform
// value is unreadable at any brightness. It is no longer the difference
// between a map and a black rectangle.
const [wx, nz] = handle.world.project(entry.city.bounds.maxLat, entry.city.bounds.minLng);
const [ex, sz] = handle.world.project(entry.city.bounds.minLat, entry.city.bounds.maxLng);
const span = Math.max(Math.abs(ex - wx), Math.abs(sz - nz));
const atmosphere = createAtmosphere({
lng: entry.city.center.lng,
metresPerUnit: handle.world.metresPerUnit,
// Pushed out with the camera. `scene.ts` now lets the orbit reach 2.0 spans
// so the viewer can get above the satellite dome, and at the old far of 2.8
// the board sat at 51% fog from that pose — the whole city washing out at
// exactly the moment the shot is meant to be the city under the
// constellation. 3.9 keeps it near a fifth, which is the haze it had at the
// old limit.
clearFog: { near: span * 1.15, far: span * 3.9 },
// The floor on how far you can see, and it has to know how big the board
// is. `minVisibilityM` defaults to 4.5 km, which is honest weather and
// completely wrong here: this board is ninety-four kilometres across, so
// real visibility correctly hides three quarters of it and the night view
// renders as a black rectangle. A map is looked at from outside the
// atmosphere it is depicting.
minVisibilityM: span * handle.world.metresPerUnit * 1.6,
// The marine layer is a fact about the eastern Pacific at this latitude,
// not a decoration. LA gets its own weather, not San Francisco's fog.
marineLayer: id === "sf" ? PACIFIC_MARINE_LAYER : null,
});
/**
* Recompute the **fog** when the camera moves, not only when the clock does.
*
* Throttled on the altitude itself rather than on time: `change` fires every
* frame under damping and building a rig walks a keyframe table and a lunar
* ephemeris, which is not work to do sixty times a second for a number that
* has not moved. Two per cent of the current altitude is under the threshold
* at which the fog planes visibly shift, and it collapses a whole chapter
* flight to a few dozen recomputations.
*
* `applyCameraFog` and not `updateSun`, and not `setLighting` either. The
* clock tick ends in `renderChrome()`, a DOM pass with no business running
* because somebody dragged the board; and the full rig ends in six layer
* setters and the environment rig's fingerprint, none of which read a
* distance. See `applyCameraFog` for the whole argument.
*/
handle.onChapterChange(() => renderChrome());
handle.onControlModeChange((mode) => adoptCityControlMode(mode));
const minimap = createMinimap({
world: handle.world,
// The pack the *World* is drawing, not the one the module imported. These
// are the same object whenever no reconciliation rule is on, and when one
// is, the plan view drawn from the raw pack is a plan of a different
// California than the one under the camera.
city: handle.world.city,
// The plan stops changing shape when the board does. See `planFrame`.
frame: planFrame(handle.world.city),
camera: handle.stageScene.camera,
controls: handle.stageScene.controls,
markerPalette: palette,
// The plan view is a 2D canvas the same size as a phone's thumb, and on a
// handheld it is drawn at the same ceiling the WebGL renderer uses. One
// definition of "phone", in `stage.ts`, read by both.
maxPixelRatio: deviceProfile().maxPixelRatio,
onSeek(lat, lng) {
if (!city || city.controlMode() !== "overview") return;
/**
* Slide the orbit target and carry the camera with it, keeping the offset
* between them. A seek is "look over there", not "go to chapter three":
* snapping to a chapter pose throws away the angle and the distance the
* user spent the last minute choosing, and doing it from a click on a map
* is the kind of surprise that stops people clicking on the map.
*
* No easing, deliberately. `flyTo` would need a pose, which is the thing
* being avoided, and an instant move is also the correct answer under
* `prefers-reduced-motion`.
*/
const { camera, controls } = city.stageScene;
const [x, z] = city.world.project(lat, lng);
const y = city.world.groundAt(lat, lng);
const dx = camera.position.x - controls.target.x;
const dy = camera.position.y - controls.target.y;
const dz = camera.position.z - controls.target.z;
controls.target.set(x, y, z);
camera.position.set(x + dx, y + dy, z + dz);
},
onHover(info) {
if (!minimapReadout) return;
minimapReadout.textContent = info
? `${info.lat.toFixed(4)}, ${info.lng.toFixed(4)}${info.district ? ` · ${info.district}` : ""}`
: "";
},
});
/**
* The plan's markers, and why this line is easy to lose.
*
* While the minimap was a module-level singleton this was `setMarkers` on the
* one plan there was. Making it per-board moved the construction into this
* function and the call went with it — except it did not, and the plan shipped
* with an empty marker layer on every board. It is invisible to every test and
* to the console: the plan still draws, still shades, still tracks the camera,
* and simply has no doors on it. The photographs are what found it — the
* Southland's orange Mateo Court dot and the Bay Area's cluster of sample
* companies, both present at `bcac6aa` and both gone.
*
* A door is one of the two ways anybody finds a studio, so this is the layer
* that matters most of the three. `MountedBoard.markers` keeps the same array
* for a board that is evicted and rebuilt.
*/
minimap.setMarkers(initialMarkers);
return {
id,
entry,
handle,
minimap,
atmosphere,
span,
flights: traffic,
trafficDial: dial,
fireBounds: drawsFire(id) ? entry.city.bounds : null,
carriesSky,
vesselsBody: null,
vesselBounds: entry.city.ports?.length ? entry.city.bounds : null,
vesselBerths: entry.city.ports?.length ? berthAnchors(entry.city.ports) : [],
vesselPorts: entry.city.ports ?? [],
harbourAtMs: 0,
markers: initialMarkers,
arrived: false,
};
}
/**
* Make a resident board the visible one, hiding the seam inside a fog dip.
*
* ## The order, which is the whole of it
*
* The outgoing board's aerial fog collapses toward the camera over
* `FOG_DIP_OUT_SECONDS`, so the last frame anybody sees of it is a flat field of
* its own horizon colour. The swap happens in that frame. The incoming board's
* fog then lifts from the same collapsed pair to whatever `Atmosphere` says the
* weather is, over `FOG_DIP_IN_SECONDS`.
*
* A **fog dip and not a crossfade**, and the argument is measured rather than
* aesthetic — see `FOG_DIP_OUT_SECONDS` in `engine/atmosphere.ts`, which carries
* both halves of it: every both-boards-live pairing breaks a desktop triangle
* cap, and a true dissolve is the one transition that puts the seam's 4.17x
* vertical deflation and its up-to-4,025 m registration slide on screen
* simultaneously and in register.
*
* `prefers-reduced-motion` collapses the whole thing to a cut, exactly as
* `arrive()` and `beginOfficeArrival` already do.
*/
async function presentBoard(record: MountedBoard): Promise<void> {
const outgoing = visibleBoard;
if (outgoing === record) {
hideSwitchProgress();
return;
}
const dips = outgoing !== null && !prefersReducedMotion();
if (dips && outgoing !== null) await runFogDip(outgoing, "out");
deactivateBoard();
// The office belongs to the board it was opened from. Kept until here rather
// than dropped at the top of the switch, so the outgoing board stays whole for
// the whole of the build behind it.
disposeLoadedOffice();
inside = false;
clearPublishedPlayInput();
controlModeState = createControlModeState();
adoptBoard(record);
stage.setScene(record.handle.stageScene);
/**
* The cache is written **here**, at the moment a board becomes the visible
* one, and not when its build finished.
*
* The ordering is load-bearing on a handheld and it was wrong once. With a
* ceiling of two and California pinned, inserting the incoming board while the
* outgoing one was still on the stage made the outgoing board the only legal
* victim — so a Bay-Area-to-Southland switch on a phone disposed the picture
* the visitor was looking at, blanked the canvas, and then swapped, which is
* precisely the teardown this whole workstream exists to remove. Writing the
* cache after `stage.setScene` means the least-recently-*shown* board is the
* one that has just been hidden, which is both the correct victim and a board
* nothing is drawing.
*/
for (const evicted of boards.put(record.id, record)) disposeBoard(evicted);
activateBoard(record);
hideSwitchProgress();
publishCameraHook(record);
/*
* Arriving is itself a reason to look ahead.
*
* The camera listeners below arm the lane as the visitor moves, but landing on
* a board is a pose change nothing fired a `change` event for — and it is the
* single most likely moment for the next board to already be worth having. The
* old prefetch, if any, was cancelled by `mountCity` before this build began.
*/
maybePrefetch(record);
// Before the lift, so the incoming board's first drawn frame is already inside
// the haze the outgoing one ended in rather than snapping to clear and then
// fogging up.
if (dips) record.handle.setAerialFog(collapsedFogFor(record));
attachCurrentWebcamFace();
moveRealtimePresence();
showPlan();
mountPoseEditor(record.handle);
refreshGodmodePlace();
updateSun();
renderChrome();
/**
* The opening move, on the first showing of a board and not on a return.
*
* Arriving somewhere is what a first sight of a board is, and it is why every
* mount used to play one. A *return* is a different thing: the point of
* keeping a board is that you come back to the pose you left it at, and an
* arrival would throw that away to re-stage a shot the visitor has already
* seen. Captures are unaffected — every harness loads a fresh page with
* `?city=`, which is always a first showing.
*
* After `updateSun`, so the first frame is lit by the real sky rather than by
* the opening climatology the scene was built with, and after `showPlan` and
* `renderChrome` so the move plays over a finished page.
*/
const queued = pendingPlace;
pendingPlace = null;
if (queued !== null && queued.board === record.id) {
// A rung on this board was what the switch was *for*, so it replaces the
// opening move rather than following it — an arrival and then a flight to
// somewhere else is two camera moves for one click.
record.arrived = true;
flyToChapter(queued.id);
} else if (!record.arrived) {
record.arrived = true;
record.handle.arrive();
}
if (dips) await runFogDip(record, "in");
}
/**
* The fog dip in progress, or `null`. Driven by the frame pump.
*
* A promise rather than a callback because `presentBoard` reads as a sequence —
* collapse, swap, lift — and that is exactly what it is.
*/
let fogDip:
| {
record: MountedBoard;
from: AerialFog;
to: AerialFog;
elapsed: number;
seconds: number;
settle: () => void;
}
| null = null;
function runFogDip(record: MountedBoard, phase: "out" | "in"): Promise<void> {
// Whoever was dipping is superseded, and has to be *told* — a second click
// inside the first switch's 0.8 s replaces `fogDip`, and an orphaned promise
// leaves the first `presentBoard` suspended at its own `await` for the rest of
// the page's life. Settling it lets that call run to its end and find nothing
// left to do, which is the correct outcome of "last click wins".
endFogDip();
const clear = aerialFogFor(record);
const hidden = collapsedFogFor(record);
const from = phase === "out" ? clear : hidden;
const to = phase === "out" ? hidden : clear;
record.handle.setAerialFog(from);
return new Promise<void>((resolve) => {
fogDip = {
record,
from,
to,
elapsed: 0,
seconds: phase === "out" ? FOG_DIP_OUT_SECONDS : FOG_DIP_IN_SECONDS,
settle: resolve,
};
});
}
/** One frame of the dip. Called from the app's pump; a no-op when none is running. */
function stepFogDip(dt: number): void {
const dip = fogDip;
if (dip === null) return;
dip.elapsed += dt;
const t = dip.seconds <= 0 ? 1 : Math.min(1, dip.elapsed / dip.seconds);
dip.record.handle.setAerialFog(dipFog(dip.from, dip.to, t));
if (t < 1) return;
endFogDip();
}
/** Finish whatever dip is running, wherever it had got to. Idempotent. */
function endFogDip(): void {
const dip = fogDip;
if (dip === null) return;
fogDip = null;
dip.settle();
}
/**
* How far this board's camera is standing back, in this board's scene units,
* floored so a camera sitting on its own target still has a fog to collapse to.
*
* The floor is a twentieth of the board's span rather than a constant, because a
* constant would be a distance in whichever board's units it was written for.
*/
function collapsedFogFor(record: MountedBoard): AerialFog {
const reach =
record.handle.cameraStandoffMetres() / record.handle.world.metresPerUnit;
return collapsedFog(Math.max(record.span * 0.05, reach));
}
/**
* The fog this board would have if nothing were dipping — the pair the clock and
* the camera agree on.
*
* Routed through the board's own `Atmosphere` rather than through the visible
* one's, because `createAtmosphere` takes `metresPerUnit` and a span-derived
* clear pair and both are per-board. A board that came back under the wrong
* atmosphere would render with fog planes tuned for a board twenty times its
* size, which on California is the difference between a map and a uniform wash.
*/
function aerialFogFor(record: MountedBoard): AerialFog {
const env = observe(
record.entry.city.center.lat,
record.entry.city.center.lng,
currentInstant(),
currentWeather(),
);
return record.atmosphere.aerial(env, {
altitudeMetres: record.handle.cameraAltitudeMetres(),
standoffMetres: record.handle.cameraStandoffMetres(),
});
}
/**
* Project a record onto the module-level "the visible board" variables.
*
* The one function that decides what `city`, `cityId`, `atmosphere`, `minimap`
* and the six board readings mean. Everything else in this file reads those and
* is unchanged by retention, which is the point.
*/
function adoptBoard(record: MountedBoard): void {
visibleBoard = record;
city = record.handle;
cityId = record.id;
atmosphere = record.atmosphere;
minimap = record.minimap;
trafficDial = record.trafficDial;
cityFlights = record.flights;
fireBounds = record.fireBounds;
boardCarriesSky = record.carriesSky;
vesselsBody = record.vesselsBody;
vesselBounds = record.vesselBounds;
vesselBerths = record.vesselBerths;
vesselPorts = record.vesselPorts;
harbourAtMs = record.harbourAtMs;
seaNote = "";
radarNote = "";
birdsNote = "";
}
/**
* Stop everything the *visible* board was subscribed to, and leave the board
* itself alone.
*
* The distinction is the reason retention is safe. A `/weather` request for San
* Francisco that lands after the visitor has moved to the Southland would put
* the marine layer over Long Beach; the fire panel is mounted into chrome this
* file does not own and would otherwise accumulate one copy per resident board;
* and the camera fog listener is bound to one board's `OrbitControls`. None of
* those are things a paused board should keep doing. Its geometry, its
* heightfield, its shader programs and its pose all stay exactly where they are.
*/
function deactivateBoard(): void {
const record = visibleBoard;
if (record !== null) {
record.vesselsBody = vesselsBody;
record.harbourAtMs = harbourAtMs;
}
weatherWatch?.stop();
weatherWatch = null;
fireWatch?.stop();
fireWatch = null;
firePanel?.dispose();
firePanel = null;
cameraFogWatch?.();
cameraFogWatch = null;
poseEditor?.destroy();
poseEditor = null;
}
/** Subscribe the board that has just come on screen. The inverse of `deactivateBoard`. */
function activateBoard(record: MountedBoard): void {
const { entry } = record;
const id = record.id;
/**
* The weather, started only now that the board is on screen.
*
* Deliberately after the build rather than beside it: a poll issued at the
* top of a two-second heightfield is a request for a city the user may
* already have left, and the first thing `stop()` would do is throw the
* answer away. Nothing on screen is waiting for it — `updateSun` runs
* immediately with `null`, which is climatology, and the observation
* replaces it when it lands.
*/
// Weather observations are metro-scoped for the same reason live aircraft
// are: one honest station cannot describe a 600 km corridor. California uses
// the local climatology model; the detailed SF and SoCal boards keep their
// live observations.
//
/*
* And the merged board uses climatology too, which took a 400 in production
* to notice.
*
* `carriesMetroDetail` is true of it — it carries both metros' districts —
* so it asked, at `entry.city.center`, which is the middle of the *state*:
* `GET /api/v1/weather?lat=37.30&lng=-119.25` → `400 bad_request, "Nothing
* this deployment serves is near 37.3,-119.25"`. The API is right. What it
* serves is San Francisco and the Southland, five hundred and sixty
* kilometres apart, and the point midway between them is farm country in the
* Sierra foothills that no station on this deployment describes.
*
* The sky already had this problem and solved it by asking per source
* (`skyRegions`, above). Weather does not get the same answer, because the
* two feeds are not the same shape: aircraft from two regions *merge* — they
* are disjoint sets of objects in one sky — and two observations do not. One
* board cannot be simultaneously foggy at the Golden Gate and clear over
* Riverside, and picking either one and applying it statewide is a worse lie
* than the climatology model, which at least varies with latitude and season.
*
* So this follows the rule the comment above already states rather than
* inventing an exception to it: a board that spans more than one served
* region takes the model. The metros keep their live observations on their
* own boards, and the day the atmosphere is regional rather than per board,
* this is the line that changes.
*/
const spansManyWeatherRegions = ONE_CALIFORNIA && id === "california";
weatherWatch =
carriesMetroDetail(entry.city) &&
!spansManyWeatherRegions &&
access.can.liveEnvironment &&
access.feeds?.weather
? tera.watchWeather(entry.city.center, () => {
updateSun();
renderChrome();
})
: null;
/**
* And the fire, started the same way and for the same reason.
*
* The panel is mounted first and applied with `null` immediately, so the very
* first frame of a fire board already carries a sentence — "no fire feed has
* answered" rather than a blank space that a viewer would read as an
* all-clear. The watch then replaces it within a request.
*
* `entry.city.bounds` is handed to both halves and they use it for different
* things: `promote()` clips the drawn set to the frame, and the panel turns an
* off-board fire into "342 km north of this frame" instead of "somewhere
* else". One rectangle, so the picture and the caption cannot disagree.
*/
// `#fire-section` and `#fire-host` are this file's, not `mount.ts`'s — the
// same arrangement `#presence-host` has, and the reason the header's rule
// about not writing to the chrome's DOM is not broken here. `showFireSection`
// owns the visibility and runs on every `renderChrome`.
if (drawsFire(id)) {
fireBounds = entry.city.bounds;
const fireHost = document.querySelector<HTMLElement>("#fire-host");
firePanel = fireHost === null ? null : mountFirePanel(fireHost, { bounds: fireBounds });
// Whatever the page already knows, re-clipped to this board. `undefined` is
// the first board of the session and is the only case that shows the fault
// sentence, which is correct: nothing has answered yet.
if (firesBody === undefined) firePanel?.apply(null);
else applyFires(firesBody);
fireWatch = tera.watchFires((body) => applyFires(body));
}
showFireSection();
/**
* The sea and the sky, in the frame the board comes on screen in.
*
* `refreshHarbour` is synchronous — the modelled body is arithmetic over
* fifteen berths — so the first visible frame of the Southland already has its
* hulls on it, on a first mount and on a return alike. On a return it is a new
* *fix* rather than a replay: the clock has moved on while the board was
* paused, and the harbour is modelled from the instant, not from a tick count.
*/
if (record.vesselBounds !== null) refreshHarbour(currentInstant().getTime());
// Asked once per board and then only when the body's own TTL has expired; the
// promotion that draws it runs on the clock, in `updateSun`, which is also
// where the sun's elevation comes from.
if (record.carriesSky) void askSky();
{
const controls = record.handle.stageScene.controls;
let lastAltitude = -1;
const onCameraMoved = () => {
const altitude = record.handle.cameraAltitudeMetres();
// Two per cent of the current altitude, floored at a metre so a camera
// resting exactly on the ground does not recompute on every event.
const moved = Math.abs(altitude - lastAltitude);
if (lastAltitude >= 0 && moved < Math.max(1, lastAltitude * 0.02)) return;
lastAltitude = altitude;
applyCameraFog();
maybeHandover(record);
maybePrefetch(record);
};
const onDragStart = () => { dragging = true; };
const onDragEnd = () => {
dragging = false;
// The notch that ended the drag is the one that may promote: the rule
// refuses while a pointer is down, so without this a descent that ends
// below a threshold would sit there until the next event.
maybeHandover(record);
maybePrefetch(record);
};
controls.addEventListener("change", onCameraMoved);
controls.addEventListener("start", onDragStart);
controls.addEventListener("end", onDragEnd);
cameraFogWatch = () => {
controls.removeEventListener("change", onCameraMoved);
controls.removeEventListener("start", onDragStart);
controls.removeEventListener("end", onDragEnd);
dragging = false;
};
}
}
/**
* Whether a pointer is currently on the board.
*
* `OrbitControls` fires `start` on the first pointer-down, the first wheel notch
* and the first pinch, and `end` when it lets go. Swapping a board out from
* under a finger that is holding it is the one thing a free-camera handover must
* never do.
*/
let dragging = false;
/**
* Free-camera promotion across the seam. **On, and `?handover=0` turns it off.**
*
* ## Why it is on now
*
* Because the owner asked three times why there are three boards, and this is
* the answer to that question rather than a nicety. A wheel notch over the Bay
* now promotes to the detailed Bay Area board and a notch back out demotes to
* the state tier: one map, three scales, entered by moving rather than by
* picking a product off a strip of tabs. Measured on the deployed bundle before
* this default changed, promotion fired at the sixteenth notch in from the state
* pose and the arrival was clean — no boot card, no tab, no click on a name.
*
* The two reservations below were real and both are now discharged. The
* oscillation one had a *cause* rather than a risk — see the arrival guard in
* `maybeHandover`, which is the whole of it. The coarse-descent one is a
* property of the packs and not of this rule, and it is recorded as such:
*
* ## What is still true, and is now a decision rather than a doubt
*
* 97.4% of California's area has no board below 242 km of stand-off. Zooming in
* over Fresno lands on 806 m lots and 2 km-wide freeway symbols with nothing to
* hand over to, and the rule correctly refuses. That picture has now been
* looked at — it is a dark, coarse blur, and it is what the state tier honestly
* is at that range. The judgement made here is that a visitor who deliberately
* zooms into the empty middle of the state and finds it coarse has learned
* something true, and that this costs less than a tab strip that tells every
* visitor there are three products before they touch anything. Authoring
* Sacramento, Fresno and the Central Valley is what retires it.
*
* ## The original argument, kept because it is the reason the guard exists
*
* The rule itself — `handover` in `engine/ladder.ts` — is pure, tested and
* shipped. What is deliberately not shipped is letting a wheel notch fire it,
* and the reasons are specific rather than cautious. A camera parked at a
* threshold oscillates without a hysteresis band and a drag guard, both of which
* are in the rule and neither of which has been photographed at every pose yet.
* And 97.4% of California's area has no board below 242 km of stand-off: two
* notches down from the state pose over Fresno lands on 806 m lots and 2 km-wide
* freeway symbols with nothing to hand over to, so the rule correctly refuses
* and the descent has to be *allowed* to look coarse. That is a picture somebody
* should look at before it is on by default.
*
* `?handover=1` turns it on. Every promotion goes through `switchCity`, which is
* what dispatches the journey event — a board that changed without one leaves
* the door into Mateo Court rejecting with `wrong-city` while the Southland is
* visibly on screen, and nothing throws.
*/
const FREE_HANDOVER = new URLSearchParams(location.search).get("handover") !== "0";
/**
* Whether a board may be built before anybody asks for it.
*
* On, and this is the single constant that turns it off — the rollback for the
* whole background lane, per the plan this was built from. It is a constant and
* not a query flag deliberately: a query flag is a thing that ships in one state
* and is measured in another, and every number quoted for this lane was measured
* with it in the state it ships in.
*/
const PREFETCH_ENABLED = true;
/**
* Does this pack carry metro detail, as opposed to being the coarse state board?
*
* **Asked of the pack, not of its id**, and that distinction is a bug this
* function exists to have fixed. Live ADS-B and live weather observations were
* gated on `id !== "california"`, which meant "not the coarse statewide board"
* on the day it was written and was correct then: live aircraft over a board at
* 1,919 m to the unit are a glyph problem, and an observation from one station
* cannot speak for a thousand kilometres of coast.
*
* `cities/unify.ts` then made a board that is *both* — the whole state, at metro
* fidelity, keeping the `california` id deliberately so that the fire gate, the
* ladder's region table and every `?city=` deep link keep working. It therefore
* inherited a gate meant for something else and shipped with live traffic and
* live weather silently off, which is exactly the class of defect that reads as
* "it feels less alive" and never as an error.
*
* `focusRegions` is the honest predicate and needs no new field: the coarse
* state pack declares none, and every pack that has ground worth drawing at
* metro resolution declares at least one.
*/
/**
* A camera the capture harness can plant, exposed on `globalThis`.
*
* **Why this exists.** Every instrument in this repo aims through a control a
* reader also uses — `look.mjs --chapter` clicks a chapter button, `shots.mjs`
* drives the mode dock — and that is the right default, because a harness with
* a back door photographs a state no visitor can reach. But it means a board
* can only be photographed *where a chapter already points*, and the merged
* board carries California's six: the whole state, the north, two corridors and
* two doors that leave the board. There is no rung anywhere near a city on it.
*
* The cost of that was three failed attempts to photograph the very thing the
* board exists for — its cities — by clicking the minimap and guessing wheel
* notches, each of which landed in open ocean or on empty coast. A picture is
* this project's acceptance instrument; a board that cannot be pointed at is a
* board whose work cannot be accepted.
*
* So: a seek, in the same coordinates a pack is authored in. It moves the
* camera and nothing else — no mode change, no board change, no state a visitor
* could not also reach by dragging. It is `globalThis` rather than a module
* export because the harness talks to a built bundle across a page boundary,
* and it is deliberately not wired to any UI.
*/
function publishCameraHook(record: MountedBoard): void {
(globalThis as { __teraCamera?: unknown }).__teraCamera = {
/**
* Look at a place from a distance, in the pack's own units: degrees for the
* target, true metres for the stand-off and the height.
*
* `metresPerUnit` for the horizontal and `metresPerUnit / exaggeration` for
* the vertical, because a stand-off is a plan measurement and a height is
* not — the same distinction `chapterStandoffMetres` and
* `chapterAltitudeMetres` are built on, and getting it wrong is a camera
* under the ground.
*/
seek(at: { lat: number; lng: number; standoffM?: number; heightM?: number; azimuth?: number }) {
const world = record.handle.world;
const scene = record.handle.stageScene;
const [x, z] = world.project(at.lat, at.lng);
const ground = world.groundAt(at.lat, at.lng);
const standoff = (at.standoffM ?? 20_000) / world.metresPerUnit;
const lift =
((at.heightM ?? (at.standoffM ?? 20_000) * 0.6) / world.metresPerUnit) *
world.city.verticalExaggeration;
const azimuth = at.azimuth ?? 0.6;
scene.controls.target.set(x, ground, z);
scene.camera.position.set(
x + Math.sin(azimuth) * standoff,
ground + lift,
z + Math.cos(azimuth) * standoff,
);
scene.controls.update();
return { x, z, ground, standoff, lift };
},
board: record.id,
};
}
function carriesMetroDetail(city: City): boolean {
return (city.focusRegions?.length ?? 0) > 0;
}
function maybeHandover(record: MountedBoard): void {
if (!FREE_HANDOVER) return;
if (inside || fogDip !== null || record !== visibleBoard) return;
/*
* The opening move is not a camera position anybody asked for, and reading a
* stand-off out of it is how this feature spent a round looking broken.
*
* `arrivalStart` begins every board's arrival at 1.5x its resting stand-off
* and `ladder.ts` demotes above 1.15x the handover ceiling, so the first
* second of *any* board is outside that board's own retention band. Landing on
* the Bay Area with the flag on therefore bounced straight back to the state
* board with no input at all — measured, twice, before this line existed. It
* is not an sf quirk and it is not a tuning problem: 1.5 > 1.15 holds for
* every board there will ever be, which is why the fix is a guard rather than
* a number. `handoverArrivalGuard.test.ts` pins that inequality so the guard
* cannot be deleted as redundant.
*/
if (record.handle.arriving()) return;
if (record.handle.controlMode() !== "overview") return;
const target = record.handle.stageScene.controls.target;
const [lat, lng] = record.handle.world.unproject(target.x, target.z);
const next = handover({
boards: CITIES.map((entry) => ({
id: entry.id,
bounds: entry.city.bounds,
metresPerUnit: 111_320 / entry.city.latScale,
})),
current: record.id,
lat,
lng,
standoffM: record.handle.cameraStandoffMetres(),
dragging,
});
if (next !== null && next !== wantedCity) switchCity(next);
}
/**
* The metro tiers the background lane may load, and the stand-off each takes
* over at. Derived from the ladder's own handover table, so there is exactly one
* place in the product that says where a board stops being the right one.
*/
const PREFETCH_TIERS: readonly PrefetchTier[] = CITIES.flatMap((entry) => {
const handoverStandoffM = HANDOVER_STANDOFF_M[entry.id];
return handoverStandoffM === undefined
? []
: [{ id: entry.id, handoverStandoffM, bounds: entry.city.bounds }];
});
/**
* Drop whatever the background lane is doing.
*
* Called before every foreground abort and on every board present, because a
* prefetch that outlives the reason it started is a build competing with a
* visitor for one terrain worker. `createScene` honours the signal by dropping
* the heightfield and resolving `null` without allocating a renderer, so an
* abandoned prefetch costs nothing to abandon.
*/
/**
* How long a board must be on screen and undisturbed before anything loads
* behind it, in milliseconds.
*
* The board that just arrived is still linking shaders, uploading buffers and
* settling its first frames; starting another build inside that window is the
* one way this feature can make the product measurably worse. Deliberately not
* `requestIdleCallback`: Safari did not ship it until recently and it is exactly
* the browser most likely to be on the tightest budget, so the deferral is a
* timer everywhere rather than a good mechanism on some platforms and none on
* others.
*/
const PREFETCH_IDLE_MS = 2_000;
let prefetchTimer: ReturnType<typeof setTimeout> | null = null;
function cancelPrefetch(): void {
if (prefetchTimer !== null) {
clearTimeout(prefetchTimer);
prefetchTimer = null;
}
prefetching?.abort();
prefetching = null;
}
/**
* Load the board the camera is heading for, before it is asked for.
*
* ## Why this is the whole of "loads in the background"
*
* Everything else was already there. `present: false` already exists on
* `SceneOptions` and is already honoured; `boards` already holds three on a
* desktop with California pinned; `presentBoard` already runs a fog dip over a
* cache hit at zero shader links and zero blocked milliseconds. The only missing
* piece was something that decides to build a board nobody has clicked on, and
* the reason it was missing is that it is the piece that can hurt: it competes
* for the terrain worker and it can relight the board on screen.
*
* Both hazards are closed rather than hoped about. The competition is closed by
* `cancelPrefetch()` running before every foreground abort. The relighting is
* closed in `environmentRig.ts` — an off-stage apply borrows the cached probe
* instead of convolving a new one and disposing the visible board's — and by
* `scene.ts` passing `offstage: !presented` on every clock-driven apply.
*
* ## Why the decision is not made here
*
* `prefetchTarget` is pure, takes scalars, and lives in `boards.ts` with the
* eviction policy it has to agree with. This function is the effects half: read
* three numbers off the camera, ask, and act. Keeping the two apart is what lets
* `prefetchPolicy.test.ts` assert against the real pack bounds — including that
* the two metros' trigger discs provably cannot overlap — with no DOM, no WebGL
* and no `three` anywhere near it.
*/
function maybePrefetch(record: MountedBoard): void {
if (!PREFETCH_ENABLED) return;
// Never behind a visitor. A foreground build, an office, or a dip in progress
// all mean something is already happening that matters more than this.
if (inside || mounting !== null || fogDip !== null) return;
if (record !== visibleBoard) return;
if (prefetching !== null) return;
const target = record.handle.stageScene.controls.target;
const [lat, lng] = record.handle.world.unproject(target.x, target.z);
const id = prefetchTarget({
tiers: PREFETCH_TIERS,
lat,
lng,
standoffM: record.handle.cameraStandoffMetres(),
resident: boards.ids(),
visible: visibleBoard?.id ?? null,
capacity: residentCapacity(deviceProfile().handheld),
pinned: ["california"],
});
if (id === null) return;
const entry = CITIES.find((candidate) => candidate.id === id);
if (entry === undefined) return;
/*
* Armed now, started later.
*
* The timer is what separates "the camera is somewhere a load would be useful"
* from "and it has been there long enough that nothing else needs the machine".
* Every gesture cancels it through `cancelPrefetch`, so a visitor sweeping the
* camera across the state re-arms it repeatedly and starts nothing until they
* stop — which is the behaviour a background lane has to have to be invisible.
*/
if (prefetchTimer !== null) clearTimeout(prefetchTimer);
prefetchTimer = setTimeout(() => {
prefetchTimer = null;
startPrefetch(entry);
}, PREFETCH_IDLE_MS);
}
/** The build itself, once the deferral above has decided it is safe to start. */
function startPrefetch(entry: { id: string; label: string; city: City }): void {
const id = entry.id;
// Re-checked after the wait, because two seconds is long enough for every one
// of these to have changed.
if (inside || mounting !== null || fogDip !== null) return;
if (prefetching !== null || boards.has(id) || visibleBoard?.id === id) return;
const controller = new AbortController();
prefetching = controller;
void (async () => {
const built = await buildBoard(entry, controller, { quiet: true });
// Four ways this stops mattering between the ask and the answer, and all
// four have to be checked *after* the await rather than before it: the
// visitor may have clicked something, the lane may have been cancelled, the
// board may have arrived by the front door in the meantime, or the cache may
// no longer have room for it without evicting what is on screen.
if (built === null) {
if (prefetching === controller) cancelPrefetch();
return;
}
const stale =
controller.signal.aborted ||
prefetching !== controller ||
boards.has(id) ||
visibleBoard?.id === id;
if (stale) {
disposeBoard(built);
if (prefetching === controller) prefetching = null;
return;
}
/*
* Parked, not presented.
*
* `presentBoard` writes the cache itself at the moment a board becomes
* visible; this one never becomes visible, so it writes its own entry — and
* it goes in as the *least* recently shown, which is what `put` already does
* for a board that has never been shown. Anything evicted to make room is
* disposed here, exactly as `presentBoard` does, because the rig's ledger is
* a strong reference and a board left in it retains the whole scene graph.
*/
for (const evicted of boards.put(id, built)) disposeBoard(evicted);
prefetching = null;
})();
}
/**
* The rectangle the plan view draws, which is no longer the board's own.
*
* ## The problem, stated as a measurement
*
* Three boards, three arbitrary rectangles. California's is 9.55 by 10.5
* degrees, the Bay Area's 0.85 by 0.89, the Southland's 1.08 by 1.66 — which on
* screen, once longitude is squashed by the cosine of each board's own centre
* latitude, is a **0.87 portrait, a 0.83 portrait and a 1.28 landscape**. So the
* plan in the corner was turning through ninety degrees between the Southland
* and the other two while the world underneath it had not moved at all. That is
* the loudest "you are somewhere else now" signal on the screen and it fires in
* the same frame the fog dip is trying to hide everything else.
*
* ## The rule, and what it is not
*
* Every board is drawn inside a rectangle with **California's proportions**,
* grown around the board's own bounds until the aspect matches and centred on
* it. California is the reference because its four edges are the only ones in
* the product that are a *place* rather than a crop: the two metro rectangles
* are framings somebody chose, and the state's are the state's.
*
* The cost of the rule is honest and small: the Bay Area's frame widens by 5.7%
* and the Southland's grows 46% taller, so the Southland sits in a band across
* the middle of its plan with water above and below it — which is, to be fair
* to it, where the Southland actually is.
*
* **This is the shape pinned, not the extent.** The design this comes from asks
* for the plan to be pinned to California's *bounds* on every board, with a zoom
* that follows the camera's ground footprint and the relief sourced from
* whichever board is resident. That is a larger change — the relief raster is
* built once into an offscreen surface and blitted under a 30 Hz cap and a
* skip-when-nothing-moved guard, and a zoom means re-rasterising against both —
* and it is not what shipped here. What shipped is the half of it that answers
* the complaint: the widget is the same shape on all three boards.
*/
const PLAN_ASPECT = planAspect(CALIFORNIA);
function planAspect(pack: City): number {
const lat = (pack.bounds.maxLat - pack.bounds.minLat);
const lng = (pack.bounds.maxLng - pack.bounds.minLng) * Math.cos((pack.center.lat * Math.PI) / 180);
return lng / lat;
}
function planFrame(pack: City): {
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
} {
const cos = Math.cos((pack.center.lat * Math.PI) / 180);
let latSpan = pack.bounds.maxLat - pack.bounds.minLat;
let lngSpan = pack.bounds.maxLng - pack.bounds.minLng;
const aspect = (lngSpan * cos) / latSpan;
// Grow the short axis. Never shrink either one: a frame that cropped would put
// part of the board off its own plan, which is a worse failure than an empty
// margin and a much less obvious one.
if (aspect < PLAN_ASPECT) lngSpan = (latSpan * PLAN_ASPECT) / cos;
else latSpan = (lngSpan * cos) / PLAN_ASPECT;
const midLat = (pack.bounds.maxLat + pack.bounds.minLat) / 2;
const midLng = (pack.bounds.maxLng + pack.bounds.minLng) / 2;
return {
minLat: midLat - latSpan / 2,
maxLat: midLat + latSpan / 2,
minLng: midLng - lngSpan / 2,
maxLng: midLng + lngSpan / 2,
};
}
/**
* Free a board for good.
*
* The order matters and `scene.ts` documents why: `SceneHandle.dispose()` takes
* the scene off the stage first and then walks it, so the renderer still has the
* bookkeeping that makes `gl.deleteProgram` actually run. It also calls
* `environmentRig.release(scene)` before anything else frees a texture — the
* rig's ledger is a **strong** reference, and a disposed board left in it is the
* whole scene graph retained, which is the exact leak that doc comment was
* written for. Retention makes this path rarer and therefore easier to get
* wrong, so it has one caller and one place to look.
*/
function disposeBoard(record: MountedBoard): void {
// A dip drives `setAerialFog` from the pump every frame, and a handle whose
// materials have been disposed is not a thing to keep writing to.
if (fogDip?.record === record) endFogDip();
if (visibleBoard === record) {
deactivateBoard();
visibleBoard = null;
city = null;
atmosphere = null;
minimap = null;
}
record.minimap?.dispose();
record.handle.dispose();
}
/**
* The opening move for a **room**, and the one place the app owns a camera.
*
* The city's arrival lives in `scene.ts`, where the scene owns its own kit. An
* office has no equivalent seam — `OfficeScene` hands out `camera` and
* `controls` and offers `flyTo(viewId)` and nothing that takes a pose — so the
* move is driven from here, off the pump this file already runs.
*
* Two rules, and both are about not breaking something that works:
*
* **The resting pose is whatever the room already chose.** It is read off the
* camera at the moment the door opens rather than computed, so it is the pack's
* `viewpoints[0]` on a first entry and *where you left it* on a second. A
* viewpoint's `focus.at` is also the walk spawn — `startDeviceFeed`'s
* neighbour above reads it to place a walker — and the LA office has already
* been bitten by an arrival shot that stood the camera inside a three-metre
* entry passage framing two brick walls. The target is not touched here. Only
* the camera moves, and only away from a pose somebody already checked.
*
* **A low pose has nowhere to fly from.** Below `OFFICE_ARRIVAL_MIN_ELEVATION`
* the viewpoint is an eye-level shot standing in a room, and lifting a camera
* that is inside a building puts the first second of the arrival inside the
* ceiling slab. Those rooms simply do not get one.
*/
let officeArrival: { from: Pose; to: Pose; elapsed: number } | null = null;
/**
* Seconds. The same length as a board's, so the two doors feel like one product
* — and bounded by the same thing: `scripts/performance-budget.mjs` measures the
* `office` scene from a three-second warm-up, and a move still running inside
* the sample window makes its geometry counts stop reproducing. See
* `ARRIVAL_SECONDS` in `scene.ts` for the argument in full.
*/
const OFFICE_ARRIVAL_SECONDS = 2.5;
/** Below this, in radians, the viewpoint is inside a room and gets no move. */
const OFFICE_ARRIVAL_MIN_ELEVATION = (15 * Math.PI) / 180;
/** How much higher the camera starts, in radians, and the ceiling on where that lands. */
const OFFICE_ARRIVAL_RISE = (11 * Math.PI) / 180;
const OFFICE_ARRIVAL_MAX_ELEVATION = (58 * Math.PI) / 180;
/** How much further out it starts, as a multiple of the resting stand-off. */
const OFFICE_ARRIVAL_STANDOFF = 1.32;
/** How far round the room it swings, in radians. */
const OFFICE_ARRIVAL_YAW = -0.4;
function beginOfficeArrival(scene: OfficeScene): void {
officeArrival = null;
if (prefersReducedMotion()) return;
const target = scene.controls.target.clone();
const rest = { position: scene.camera.position.clone(), target };
const dx = rest.position.x - target.x;
const dy = rest.position.y - target.y;
const dz = rest.position.z - target.z;
const reach = Math.hypot(dx, dy, dz);
if (reach <= 0) return;
const elevation = Math.asin(Math.max(-1, Math.min(1, dy / reach)));
if (elevation < OFFICE_ARRIVAL_MIN_ELEVATION) return;
const raised = Math.min(elevation + OFFICE_ARRIVAL_RISE, OFFICE_ARRIVAL_MAX_ELEVATION);
const flat = Math.hypot(dx, dz);
const cos = Math.cos(OFFICE_ARRIVAL_YAW);
const sin = Math.sin(OFFICE_ARRIVAL_YAW);
const ax = flat > 0 ? (dx * cos - dz * sin) / flat : 0;
const az = flat > 0 ? (dx * sin + dz * cos) / flat : 1;
const startReach = Math.min(reach * OFFICE_ARRIVAL_STANDOFF, scene.controls.maxDistance * 0.995);
const horizontal = Math.cos(raised) * startReach;
const from: Pose = {
target: target.clone(),
position: new Vector3(
target.x + ax * horizontal,
target.y + Math.sin(raised) * startReach,
target.z + az * horizontal,
),
};
scene.camera.position.copy(from.position);
scene.controls.target.copy(from.target);
scene.controls.update();
officeArrival = { from, to: rest, elapsed: 0 };
}
/** Any input, any view, any change of mode: the move is over. */
function cancelOfficeArrival(): void {
officeArrival = null;
}
function stepOfficeArrival(dt: number): void {
if (officeArrival === null) return;
if (!inside || !office || controlModeState.mode !== "office-overview") {
officeArrival = null;
return;
}
officeArrival.elapsed += dt;
const t = Math.min(1, officeArrival.elapsed / OFFICE_ARRIVAL_SECONDS);
const e = t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
office.camera.position.lerpVectors(officeArrival.from.position, officeArrival.to.position, e);
office.controls.target.lerpVectors(officeArrival.from.target, officeArrival.to.target, e);
office.controls.update();
if (t >= 1) officeArrival = null;
}
/**
* Whether this visitor has asked the platform for less movement.
*
* The same question `scene.ts` and `scenekit.ts` ask, asked here because this
* file drives a camera of its own. Read at the moment it is needed rather than
* cached: the only callers ask once per door, and the query is a property read.
*/
function prefersReducedMotion(): boolean {
if (typeof window.matchMedia !== "function") return false;
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
/**
* The app's own frame pump, beside the renderer's.
*
* `Stage` owns the render loop and neither scene handle exposes a per-frame
* hook, so the alternative is adding an `onTick` to both for a handful of call
* sites. This is the smaller change and it costs nothing measurable: an idle
* `tick()` is a timestamp comparison and a dirty flag, 0.0002 ms, and the loop
* keeps running unchanged across a city swap, across the office swap, and
* during the window where there is no scene at all.
*
* What lives on it, and why each is here rather than in a scene:
*
* - the two plan widgets, which are 2-D canvases the engine draws;
* - the pose editor, which is `null` for everyone who is not god;
* - the studio hardware and the car outside, which are **simulations** rather
* than layers — both are fixed-step state machines that the arena also drives,
* and putting their clocks in a scene would mean the arena and the renderer
* stepping two different copies;
* - the chrome, but only while a body is under control. See below.
*/
let lastPumpAt = performance.now();
requestAnimationFrame(function pump(now) {
requestAnimationFrame(pump);
/**
* Clamped, because a backgrounded tab comes back with a `dt` of minutes and
* neither simulator should be asked to catch up on a room nobody was in. Both
* of them drop the excess internally as well; this is the cheaper half of the
* same decision, made before the call rather than inside it.
*/
const dt = Math.min(0.1, Math.max(0, (now - lastPumpAt) / 1000));
lastPumpAt = now;
syncJourneyVehicle(now);
stepFogDip(dt);
stepOfficeArrival(dt);
updateLocalPlayerMaps();
// Only the mounted one. The other is still constructed and still holds a live
// camera, but its canvas is out of the document, so `clientWidth` is 0, every
// `resize()` puts it back to `ready = false`, and ticking it would be a
// function call that exists to do nothing sixty times a second.
if (inside) officePlan?.tick();
else minimap?.tick();
if (inside) {
// A no-op on the API strategy, which is driven by its own poll — see
// `devices/adapter.ts`, which owns that choice and nothing above it knows
// which of the two it got.
deviceSource?.tick(dt);
stepVehicleTelemetry(dt);
}
poseEditor?.tick();
publishRealtimePresence(now);
pollLiveness();
/**
* The chrome, on the frames where something in it actually moves.
*
* `chromeState` is pure and `mount.apply` compares before it writes, so
* calling this unconditionally would be correct — and it would also rebuild
* nothing, sixty times a second, for the ninety-odd percent of a session
* spent in an orbit camera. The one part of the interface that changes per
* frame is the play HUD's speed and altitude, and `playHudKindFor` is exactly
* the test for "is a controller reporting numbers right now". Everything else
* calls `renderChrome()` at the moment it changes, which is what makes this
* an optimisation rather than a second source of truth.
*/
if (playHudKindFor(controlModeState.mode) !== null) renderChrome();
});
function updateLocalPlayerMaps(): void {
if (!city) return;
if (inside) {
minimap?.setPlayer(null);
const walker = controlModeState.mode === "office-walk" ? office?.walker?.state() : null;
officePlan?.setPlayer(walker
? {
levelId: walker.levelId,
x: walker.position.x,
z: walker.position.z,
headingRad: Math.atan2(-walker.facing.x, -walker.facing.z),
kind: walker.actor,
}
: null);
return;
}
officePlan?.setPlayer(null);
if (city.controlMode() === "drive") {
const state = city.vehicleState();
minimap?.setPlayer(state
? { lat: state.lat, lng: state.lng, headingDeg: state.headingDeg, kind: "vehicle" }
: null);
return;
}
if (city.controlMode() === "aircraft") {
const state = city.aircraftState();
minimap?.setPlayer(state
? { lat: state.lat, lng: state.lng, headingDeg: state.headingDeg, kind: "aircraft" }
: null);
return;
}
if (city.controlMode() === "actor") {
const state = city.actorState();
const pack = CITIES.find((candidate) => candidate.id === cityId)?.city;
if (!state || !pack) {
minimap?.setPlayer(null);
return;
}
const anchor = cityId === "california" ? { lat: 35.5, lng: -119.5 } : pack.center;
const [originX, originZ] = city.world.project(anchor.lat, anchor.lng);
const [lat, lng] = city.world.unproject(
originX + state.x / city.world.metresPerUnit,
originZ + state.z / city.world.metresPerUnit,
);
minimap?.setPlayer({
lat,
lng,
headingDeg: ((-state.yaw * 180 / Math.PI) % 360 + 360) % 360,
kind: "actor",
});
return;
}
minimap?.setPlayer(null);
}
/**
* Whether the corner label is still telling the truth, once a second.
*
* The two live feeds settle on their own schedule and neither has an event to
* subscribe to: `TrafficSource.live()` flips when a `/flights` body lands
* inside the region, which is somewhere in the first fifteen seconds, and the
* weather watch fires its own callback but only when a *poll* settles. A
* one-second sample is late by nothing anybody can perceive and costs a
* subtraction on the frames it skips.
*/
let livenessCheckedAt = 0;
let journeySyncedAt = 0;
let previousVehicleProgress: number | null = null;
function pollLiveness() {
const now = performance.now();
if (now - livenessCheckedAt < 1000) return;
livenessCheckedAt = now;
renderChrome();
}
/** Persist route progress cheaply and turn the hero's wrap into a real scale door. */
function syncJourneyVehicle(now: number): void {
if (!routeDriveIsActive() || !city || journey.route === null || journey.vehicle === null) {
previousVehicleProgress = null;
return;
}
const vehicle = city.vehicleState();
if (!vehicle) return;
const progress = vehicle.progress;
const arrived = journey.route.direction === 1
? previousVehicleProgress !== null && previousVehicleProgress > 0.94 && progress < 0.06
: previousVehicleProgress !== null && previousVehicleProgress < 0.06 && progress > 0.94;
previousVehicleProgress = progress;
if (arrived) {
const endpoint = journey.route.direction === 1 ? "san-francisco" : "los-angeles";
dispatchJourney({ type: "reach-route-endpoint", endpoint });
dispatchJourney({ type: "exit-vehicle" });
switchCity(endpoint === "san-francisco" ? "sf" : "socal");
return;
}
if (now - journeySyncedAt < 500) return;
journeySyncedAt = now;
dispatchJourney({ type: "update-route-progress", progress });
}
// ---- Office ---------------------------------------------------------------
/**
* Everyone gets in. The tier picks which building they get, not whether the
* door opens.
*
* The office used to be members-only, and the anonymous view of this site was a
* map with a greyed-out button on it — the single most interesting thing the
* project does, visible only as something you cannot have. At `"public"` depth
* the same shell, the same furniture and the same named viewpoints are built,
* and the only thing missing is the people. That is withheld because the API
* refuses occupancy to an anonymous caller, not because this function declined
* to draw it.
*/
async function enterOffice() {
if (!city) return;
// A city can have more than one authored door. Leaving one office parks its
// scene for a cheap re-entry, but clicking a different door must not reopen
// that cached building under the new office id. Tear down every resource
// owned by the old room before loading the requested one; the parked city
// actor, journey identity and local profile deliberately live elsewhere.
if (office && builtOfficeId !== officeId) disposeLoadedOffice();
if (!office) {
const built = await loadOffice();
// The chunk arrived after the user had already left for the other city, or
// it did not arrive at all. Either way there is no room to walk into and
// `loadOffice` has already said so on the button.
if (!built || !city) return;
const { createOfficeScene, createOfficeMinimap, pack, materials, robotOperations } = built;
const depth = access.can.officeDepth;
// Keep the remote-avatar renderer behind the authenticated boundary. The
// public Office door can build its full local scene without downloading it.
const createOfficePeers = access.subject !== null
? (await import("./realtime/scenePeers.ts")).createScenePeers
: null;
/**
* The building's own sky, built before the scene because the scene wants the
* opening rig and the horizon drop at construction.
*
* `metresPerUnit: 1` — an office is authored in metres, and this is the only
* thing `Atmosphere` needs to know about scale. The fog it computes from
* that is then thrown away and replaced by `officeDaylight`, because a fog
* sized for a fifty-metre room is a fog inside the room.
*/
officeAtmosphere = pack.site
? createAtmosphere({
lng: pack.site.lng,
metresPerUnit: 1,
/**
* No marine layer, deliberately — unlike the city, which gets one.
*
* The model is a fact about sea level on this coast, and both of these
* buildings look *out over* it: one from 188 m up a tower, one across
* an estuary. Handing it to the office put the observer inside the fog
* it describes, which greyed the sky to near-white at one in the
* afternoon and took the view with it. The city keeps its fog; the
* rooms that look at the city do not stand in it.
*/
marineLayer: null,
})
: null;
office = createOfficeScene(pack, {
dom: city.stage.renderer.domElement,
// The page's one environment map, the same object the city holds. Indoors
// it is the difference between `deviceMesh`, `chairBase` and the Model X's
// clearcoat reading as metal and reading as grey plastic.
environment,
/**
* The same aeroplanes the board outside is drawing.
*
* `dial.source` rather than the raw feed, so a sky somebody turned up in
* godmode is the sky in both places; and the *city's* source rather than a
* second subscription, so the office is a second reader of one feed rather
* than a second poller of a volunteer-funded API. The office never
* disposes it — see the note on `OfficeSceneOptions.flights`.
*/
...(trafficDial ? { flights: trafficDial.source } : {}),
/**
* The car on the apron. `corridor` detail because it is parked and the
* arrival viewpoint looks at it from across a courtyard: the 22 extra draw
* calls `follow` buys are mirrors, glass frames and brake calipers, none
* of which resolve at that range.
*
* The seed is the office id rather than a constant, so the two studios do
* not park identical cars at identical angles — which is the single
* clearest tell that a scene was generated.
*/
exteriorVehicle: { detail: "corridor", seed: seedForOffice(officeId) },
// A visitor owns one local actor. Anonymous visitors are the promised
// office dog; a signed-in visitor gets the procedural humanoid. The
// arrival viewpoint is already a pack-authored clear point on a floor,
// which makes it the honest spawn and keeps coordinates out of the app.
walker: {
levelId: pack.viewpoints[0]?.levelId ?? pack.levels[0]?.id ?? "level-1",
position: pack.viewpoints[0]?.focus.at ?? { x: 0, z: 0 },
facing: pack.viewpoints[0]
? {
x: -Math.sin(pack.viewpoints[0].focus.rotation),
z: -Math.cos(pack.viewpoints[0].focus.rotation),
}
: { x: 0, z: -1 },
actor: actorKindForPresence(access.subject !== null, "office") === "dog"
? { kind: "anonymous-dog", coatColor: 0x17191c, collarColor: 0xf2b134 }
: { kind: "humanoid", ...(humanoidAppearance() ?? { outfitColor: 0x151a20, accentColor: 0xf2b134 }) },
},
// Only when there is no sky to put behind it. A sited office computes a
// gradient and a horizon; painting the old flat colour over that is the
// bug that looks exactly like the sky not working.
...(pack.site ? {} : { background: 0x11161c }),
...(pack.site ? { lighting: officeLighting(pack.site) } : {}),
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
// Only offices with explicit, validated simulated operations get robots.
// No geometry-derived random errands and no implication of live work.
...(robotOperations ? { robotOperations } : {}),
depth,
materials,
// Ignored entirely at `"public"` depth, where no layer is built to colour.
presencePalette: SAMPLE_PRESENCE_PALETTE,
// Two different questions, so two different callbacks. `onPresencePick`
// answers "who is at this desk"; `onPlacePick` answers only "this is a
// desk, and it is the fourteenth one" — which is all a stranger is told.
...(depth === "full"
? { onPresencePick: (p) => showDetail(p ? p.label : null) }
: { onPlacePick: (place) => showDetail(place ? place.label : null) }),
...(createOfficePeers ? { realtimePeers: { create: createOfficePeers } } : {}),
});
builtOfficeId = officeId;
office.onViewChange(() => renderChrome());
// Registered here rather than on every entry, because this block runs once
// per office and the scene is retained between visits. The first drag,
// wheel or pinch ends the opening move wherever it has got to — a camera
// that finished its arc anyway would be arguing with somebody who has
// already started using it.
office.controls.addEventListener("start", cancelOfficeArrival);
officePlan = buildOfficePlan(createOfficeMinimap, office);
startDeviceFeed(built.createDeviceSource, office.devices);
startVehicleTelemetry(built.createSimulatedVehicleTelemetry);
}
requestControlMode("overview");
city.stage.setScene(office);
inside = true;
controlModeState = {
mode: "office-overview",
revision: controlModeState.revision + 1,
};
attachCurrentWebcamFace();
moveRealtimePresence();
const desiredCity: JourneyCity = officeId === "mateo-court" ? "socal" : "bay-area";
journeyToCity(desiredCity);
dispatchJourney({ type: "enter-office", officeId: officeId as "lumbridge-hq" | "frontier-valley" | "mateo-court" });
showPlan();
showDetail(null);
refreshGodmodePlace();
renderChrome();
// Once the room is up and the panel beside it is drawn. See
// `beginOfficeArrival` for the two rules it obeys.
beginOfficeArrival(office);
// After the room is on screen, not before. The first ask is a second request
// and the building is worth looking at while it is in flight; awaiting it here
// would hold the door shut on a network round trip to draw people into a scene
// the user cannot see yet.
watchOccupancy();
}
/**
* The plan panel, for the office.
*
* Built once alongside the scene rather than on every entry, because it reads
* that scene's camera and controls by reference — a second one built against a
* disposed office would draw a footprint from a camera nobody is moving.
*
* The constructor is passed in rather than imported, for the reason written at
* the import block: naming the module here as a value would put the furniture
* catalogue back in the entry chunk.
*/
function buildOfficePlan(
create: typeof import("./engine/officeMinimap.ts").createOfficeMinimap,
scene: OfficeScene,
): OfficeMinimap {
const panel = create({
plan: scene.plan,
camera: scene.camera,
controls: scene.controls,
// The same ceiling the city plan takes, from the same place. Two 2D canvases
// in one corner of one page disagreeing about what a phone can afford is the
// exact drift `deviceProfile` exists in one file to prevent.
maxPixelRatio: deviceProfile().maxPixelRatio,
onSeek(x, z) {
if (controlModeState.mode !== "office-overview") return;
/**
* The same move the city plan makes, and for the same reason: slide the
* orbit target and carry the camera with it, keeping the offset between
* them. A seek is "look over there", not "go to viewpoint three".
*
* The height is deliberately untouched. Indoors that matters more than it
* does outdoors — the difference between a viewpoint two metres off the
* floor and one fourteen metres up is the difference between standing in
* the room and looking down into it, and clicking the plan should not
* silently move you between those two.
*/
const { camera, controls } = scene;
const dx = camera.position.x - controls.target.x;
const dy = camera.position.y - controls.target.y;
const dz = camera.position.z - controls.target.z;
controls.target.set(x, controls.target.y, z);
camera.position.set(x + dx, controls.target.y + dy, z + dz);
},
onHover(info) {
if (!minimapReadout) return;
if (!info) {
minimapReadout.textContent = "";
return;
}
// Metres, not degrees. The city widget prints a latitude and a longitude
// because that is the address of a place on a map; the address of a place
// in a building is where it is on the floor, in the units the pack was
// authored in, so a self-hoster can read a number off this and type it
// into their own office file.
const where = `${info.x.toFixed(1)}, ${info.z.toFixed(1)} m`;
// The storey is named only when there is more than one to be on. On a
// single-level pack — which is the reference office and will be most
// packs — "Level 1" on every hover is a word that never changes and
// therefore never informs.
const level = scene.plan.levels.length > 1 && info.level ? ` · ${info.level}` : "";
// The person last, and after the room, because the readout is read left to
// right as an address getting more specific: the floor, then the room,
// then who is in it.
const room = info.room ? ` · ${info.room}` : "";
const person = info.person ? ` · ${info.person}` : "";
minimapReadout.textContent = `${where}${level}${room}${person}`;
},
});
/**
* Once, not per frame.
*
* `scene.robots()` hands back a stable array of vectors the robot layer
* mutates in place, so the plan reads this frame's positions through a
* reference taken here — the same handshake `officeScene` already makes with
* `luminaires.setWalkers`. Polling it every frame would allocate nothing and
* would imply the array were a snapshot, which it is not.
*/
panel.setRobots(scene.robots());
return panel;
}
/**
* Put the plan for wherever you are into the panel.
*
* One function rather than a `replaceChildren` at each of the call sites,
* because the failure it prevents is silent: the panel keeps showing a widget
* that is still ticking and still correct *about somewhere else*, which looks
* exactly like a working map.
*/
function showPlan() {
const widget = inside ? officePlan : minimap;
if (!minimapFrame || !widget) return;
minimapFrame.replaceChildren(widget.canvas);
// The canvas has just re-entered the document with a real box for the first
// time, or the first time since it left it. Nothing else tells it that: the
// `ResizeObserver` fires on a change of size, and going from detached to
// attached at the same size is not one.
widget.resize();
}
function leaveOffice() {
if (!city) return;
// Before the scene swap, so the last thing the watch can do is abort a request
// rather than publish into a room the user has already left.
stopWatchingOccupancy();
disposeOfficeScreenUi();
clearPublishedPlayInput();
office?.walker?.setActive(false);
dispatchJourney({ type: "leave-office" });
city.stage.setScene(city.stageScene);
inside = false;
controlModeState = {
mode: "overview",
revision: controlModeState.revision + 1,
};
city.setControlMode("overview");
attachCurrentWebcamFace();
moveRealtimePresence();
showPlan();
showDetail(null);
refreshGodmodePlace();
renderChrome();
}
/** Dispose everything whose coordinates or subscriptions belong to one office. */
function disposeLoadedOffice(): void {
stopWatchingOccupancy();
disposeOfficeScreenUi();
// Before the scene, so a reading in flight cannot land on a disposed layer.
// `stop()` is idempotent and is the only thing that owns a timer down there.
deviceSource?.stop();
deviceSource = null;
deviceStates = [];
vehicleTelemetry = null;
officePlan?.dispose();
officePlan = null;
office?.dispose();
office = null;
builtOfficeId = null;
officeAtmosphere = null;
}
/**
* A stable integer per building, for the things that must look the same twice.
*
* FNV-1a over the id, which is the same hash the arena's checksums use and is
* chosen for the same property: it is short, it has no dependencies, and it
* gives two ids that differ by one character completely different seeds — so
* `lumbridge-hq` and `mateo-court` genuinely park different cars rather than
* two that differ in the third decimal.
*/
function seedForOffice(id: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < id.length; i += 1) {
hash ^= id.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
/**
* Point the hardware in this room at whatever will answer for it.
*
* The choice — the deployment's device route, or the fixed-step simulator in
* this tab — is made inside `createDeviceSource` and deliberately not here.
* That is the one place the anon-first fallback lives, and the reason it is not
* a branch in this file is that the two halves would drift: the route refuses an
* anonymous read, which is correct, and an anonymous visitor is the audience
* this product is designed for, so the refusal must produce a working
* instrument rather than a dead one.
*
* `serverHasDevices` is the same short-circuit `access.feeds` performs for
* weather and markers: a box with no device source answers with an empty body,
* and asking it once per TTL per tab forever to be told nothing is a request
* nobody needs.
*
* **It is two questions and it used to ask only one.** `access.feeds.devices`
* says the deployment has a source; `access.can.liveDevices` says this viewer
* may read it. On cloud-2 the first is true and, for the visitor this product
* is designed for, the second is false — the route is members-only and answers
* 401 — so passing the deployment's answer alone sent every anonymous viewer
* down the API strategy to be refused. `apiSource` then renders `atRest()`
* forever: a rack of powered-off instruments, in a room the panel beside them
* describes as running, with an exponential back-off quietly retrying a request
* that cannot succeed. Both halves, and the fallback is the simulator that was
* always meant to serve this case.
*/
function startDeviceFeed(
create: typeof import("./devices/adapter.ts").createDeviceSource,
declarations: readonly DeviceDeclaration[],
): void {
deviceSource?.stop();
deviceStates = [];
if (declarations.length === 0) {
deviceSource = null;
return;
}
deviceSource = create({
declarations,
client: tera,
officeId: officePack?.id ?? officeId,
serverHasDevices: access.feeds?.devices !== false && access.can.liveDevices,
/**
* And the tier itself, which is belt and braces on purpose.
*
* `can.liveDevices` above is *this app's* answer to "may this viewer read
* the route", and it is the one CONTRACT §6 names. `viewerTier` is the
* *fact*, and `adapter.ts` gates on it independently, so a future call site
* that forgets the capability still cannot ship an anonymous visitor a rack
* of dead instruments. Two gates for one decision is the right number when
* one of them is a bug that already shipped once.
*/
viewerTier: access.tier,
seed: seedForOffice(officeId),
onReading: (reading) => {
deviceStates = reading.states;
// The hardware in the room and the panel beside it, from one reading, so
// a lit indicator and a lit control can never disagree about a mic.
office?.setDeviceStates(reading.states);
chrome?.applyDeviceStates(reading.states);
},
});
const opening = deviceSource.current();
deviceStates = opening.states;
office?.setDeviceStates(opening.states);
chrome?.applyDeviceStates(opening.states);
}
/**
* Send one command, and say nothing if it is refused.
*
* `normalizeDeviceCommand` has already run inside the panel, so anything that
* reaches here is well formed for a device this office declared. A `null` back
* is a *deployment* refusal — an anonymous caller writing to a real room — and
* the panel is left showing the last reading rather than a control that appears
* to have worked. The refusal is not narrated because the sign-in offer is
* already on the same screen.
*/
function sendDeviceCommand(command: DeviceCommand): void {
void deviceSource?.command(command).then((state) => {
if (state === null) return;
deviceStates = deviceStates.map((current) => (current.id === state.id ? state : current));
office?.setDeviceStates(deviceStates);
chrome?.applyDeviceStates(deviceStates);
});
}
/** Bring the studio hardware panel into view, opening the column if it is shut. */
function openDevicePanel(): void {
panelOpen = true;
panelChosen = true;
renderChrome();
document.querySelector<HTMLElement>("#device-section")?.scrollIntoView({
block: "nearest",
// A jump rather than a glide, and not only for `prefers-reduced-motion`: the
// panel it is scrolling inside has just been opened in the same frame.
behavior: "auto",
});
}
/**
* The car outside, as its own fixed-step simulation.
*
* Seeded per office and stepped from the frame pump rather than from the office
* scene, because this is the *same* module the arena drives — a state machine
* with a snapshot and a restore, not a render layer. Keeping its clock here is
* what lets the renderer and the arena hold two independent instances of one
* implementation instead of two implementations.
*
* The ambient temperature is a constant and that is a gap rather than a choice:
* `WeatherObservation` carries cloud, precipitation, visibility and wind, and no
* temperature at all, so there is nothing live to couple to. Inventing one from
* the sun's elevation would be publishing a reading nobody took, which is the
* one thing the whole telemetry layer is arranged not to do.
*/
function startVehicleTelemetry(
create: typeof import("./transport/vehicleTelemetry.ts").createSimulatedVehicleTelemetry,
): void {
vehicleTelemetry = create({
seed: seedForOffice(officeId),
fixedStepSeconds: VEHICLE_STEP_SECONDS,
ambientC: DEFAULT_AMBIENT_C,
});
office?.setVehicleTelemetry(vehicleTelemetry.current());
}
/** Seconds of simulated vehicle time per fixed step. */
const VEHICLE_STEP_SECONDS = 0.5;
/**
* The ambient temperature the parked car sits in, in Celsius.
*
* A mild Californian afternoon, which is what both studios stand in. See
* `startVehicleTelemetry` for why it is not read off the weather feed.
*/
const DEFAULT_AMBIENT_C = 19;
let vehicleStepDebt = 0;
/**
* Advance the car by whole fixed steps, and only ever by whole ones.
*
* A fixed-step simulator that is handed a variable `dt` stops being
* reproducible, and reproducibility is the entire reason this module is shared
* with the arena. So the frame's elapsed time is accumulated and spent in whole
* steps, with the remainder carried; `apply` is signature-guarded downstream, so
* the frames that spend no step cost a comparison.
*/
function stepVehicleTelemetry(dt: number): void {
const source = vehicleTelemetry;
if (!source || !office) return;
vehicleStepDebt = Math.min(vehicleStepDebt + dt, VEHICLE_STEP_SECONDS * 4);
while (vehicleStepDebt >= VEHICLE_STEP_SECONDS) {
vehicleStepDebt -= VEHICLE_STEP_SECONDS;
source.stepFixed();
}
office.setVehicleTelemetry(source.current());
}
/**
* Fetch Spaces.
*
* The office is the largest thing in this build that most visitors never open,
* so it is a chunk the door fetches on the way through rather than bytes every
* map visitor downloads. Measured entry chunk: 780.18 kB before the split,
* 720.89 after — smaller than the chunks themselves because three.js is shared
* and stays where it was.
*
* One `Promise.all` because they are one arrival: a pack without its builder is
* a data file nobody can draw, so the fetches overlap rather than queue.
*
* No retry and no cache-busting, deliberately. A failed chunk fetch is a deploy
* that moved the file under an open tab; the honest answer is to say the door
* did not open and let the next click try again, which it can, because a
* rejected dynamic import is not memoised by the browser.
*/
async function loadOffice(): Promise<{
createOfficeScene: typeof import("./interiors/officeScene.ts").createOfficeScene;
createOfficeMinimap: typeof import("./engine/officeMinimap.ts").createOfficeMinimap;
createDeviceSource: typeof import("./devices/adapter.ts").createDeviceSource;
createSimulatedVehicleTelemetry:
typeof import("./transport/vehicleTelemetry.ts").createSimulatedVehicleTelemetry;
pack: Office;
materials: MaterialRegistry;
robotOperations: RobotOperationsDefinition | null;
} | null> {
try {
// More modules and still one arrival. The plan renderer reads prop
// footprints off the asset registry, so it is already downstream of the
// furniture catalogue this chunk exists to hold back — asking for it here
// costs nothing beyond the module itself, and asking for it anywhere else
// would cost the whole catalogue in the entry chunk.
const entry = OFFICES.find((o) => o.id === officeId) ?? OFFICES[0];
if (!entry) return null;
const [interiors, pack, assets, plan, operations, devices, telemetry] = await Promise.all([
import("./interiors/officeScene.ts"),
entry.load(),
import("./assets/materials.ts"),
import("./engine/officeMinimap.ts"),
entry.loadOperations?.() ?? Promise.resolve(null),
// Neither of these imports THREE, the DOM or the network, and neither has
// anything to say until somebody is standing in a studio — so both travel
// with the furniture rather than in the bundle every map visitor fetches.
import("./devices/adapter.ts"),
import("./transport/vehicleTelemetry.ts"),
]);
// Assigned rather than memoised with `??=`: the memo was what made this
// single-office forever, quietly serving the first pack fetched for every
// later request whatever id was asked for.
officePack = pack.default;
officeMaterials ??= new assets.MaterialRegistry({ quality: officeMaterialQuality() });
return {
createOfficeScene: interiors.createOfficeScene,
createOfficeMinimap: plan.createOfficeMinimap,
createDeviceSource: devices.createDeviceSource,
createSimulatedVehicleTelemetry: telemetry.createSimulatedVehicleTelemetry,
pack: officePack,
materials: officeMaterials,
robotOperations: operations?.default ?? null,
};
} catch {
showDetail("The office did not load. Check the connection and try the door again.");
return null;
}
}
/**
* How much texture a studio may draw, decided by the device rather than by a
* constant.
*
* This said `"high"` for every visitor, which meant `materials.ts` had a fully
* implemented `low` and `medium` path — different roughness handling, a cheaper
* glazing fallback, and 256px textures instead of 512 — that **no caller could
* ever reach**. The documented mobile escape hatch had never actually existed,
* and a phone was being handed desktop-grade texture memory for a canvas at
* 1.5× pixel ratio.
*
* `medium` rather than `low` on a handheld: `low` drops to flat Lambert with no
* maps at all, which is the setting that makes an office open on an integrated
* GPU and is far more than a modern phone needs. `medium` is the same physically
* shaded materials at half the texture resolution, which is the actual
* difference a phone can feel.
*
* `deviceProfile()` is the one definition of "phone" on this page — the pixel
* ratio cap, the shadow map size and now the texture budget all read it, so they
* cannot disagree about what a phone is.
*/
function officeMaterialQuality(): MaterialQuality {
return deviceProfile().handheld ? "medium" : "high";
}
/** The office's name, for the two bits of chrome that say where you are. */
function officeName(): string {
return officePack?.name ?? "Spaces";
}
/**
* Who is in the building, asked for once per entry.
*
* Only at full depth, and that is not an optimisation: `createOfficeScene` at
* `"public"` builds no presence layer at all, so there is nothing to populate
* and the request would be one the session is going to be refused anyway.
*
* The fallback is `markers`' fallback, one room in. An API that answers is
* believed, including when it answers with nobody — an office where everyone has
* gone home is a real fact about an office, and overwriting it with invented
* people to liven up the demo is the one thing this file must never do.
*/
function watchOccupancy() {
stopWatchingOccupancy();
const scene = office;
if (!scene || scene.depth !== "full") return;
presenceWatch = tera.watchPresence(officePack?.id ?? "lumbridge-hq", (body) => {
livePresence = body?.people ?? null;
presenceIsSample = false;
// The scene may have been torn down between a request going out and coming
// back — a city switch disposes the office — and writing people into a
// disposed layer is a use-after-free with a friendly name. The watch's own
// `stop()` already drops late answers; this is the belt to that brace,
// because the office can also be *replaced* (signing in rebuilds it at full
// depth) without the watch having been stopped in between.
if (office !== scene) return;
applyPresence();
renderChrome();
});
}
function stopWatchingOccupancy() {
presenceWatch?.stop();
presenceWatch = null;
// Or the next building — or the next entry into this one — opens wearing the
// previous deployment's roster while its own request is still in the air.
livePresence = null;
}
/**
* Put whoever is in the building on screen.
*
* The live roster if there is one, and otherwise the sample one **as it would be
* at the instant being rendered**. That second half is what stops the office
* showing a full complement of seated people at one in the morning, under house
* lights that came on because the sun is down — which was the least believable
* thing left in the room once the clock became real.
*
* A live answer always wins. An API that says the building is empty is telling
* the truth about the building, and dressing it with invented people would be
* the one lie this whole layer is arranged to avoid.
*/
function applyPresence() {
if (!office || office.depth !== "full") return;
const fallback = samplePresenceForOfficeAt(officePack?.id ?? officeId, currentInstant());
const people = livePresence ?? fallback;
presenceIsSample = livePresence === null && fallback.length > 0;
office.setPresence(people);
officePlan?.setPresence(people);
}
// ---- Chrome ---------------------------------------------------------------
//
// Six DOM handles and no more. Everything else on this page belongs to
// `ui/mount.ts`, which is the only module in the product that writes to it —
// these six are the hosts of things `mount` deliberately does not own: two
// widgets built by the engine, one overlay built by `profile/`, one by
// `media/`, and one indicator built by `realtime/`.
const minimapFrame = document.querySelector<HTMLElement>("#minimap .minimap-frame");
const minimapReadout = document.querySelector<HTMLElement>("#minimap-readout");
const presenceHost = document.querySelector<HTMLElement>("#presence-host");
const webcamFaceIndicator = document.querySelector<HTMLElement>("#webcam-face-indicator");
const profileOverlay = document.querySelector<HTMLElement>("#profile-overlay");
const screensOverlay = document.querySelector<HTMLElement>("#screens-overlay");
let profileEditor: ProfileEditor | null = null;
let webcamFacePanel: WebcamFacePanel | null = null;
let webcamFaceTexture: WebcamFaceTextureAdapter | null = null;
let webcamFaceConsent = createWebcamFaceConsent();
let webcamCapture: WebcamCaptureController | null = null;
/** Whether hosted presence has a widget on screen; the chrome makes room for it. */
let presenceMounted = false;
function availableControlModes() {
const route = city?.current();
return {
insideOffice: inside,
drive: !inside && city?.vehicleState() !== null &&
(route === "la-sf-us-101" || route === "la-sf-i-5"),
actor: !inside && city?.actorState() !== null,
aircraft: !inside && city?.aircraftState() !== null,
officeWalk: inside && office?.walker !== null && office?.walker !== undefined,
};
}
/**
* The same availability, as the list the chrome takes.
*
* Two shapes for one fact is a smell and is the lesser one here:
* `transitionControlMode` was written against the record and is tested against
* it, and `chromeState` takes a list because a list is what a dock renders. This
* is the one conversion, in one place, rather than each caller deriving its own.
*/
function availableModes(): ControlMode[] {
const can = availableControlModes();
const modes: ControlMode[] = [inside ? "office-overview" : "overview"];
if (can.drive) modes.push("drive");
if (can.actor) modes.push("actor");
if (can.aircraft) modes.push("aircraft");
if (can.officeWalk) modes.push("office-walk");
return modes;
}
function clearPublishedPlayInput(): void {
playInput.clearAll();
city?.setVehicleActions({});
city?.setActorActions({});
city?.setAircraftActions({});
office?.walker?.setAction({ x: 0, z: 0 });
}
function adoptCityControlMode(mode: ReturnType<typeof cityControlMode>): void {
if (inside) return;
const previous = controlModeState.mode;
if (controlModeState.mode !== mode) {
clearPublishedPlayInput();
controlModeState = { mode, revision: controlModeState.revision + 1 };
}
if (previous === "overview" && mode !== "overview") closePanelForPlay();
renderChrome();
}
/** One transaction updates Journey, simulation ownership, input and chrome. */
function requestControlMode(requested: ControlMode): boolean {
const transition = transitionControlMode(controlModeState, requested, availableControlModes());
if (transition.changed) clearPublishedPlayInput();
controlModeState = transition.state;
const next = transition.state.mode;
if (inside) {
city?.setControlMode("overview");
const walking = next === "office-walk";
office?.walker?.setActive(walking);
if (!walking) office?.walker?.setAction({ x: 0, z: 0 });
} else {
office?.walker?.setActive(false);
city?.setControlMode(cityControlMode(next));
}
if (next === "drive") {
const routeId = city?.current();
if (routeId === "la-sf-us-101" || routeId === "la-sf-i-5") {
dispatchJourney({ type: "set-mode", mode: "play" });
if (journey.route?.routeId !== routeId) {
dispatchJourney({ type: "select-route", routeId, direction: 1 });
}
if (!journey.vehicle) dispatchJourney({ type: "enter-vehicle", vehicleId: "model-x-black" });
}
} else if (journey.vehicle) {
dispatchJourney({ type: "exit-vehicle" });
}
if (next !== "overview" && next !== "office-overview") {
dispatchJourney({ type: "set-mode", mode: "play" });
} else if (!journey.vehicle) {
dispatchJourney({ type: "set-mode", mode: "observe" });
}
if (
transition.changed &&
(transition.previous === "overview" || transition.previous === "office-overview") &&
next !== "overview" && next !== "office-overview"
) {
closePanelForPlay();
}
renderChrome();
publishPlayActions();
return transition.accepted;
}
/** One authored sentence in the detail card, or nothing. */
function showDetail(text: string | null) {
detail = text === null ? null : { kind: "text", text };
renderChrome();
}
/**
* One aeroplane in the detail card, for anybody at all.
*
* Owner decision 2: the flight card is part of the demo an anonymous visitor
* gets, and there is nothing here an account could grant — an ADS-B position is
* broadcast unencrypted by the aircraft to anybody with a receiver. What the
* *deployment* knows and the engine does not is provenance, which is why the
* live source is asked first: `TrafficSource.detail` carries the transponder
* address off the wire body and the credit line the feed asks to be shown
* beside its data, and neither is recoverable from the position alone.
*
* The fallback is not a degraded case. It is the zero-config clone, the
* California corridor board and every aircraft the godmode dial invented, and
* for all three the honest answer is a card that says "simulated track" rather
* than a hex address nobody transmitted. `aircraftDetail` defaults `observed` to
* false precisely so that forgetting to say so cannot produce the other claim.
*/
function showAircraftDetail(aircraft: Aircraft | null): void {
if (aircraft === null) {
if (detail?.kind === "aircraft") showDetail(null);
return;
}
const centre = CITIES.find((c) => c.id === cityId)?.city.center;
const resolved = cityFlights?.detail(aircraft.id)
?? aircraftDetail(aircraft, { observed: false, ...(centre ? { from: centre } : {}) });
const card: AircraftDetailInput = {
// The hex when the feed gave one; the source's own id otherwise, which the
// card only ever shows for a track it has already labelled as simulated.
id: resolved.icao24 ?? resolved.id,
callsign: resolved.callsign,
lat: resolved.lat,
lng: resolved.lng,
altitude: resolved.altitudeM,
heading: resolved.headingDeg,
// The four the feed carries and the simulator does not. All four are `null`
// for a synthetic track and for a server one version behind, and the card
// drops the row rather than printing "unknown" — so there is no degraded
// state to handle here, only a shorter card.
type: resolved.type,
registration: resolved.registration,
groundSpeedKt: resolved.groundSpeedKt,
verticalRateFpm: resolved.verticalRateFpm,
synthetic: !resolved.observed,
attribution: resolved.attribution.length > 0 ? resolved.attribution.join(" · ") : null,
};
detail = { kind: "aircraft", aircraft: card };
renderChrome();
}
/** The board strip: worlds outside a building, buildings inside one. */
function boardTabs() {
return inside
? OFFICES.map((o) => ({ id: o.id, label: o.label, status: o.status }))
: CITIES.map((c) => ({ id: c.id, label: c.label, status: null }));
}
/**
* Whether the three-board tab strip is on screen.
*
* Off for everybody by default, which is the point of this round: a strip of
* three tabs naming three *modes* is the most direct statement the interface can
* make that this is three products rather than one place, and the places list is
* what replaced it. It is kept rather than deleted because it is still the only
* control that addresses a board by id, which is exactly what somebody working
* on a board wants — so `?boards=1` brings it back, and godmode has it always.
*
* Inside a building the same strip is the *studio* picker and there is nothing
* to hide: three studios are three places, and the list below them is the room's
* viewpoints rather than a second tour.
*/
const BOARD_TABS_FORCED = new URLSearchParams(location.search).get("boards") === "1";
function boardTabsVisible(): boolean {
return inside || BOARD_TABS_FORCED || access.can.debug;
}
/**
* The ladder, in the shape the left column takes it.
*
* Every rung of all three boards, every time — the list does not change when the
* board does, because that is the whole idea. What changes is which row is lit.
*/
function placeRows() {
return PLACES.map((rung) => ({
key: rung.key,
id: rung.id,
board: rung.board,
region: REGION_LABELS[rung.region],
label: rung.label,
standoffM: rung.standoffM,
description: rung.description,
}));
}
/**
* Whatever the mode's controller is reporting, in the shape the HUD formats.
*
* Keyed off `playHudKindFor` rather than off a second `switch` on the mode, so
* the two orbit states are excluded once, in the module that also decides the
* card's visibility. `null` whenever the controller has nothing to say — a
* scene that has not built an aircraft cannot report on one — and the HUD is
* hidden rather than blank in that case.
*/
function playTelemetry(): PlayTelemetry | null {
switch (playHudKindFor(controlModeState.mode)) {
case "drive": {
const state = city?.vehicleState();
if (!state) return null;
return {
kind: "drive",
speedMps: state.speedMps,
roadName: state.roadName,
driveMode: state.mode,
progress: state.progress,
camera: city?.vehicleCamera() ?? "chase",
guardrailContact: state.guardrailContact,
collisionRisk: state.collisionRisk,
};
}
case "actor": {
const state = city?.actorState();
if (!state) return null;
return {
kind: "actor",
actor: state.kind,
speedMps: state.speedMps,
altitudeM: Math.max(0, state.y),
distanceM: state.distanceM,
pose: state.kind === "crow" ? state.crowPose : state.mode,
flightEnergy: state.flightEnergy,
displayName: state.identity.displayName,
atAltitudeBound: state.altitudeBoundContact !== "none",
};
}
case "aircraft": {
const state = city?.aircraftState();
if (!state) return null;
return {
kind: "aircraft",
speedMps: state.speedMps,
altitudeM: state.altitudeM,
flightMode: state.mode,
batteryWh: state.batteryWh,
stalled: state.stalled,
hardLanding: state.hardLanding,
envelopeContact: state.envelopeContact,
};
}
case "office-walk": {
const state = office?.walker?.state();
if (!state) return null;
return {
kind: "office-walk",
officeLabel: officeName(),
distanceM: state.distance,
x: state.position.x,
z: state.position.z,
};
}
default:
return null;
}
}
/** The body under control, for the labels that name it. */
function chromeActor() {
if (inside) {
const walker = office?.walker?.state();
if (!walker) return {};
return { kind: walker.actor === "anonymous-dog" ? ("dog" as const) : ("humanoid" as const) };
}
if (controlModeState.mode === "aircraft") return { flying: true };
const state = city?.actorState();
if (!state) return {};
return { kind: state.kind, flying: state.mode === "flight" };
}
/**
* Everybody who is owed a credit for what is currently on screen.
*
* MET Norway and Open-Meteo publish under CC BY 4.0 and the ADS-B feeds ask to
* be named for the positions; all of it arrived, and until the `?` sheet started
* printing it, all of it was read by nobody — a licence obligation plumbed to
* within one line of being met. The weather override is deliberately excluded:
* a sky somebody typed is not MET Norway's sky and must not be attributed to
* them.
*/
function creditLines(): string[] {
const lines: string[] = [];
if (weatherOverride === null) lines.push(...(weatherWatch?.current().attribution ?? []));
lines.push(...(cityFlights?.attribution() ?? []));
/**
* The three newest feeds, each named by whatever answered for it.
*
* The modelled harbour is in this list for the opposite of the usual reason:
* its attribution line exists to say that nothing out there is being claimed —
* "not an observation of any vessel" — and the sheet that prints licence
* obligations is the right place for a disclaimer of one. Radar and birds
* carry NWS and BirdCast the day a projection is configured, and carry nothing
* before that, which is why this reads the body rather than a constant.
*/
if (vesselBounds !== null) lines.push(...(vesselsBody?.attribution ?? []));
if (boardCarriesSky) {
lines.push(...(radarBody?.attribution ?? []));
lines.push(...(birdsBody?.attribution ?? []));
}
return lines.filter((line) => line.trim() !== "");
}
/**
* Everything the interface needs to know, as one plain object.
*
* This function is the whole of the coupling between the application and its
* chrome. It reads module state and returns data; it writes nothing and it
* decides nothing — every "should this be visible" question in the product is
* answered by `chromeState`, and every answer is applied by `mount`. Adding a
* piece of chrome is a field here, a decision there and a write in the applier,
* in that order, and never a fortieth `element.hidden =` in this file.
*/
function chromeInputs(): ChromeInputs {
const entry = CITIES.find((c) => c.id === cityId);
const selectedOffice = OFFICES.find((o) => o.id === officeId) ?? OFFICES[0];
const mediaSurfaces = inside && office?.depth === "full" ? office.listMediaSurfaces() : [];
const robotActivity = inside ? office?.robotActivityInfo() ?? null : null;
const views: View[] = inside && office ? office.views : city?.chapters ?? [];
const activeViewId = inside && office ? office.current() : city?.current() ?? null;
return {
mode: controlModeState.mode,
available: availableModes(),
access: {
tier: access.tier,
signInUrl: access.signInUrl,
subject: access.subject,
displayName: localProfile?.displayName ?? null,
hasProfile: localProfile !== null,
},
inside,
officeDepth: inside ? office?.depth ?? null : null,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
// The class is set by the first touch this page ever sees and never
// removed, because a laptop with a touchscreen has both and the one the
// person actually used is the one worth believing.
coarsePointer:
document.body.classList.contains("touch-capable") ||
window.matchMedia?.("(pointer: coarse)").matches === true,
},
feeds: {
markers: liveData,
// An override is a sky somebody invented, so it retires the claim for as
// long as it is up: the label is about what is on screen, not about what
// the deployment could have shown.
weather: weatherOverride === null && (weatherWatch?.current().live ?? false),
flights: cityFlights?.live() ?? false,
weatherOverridden: weatherOverride !== null,
sampleOccupancy: presenceIsSample,
credits: creditLines(),
},
degraded: access.degraded ?? [],
devices: inside ? office?.devices ?? [] : [],
firstVisit,
panelOpen,
planOpen,
board: {
cityId,
cityLabel: entry?.city.name ?? "",
officeId,
officeLabel: inside ? officeName() : selectedOffice?.label ?? "the studio",
officeStatus: selectedOffice?.status ?? "building",
isCalifornia: cityId === "california",
},
actor: chromeActor(),
office: {
mediaSurfaceCount: mediaSurfaces.length,
mediaSurfaceActiveCount: mediaSurfaces.filter((surface) => surface.bound).length,
robotDisclosure: robotActivity?.disclosure ?? null,
// Recomputed here rather than cached beside the promotion, because the
// load is a function of *where the building is* as well as what is
// burning, and this is the one place that knows both.
smokeNote: officeSmokeNote(),
walkable: office?.walker != null,
},
cameraLive: webcamCapture?.status() === "active",
presenceVisible: presenceMounted,
boards: boardTabs(),
boardsVisible: boardTabsVisible(),
places: inside ? [] : placeRows(),
activePlaceKey: activePlace()?.key ?? null,
views: views.map((view) => ({
id: view.id,
number: view.number ?? null,
shortLabel: view.shortLabel,
...(view.description === undefined ? {} : { description: view.description }),
})),
activeViewId,
telemetry: playTelemetry(),
detail,
clockLabel,
};
}
/**
* Draw the interface. One call, and it replaces forty.
*
* Cheap enough to run from the frame pump — `chromeState` is pure arithmetic and
* string building, and the applier compares before it writes and rebuilds a
* subtree only when that subtree's signature moves — but it is still called on
* *changes* rather than unconditionally, and the pump only joins in while a body
* is under control. See `pumpChrome`.
*
* The two plan widgets are updated here rather than inside the applier because
* neither is chrome: they are canvases the engine draws, holding a live camera,
* and `mount.ts` is deliberately ignorant of THREE.
*/
function renderChrome(): void {
// The one place the mode the app *thinks* it is in is reconciled with the mode
// the scene is actually in. A scene can change it on its own — a chapter that
// is a driving route puts the city into `drive` — and a dock that disagreed
// with the camera was the oldest bug in this file.
if (city) {
const walking = inside && (office?.walker?.active() ?? false);
const actual: ControlMode = inside
? walking ? "office-walk" : "office-overview"
: city.controlMode();
if (controlModeState.mode !== actual) {
controlModeState = { mode: actual, revision: controlModeState.revision + 1 };
}
}
chrome?.apply(chromeState(chromeInputs()));
showFireSection();
showBoardNotes();
// Each plan rings the entry its own legend is showing as current. Both are
// updated whichever one is mounted, so the hidden one is already right when it
// comes back rather than correcting itself a frame after it appears.
if (city) minimap?.setChapters(city.chapters, city.current());
officePlan?.setActiveView(office?.current() ?? null);
}
/**
* Walk out of one building and into another.
*
* A full teardown and rebuild rather than a swap, because everything an office
* scene holds is derived from its pack: the shell, the plan panel, the camera
* limits, the horizon drop, the light rig, the hardware on the desks and the car
* on the apron. The expensive part — the texture registry — is deliberately
* *not* rebuilt, which is the same trick that makes signing in cheap:
* `officeMaterials` outlives every scene that borrows it, so a switch costs
* geometry and not the thing that draws the wood grain.
*/
async function switchOffice(id: string) {
// `entering` is the same guard `toggleOffice` uses, and this has to share it.
// Without it, two clicks inside the loading window build two office scenes and
// the first is parked on the stage with nothing holding a reference to it —
// a whole `OfficeScene`, its geometry and its plan panel, leaked per click.
if (entering || id === officeId || !OFFICES.some((o) => o.id === id)) return;
const previous = officeId;
officeId = id;
// The roster, screen session, hardware feed, plan and scene all belong to the
// building being left.
disposeLoadedOffice();
entering = true;
try {
await building("Opening the door…", () => enterOffice());
} finally {
entering = false;
}
/**
* The old room is already gone by the time we find out whether the new one
* arrived, and a failed chunk fetch is a real case — `loadOffice` says so on
* the card and returns null.
*
* So there is no room on the stage and `inside` still claims there is. Put the
* viewer back in the city rather than in an empty scene, and put the door back
* on the building they came from, or the picker is wedged pointing at an
* office that will not open.
*/
if (office || !city) return;
officeId = previous;
inside = false;
city.stage.setScene(city.stageScene);
showPlan();
renderChrome();
}
// ---- Hosted realtime presence --------------------------------------------
function realtimeInterest(): InterestCell {
if (inside) return { kind: "office", officeId };
if (cityId === "california") return { kind: "california-tile", x: 0, y: 0, level: 0 };
return { kind: "city", cityId: cityId === "socal" ? "socal" : "bay-area" };
}
function degrees(radians: number): number {
return ((radians * 180 / Math.PI + 540) % 360) - 180;
}
function realtimePose(): EntityPoseSnapshot | null {
if (access.subject === null) return null;
const timestampMs = Date.now();
if (inside) {
const walker = office?.walker;
if (!walker) return null;
const state = walker.state();
const headingDeg = -degrees(Math.atan2(-state.facing.x, -state.facing.z));
return {
entity: "actor",
actorId: realtimeActorId,
kind: state.actor === "anonymous-dog" ? "dog" : "humanoid",
sequence: 0,
timestampMs,
pose: {
space: "local",
cell: { kind: "office", officeId },
xM: state.position.x,
yM: office?.plan.level(state.levelId)?.floorY ?? 0,
zM: state.position.z,
headingDeg,
pitchDeg: 0,
},
velocity: {
xMps: state.action.x * 1.6,
yMps: 0,
zMps: state.action.z * 1.6,
yawDegPerSec: 0,
},
};
}
const aircraft = city?.aircraftActive() ? city.aircraftState() : null;
if (aircraft) {
return createAircraftPoseSnapshot(
{ aircraftId: realtimeAircraftId, pilotActorId: realtimeActorId },
aircraft,
0,
timestampMs,
);
}
const vehicle = journey.vehicle !== null ? city?.vehicleState() : null;
if (vehicle) {
const heading = vehicle.headingDeg * Math.PI / 180;
return {
entity: "vehicle",
vehicleId: realtimeVehicleId,
kind: "model-x",
driverActorId: realtimeActorId,
sequence: 0,
timestampMs,
pose: {
space: "geographic",
lat: vehicle.lat,
lng: vehicle.lng,
altitudeM: 0,
headingDeg: vehicle.headingDeg,
pitchDeg: 0,
},
velocity: {
xMps: Math.sin(heading) * vehicle.speedMps,
yMps: 0,
zMps: Math.cos(heading) * vehicle.speedMps,
yawDegPerSec: 0,
},
steering: vehicle.steering,
wheelRadians: vehicle.wheelRadians,
};
}
const actor = city?.actorState();
if (!actor || !city) return null;
const headingDeg = -degrees(actor.yaw);
const velocity = {
xMps: -Math.sin(actor.yaw) * actor.speedMps,
yMps: actor.verticalSpeedMps,
// Geographic velocity is east/up/north; scene +Z is south.
zMps: Math.cos(actor.yaw) * actor.speedMps,
yawDegPerSec: 0,
};
const origin = cityId === "california" ? { lat: 35.5, lng: -119.5 } : city.world.city.center;
const [originX, originZ] = city.world.project(origin.lat, origin.lng);
const [lat, lng] = city.world.unproject(
originX + actor.x / city.world.metresPerUnit,
originZ + actor.z / city.world.metresPerUnit,
);
return {
entity: "actor",
actorId: realtimeActorId,
kind: actor.kind,
sequence: 0,
timestampMs,
pose: {
space: "geographic",
lat,
lng,
altitudeM: actor.y,
headingDeg,
pitchDeg: degrees(actor.pitch),
},
velocity,
};
}
function ownRealtimeEntity(snapshot: EntityPoseSnapshot): boolean {
if (snapshot.entity === "actor") return snapshot.actorId === realtimeClient?.state().actorId;
if (snapshot.entity === "vehicle") {
return snapshot.driverActorId === realtimeClient?.state().actorId || snapshot.vehicleId === realtimeVehicleId;
}
return snapshot.pilotActorId === realtimeClient?.state().actorId ||
snapshot.aircraftId === realtimeAircraftId;
}
function clearRealtimePeers(): void {
city?.clearRemoteEntities();
office?.clearRemoteEntities();
updatePresenceIndicator();
}
function applyRealtimeMessage(message: ServerRealtimeMessage): void {
if (message.type === "membership-revoked") {
stopWebcamFace(true);
clearRealtimePeers();
return;
}
const updates = message.type === "pose-delta"
? message.updates
: message.type === "join-grant"
? message.initial
: message.type === "resume-grant"
? message.snapshot
: [];
const target = inside ? office : city;
for (const snapshot of updates) {
if (!ownRealtimeEntity(snapshot)) target?.upsertRemoteSnapshot(snapshot);
}
if (message.type === "pose-delta") {
for (const id of message.removedEntityIds) {
if (
id !== `actor:${realtimeClient?.state().actorId}` &&
id !== `vehicle:${realtimeVehicleId}` &&
id !== `aircraft:${realtimeAircraftId}`
) {
target?.removeRemoteEntity(id);
}
}
}
updatePresenceIndicator();
}
function updatePresenceIndicator(state = realtimeClient?.state()): void {
if (!presenceIndicator || access.subject === null) return;
const connection: "connecting" | "live" | "reconnecting" | "offline" =
state?.status === "streaming" ? "live"
: state?.status === "reconnecting" ? "reconnecting"
: state?.status === "left" || state?.status === "disposed" ? "offline"
: "connecting";
const nearbyPeerCount = connection === "live"
? (inside ? office?.remoteEntityCount() : city?.remoteEntityCount()) ?? 0
: 0;
presenceIndicator.update({ signedIn: true, connection, nearbyPeerCount });
}
async function joinRealtimePresence(operation: number): Promise<void> {
const client = realtimeClient;
const pose = realtimePose();
if (!client || !pose) return;
try {
const grant = await client.join(realtimeInterest(), pose);
if (operation !== realtimeOperation) return;
applyRealtimeMessage(grant);
} catch {
if (operation === realtimeOperation) {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
}
}
function moveRealtimePresence(): void {
clearRealtimePeers();
const client = realtimeClient;
if (!client || access.subject === null) return;
const operation = ++realtimeOperation;
if (client.state().sessionId === null) {
void joinRealtimePresence(operation);
return;
}
updatePresenceIndicator({ ...client.state(), status: "reconnecting" });
void client.moveInterest(realtimeInterest())
.then((grant) => {
if (operation === realtimeOperation) applyRealtimeMessage(grant);
})
.catch(() => {
if (operation === realtimeOperation) {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
});
}
function publishRealtimePresence(now: number): void {
if (!realtimeClient || now - lastRealtimePublishAt < 100) return;
const pose = realtimePose();
if (!pose || realtimeClient.state().sessionId === null) return;
lastRealtimePublishAt = now;
try {
realtimeClient.publishPose(pose);
} catch {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
}
async function initializeRealtimePresence(): Promise<void> {
if (!realtimePageActive || access.subject === null || !presenceHost || realtimeClient) return;
const [{ createRealtimeClient }, { createPresenceIndicator }] = await Promise.all([
import("./realtime/client.ts"),
import("./realtime/presenceIndicator.ts"),
]);
// `pagehide` can overtake the lazy chunks. Never construct a fresh client
// after the page has already run its terminal cleanup.
if (!realtimePageActive || access.subject === null || realtimeClient) return;
presenceHost.hidden = false;
// The class that makes room for it is `chromeState`'s to add, from
// `presenceVisible` — one place decides what the top-right column contains.
presenceMounted = true;
presenceIndicator = createPresenceIndicator({
container: presenceHost,
onRetry: () => moveRealtimePresence(),
});
presenceIndicator.update({ signedIn: true, connection: "connecting", nearbyPeerCount: 0 });
realtimeClient = createRealtimeClient({
actorId: realtimeActorId,
authenticatedFetch: authFetch,
sendIntervalMs: 100,
});
stopRealtimeSubscription = realtimeClient.subscribe({
onMessage: applyRealtimeMessage,
onStateChange: updatePresenceIndicator,
onError: () => presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 }),
});
moveRealtimePresence();
}
window.addEventListener("pagehide", () => {
realtimePageActive = false;
stopWebcamFace(true);
disposeOfficeScreenUi();
realtimeOperation += 1;
stopRealtimeSubscription?.();
stopRealtimeSubscription = null;
clearRealtimePeers();
// Page teardown cannot await the leave round trip. Contain a rotated-token
// or offline failure so navigation never produces an unhandled rejection;
// the server's disconnect grace still removes the ephemeral session.
void realtimeClient?.dispose().catch(() => undefined);
realtimeClient = null;
presenceIndicator?.dispose();
presenceIndicator = null;
// The one thing on this page that owns GPU memory neither scene created. It
// is disposed by whoever owns the `Stage`, which is this file, and never by a
// scene — the two kinds of environment are shared between every city and every
// office that ever existed on this page.
environment.dispose();
});
function applyProfilePreview(profile: LocalProfile): void {
if (access.subject === null) return;
if (inside) {
const appearance = resolveHumanoidAppearance(profile.appearance);
office?.walker?.setAppearance({
kind: "humanoid",
skinTone: appearance.skinTone,
outfitColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
});
} else {
city?.setActorIdentity(signedInActorIdentity(profile));
}
}
function cameraSupported(): boolean {
return typeof navigator.mediaDevices?.getUserMedia === "function";
}
/** Attach the one ephemeral texture to whichever signed-in humanoid is current. */
function attachCurrentWebcamFace(): void {
const texture = webcamFaceTexture?.texture() ?? null;
if (!texture || access.subject === null) return;
city?.clearActorFaceTexture();
office?.walker?.clearFaceTexture();
if (inside) office?.walker?.attachFaceTexture(texture);
else city?.attachActorFaceTexture(texture);
}
/**
* Stop app-owned capture. The texture adapter deliberately never owns tracks,
* so this boundary must stop every one before dropping the stream reference.
*/
function stopWebcamFace(revoke = false): void {
city?.clearActorFaceTexture();
office?.walker?.clearFaceTexture();
webcamFaceTexture?.clear();
webcamCapture?.stop();
if (revoke) webcamFaceConsent.revoke();
else webcamFaceConsent.stop();
webcamFaceTexture?.sync();
webcamFacePanel?.update(cameraSupported() ? "off" : "unsupported");
}
async function startWebcamFace(): Promise<void> {
if (access.subject === null || !localProfile || !cameraSupported() || webcamCapture?.status() === "active" ||
webcamCapture?.status() === "requesting") return;
webcamFacePanel?.update("requesting");
webcamFaceConsent.requestStart();
webcamCapture ??= createWebcamCapture({
acquire: () => navigator.mediaDevices.getUserMedia({
audio: false,
video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 640 } },
}),
createVideo: () => {
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
video.autoplay = true;
video.hidden = true;
document.body.append(video);
return video;
},
});
let binding: Awaited<ReturnType<WebcamCaptureController["start"]>>;
try {
binding = await webcamCapture.start();
} catch (error) {
webcamFaceConsent.stop();
const denied = error instanceof DOMException && (error.name === "NotAllowedError" || error.name === "SecurityError");
webcamFacePanel?.update(
"error",
denied ? "Camera permission was denied. Nothing was captured." : "Camera could not start. Check the device and try again.",
);
return;
}
// Null means Stop/pagehide overtook an asynchronous permission or play step.
if (!binding) return;
if (access.subject === null) {
stopWebcamFace(true);
return;
}
try {
const consent = webcamFaceConsent.start(true);
if (!consent.accepted) throw new Error("camera consent was no longer active");
webcamFaceTexture ??= createWebcamFaceTexture({ consent: webcamFaceConsent });
webcamFaceTexture.bind(binding);
for (const track of binding.stream.getVideoTracks()) {
track.addEventListener("ended", () => {
if (webcamCapture?.binding()?.stream === binding.stream) stopWebcamFace();
}, { once: true });
}
attachCurrentWebcamFace();
webcamFacePanel?.update("active");
// Native permission UI can restore focus to the page after the promise
// resolves. Put it back inside the still-open modal on the next frame.
requestAnimationFrame(() => {
if (profileEditor?.state().open) webcamFacePanel?.focusStop();
});
} catch {
stopWebcamFace();
webcamFacePanel?.update("error", "Camera preview could not start. Nothing was retained.");
}
}
function ensureProfileEditor(): ProfileEditor | null {
if (profileEditor) return profileEditor;
if (!profileOverlay || !localProfile || access.subject === null) return null;
profileEditor = createProfileEditor({
container: profileOverlay,
profile: localProfile,
identityId: access.subject,
onPreview: applyProfilePreview,
onSave(profile) {
localProfile = profile;
saveLocalProfile(sessionStorage, LOCAL_PROFILE_KEY, profile);
dispatchJourney({
type: "sign-in-actor-swap",
actor: {
id: access.subject ?? "anonymous",
kind: "humanoid",
signedIn: true,
profile: { displayName: profile.displayName, color: humanoidAppearance()?.accentColor },
},
});
city?.setActorIdentity(signedInActorIdentity(profile));
applyProfilePreview(profile);
renderChrome();
profileOverlay.hidden = true;
},
onCancel() {
profileOverlay.hidden = true;
},
});
profileEditor.root.addEventListener("keydown", (event) => event.stopPropagation());
if (webcamFaceIndicator) {
const host = document.createElement("div");
profileEditor.root.insertBefore(host, profileEditor.root.querySelector("form")?.nextSibling ?? null);
webcamFacePanel = createWebcamFacePanel({
container: host,
indicatorContainer: webcamFaceIndicator,
supported: cameraSupported(),
onStart: startWebcamFace,
onStop: () => stopWebcamFace(),
});
if (webcamCapture?.status() === "active") webcamFacePanel.update("active");
profileEditor.registerFocusables(webcamFacePanel.focusables);
}
return profileEditor;
}
function openProfileEditor(): void {
if (!localProfile || !profileOverlay) return;
const editor = ensureProfileEditor();
if (!editor) return;
editor.update(localProfile);
profileOverlay.hidden = false;
editor.open();
}
function screenBinding(surface: MediaSurfaceDescriptor) {
return {
officeId: surface.officeId,
levelId: surface.levelId,
roomId: surface.roomId,
screenId: surface.screenId,
};
}
function remotePanelStatus(screenId: string, status: OfficeScreenRemoteStatus): void {
officeScreenPanel?.setRemoteStatus(screenId, status);
renderChrome();
}
function isRemoteAuthorizationError(error: unknown): boolean {
return error instanceof Error && /\((?:401|403)\)/.test(error.message);
}
function releaseRemoteViewer(
status: OfficeScreenRemoteStatus = "off",
action: "revoke" | "dispose" = "revoke",
): void {
const viewing = remoteViewedScreen;
const pendingScreenId = remoteViewerRequestScreenId;
remoteMediaOperation += 1;
remoteViewerRequestScreenId = null;
if (pendingScreenId && pendingScreenId !== viewing?.screenId) {
remotePanelStatus(pendingScreenId, status);
}
if (!viewing) return;
remoteViewedScreen = null;
office?.clearMediaSurface(viewing.screenId);
// `OfficeScreenPanel` owns defensive descriptor snapshots rather than a
// live view of OfficeScene. Refresh after clear just as the bind path does,
// otherwise the panel can keep saying "Media active" after remote teardown.
officeScreenPanel?.update(office?.listMediaSurfaces() ?? []);
viewing.texture.dispose();
viewing.video.pause();
viewing.video.srcObject = null;
remotePanelStatus(viewing.screenId, status);
const finish = action === "revoke" ? viewing.remote.revoke() : viewing.remote.dispose();
void finish.catch(() => undefined);
}
function syncRemoteViewerState(
viewing: NonNullable<typeof remoteViewedScreen>,
state: RemoteMediaState,
): void {
if (remoteViewedScreen !== viewing) return;
if (state.status === "reconnecting") remotePanelStatus(viewing.screenId, "reconnecting");
else if (state.status === "connecting") remotePanelStatus(viewing.screenId, "connecting");
else if (state.status === "live") remotePanelStatus(viewing.screenId, "live");
if (state.hasRemoteVideo && !viewing.bound) {
// Playback is a consequence of the user's screen-specific opt-in click;
// the transport itself deliberately never calls `play()`.
void viewing.video.play().then(() => {
if (remoteViewedScreen !== viewing || !office || !inside) return;
viewing.bound = office.bindMediaSurface(
viewing.screenId,
{ canView: true, optedIn: true },
viewing.texture,
);
officeScreenPanel?.update(office.listMediaSurfaces());
remotePanelStatus(viewing.screenId, viewing.bound ? "live" : "error");
}).catch(() => {
if (remoteViewedScreen === viewing) {
showDetail("The remote screen arrived, but this browser could not start video playback.");
releaseRemoteViewer("error", "dispose");
}
});
}
if (state.status === "stopped" || state.status === "revoked") {
showDetail(state.status === "revoked" ? "Remote screen access was revoked." : "The remote screen share stopped.");
releaseRemoteViewer("stopped", "dispose");
}
}
async function startRemoteViewer(surface: MediaSurfaceDescriptor): Promise<void> {
if (access.subject === null || !inside || !office || office.depth !== "full") return;
releaseRemoteViewer("off");
const operation = ++remoteMediaOperation;
remoteViewerRequestScreenId = surface.screenId;
remotePanelStatus(surface.screenId, "connecting");
showDetail(`Connecting to ${surface.screenId}`);
try {
const { createRemoteOfficeMedia } = await import("./media/remoteMedia.ts");
const optedIn = officeScreenPanel?.state().optedInScreenIds.includes(surface.screenId) ?? false;
if (operation !== remoteMediaOperation || access.subject === null || !inside || !office ||
office.depth !== "full" || officeId !== surface.officeId || !optedIn) return;
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
video.autoplay = false;
const texture = new VideoTexture(video);
texture.colorSpace = SRGBColorSpace;
texture.generateMipmaps = false;
let viewing: NonNullable<typeof remoteViewedScreen>;
const remote = createRemoteOfficeMedia({
role: "viewer",
binding: screenBinding(surface),
authenticatedFetch: authFetch,
onStateChange: (state) => { if (viewing) syncRemoteViewerState(viewing, state); },
onError: (error) => {
if (remoteViewedScreen !== viewing) return;
if (isRemoteAuthorizationError(error)) {
showDetail("Remote screen authorization ended.");
releaseRemoteViewer("unavailable", "dispose");
} else {
remotePanelStatus(surface.screenId, "reconnecting");
}
},
});
viewing = { screenId: surface.screenId, officeId: surface.officeId, video, texture, remote, bound: false };
remoteViewerRequestScreenId = null;
remoteViewedScreen = viewing;
await remote.startViewer({ viewerOptIn: true, video });
if (remoteViewedScreen === viewing) showDetail(`Waiting for ${surface.screenId} remote video…`);
} catch (error) {
if (operation !== remoteMediaOperation) return;
remoteViewerRequestScreenId = null;
releaseRemoteViewer("unavailable", "dispose");
remotePanelStatus(surface.screenId, "unavailable");
showDetail(isRemoteAuthorizationError(error)
? "Remote screen authorization ended. Sign in again to reconnect."
: "No authorized remote share is available for that screen.");
}
}
async function startRemotePresenter(
surface: MediaSurfaceDescriptor,
shared: NonNullable<typeof sharedScreen>,
): Promise<void> {
if (access.subject === null || sharedScreen !== shared || !inside || office?.depth !== "full") return;
remotePanelStatus(surface.screenId, "connecting");
try {
const { createRemoteOfficeMedia } = await import("./media/remoteMedia.ts");
if (access.subject === null || sharedScreen !== shared || !inside || office?.depth !== "full") return;
const remote = createRemoteOfficeMedia({
role: "presenter",
binding: screenBinding(surface),
authenticatedFetch: authFetch,
onStateChange(state) {
if (sharedScreen !== shared) return;
if (state.status === "live") remotePanelStatus(surface.screenId, "live");
else if (state.status === "reconnecting") remotePanelStatus(surface.screenId, "reconnecting");
else if (state.status === "connecting") remotePanelStatus(surface.screenId, "connecting");
else if (state.status === "stopped" || state.status === "revoked") {
showDetail("The hosted screen share ended.");
queueMicrotask(() => { if (sharedScreen === shared) stopLocalScreenShare(); });
}
},
onError: (error) => {
if (sharedScreen !== shared) return;
if (isRemoteAuthorizationError(error)) {
showDetail("Screen sharing stopped because authorization ended.");
queueMicrotask(() => { if (sharedScreen === shared) stopLocalScreenShare(); });
} else {
remotePanelStatus(surface.screenId, "reconnecting");
}
},
});
shared.remote = remote;
await remote.startPresenter({ consent: { authorized: true, optedIn: true }, stream: shared.stream, video: shared.video });
if (sharedScreen === shared) showDetail(`Sharing ${surface.screenId} locally and to authorized remote viewers.`);
} catch (error) {
if (sharedScreen !== shared) return;
if (isRemoteAuthorizationError(error)) {
showDetail("Screen sharing stopped because authorization ended. Sign in again to share.");
stopLocalScreenShare();
return;
}
void shared.remote?.dispose().catch(() => undefined);
shared.remote = null;
remotePanelStatus(surface.screenId, "unavailable");
showDetail(`Sharing locally to ${surface.screenId}; hosted sharing is unavailable.`);
}
}
function stopLocalScreenShare(): void {
const shared = sharedScreen;
if (!shared) return;
sharedScreen = null;
remoteMediaOperation += 1;
office?.clearMediaSurface(shared.screenId);
remotePanelStatus(shared.screenId, "stopped");
void shared.remote?.stop().catch(() => undefined);
for (const track of shared.stream.getTracks()) track.stop();
shared.texture.dispose();
shared.video.pause();
shared.video.srcObject = null;
officeScreenPanel?.update(office?.listMediaSurfaces() ?? []);
renderChrome();
}
function disposeOfficeScreenUi(): void {
stopLocalScreenShare();
releaseRemoteViewer("off", "dispose");
officeScreenPanel?.dispose();
officeScreenPanel = null;
if (screensOverlay) screensOverlay.hidden = true;
}
async function startLocalScreenShare(surface: MediaSurfaceDescriptor): Promise<void> {
if (!office || !inside || office.depth !== "full") return;
// One authored surface role at a time. A viewer texture must not survive
// behind a new presenter preview or be cleared later over the presenter.
releaseRemoteViewer("off");
if (!navigator.mediaDevices?.getDisplayMedia) {
showDetail("This browser does not provide tab or window sharing.");
return;
}
let pendingStream: MediaStream | null = null;
let pendingVideo: HTMLVideoElement | null = null;
let pendingTexture: VideoTexture | null = null;
try {
showDetail("Choose a tab or window. Nothing is captured until you approve the browser prompt.");
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: "browser",
width: { ideal: 1_280, max: 1_280 },
height: { ideal: 720, max: 720 },
frameRate: { ideal: 15, max: 15 },
},
audio: false,
// Chromium honours these as chooser preferences. Other browsers ignore
// unknown dictionary members and still require the same explicit prompt.
monitorTypeSurfaces: "exclude",
selfBrowserSurface: "exclude",
surfaceSwitching: "include",
} as DisplayMediaStreamOptions);
pendingStream = stream;
const video = document.createElement("video");
pendingVideo = video;
video.muted = true;
video.playsInline = true;
video.srcObject = stream;
await video.play();
const texture = new VideoTexture(video);
pendingTexture = texture;
texture.colorSpace = SRGBColorSpace;
texture.generateMipmaps = false;
stopLocalScreenShare();
if (!office?.bindMediaSurface(surface.screenId, { canView: true, optedIn: true }, texture)) {
for (const track of stream.getTracks()) track.stop();
texture.dispose();
video.pause();
video.srcObject = null;
showDetail("That screen is no longer available.");
return;
}
const shared = { screenId: surface.screenId, stream, video, texture, remote: null };
sharedScreen = shared;
pendingStream = null;
pendingVideo = null;
pendingTexture = null;
stream.getVideoTracks()[0]?.addEventListener("ended", () => stopLocalScreenShare(), { once: true });
officeScreenPanel?.update(office.listMediaSurfaces());
renderChrome();
showDetail(`Sharing locally to ${surface.screenId}. Use Office screens to stop.`);
// Anonymous/self-host-only behavior ends here exactly as before. The
// transport chunk is fetched only for a signed-in explicit share.
if (access.subject !== null) void startRemotePresenter(surface, shared);
} catch (error) {
for (const track of pendingStream?.getTracks() ?? []) track.stop();
pendingTexture?.dispose();
if (pendingVideo) {
pendingVideo.pause();
pendingVideo.srcObject = null;
}
if ((error as DOMException)?.name !== "NotAllowedError") {
showDetail("The screen preview could not start. Try again from Office screens.");
}
}
}
function ensureOfficeScreenPanel(): OfficeScreenPanel | null {
if (!screensOverlay || !inside || !office || office.depth !== "full") return null;
if (officeScreenPanel) {
officeScreenPanel.update(office.listMediaSurfaces());
return officeScreenPanel;
}
officeScreenPanel = createOfficeScreenPanel({
container: screensOverlay,
surfaces: office.listMediaSurfaces(),
onRequestShare: (surface) => { void startLocalScreenShare(surface); },
onSelect(surface) {
if ((remoteViewedScreen && remoteViewedScreen.screenId !== surface.screenId) ||
(remoteViewerRequestScreenId !== null && remoteViewerRequestScreenId !== surface.screenId)) {
releaseRemoteViewer("off");
}
},
onStopShare: (surface) => {
if (sharedScreen?.screenId === surface.screenId) stopLocalScreenShare();
if (remoteViewedScreen?.screenId === surface.screenId || remoteViewerRequestScreenId === surface.screenId) {
releaseRemoteViewer("stopped");
}
},
onViewerOptIn(surface, optedIn) {
if (!optedIn) {
if (remoteViewedScreen?.screenId === surface.screenId || remoteViewerRequestScreenId === surface.screenId) {
releaseRemoteViewer("off");
} else {
remotePanelStatus(surface.screenId, "off");
}
} else if (access.subject !== null) {
void startRemoteViewer(surface);
}
},
});
const syncOverlay = () => queueMicrotask(() => {
if (officeScreenPanel && !officeScreenPanel.state().open) screensOverlay.hidden = true;
});
officeScreenPanel.root.addEventListener("click", syncOverlay);
officeScreenPanel.root.addEventListener("keydown", (event) => {
event.stopPropagation();
syncOverlay();
});
return officeScreenPanel;
}
function openOfficeScreens(): void {
const panel = ensureOfficeScreenPanel();
if (!panel || !screensOverlay) return;
screensOverlay.hidden = false;
panel.open();
}
// The `Office screens →` button is bound by `mount`; this is the overlay's own
// backdrop, which is not chrome and stays here with the panel it dismisses.
screensOverlay?.addEventListener("click", (event) => {
if (event.target !== screensOverlay) return;
officeScreenPanel?.close();
screensOverlay.hidden = true;
});
// ---- Navigation -------------------------------------------------------------
/** The views on offer right now — city chapters, or office viewpoints inside. */
function currentViews(): View[] {
if (inside && office) return office.views;
return city?.chapters ?? [];
}
function flyToIndex(index: number) {
const view = currentViews()[index];
if (!view) return;
flyToChapter(view.id);
}
/**
* Go to a named view: a city chapter outside, a viewpoint inside.
*
* Split out of `flyToIndex` because the places list and the ladder keys address
* a rung by **identity**, and a position in a list is the one thing about a
* chapter that this round is deliberately not allowed to change — twenty-nine
* capture guards aim at `#chapters .chapter` by index and would silently shoot
* the wrong frame if it moved. Everything that used to be reached by index still
* is; nothing new is.
*/
function flyToChapter(viewId: string) {
const view = currentViews().find((candidate) => candidate.id === viewId);
if (!view) return;
if (inside && office) {
requestControlMode("office-overview");
cancelOfficeArrival();
office.flyTo(view.id);
renderChrome();
}
else {
/*
* A door is a door only while there is somewhere else to go.
*
* `los-angeles` and `san-francisco` carry real authored poses — 38 units at
* 26 of height, which is a 73 km stand-off over the city — and on the three
* -board product those poses are never used, because this line intercepts
* the click and switches boards instead. On the merged board both cities
* are *on this board*, so the interception is what stops you looking at
* them: it is the reason a visitor who clicks SF ends up somewhere the
* merged board is not.
*
* Left alone, the fall-through below flies the authored pose, and the
* offices stay reachable the way they already are on every other board —
* the amber pins, which `scene.ts` gates on the board's own bounds and the
* merged board's bounds contain both.
*/
const destination = cityId === "california" && !ONE_CALIFORNIA
? CALIFORNIA_DESTINATIONS.get(view.id)
: undefined;
if (destination) {
requestControlMode("overview");
officeId = destination.officeId;
journeyToCity(destination.cityId === "socal" ? "socal" : "bay-area");
switchCity(destination.cityId);
return;
}
if (view.id === "la-sf-us-101" || view.id === "la-sf-i-5") {
city?.flyTo(view.id);
requestControlMode("drive");
closePanelForPlay();
renderChrome();
return;
}
requestControlMode("overview");
city?.flyTo(view.id);
renderChrome();
}
}
/**
* Step out of the office to the city, by whichever route keeps the URL honest.
*
* Returns true when it has started a navigation and the caller should stop — the
* page is on its way out, and anything after this runs against a document that
* is about to be replaced.
*
* The `?view=office` case is the cheap half of the same fix. There is no other
* host to go to, so the scene swap stands, but the query that put us in the
* office is dropped from the address bar on the way out. `replaceState` rather
* than `pushState`: the office and the city are one page, and a back button that
* walks a history of scene swaps is a back button that does not leave.
*/
function leaveToCity(cityWanted?: string): boolean {
const url = cityDoorUrl(cityWanted);
if (url !== null) {
location.assign(url);
return true;
}
const here = new URL(location.href);
if (here.searchParams.get("view") === "office") {
here.searchParams.delete("view");
history.replaceState(null, "", `${here.pathname}${here.search}${here.hash}`);
}
return false;
}
function switchCity(id: string) {
requestControlMode(inside ? "office-overview" : "overview");
if (inside && leaveToCity(id)) return;
if (inside) leaveOffice();
if (id === wantedCity) return;
// Arriving at a detailed board should put its local front door under the
// existing Office button. The California overview keeps whichever building
// the traveller last visited; it is a scale, not a fourth office location.
if (id === "socal") officeId = "mateo-court";
else if (id === "sf" && officeId === "mateo-court") officeId = "lumbridge-hq";
if (id === "california") {
if (journey.vehicle) dispatchJourney({ type: "exit-vehicle" });
if (journey.location.scale === "office") dispatchJourney({ type: "leave-office" });
if (journey.location.scale !== "california") dispatchJourney({ type: "return-to-california" });
} else {
journeyToCity(id === "socal" ? "socal" : "bay-area");
}
wantedCity = id;
/**
* No boot card, and no `building()` wrapper.
*
* `building()` puts an opaque full-screen panel up and holds it until a frame
* of the new scene has been drawn — correct while the outgoing board was
* disposed at the top of `mountCity` and there was genuinely nothing to look
* at, and wrong now that there is. The board you are on stays on screen and
* stays draggable for the whole build; `mountCity` raises the switch pill and
* `presentBoard` runs the fog dip. This is the line the owner feels.
*/
void mountCity(id);
}
function stepCity(delta: number) {
const at = CITIES.findIndex((c) => c.id === wantedCity);
const next = CITIES[(at + delta + CITIES.length) % CITIES.length];
if (next) switchCity(next.id);
}
/**
* Step one rung of the ladder, in the direction the list reads.
*
* `[` and `]` used to step the *board*, three ways round a cycle — which stopped
* being a true sentence the moment boards stopped being modes. A rung is what
* the left column now offers, and a rung on another board is reached exactly as
* a click on it would be.
*/
function stepLadder(delta: number): void {
if (inside) return;
const rows = PLACES;
const at = rows.findIndex((rung) => rung.key === activePlace()?.key);
const next = rows[Math.min(rows.length - 1, Math.max(0, (at < 0 ? 0 : at) + delta))];
if (next && next.key !== rows[at]?.key) goToPlace(next);
}
/**
* The rung the camera is standing on, or `null` inside a building.
*
* Recomputed rather than stored: it is a function of one live number
* (`cameraStandoffMetres`) and one live id (`SceneHandle.current()`), and a
* cached copy of a derived value is how a highlight and a camera start
* disagreeing.
*/
function activePlace(): LadderRung | null {
if (inside || !city) return null;
return activeRung(LADDER, cityId, city.cameraStandoffMetres(), city.current());
}
/**
* Go to a rung, on this board or another one.
*
* The cross-board case is the whole reason the ladder exists, and it has to
* dispatch the journey event: `journey/state.ts` models a board change as an
* explicit event and gates `enter-office` on `OFFICES[officeId] === location.scale`.
* A board that changed without one leaves the door into Mateo Court rejecting
* with `wrong-city` while the Southland is visibly on screen, and nothing throws.
* `switchCity` already dispatches; this routes through it rather than around it.
*/
function goToPlace(rung: LadderRung): void {
if (rung.board !== cityId) {
pendingPlace = rung;
switchCity(rung.board);
return;
}
flyToChapter(rung.id);
}
/**
* The rung to fly to once the board it is on has arrived, or `null`.
*
* A board switch is asynchronous and a rung is a pose on the board that is
* coming, so the two cannot be issued in one statement. Cleared by
* `presentBoard` whichever way the switch went, so a cancelled build cannot
* leave a flight queued against a board nobody asked for.
*/
let pendingPlace: LadderRung | null = null;
/**
* Guards the door against the second click.
*
* Entering now begins with a network fetch for the Spaces chunk, so the window
* between the click and the room is wide enough to click in again — and two
* `enterOffice()` calls in that window build two office scenes, park the second
* on the stage and leak the first, textures and all. One flag, cleared in a
* `finally` so a failed fetch does not wedge the door shut.
*/
let entering = false;
async function toggleOffice() {
if (inside) {
if (leaveToCity()) return;
leaveOffice();
return;
}
if (entering) return;
// Only the first entry fetches or builds anything; after that the office is
// parked in memory next to the paused city and the swap is a pointer.
if (office) {
void enterOffice();
return;
}
entering = true;
try {
/*
* The boot card is the whole of the "something is happening" story now, and
* it is enough: `building()` puts it up before the fetch and takes it down
* only once a frame of the new scene is on the glass. The old arrangement
* also wrote "Opening the office…" onto the door itself, which meant this
* file reaching into a label `mount.ts` now owns to say something the card
* in front of it was already saying.
*/
await building("Fetching the office…", () => enterOffice());
} finally {
entering = false;
// Writes the real label whichever way it went — "← Back to the city" if we
// are in, the door again if the chunk fetch failed.
renderChrome();
}
}
/** Switch between the authored dollhouse camera and the local possessed actor. */
function toggleOfficeWalk(): boolean {
if (!inside) {
if (!city?.actorState()) return false;
const active = city.controlMode() !== "actor";
requestControlMode(active ? "actor" : "overview");
if (active) closePanelForPlay();
renderChrome();
return true;
}
const walker = office?.walker;
if (!walker) return false;
const active = !walker.active();
requestControlMode(active ? "office-walk" : "office-overview");
if (active) closePanelForPlay();
renderChrome();
return true;
}
function toggleAircraft(): boolean {
if (inside || cityId !== "california" || !city?.aircraftState()) return false;
const active = city.controlMode() !== "aircraft";
requestControlMode(active ? "aircraft" : "overview");
if (active) closePanelForPlay();
renderChrome();
return true;
}
/**
* Clicking a building on the city walks into it.
*
* On the canvas rather than on anything the engine owns, because the engine
* reports picks by *hover* — `onMarkerPick` fires as the pointer crosses a pin
* and again with `null` as it leaves — so there is no click event to hang this
* on down there. `hoveredMarker` is whatever that hover last resolved, which is
* exactly what a click on the same pixel means.
*
* Guarded on not already being inside: the city's canvas is the office's canvas
* too, they share one renderer, and a stray click on the floor of a room should
* not re-enter the building you are standing in.
*/
canvas.addEventListener("click", () => {
if (inside) return;
const marker = hoveredMarker;
if (!marker) return;
const id = officeIdOf(marker);
if (id === null || entering) return;
entering = true;
officeId = id;
void building(`Opening ${marker.label}`, () => enterOffice()).finally(() => {
entering = false;
renderChrome();
});
});
// ---- Panels, plan and overlays ----------------------------------------------
/**
* Get the left column out of the way when a body takes over.
*
* Only on a phone, where `#panel` is a bottom sheet covering a third of the
* screen: possessing an actor and then being unable to see it is not a trade
* anybody would make, and the sheet has a toggle to bring it back. Above that
* width the column is furniture beside the map rather than on top of it, and
* closing it would be taking away a thing that costs nothing.
*
* `panelChosen` is set for the same reason a keypress sets it: once anything has
* had an opinion about this panel, the viewport stops having one.
*
* The plan view is deliberately **not** closed here any more. It used to be, and
* the reason was that on a phone it was a bottom sheet in exactly the place the
* joystick now occupies; `ui/chromeState.ts` moved it to a glanceable corner and
* closing it is now taking the map away at the moment somebody is navigating.
*/
function closePanelForPlay(): void {
if (window.innerWidth > 600) return;
panelOpen = false;
panelChosen = true;
}
/**
* One body, two ways in, and the second one is the point.
*
* This lived inline in the `M` branch of the keydown handler and was reachable
* from nowhere else, which made the plan view unreachable on any touch device:
* a phone starts without `M` and had no button. `#plan-toggle` and the touch
* pad's Map button are that way in, and both arrive here through `mount`'s
* `onTogglePlan`, so the key and the buttons cannot disagree about the state.
*/
function togglePlan(): void {
setPlanOpen(!planOpen);
}
function setPlanOpen(open: boolean): void {
planOpen = open;
planChosen = true;
renderChrome();
}
function setPanelOpen(open: boolean): void {
panelOpen = open;
panelChosen = true;
renderChrome();
}
/**
* The viewport crossed a published breakpoint — a rotation, a window drag, a
* tablet turning over.
*
* Both seeds are re-run, and that "both" is the fix: `resize` used to re-seed
* the plan and, for no stated reason, not the panel, so rotating a tablet from
* portrait to landscape left the sheet closed over a layout wide enough that the
* toggle which would reopen it is not drawn. Either is skipped once its own
* control has been touched.
*/
function adoptLayout(_layout: ChromeLayout): void {
if (!panelChosen) panelOpen = seedPanelOpen(window.innerWidth);
if (!planChosen) planOpen = seedPlanOpen(window.innerWidth);
renderChrome();
}
function routeDriveIsActive(): boolean {
const state = !inside ? city?.vehicleState() : null;
return state !== null && state !== undefined && city?.controlMode() === "drive" &&
city.current() === state.routeId;
}
function toggleVehicleCamera(): boolean {
if (!routeDriveIsActive() || !city) return false;
city.setVehicleCamera(city.vehicleCamera() === "driver" ? "chase" : "driver");
return true;
}
function cameraForward(
camera: { position: { x: number; z: number } },
controls: { target: { x: number; z: number } },
) {
return { x: controls.target.x - camera.position.x, z: controls.target.z - camera.position.z };
}
function publishPlayActions(): boolean {
const input = playInput.snapshot();
const requests = playInput.consumeRequests();
if (routeDriveIsActive() && city) {
city.setVehicleActions({
...vehicleActionsFromPlay(input),
modeRequest: requests.has("assist") ? "assisted" : "none",
reset: requests.has("reset"),
});
if (requests.has("camera")) toggleVehicleCamera();
return true;
}
const walker = inside && controlModeState.mode === "office-walk" ? office?.walker : null;
if (walker?.active() && office) {
walker.setAction(cameraRelativePlanar(input, cameraForward(office.camera, office.controls)));
return true;
}
if (!inside && city?.controlMode() === "actor") {
const state = city.actorState();
if (!state) return false;
if (state.kind === "crow" && state.mode === "flight") {
city.setActorActions({
...crowActionsFromPlay(input), modeRequest: "none", kindRequest: "none", reset: false,
});
} else {
city.setActorActions({
...groundActorActionsFromPlay(
input,
state.yaw,
cameraForward(city.stageScene.camera, city.stageScene.controls),
),
pitch: 0, climb: 0, glide: false, modeRequest: "none", kindRequest: "none", reset: false,
});
}
return true;
}
if (!inside && city?.controlMode() === "aircraft") {
city.setAircraftActions({
...aircraftActionsFromPlay(input),
modeRequest: requests.has("assist") ? "assisted" : "none",
reset: requests.has("reset"),
});
return true;
}
return false;
}
window.addEventListener("keyup", (event) => {
const control = controlForKey(event.key);
if (!control) return;
playInput.setDigital("keyboard", control, false);
if (publishPlayActions()) event.preventDefault();
});
function releaseAllPlayInput(): void {
clearPublishedPlayInput();
publishPlayActions();
}
window.addEventListener("blur", releaseAllPlayInput);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "visible") releaseAllPlayInput();
});
let gamepadButtons: StandardPlayGamepadButtons = { assist: false, reset: false, camera: false };
function pollPlayGamepad() {
try {
const pad = navigator.getGamepads?.().find((candidate) => candidate !== null);
if (pad) {
const sample = sampleStandardPlayGamepad(pad, gamepadButtons);
gamepadButtons = sample.buttons;
playInput.clearSource("gamepad");
playInput.setAxes("gamepad", sample.axes);
for (const control of sample.digital) playInput.setDigital("gamepad", control, true);
for (const request of sample.requests) playInput.request(request);
publishPlayActions();
} else {
playInput.clearSource("gamepad");
gamepadButtons = { assist: false, reset: false, camera: 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(pollPlayGamepad);
}
requestAnimationFrame(pollPlayGamepad);
window.addEventListener("pointerdown", (event) => {
if (event.pointerType === "touch") document.body.classList.add("touch-capable");
}, { capture: true });
/**
* Keyboard access to everything the pointer can reach.
*
* Bound to `window` rather than to the canvas, because the canvas is only
* focusable by accident and a shortcut that stops working when you tab to the
* legend is worse than no shortcut. The guard is the usual one: a keystroke that
* lands in a text field or on the plan view's own arrow-key handler belongs to
* that control, not to this.
*
* Every binding below is resolved through `ui/shortcuts.ts`, which is also what
* renders the `?` sheet — so a key that works and a key that is documented are
* now the same list. They were not: the sheet had no row for `G` while `g` was
* bound to glide *and* inserted at runtime meaning godmode, and Space was
* documented as "Handbrake while driving" while being the generic primary in
* every mode.
*/
window.addEventListener("keydown", (event) => {
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
const target = event.target;
if (
target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement ||
(target instanceof HTMLElement && target.isContentEditable)
) {
return;
}
if (event.key === "Escape") {
// Through the applier rather than at `#shortcuts.hidden`, so the focus
// return and the `onShortcuts` callback stay correct whichever way the
// sheet was opened.
if (chrome?.shortcutsOpen() === true) chrome.closeShortcuts();
else if (inside) {
if (!leaveToCity()) leaveOffice();
}
else showDetail(null);
return;
}
if (event.key === "?") {
if (chrome?.shortcutsOpen() === true) chrome.closeShortcuts();
else chrome?.openShortcuts();
event.preventDefault();
return;
}
if (event.key >= "1" && event.key <= "9") {
/**
* A digit addresses whatever list is on screen.
*
* Inside a building that is still the viewpoint list. Outside it, the places
* list replaced the chapter list, so a digit that flew to the current
* board's nth chapter would be flying to a row nobody can see — and on the
* Bay Area board, 1-9 would silently mean nine different places from the
* nine printed in the column.
*/
if (inside) flyToIndex(Number(event.key) - 1);
else {
const rung = PLACES[Number(event.key) - 1];
if (rung) goToPlace(rung);
}
return;
}
if (event.key === "[") {
if (boardTabsVisible()) stepCity(-1);
else stepLadder(-1);
return;
}
if (event.key === "]") {
if (boardTabsVisible()) stepCity(1);
else stepLadder(1);
return;
}
const lower = event.key.toLowerCase();
const control = controlForKey(event.key);
if (control) {
playInput.setDigital("keyboard", control, true);
if (publishPlayActions()) {
event.preventDefault();
return;
}
}
/**
* The three one-shot requests — assist, reset, camera — from the same table
* the held controls come from.
*
* This was three hand-written `lower === "p"`-shaped branches, each carrying
* its own copy of "which modes is this meaningful in". The mode test is still
* here, because it is a fact about the simulation rather than about the
* keyboard: only a drive has a camera to swap and only a drive or a flight has
* an assist to resume, and a `P` that silently queued a request in overview
* would be a key that does nothing while claiming to do something.
*/
const edge = edgeForKey(event.key);
if (edge !== null) {
const driving = routeDriveIsActive();
const flying = city?.controlMode() === "aircraft";
const meaningful = edge === "camera" ? driving : driving || flying;
if (meaningful) {
playInput.request(edge);
publishPlayActions();
event.preventDefault();
return;
}
}
if (lower === "v" && toggleOfficeWalk()) {
event.preventDefault();
return;
}
if (lower === "m") {
togglePlan();
return;
}
if (lower === "o") void toggleOffice();
});
// ---- Time -------------------------------------------------------------------
/**
* One writer for one override.
*
* The `#hour` slider is gone: `capabilitiesFor` hands `timeControl` and `debug`
* to the same tier, so the only person who could see the scrubber was the person
* who can open the godmode panel — and it wrote an hour onto *today*, silently
* discarding whatever date the panel had set.
*
* The clock line stays visible to everyone; a map that will not say what time it
* is showing is worse than one you cannot scrub. This gate is belt and braces:
* "no control" and "no override" are two different facts, and the renderer
* depends on the second.
*/
function applyTimeControl() {
if (!access.can.timeControl) instantOverride = null;
}
// ---- Instruments ------------------------------------------------------------
/**
* The godmode panel and the pose editor, for the one visitor in a deployment
* who has them.
*
* **Constructed, not hidden.** Everything in this section is behind
* `access.can.debug`, and for anyone else the result is no element, no
* `<style>`, no key binding and no bytes: `src/tools/` is reached only through
* the `await import()` below, so a non-god browser never fetches the chunk. A
* hidden instrument is still an instrument you shipped to a stranger.
*/
/** Cached across boards: the module is fetched once, the tool is rebuilt per city. */
let poseEditorFactory: typeof import("./tools/poseEditor.ts").createPoseEditor | null = null;
let poseDockBody: HTMLElement | null = null;
let poseDockOpen = true;
/**
* Where the panel thinks it is standing.
*
* `null` while there is no board. The stage travels with the place even though
* there is only ever one, because what the panel is told is *which board these
* counters are about* — handing it the stage separately would let it print
* SoCal's draw calls under the Bay Area's name for the length of a switch.
*
* `marineStrength` is deliberately not wired: it is private to `atmosphere.ts`,
* and the panel prints "no hook wired" rather than a guess.
*/
function godmodePlace(): GodmodePlace | null {
if (!city) return null;
const entry = CITIES.find((c) => c.id === cityId);
const active = entry?.city ?? SAN_FRANCISCO;
const world = city.world;
const label = entry?.label ?? cityId;
return {
// The office is a different board for accounting: it is the ledger key, and
// its draw calls are not the city's.
label: inside ? `${label} · office` : label,
lat: active.center.lat,
lng: active.center.lng,
stage: city.stage,
metresPerUnit: world.metresPerUnit,
unproject: (x, z) => world.unproject(x, z),
};
}
/** `setPlace` when there is a place. Called on every board and room change. */
function refreshGodmodePlace() {
const place = godmodePlace();
if (place) godmode?.setPlace(place);
}
async function mountGodmode() {
if (!access.can.debug || godmode) return;
const place = godmodePlace();
if (!place) return;
const [tools, poses] = await Promise.all([
import("./tools/index.ts"),
import("./tools/poseEditor.ts"),
]);
poseEditorFactory = poses.createPoseEditor;
godmode = tools.createGodmode({
container: document.body,
initial: { instant: currentInstant(), place },
onTimeChange(instant) {
instantOverride = instant;
updateSun();
},
onWeatherOverride(w) {
weatherOverride = w;
updateSun();
// The corner label has to stop claiming live weather the moment the sky
// on screen is one somebody typed.
renderChrome();
},
/**
* Both dials read through the module-level handles rather than closing over
* a board, because the panel outlives the city: it is mounted once and every
* later `mountCity` swaps `city` and `trafficDial` underneath it. A closure
* over the board that was current when the panel opened would go on driving
* a disposed scene after the first city switch.
*/
/**
* Read through the module-level `office`, never closed over — the panel is
* mounted once and every `switchOffice` replaces the scene underneath it.
*/
office: {
onHouseLights(mode) {
houseLights = mode;
// `officeLighting` is the only caller of `setSolarElevation` and
// `updateSun` is the only caller of that, so this is the whole apply.
updateSun();
},
onRobotsVisible(visible) {
office?.setRobotsVisible(visible);
},
onCeilingsVisible(visible) {
office?.setCeilingsVisible(visible);
},
read() {
// Only while a room is actually on the stage. Out in the city the office
// is paused and kept, and a dimmer aimed at a scene nobody is rendering
// is exactly the dead panel this section is arranged to avoid.
if (!inside || !office) return null;
return {
id: officeId,
depth: office.depth,
houseLevel: office.houseLevel(),
robots: office.robots().length,
};
},
},
sky: {
onExtraTraffic(count) {
trafficDial?.setExtra(count);
},
onSatellitesVisible(visible) {
satellitesVisible = visible;
city?.setSatellitesVisible(visible);
},
read() {
const counts = city?.satelliteCounts();
return {
extraTraffic: trafficDial?.extra() ?? 0,
trafficIsLive: cityFlights?.live() ?? false,
// `total: 0` is a board with no catalogue, which is not the same as a
// catalogue with nothing above the horizon — the panel says so.
satellites: counts && counts.total > 0 ? counts : null,
};
},
},
});
// The city was built before this chunk arrived, so its pose editor is built
// here; every later board gets one from `mountCity`.
if (city) mountPoseEditor(city);
// The godmode row in the `?` sheet is no longer inserted from here: it is a
// `KEYMAP` entry scoped to the god tier and rendered by `renderShortcutSheet`,
// which is the same table the key handler reads. The hand-written row said
// `G`, and `G` is glide.
renderChrome();
}
/**
* The pose editor, rebuilt with the board.
*
* It holds a `World`, a camera and a controls, all three of which die with the
* city — the same reason the plan view is rebuilt rather than re-pointed.
*/
function mountPoseEditor(handle: SceneHandle) {
if (!access.can.debug || !poseEditorFactory) return;
const dock = instrumentDock();
poseEditor = poseEditorFactory({
container: dock,
allowed: access.can.debug,
world: handle.world,
camera: handle.stageScene.camera,
controls: handle.stageScene.controls,
/**
* A cut rather than a flight, for now.
*
* The tool is forbidden from writing the camera itself and asks for
* `SceneKit.flyTo`, which `SceneHandle` does not expose — it offers
* `flyTo(chapterId)` and nothing that takes a pose. Moving both ends of the
* orbit at once is exactly what the plan view's `onSeek` does a few hundred
* lines up and it lands on the right pose; what it does not do is ease, so
* re-flying a captured chapter jumps. `scene.ts` is another agent's file
* this week; the two-line addition that upgrades this is in the handoff.
*/
flyTo: (pose: Pose) => {
handle.stageScene.camera.position.copy(pose.position);
handle.stageScene.controls.target.copy(pose.target);
handle.stageScene.controls.update();
},
existingChapters: handle.chapters,
});
poseEditor.setVisible(poseDockOpen);
}
/**
* The dock, built once and reused across boards.
*
* Bottom right, above the key-hint rail and below the plan view, which is the
* one column of this layout with room in it. Its geometry used to be set here as
* element styles rather than as a rule, because every visitor downloads
* `index.html` and a `#pose-dock { … }` sitting in it that can never match is
* the one trace this arrangement would otherwise leave behind. That reason still
* holds and the styles have moved anyway: `tools/godmode.ts` injects its own
* stylesheet, that stylesheet is fetched only behind `access.can.debug`, and a
* rule in it can carry the `env(safe-area-inset-bottom)` and the phone
* breakpoint an inline style could not.
*/
function instrumentDock(): HTMLElement {
if (poseDockBody) return poseDockBody;
const dock = document.createElement("aside");
dock.id = "pose-dock";
dock.setAttribute("aria-label", "Pose editor");
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "help";
toggle.setAttribute("aria-expanded", "true");
const body = document.createElement("div");
function applyDock() {
toggle.textContent = poseDockOpen ? "poses ▾" : "poses ▴";
toggle.setAttribute("aria-expanded", String(poseDockOpen));
body.hidden = !poseDockOpen;
// Collapsed is genuinely idle: `setVisible(false)` is what stops the tool
// reading the camera on every frame of a map nobody is authoring against.
poseEditor?.setVisible(poseDockOpen);
}
toggle.addEventListener("click", () => {
poseDockOpen = !poseDockOpen;
applyDock();
});
dock.append(toggle, body);
document.body.append(dock);
poseDockBody = body;
applyDock();
return body;
}
// ---- The boot card ----------------------------------------------------------
const bootCard = document.querySelector<HTMLElement>("#boot");
const bootStep = document.querySelector<HTMLElement>("#boot-step");
const switchPill = document.querySelector<HTMLElement>("#switching");
const switchStep = document.querySelector<HTMLElement>("#switching-step");
/**
* What a board switch says now that it has stopped covering the screen.
*
* The boot card is the *first* board of a session and nothing else. Every switch
* after it builds behind a board that is still on screen and still draggable, so
* the only honest thing left to say is that something is happening — and a pill
* in the bottom corner says it without taking the picture away. It never takes a
* pointer event, which is the point: the board underneath stays usable.
*
* `fraction` is the heightfield's, straight off the Worker, and `null` before it
* has reported anything.
*/
function showSwitchProgress(label: string, fraction: number | null): void {
if (switchStep === null || switchPill === null) return;
// Never over the boot card. On the first mount of the session the card is the
// whole story and a second progress affordance under it is noise.
if (bootCard?.hidden === false) return;
const text =
fraction === null
? `Building ${label}`
: `Building ${label}… terrain ${Math.round(fraction * 100)}%`;
if (switchStep.textContent !== text) switchStep.textContent = text;
if (switchPill.hidden) switchPill.hidden = false;
}
function hideSwitchProgress(): void {
if (switchPill !== null && !switchPill.hidden) switchPill.hidden = true;
}
/**
* Wait until the browser has actually put pixels on the glass.
*
* Writing to `textContent` and then immediately building a heightfield paints
* nothing: the style change and the two seconds of synchronous work are in the
* same task, so the frame the user sees is the one *after* the work. Two
* `requestAnimationFrame`s straddle a paint, which is the whole trick — a
* single one still runs before it.
*/
function painted(): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
}
/**
* Wait until the renderer has actually drawn the thing that was being built.
*
* `painted()` is not enough: the heightfield builds in a Worker, so
* `createScene` resolves while the main thread is idle and two animation frames
* go by in 33 ms — long before the first WebGL draw. The card faded on an empty
* canvas. `renderer.info.render.frame` is the renderer counting its own draws
* and is the one witness a fast frame cannot fool.
*
* The timeout bounds a promise that would otherwise never settle: an abandoned
* build never draws, and the boot card must not outlive it.
*/
function drawn(timeoutMs = 4000): Promise<void> {
const from = stage.renderer.info.render.frame;
const deadline = performance.now() + timeoutMs;
return new Promise((resolve) => {
requestAnimationFrame(function wait() {
if (stage.renderer.info.render.frame > from || performance.now() > deadline) {
void painted().then(resolve);
return;
}
requestAnimationFrame(wait);
});
});
}
/**
* Which `building()` call the boot card belongs to.
*
* A generation counter rather than a boolean because two builds can overlap:
* clicking SoCal while the Bay Area is still building leaves the first
* `building()` running, and it must not fade a card the second one is using.
* Last one in owns it.
*/
let bootGeneration = 0;
/**
* Run something slow with the boot card up and a sentence saying what it is.
*
* Naming the work is most of the value: a blank page for two seconds reads as
* broken, and "Building the Bay Area…" for two seconds reads as busy. The
* percentage on top of it comes from `bootProgress`, which the heightfield
* drives directly from the Worker.
*
* `work` may be synchronous or not. It became "or not" when `createScene` did,
* and the flattening matters: without the `await` here the card faded about
* 800 ms in, while the city was still building, because a promise is a truthy
* value that returns instantly.
*/
async function building<T>(label: string, work: () => T | Promise<T>): Promise<T> {
const generation = ++bootGeneration;
if (bootStep) bootStep.textContent = label;
if (bootCard) {
bootCard.hidden = false;
bootCard.classList.remove("done");
}
await painted();
const result = await work();
await drawn();
// Superseded while we were building: the card belongs to a later call now and
// fading it would uncover a city that does not exist yet.
if (generation !== bootGeneration) return result;
bootCard?.classList.add("done");
window.setTimeout(() => {
if (bootCard?.classList.contains("done")) bootCard.hidden = true;
}, 300);
return result;
}
/**
* The heightfield's progress, on the card that is already on screen.
*
* Nothing is written while the build is on the main thread, and that is not an
* oversight: `onMainThread` means the page is frozen for the duration, so every
* one of these writes would land in the same task and exactly one of them —
* the last — would ever be seen. The fallback build is the case where the
* static label is all the honesty available.
*
* The caller is responsible for not reporting an abandoned build; see the
* `onProgress` passed by `mountCity`.
*/
function bootProgress(label: string, fraction: number, onMainThread: boolean) {
if (onMainThread) return;
// Two destinations and exactly one of them is ever up: the card on the first
// mount of the session, the pill on every switch after it.
if (bootCard?.hidden === false) {
if (bootStep) bootStep.textContent = `Building ${label}… terrain ${Math.round(fraction * 100)}%`;
return;
}
showSwitchProgress(label, fraction);
}
// ---- Boot -----------------------------------------------------------------
/**
* Bind the interface to the page, once.
*
* Every handler is one line and every one of them ends in a function that
* already existed — that is the measure of whether the chrome extraction
* worked. `mount` owns the listeners, the joystick, the shortcuts dialog, the
* onboarding coach and the device panel from here on; this file owns what the
* buttons mean.
*/
function mountInterface(): ChromeHandle {
return mountChrome(document, {
onEnter: () => void toggleOffice(),
onWalk: () => { toggleOfficeWalk(); },
onFly: () => { toggleAircraft(); },
onScreens: () => openOfficeScreens(),
onDevices: () => openDevicePanel(),
onDeviceCommand: (command) => sendDeviceCommand(command),
onSelectBoard: (id) => {
if (inside) void switchOffice(id);
else switchCity(id);
},
onSelectView: (_id, index) => flyToIndex(index),
onSelectPlace: (key) => {
const rung = LADDER.find((candidate) => candidate.key === key);
if (rung) goToPlace(rung);
},
onMode: (mode) => {
requestControlMode(mode);
if (mode !== "overview" && mode !== "office-overview") closePanelForPlay();
renderChrome();
},
onTogglePanel: (open) => setPanelOpen(open),
onTogglePlan: (open) => setPlanOpen(open),
onDismissDetail: () => showDetail(null),
onCharacter: () => openProfileEditor(),
// A held touch button is one source per control rather than one per pointer:
// `mount` captures the pointer and guarantees the matching release, so the
// per-pointer bookkeeping this file used to do has an owner now.
onTouchHold: (control, pressed) => {
playInput.setDigital(`touch:${control}`, control, pressed);
publishPlayActions();
},
onTouchEdge: (edge) => {
playInput.request(edge);
publishPlayActions();
},
onStick: (axes) => {
if (axes === null) playInput.clearSource("stick");
else playInput.setAxes("stick", axes);
publishPlayActions();
},
onOnboardingFinished: () => {
// The coach has recorded itself as seen; this is the app agreeing, so the
// next `renderChrome` does not put it straight back up.
firstVisit = false;
renderChrome();
},
onLayoutChange: (layout) => adoptLayout(layout),
});
}
/**
* Access first, then data, then the board.
*
* The order is load-bearing in both directions and it used to be wrong. Markers
* were fetched before the tier was known, which is a request an anonymous
* visitor should not be making; and both decisions have to be settled before
* the *first* `mountCity`, because `markerPalette` is fixed at scene
* construction — the sample palette's keys are not the API's — and the flight
* source is chosen in the same call.
*
* Everything about the API remains optional. No server means the bundled sample
* set, the simulated traffic, the local hardware simulator, and a label at the
* bottom of the screen saying which of them you are looking at.
*/
async function boot() {
chrome = mountInterface();
renderChrome();
if (bootStep) bootStep.textContent = "Asking the deployment who you are…";
access = await resolveAccess();
if (access.subject !== null) {
const loadedProfile = loadLocalProfile(sessionStorage, LOCAL_PROFILE_KEY);
localProfile = loadedProfile.status === "loaded"
? loadedProfile.profile
: createDefaultLocalProfile(access.subject, access.subject);
if (loadedProfile.status !== "loaded" || loadedProfile.migrated) {
saveLocalProfile(sessionStorage, LOCAL_PROFILE_KEY, localProfile);
}
} else {
localProfile = null;
}
dispatchJourney({
type: "sign-in-actor-swap",
actor: access.subject === null
? {
id: "anonymous",
kind: "crow",
signedIn: false,
profile: { displayName: "Guest" },
}
: {
id: access.subject,
kind: "humanoid",
signedIn: true,
profile: {
displayName: localProfile?.displayName ?? access.subject,
color: humanoidAppearance()?.accentColor,
},
},
});
applyTimeControl();
renderChrome();
// Both gates, for the reason `mountCity` gives at length: the tier says
// whether this visitor may ask, `feeds` says whether there is anything to
// ask. A box with `markers: "none"` serves the public empty body to everyone,
// so the request buys a round trip and lands on the same sample set.
if (access.can.liveMarkers && access.feeds?.markers) {
try {
const feed = await tera.markers();
markers = feed.value;
// The caller's palette, plus the door colour it cannot know about.
palette = { ...feed.palette, ...OFFICE_PALETTE };
liveData = feed.live;
} catch {
// A missing API is the self-host default, not an error.
}
}
/**
* Which metro to build, from the URL if it names one this build has.
*
* The parameter exists to make leaving the office lossless — `office.` sends
* you to `tera.?city=socal` rather than dropping you in the Bay Area — but it
* is read unconditionally, because a deep link to a city is a reasonable thing
* to want on its own and a parameter that only works when it arrives from one
* particular page is a trap. An unknown id falls back rather than failing:
* `?city=paris` on a build with two cities in it should show a city, not a
* black screen.
*/
const wanted = new URLSearchParams(location.search).get("city");
const fallback = CITIES.find((c) => c.id === DEFAULT_CITY_ID) ?? CITIES[0];
const first = CITIES.find((c) => c.id === wanted) ?? fallback;
await building(`Building ${first?.label ?? "the city"}`, () =>
mountCity(first?.id ?? DEFAULT_CITY_ID),
);
// There must be a mounted scene before remote snapshots have anywhere to go.
// Anonymous and zero-server builds retain their existing local-only path.
// Hosted presence is an enhancement: a failed lazy chunk or unavailable API
// must not stop profile/tools initialization after the world is already live.
await initializeRealtimePresence().catch(() => undefined);
// The instruments, after the first board, because the panel reads a live
// stage and there is not one before this line.
await mountGodmode();
// The `office.` front door. The city is already standing behind this, so the
// back button is a scene swap and not a rebuild.
if (OPENS_IN_OFFICE) await building("Fetching the office…", () => enterOffice());
/**
* The wall clock, once a minute.
*
* Skipped entirely while an override is up — the whole point of an override
* is that the map has stopped following the clock — and the panel is told the
* new instant only when it is not the one choosing it, so its own readouts
* stay pinned to what is being rendered rather than fighting it.
*/
window.setInterval(() => {
if (instantOverride !== null) return;
updateSun();
godmode?.setInstant(new Date());
}, 60_000);
}
void boot();