1
0

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:
Karti Tripathi
2026-08-04 21:49:08 -07:00
commit 67f8df8d53
20 changed files with 4789 additions and 0 deletions
+239
View File
@@ -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;
}