/** * The contract between the engine and everything else. * * The engine renders a `City` and a list of `Marker`s. It does not know what a * marker *is* — not that markers are companies, not that a red one means a * rejection. That mapping lives in an adapter, outside this package, which is * what lets one renderer serve a private career map, a public sector map, and * whatever anyone else builds, without any of them being a fork. * * See ARCHITECTURE.md §3.3. */ /** `[latitude, longitude]`, always in that order. */ export type LatLng = [number, number]; // ---- Geography ------------------------------------------------------------ /** * A hill, as a radial peak summed into the heightfield. * * `elevation` is metres above sea level at the summit. `radius` is roughly * where the hill meets the flats, in degrees of latitude. */ export interface Hill { name: string; lat: number; lng: number; elevation: number; radius: number; } /** * Where buildings go, how tall, and on what street grid. * * `gridAngle` is the district's street bearing in radians. It is per-district * rather than per-city because that is the fact on the ground in San Francisco: * the grid north of Market and the grid south of it are 46° out of true, and * reproducing that is most of what makes the city recognisable from above. */ export interface District { id: string; name: string; polygon: LatLng[]; /** Street bearing, radians clockwise from true north. */ gridAngle: number; minHeight: number; maxHeight: number; /** Chance a given lot gets a tower rather than a low-rise. */ towerChance: number; /** Facade palette key; see `blocks.ts`. */ palette: "downtown" | "residential" | "industrial"; /** Fraction of lots that get built on at all. Defaults to 0.88. */ coverage?: number; } /** A building placed by hand because the eye goes looking for it. */ export interface Landmark { name: string; lat: number; lng: number; /** Roof height in metres. */ height: number; /** Half-width in degrees of longitude. */ footprint: number; shape: "box" | "pyramid" | "tower" | "cylinder"; color?: number; label?: boolean; } export interface Bridge { name: string; /** Deck centreline. Both ends should run onto land. */ path: LatLng[]; towers: LatLng[]; towerHeight: number; deckHeight: number; /** Suspension sag as a fraction of tower height. */ sag: number; color: number; } export interface Road { path: LatLng[]; width: number; kind: "street" | "freeway"; } /** * A named destination, as the interface knows it: what the legend prints and * what `flyTo` is keyed on. * * Split out of `Chapter` because an office has exactly the same idea — a short * list of places you can jump to — but positions them in metres, not in * latitude and longitude. Only this half of a chapter is shared; the pose is * not. `number` and `description` are optional here and required on `Chapter`, * because a city's chapters are a numbered tour with a sentence each and an * office's views are usually just "Reception" and "The desk bay". */ export interface View { id: string; label: string; shortLabel: string; number?: string; description?: string; } /** A camera destination, and a sentence about why it is on the map. */ export interface Chapter extends View { number: string; description: string; focus: { lat: number; lng: number; distance: number; height: number; rotation: number; }; } /** * A rectangle rendered at fine terrain resolution. * * SF declares one covering the whole city and behaves as if this did not exist. * LA needs six — DTLA, Santa Monica, Culver, Irvine, Pasadena, downtown * Riverside — with the basin between them coarse, because LA/OC/Riverside is * roughly fourteen times SF's area and a uniform 45 m lattice over it would be * 4.6M points. See ARCHITECTURE.md §5. */ export interface FocusRegion { minLat: number; maxLat: number; minLng: number; maxLng: number; } /** * Everything the engine needs to draw a place. Pure data — a city pack must * contain no code, so that adding one is a contribution anybody can review. */ export interface City { id: string; name: string; /** Map centre, and the origin of scene space. */ center: { lat: number; lng: number }; /** Scene bounds. Everything outside this is open water or off-frame. */ bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }; /** * Degrees to scene units, for latitude. Longitude is derived as * `latScale * cos(center.lat)` so the place keeps its true proportions. */ latScale: number; /** * How much taller than life the vertical is. Terrain and buildings share it, * so they stay honest relative to each other. */ verticalExaggeration: number; /** Ground-cell size inside a focus region, in degrees. */ cellLat: number; cellLng: number; /** Multiplier applied to cell size outside every focus region. 1 = uniform. */ coarseFactor?: number; focusRegions?: FocusRegion[]; /** Distance from open water, in degrees, over which relief ramps to zero. */ coastFalloff: number; landmasses: LatLng[][]; parks: LatLng[][]; inlandWater: LatLng[][]; hills: Hill[]; districts: District[]; landmarks: Landmark[]; bridges: Bridge[]; roads: Road[]; chapters: Chapter[]; /** Palette overrides; every field is optional. */ palette?: Partial; } export interface ScenePalette { skyTop: number; skyHorizon: number; sea: number; lake: number; shore: number; sand: number; flats: number; upland: number; park: number; parkHigh: number; /** * Bare high ground, and **optional on purpose**. * * The unpainted ramp is `flats → upland` over the first 150 m and then flat * forever, which is right for a city — a hill in San Francisco is built to its * summit and the buildings do the talking. On a board measured in hundreds of * kilometres it is not: the Sierra crest at 4,000 m and the Mojave floor at * 600 come out the same number, so a granite skyline and a creosote flat are * one colour and the only thing separating them is the shading. * * A pack that declares this gets a second stop above `upland` — see * `groundColor` in `terrain.ts` for the two elevations it ramps between. A * pack that does not is rendered exactly as before, which is why this is * optional rather than a tenth required colour that every existing pack would * have to answer for. */ alpine?: number; } // ---- Lighting ------------------------------------------------------------- /** * Everything the light rig needs, as plain numbers. * * This is a *state*, not an observation: `Environment` — `{ time, sun, weather }` * — is what the world is doing, and a `LightingState` is what that means for the * rig. `Atmosphere` owns the conversion and is the only thing allowed to make * one; a scene applies it and never writes back. Two modules both constructing * and mutating the same three lights is the failure this shape exists to * prevent. See CONTRACT.md §4. * * Colours are `0xrrggbb`, matching `ScenePalette` and three.js. */ export interface LightingState { sun: { /** * Unit vector from the scene toward the sun. Distance is deliberately * absent: how far away to place the light is a fact about the scale of the * scene, and the sun does not know whether it is shining on 94 m per unit * or on 1 m per unit. */ direction: [number, number, number]; color: number; intensity: number; }; hemisphere: { sky: number; ground: number; intensity: number }; ambient: { color: number; intensity: number }; /** * Background gradient, or `null` to leave the background alone — which is * what an interior wants, since it has walls and no horizon. */ sky: { top: number; horizon: number } | null; /** `null` for no fog at all. An office gets none. */ fog: { color: number; near: number; far: number } | null; } // ---- Markers -------------------------------------------------------------- /** * A thing worth pointing at, minus where it is. * * `colorKey` is deliberately opaque to the engine — it indexes into a palette * the caller supplies. The engine will not learn what "rejected" means. * * This is the half that survives a change of coordinate system: a pin on a city * at 37.79 N, -122.40 E and a pin on a desk 4.2 m along the east wall are the * same kind of thing to everything downstream of the geometry, so an office can * carry its own positions and still hand a `Pin` to the same detail card. */ export interface Pin { id: string; label: string; colorKey: string; /** Optional href for the detail card. */ url?: string; /** Optional one-liner for the detail card. */ blurb?: string; } /** * A small, map-scale building drawn in place of a pin. * * This is deliberately a glyph rather than an architectural model. A city is * normally viewed from kilometres away, so loading a façade kit with one mesh * per window buys triangles nobody can see and gives up the one-draw-call city * that `blocks.ts` works hard to preserve. The glyph keeps the useful grammar * — a ground floor, repeated bays, a roof line and a deterministic silhouette * — and expresses it in a handful of procedural meshes. * * Metres are used here because these values describe a real building even * though the city scene does not: `World` converts horizontal metres with * `metresPerUnit` and vertical metres with the city's exaggeration. */ export interface BuildingGlyph { kind: "building"; width: number; depth: number; height: number; /** Approximate occupied floors; used to choose the façade rhythm. */ storeys: number; /** Compass bearing of local −Z, degrees clockwise from true north. */ heading: number; /** The silhouette family, not a tenant or product category. */ profile: "tower" | "hangar" | "courtyard" | "block"; /** Stable variation for bay widths and lit panes. */ seed?: number; /** Neutral shell colour. The marker palette still supplies the door/accent. */ bodyColor?: number; } /** A `Pin` placed on a city, in degrees. */ export interface Marker extends Pin { lat: number; lng: number; /** * False when the position is a placeholder rather than a real address. * Rendered distinctly, because inventing a location on a map whose premise * is that it is real is worse than admitting the gap. */ located?: boolean; /** Optional map-scale representation. Omit it for the ordinary pin. */ glyph?: BuildingGlyph; } /** Caller-supplied `colorKey` -> colour. */ export type MarkerPalette = Record; // ---- Flights -------------------------------------------------------------- export interface Aircraft { id: string; lat: number; lng: number; /** Barometric altitude in metres. */ altitude: number; /** Degrees clockwise from true north. */ heading: number; callsign?: string; } /** * Where aircraft come from. * * An interface rather than a client, because this repo must not ship one for the * obvious source: FlightRadar24's terms do not permit scraping and do not permit * redistributing the data, so a client for it in an Apache-2.0 repo would be * publishing instructions for violating a ToS. This package ships a simulator * and open community sources; anything commercial is an adapter in a private * deployment. See ARCHITECTURE.md §4. */ export interface FlightSource { /** Current traffic. Called on a timer; must be cheap and must not throw. */ poll(): Promise | Aircraft[]; /** Seconds between polls. */ interval: number; dispose?(): void; } // ---- Satellites ----------------------------------------------------------- /** * Which constellation a satellite belongs to, as far as anyone looking up cares. * * A **display bucket and not a taxonomy**: no orbital regime, no operator, no * launch date. `engine/satellites.ts` picks a colour from it and nothing else * reads it, and a field that carried more would be a field that got wrong. * * `starlink` is broken out from `comms` because it is the reason the layer * exists — it is the constellation people can see with their eyes, in a train, * forty minutes after sunset. `other` is not a failure; most of the catalogue is * other. * * This lives here rather than in `server/wire.ts` for the same reason `Marker` * does: the renderer owns the vocabulary and the wire carries it, so a body off * the network is handed to the engine as-is with no adapter in between. */ export type SatelliteGroup = | "starlink" | "comms" | "navigation" | "station" | "weather" | "other";