/** * 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 { detailLotMetres } from "./engine/blocks.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 { InstancedMesh, 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: the * pack still opens its chapter list on `california-overview` so capture and * `?city=california` frame the state, and a bare URL passes `openingChapter: * "fidi"` so a visitor's first frame is a city they can read. */ 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([ ["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 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 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("#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 = createBoardCache({ 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 | 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 | 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) { 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 = 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): 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("#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 { 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(`#${sectionId}`); if (section === null) return; let any = false; for (const [id, text] of notes) { if (text !== "") any = true; const line = document.querySelector(`#${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 { 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 { 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, // Bare URL on one California: land in the city, not on 1,551 km of state. // `?city=california` (and every capture shot) still opens on chapters[0]. ...(ONE_CALIFORNIA && CITY_PARAM === null ? { openingChapter: "fidi" } : {}), // 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 { 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 { // 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((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("#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 = record.handle.groundWorldY(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 * record.handle.reliefScale(); 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 }; }, /** * What the city layer is currently drawing: how many lots are packed, and * which rung of `DETAIL_LOT_TIERS` they are lotted at. * * The same argument as `seek`, one floor down. Lot size follows the camera * now, and "did the board coarsen when I pulled back" is not a question a * triangle total can answer — a total is the terrain and the water and the * roads as well, and at the poses that matter those are more than half of * it. This reads the answer off the mesh instead. It observes and changes * nothing. */ lots() { const found: InstancedMesh[] = []; record.handle.stageScene.scene.traverse((child) => { if (child instanceof InstancedMesh && child.name === "blocks") found.push(child); }); const mesh = found[0]; if (mesh === undefined) return null; const store = mesh.userData.detail as | { tier?: number; detailTier?: number | null } | undefined; /* * `detailTier` and not `tier`, because they answer different questions and * only one of them is the one being asked. `tier` is an index and is * always valid; `detailTier` is `null` at a pose where no metro detail is * drawn at all. Reporting `tier` printed "40 m" at the whole-board pose, * on a board drawing nothing but 806 m base lots — an instrument lying in * the direction of "the fine rung is cheap", which is precisely the * direction that would get somebody to spend a budget they do not have. */ const drawn = store?.detailTier ?? null; return { packed: mesh.count, capacity: (mesh.instanceMatrix.array.length / 16) | 0, tier: drawn, lotMetres: drawn === null ? null : detailLotMetres(drawn), }; }, 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 | 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("#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("#minimap .minimap-frame"); const minimapReadout = document.querySelector("#minimap-readout"); const presenceHost = document.querySelector("#presence-host"); const webcamFaceIndicator = document.querySelector("#webcam-face-indicator"); const profileOverlay = document.querySelector("#profile-overlay"); const screensOverlay = document.querySelector("#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): 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 { 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 { 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 { 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>; 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, 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 { 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; 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, ): Promise { 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 { 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 * `