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
+141
View File
@@ -0,0 +1,141 @@
/**
* Bridges and roads — the lines that tie the landmasses together and give the
* grid something to hang off.
*
* Roads follow the terrain: each path is resampled far more finely than it is
* written in the city pack, and every sample takes its height from the ground,
* so a street climbs out of the flats instead of burrowing through the hill.
*/
import * as THREE from "three";
import type { Bridge, LatLng } from "./types.ts";
import type { World } from "./world.ts";
/** Resample a lat/lng path into scene-space points that ride the ground. */
function drapePath(world: World, path: LatLng[], samplesPerLeg = 14, lift = 0.05): THREE.Vector3[] {
const out: THREE.Vector3[] = [];
for (let i = 0; i < path.length - 1; i++) {
const from = path[i];
const to = path[i + 1];
if (!from || !to) continue;
const [lat0, lng0] = from;
const [lat1, lng1] = to;
const steps = i === path.length - 2 ? samplesPerLeg : samplesPerLeg - 1;
for (let s = 0; s <= steps; s++) {
const t = s / samplesPerLeg;
const lat = lat0 + (lat1 - lat0) * t;
const lng = lng0 + (lng1 - lng0) * t;
const [x, z] = world.project(lat, lng);
out.push(new THREE.Vector3(x, world.groundAt(lat, lng) + lift, z));
}
}
return out;
}
function ribbon(points: THREE.Vector3[], width: number, color: number): THREE.Mesh {
const curve = new THREE.CatmullRomCurve3(points);
const geo = new THREE.TubeGeometry(curve, points.length * 2, width / 2, 4, false);
const mesh = new THREE.Mesh(geo, new THREE.MeshLambertMaterial({ color }));
mesh.receiveShadow = true;
return mesh;
}
export function createRoads(world: World): THREE.Group {
const group = new THREE.Group();
group.name = "roads";
for (const road of world.city.roads) {
const color = road.kind === "freeway" ? 0x7d7166 : 0x8b8578;
group.add(ribbon(drapePath(world, road.path), road.width, color));
}
return group;
}
/**
* A suspension bridge: deck, towers, and a main cable sagging between them.
*
* The cable is the detail worth the code. Two orange towers with a straight
* line between them read as a trestle; the catenary is what makes the shape at
* the mouth of the bay unmistakably the Golden Gate.
*/
export function createBridge(world: World, bridge: Bridge): THREE.Group {
const group = new THREE.Group();
group.name = bridge.name;
const deckY = world.metres(bridge.deckHeight);
const towerY = world.metres(bridge.towerHeight);
const material = () => new THREE.MeshLambertMaterial({ color: bridge.color });
const deckPoints = bridge.path.map(([lat, lng]) => {
const [x, z] = world.project(lat, lng);
return new THREE.Vector3(x, deckY, z);
});
const deck = ribbon(deckPoints, 0.5, bridge.color);
deck.castShadow = true;
group.add(deck);
const towerTops: THREE.Vector3[] = [];
for (const [lat, lng] of bridge.towers) {
const [x, z] = world.project(lat, lng);
const geo = new THREE.BoxGeometry(0.34, towerY, 0.34);
geo.translate(0, towerY / 2, 0);
const tower = new THREE.Mesh(geo, material());
tower.position.set(x, 0, z);
tower.castShadow = true;
group.add(tower);
// Cross-braces, which is most of what you see of a tower at distance.
for (const frac of [0.55, 0.82]) {
const brace = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.16, 0.4), material());
brace.position.set(x, towerY * frac, z);
group.add(brace);
}
towerTops.push(new THREE.Vector3(x, towerY, z));
}
const anchors = [deckPoints[0], ...towerTops, deckPoints[deckPoints.length - 1]];
for (let i = 0; i < anchors.length - 1; i++) {
const a = anchors[i];
const b = anchors[i + 1];
if (!a || !b) continue;
const isMainSpan = i > 0 && i < anchors.length - 2;
const sag = bridge.sag * towerY * (isMainSpan ? 1 : 0.42);
const pts: THREE.Vector3[] = [];
for (let s = 0; s <= 18; s++) {
const t = s / 18;
const p = a.clone().lerp(b, t);
p.y -= Math.sin(t * Math.PI) * sag;
pts.push(p);
}
group.add(
new THREE.Mesh(
new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 24, 0.055, 5, false),
material(),
),
);
// Vertical hangers down to the deck.
for (let s = 2; s < 18; s += 2) {
const t = s / 18;
const p = a.clone().lerp(b, t);
const top = p.y - Math.sin(t * Math.PI) * sag;
if (top <= deckY + 0.2) continue;
const h = top - deckY;
const geo = new THREE.BoxGeometry(0.035, h, 0.035);
geo.translate(0, h / 2, 0);
const hanger = new THREE.Mesh(geo, material());
hanger.position.set(p.x, deckY, p.z);
group.add(hanger);
}
}
return group;
}
export function createBridges(world: World): THREE.Group {
const group = new THREE.Group();
group.name = "bridges";
for (const b of world.city.bridges) group.add(createBridge(world, b));
return group;
}