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:
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* The standalone demo: San Francisco, simulated traffic, chapter legend.
|
||||
*
|
||||
* Deliberately ships **no company data**. Markers are demonstrated using the
|
||||
* city's own landmarks — buildings, not businesses — because company positions
|
||||
* are geocoded (ODbL) and company pipeline status is private, and neither
|
||||
* belongs in this repo. Real markers arrive at runtime from an adapter; see
|
||||
* `src/adapters/` and ARCHITECTURE.md §3.
|
||||
*/
|
||||
|
||||
import { createScene } from "./engine/scene.ts";
|
||||
import { SimulatedFlights, type SimRoute } from "./engine/flights.ts";
|
||||
import type { Marker, MarkerPalette } from "./engine/types.ts";
|
||||
import SAN_FRANCISCO from "./cities/sf.ts";
|
||||
|
||||
/**
|
||||
* Bay Area traffic, roughly where it actually is: SFO sits south of frame and
|
||||
* its arrivals run down the peninsula, Oakland is east across the bay, and the
|
||||
* coastal departures turn out over the Pacific.
|
||||
*/
|
||||
const ROUTES: SimRoute[] = [
|
||||
{ callsign: "UAL 1", from: [37.95, -122.36], to: [37.66, -122.4], fromAlt: 2400, toAlt: 500, duration: 190 },
|
||||
{ callsign: "ASA 22", from: [37.93, -122.31], to: [37.65, -122.38], fromAlt: 2100, toAlt: 450, duration: 210 },
|
||||
{ callsign: "SWA 118", from: [37.64, -122.39], to: [37.9, -122.62], fromAlt: 700, toAlt: 5200, duration: 165 },
|
||||
{ callsign: "DAL 407", from: [37.7, -122.21], to: [37.88, -122.55], fromAlt: 1800, toAlt: 6100, duration: 230 },
|
||||
{ callsign: "UAL 88", from: [37.62, -122.6], to: [37.95, -122.28], fromAlt: 6800, toAlt: 8200, duration: 260 },
|
||||
{ callsign: "N512SP", from: [37.83, -122.56], to: [37.7, -122.22], fromAlt: 1100, toAlt: 1300, duration: 300 },
|
||||
{ callsign: "JBU 915", from: [37.96, -122.48], to: [37.63, -122.36], fromAlt: 3100, toAlt: 600, duration: 205 },
|
||||
];
|
||||
|
||||
const MARKER_PALETTE: MarkerPalette = {
|
||||
landmark: 0xf2b134,
|
||||
neutral: 0x9aa4ad,
|
||||
};
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#scene");
|
||||
if (!canvas) throw new Error("#scene canvas missing");
|
||||
|
||||
const scene = createScene(canvas, {
|
||||
city: SAN_FRANCISCO,
|
||||
markerPalette: MARKER_PALETTE,
|
||||
flights: new SimulatedFlights(ROUTES),
|
||||
onMarkerPick: (marker) => {
|
||||
const card = document.querySelector<HTMLElement>("#detail");
|
||||
if (!card) return;
|
||||
if (!marker) {
|
||||
card.hidden = true;
|
||||
return;
|
||||
}
|
||||
card.hidden = false;
|
||||
card.textContent = marker.label;
|
||||
},
|
||||
});
|
||||
|
||||
// Demo markers: the city's own named buildings.
|
||||
const demoMarkers: Marker[] = SAN_FRANCISCO.landmarks
|
||||
.filter((l) => l.label)
|
||||
.map((l) => ({
|
||||
id: l.name,
|
||||
lat: l.lat,
|
||||
lng: l.lng,
|
||||
label: l.name,
|
||||
colorKey: "landmark",
|
||||
located: true,
|
||||
}));
|
||||
scene.setMarkers(demoMarkers);
|
||||
|
||||
// ---- Chapter legend -------------------------------------------------------
|
||||
|
||||
const nav = document.querySelector<HTMLElement>("#chapters");
|
||||
const blurb = document.querySelector<HTMLElement>("#blurb");
|
||||
|
||||
function renderLegend(activeId: string) {
|
||||
if (!nav) return;
|
||||
nav.replaceChildren();
|
||||
for (const chapter of scene.chapters) {
|
||||
const button = document.createElement("button");
|
||||
button.className = chapter.id === activeId ? "chapter active" : "chapter";
|
||||
button.innerHTML = `<span class="num">${chapter.number}</span><span>${chapter.shortLabel}</span>`;
|
||||
button.addEventListener("click", () => scene.flyTo(chapter.id));
|
||||
nav.append(button);
|
||||
}
|
||||
const active = scene.chapters.find((c) => c.id === activeId);
|
||||
if (blurb && active) blurb.textContent = active.description;
|
||||
}
|
||||
|
||||
renderLegend(scene.current());
|
||||
scene.onChapterChange(renderLegend);
|
||||
Reference in New Issue
Block a user