Lumbridge Simulate Engine — the city, and the licence it can actually ship under
LSE is the third of the three, beside lumbridge-compute and lumbridge-bench: a 3D engine for walkable places. This first commit is the outside of the world — San Francisco — plus the seams the inside will attach to. The engine renders a City and a list of Markers and knows nothing else. It does not know markers are usually companies and it will never learn that "rejected" is red; that mapping lives in an adapter. Which is what lets one renderer serve a private map, a public one, and a self-hoster with no Lumbridge account, none of them a fork of the others. Three things were designed around the licence rather than discovered after it, because each one is a promise Apache 2.0 makes that is easy to break by accident. No trademarks in the repo — logos are fetched at runtime, and public/logos/ is gitignored. No OpenStreetMap-derived coordinates, which is why every coastline in cities/sf.ts was traced by hand: Nominatim output is ODbL, share-alike, and would attach to the whole pack. And no FlightRadar24 client — their terms forbid scraping and redistribution, so flights are an interface with a simulator and open community ADS-B behind it. The privacy constraint and the licence constraint turned out to want the same thing. Geocoded company positions and pipeline status both stay behind Workie's API; the open repo holds the city and the renderer. The tempting shortcut — commit an sf-companies.json — breaks both at once. Ported out of Workie, where a 3D city engine had no business living. Workie's /live is deleted rather than deprecated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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 camera destination, and a sentence about why it is on the map. */
|
||||
export interface Chapter {
|
||||
id: string;
|
||||
number: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
focus: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
distance: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
};
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ScenePalette>;
|
||||
}
|
||||
|
||||
export interface ScenePalette {
|
||||
skyTop: number;
|
||||
skyHorizon: number;
|
||||
sea: number;
|
||||
lake: number;
|
||||
shore: number;
|
||||
sand: number;
|
||||
flats: number;
|
||||
upland: number;
|
||||
park: number;
|
||||
parkHigh: number;
|
||||
}
|
||||
|
||||
// ---- Markers --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A thing on the map.
|
||||
*
|
||||
* `colorKey` is deliberately opaque to the engine — it indexes into a palette
|
||||
* the caller supplies. The engine will not learn what "rejected" means.
|
||||
*/
|
||||
export interface Marker {
|
||||
id: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
label: string;
|
||||
colorKey: string;
|
||||
/** Optional href for the detail card. */
|
||||
url?: string;
|
||||
/** Optional one-liner for the detail card. */
|
||||
blurb?: string;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/** Caller-supplied `colorKey` -> colour. */
|
||||
export type MarkerPalette = Record<string, number>;
|
||||
|
||||
// ---- 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 the obvious source — FlightRadar24
|
||||
* — cannot ship in an Apache-2.0 repo: their terms forbid scraping and forbid
|
||||
* redistributing the data. 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[]> | Aircraft[];
|
||||
/** Seconds between polls. */
|
||||
interval: number;
|
||||
dispose?(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user