d464459838
Ten agents wrote this in parallel against CONTRACT.md, which exists because the five design agents before them collided on fifteen blocking points — four files specified twice with incompatible contents, three separate backends for one box, and `Environment` exported twice meaning different things. What landed: a Stage owning only the renderer and the loop, with the city and an office as two scenes over it. They cannot share one — San Francisco is ~94 m per scene unit with 3.6x vertical exaggeration and an office is 1 unit = 1 m — and the city is paused rather than disposed on the way in, because rebuilding its 336,864-point heightfield costs about a second on the way back out. Offices are data. `src/offices/lumbridge-hq.ts` is fifteen rooms and seventy-six seats, and it is the file a self-hoster copies. Walls are a segment list with 1-D openings, so doors and windows are holes punched in a wall rather than placed objects, and the pass that splits a wall around its openings hands the walk-mode collider its segments for free. The sun is real. `solar.ts` is a NOAA/Meeus implementation with no imports at all — not even three.js — so time of day keeps working on a laptop in a field. Verified against known values: 75.45 degrees at the June solstice in SF, 28.79 at December, sunset at 03:15Z. The first screenshot after wiring it was a black rectangle, which turned out to be correct: it was midnight in San Francisco. Presence binds to a seat id and never to a coordinate. The pack knows where `eng-04` is; who is sitting in it is private data behind an API. Same shape as the marker rule, one level in. Two corrections to ARCHITECTURE.md are in here. Containment does not discharge ODbL — publishing OSM-derived coordinates is Public Use of a Derivative Database wherever the rows live, so the rule is about the geocoder (US Census, public domain) and not the storage. And a person at a desk is not a Marker; markers are geographic. One contract gap surfaced only in a screenshot: two agents read `height` on a viewpoint differently, so the establishing shot aimed at empty air fourteen metres above the roof. It now means what the same field means for a city. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
338 lines
16 KiB
Markdown
338 lines
16 KiB
Markdown
# Tera — architecture
|
||
|
||
**Tera** is the map view: a city from above, rendered in three.js, public and
|
||
self-hostable. **Spaces** is the other half — the offices you walk into. They
|
||
are one engine and one asset library, seen from outside and from inside.
|
||
|
||
Lives at `tera.lumbridgecorp.com`. Apache 2.0, on the fleet's own Gitea.
|
||
|
||
## The names
|
||
|
||
Getting these straight early matters, because two of them were nearly overloaded
|
||
onto things that already exist.
|
||
|
||
| name | what it is |
|
||
| --- | --- |
|
||
| **Lumbridge Simulate** | the product line these sit under |
|
||
| **Tera** | the map view. Cities — SF, LA, NYC — from above. Public, no login. |
|
||
| **Spaces** | the interiors product. Offices you enter, host, and self-host. |
|
||
| **Office** | one building's interior, belonging to a tenant. You go *into* an office. |
|
||
| **Space** | a room or zone inside an Office — a desk bay, a meeting room, a lounge. |
|
||
|
||
Two things this avoids. `lumbridge-simulate` is already a **Rust crate** in the
|
||
private monorepo — the Governor's memory-admission simulator — and in its
|
||
vocabulary a **"Scene" is a set of models**, not a three.js scene. This engine
|
||
therefore does not take either word for itself: it is Tera, and its three.js
|
||
scene stays an implementation detail behind `createScene()`.
|
||
|
||
One overload survives on purpose: *Spaces* the product versus *a Space* the
|
||
room. It sits at two different levels and matches how people already talk about
|
||
offices, so it is kept rather than invented around.
|
||
|
||
---
|
||
|
||
## 1. Why this is its own repo, and why it is public
|
||
|
||
Tera began as `/live` inside Workie, the private career portal. That is now
|
||
**deleted**, not deprecated — a 3D city engine has no business living in a job
|
||
tracker, and Workie is a private repo besides.
|
||
|
||
**You cannot cleanly Apache-2.0 a subdirectory of a private repo.** The licence
|
||
attaches to a distribution, and a consumer has to be able to clone the thing the
|
||
licence covers. A folder inside `lumbridgecorp-website` or inside
|
||
`lumbridge-runtime` would mean every release is a manual extraction, and every
|
||
extraction is a chance to ship something private by accident. The point of this
|
||
project — a reusable asset library other people can build on and self-host — needs
|
||
a repo that *is* the open-source artefact.
|
||
|
||
So: **`lumbridge-public/tera` on the fleet's own Gitea.** Not
|
||
GitHub. Self-hosting the forge is the same claim the engine makes: the whole
|
||
thing runs on hardware you own, and nothing about it requires somebody else's
|
||
platform to stay alive.
|
||
|
||
### The 2D world comes down
|
||
|
||
The existing Lumbridge world at `world1.lumbridgecorp.com` is a Phaser 2D
|
||
office, and its tileset is exactly what stops it being open-sourceable. It has
|
||
no real users, so it is being retired rather than migrated — Tera and Spaces
|
||
replace it outright, with original assets, which is the whole point.
|
||
|
||
That reframes this repo: it is not a map that happens to sit next to the
|
||
platform, it is the platform's renderer. The city is the outside of the world,
|
||
offices are the inside, and a tenant moves between them.
|
||
|
||
---
|
||
|
||
## 2. Layout
|
||
|
||
```
|
||
lumbridge-simulate/
|
||
├── src/engine/ # renderer. Knows about terrain, blocks, streets, flights.
|
||
│ ├── types.ts # City, District, Hill, Marker, FlightSource
|
||
│ ├── world.ts # projection + cached heightfield, built per city
|
||
│ ├── terrain.ts # relief, coastline, parks
|
||
│ ├── blocks.ts # the built city, on a per-district street lattice
|
||
│ ├── structures.ts # bridges, streets, freeways
|
||
│ ├── markers.ts # pins. Takes Marker[], knows nothing about companies.
|
||
│ ├── flights.ts # FlightSource interface + a simulated implementation
|
||
│ └── scene.ts # lights, sky, camera flights, render loop
|
||
├── src/interiors/ # the inside of the world — floorplans, desks, presence
|
||
├── src/assets/ # the library. Original meshes and materials, Apache 2.0.
|
||
├── src/cities/ # data packs. Pure geography, no code.
|
||
│ ├── sf.ts # ~1000 lines of coastline, hills, districts, landmarks
|
||
│ └── la.ts # LA / OC / Riverside
|
||
├── src/adapters/ # where outside data plugs in
|
||
│ └── workie.ts # Workie API -> Marker[]
|
||
└── src/main.ts # the standalone demo app
|
||
```
|
||
|
||
The split that matters: **`engine` never imports `cities`, and neither imports
|
||
`adapters`.** A city is data handed to the engine. A marker is a `{id, lat, lng,
|
||
label, colorKey}` handed to the engine. The engine has no idea Workie exists,
|
||
which is what makes it publishable, self-hostable by someone with no Lumbridge
|
||
account at all, and free of anyone's private data.
|
||
|
||
`interiors` and `assets` sit beside `engine` rather than inside it: an office
|
||
and a city share the projection, camera, lighting and render loop, and differ
|
||
only in what they put in the scene. That is why `scene.ts` owns the loop and
|
||
knows nothing about terrain specifically.
|
||
|
||
---
|
||
|
||
## 3. Three rules that keep Apache 2.0 honest
|
||
|
||
Apache 2.0 is a promise that everything in the repo is ours to give away. Three
|
||
things would quietly break that promise, and all three are easy to walk into.
|
||
|
||
### 3.1 No trademarks in the repo
|
||
|
||
Company logos are **trademarks**, not code. Salesforce's logo is not ours to
|
||
relicense no matter how the file got here, and an Apache-2.0 repo containing
|
||
`logos/salesforce.svg` is making a claim it cannot back.
|
||
|
||
The NYC atlas ships 47 of these in `public/logos/`. We do not.
|
||
|
||
- Logos are fetched **at runtime**, client-side, and cached in the browser.
|
||
- The repo carries a fetch script and zero logo files. `public/logos/` is
|
||
`.gitignore`d, and CI fails if anything lands there.
|
||
- `NOTICE` carries the standard "trademarks are the property of their
|
||
respective owners; their use here is nominative" line.
|
||
|
||
### 3.2 No OSM-derived coordinates in the repo
|
||
|
||
This is the subtle one, and it is the reason the geography in `cities/sf.ts` is
|
||
hand-traced rather than imported.
|
||
|
||
OpenStreetMap data — including anything that comes out of Nominatim geocoding —
|
||
is **ODbL**. ODbL is share-alike: a "derivative database" has to be released
|
||
under ODbL too. Committing a table of company lat/lngs geocoded from Nominatim
|
||
into an Apache-2.0 repo mixes an incompatible share-alike obligation into a
|
||
permissive one. That is exactly the kind of thing that makes a repo unusable for
|
||
the people we want using it.
|
||
|
||
So:
|
||
|
||
- **Geography** (coastlines, hills, districts) is traced by hand from scratch.
|
||
Original expression, ours, Apache 2.0. This is already true for SF.
|
||
- **Elevation**, when we want real terrain, comes from **USGS/SRTM**, which is
|
||
US-government public domain. Not OSM.
|
||
- **Geocoded company coordinates never enter this repo.** They live in a private
|
||
database and arrive over the API at runtime.
|
||
|
||
There is a real result here: the licence constraint and the privacy constraint
|
||
want the same thing. Company positions and pipeline status both stay behind the
|
||
API; the open-source repo holds the city and the renderer. The tempting shortcut
|
||
— "just commit an `sf-companies.json`" — breaks both at once.
|
||
|
||
#### Correction: containment is not discharge
|
||
|
||
An earlier version of this section stopped at the paragraph above, and it was
|
||
**wrong** in a way worth recording rather than quietly fixing.
|
||
|
||
Keeping OSM-derived coordinates out of the repo solves licence *mixing inside
|
||
the repo*. It does not touch ODbL's actual trigger. Serving a snapshot of those
|
||
coordinates from a public endpoint is **Publicly Using a Derivative Database**,
|
||
which brings ODbL §4.3 attribution and §4.4 share-alike onto the served data
|
||
**regardless of where the rows are stored**. Moving the table off-disk hides the
|
||
obligation; it does not discharge it.
|
||
|
||
So the rule is about the *geocoder*, not the storage:
|
||
|
||
- The sanctioned geocoder is the **US Census Geocoder**
|
||
(`geocoding.geo.census.gov`) — a US Government work in the public domain,
|
||
keyless, covering every city planned here.
|
||
- **Google, Mapbox and HERE do not solve this either.** Their terms restrict
|
||
storing and redistributing returned coordinates, which is precisely what a
|
||
public snapshot does. "Not OSM" is not the test; "may be redistributed" is.
|
||
- Every synced row carries a **provenance** field, and the public-shape
|
||
assertion rejects any row whose provenance is not on a non-ODbL allowlist. The
|
||
gate that already checks field *names* now also checks where a coordinate
|
||
came from.
|
||
|
||
Nothing had been built against the old reasoning when this was caught, which is
|
||
the only reason it was cheap.
|
||
|
||
### 3.3 The engine takes no position on what a marker means
|
||
|
||
`markers.ts` renders `Marker[]`. A marker has a `colorKey`, not a `status`. The
|
||
mapping from "rejected" to red lives in the *adapter*, in the consuming app.
|
||
|
||
That is what lets the same engine serve the private Workie build (coloured by
|
||
pipeline state), the public Lumbridge demo (coloured by sector), and whatever
|
||
someone else builds from this repo, without any of them being a fork.
|
||
|
||
---
|
||
|
||
## 4. Flights: FlightRadar24 is the wrong source for this
|
||
|
||
Real aircraft over the Bay is a great idea. FR24 specifically is a problem, and
|
||
it is worth being clear about why before any code is written against it.
|
||
|
||
FlightRadar24's terms prohibit scraping and prohibit redistributing their data;
|
||
their commercial API is licensed per-seat and explicitly does not grant
|
||
redistribution. An Apache-2.0 repo shipping an FR24 client is publishing
|
||
instructions for violating a ToS, and any FR24 data that reached the browser
|
||
could not be relicensed onward. Neither is fatal to a private deployment, but
|
||
both are fatal to the open-source story that is the point of this repo.
|
||
|
||
The engine therefore takes a **`FlightSource` interface**, and the repo ships
|
||
only sources it can actually give away:
|
||
|
||
| source | licence | notes |
|
||
| --- | --- | --- |
|
||
| **`SimulatedFlights`** | ours, Apache 2.0 | great-circle tracks on real SFO/OAK/SJC approach and departure paths. **This is what ships first**, and it is genuinely enough — the map wants convincing motion, not a spotter's log. |
|
||
| **`adsb.lol` / `airplanes.live`** | community, open terms | free, no key, real aircraft. The default "real data" adapter. |
|
||
| **OpenSky Network** | free, non-commercial | fine for a demo, awkward for a commercial product page. |
|
||
| **your own receiver** | *no licence at all* | an RTL-SDR + `dump1090` on any fleet box in the Bay produces first-party ADS-B. Nothing to comply with, and it is very on-brand. |
|
||
| FlightRadar24 | commercial, no redistribution | if it is ever wanted, it is a **private adapter in the Lumbridge deployment**, not in this repo. |
|
||
|
||
Recommendation: build `SimulatedFlights` now, `adsb.lol` next, and treat a
|
||
receiver as the eventual real answer.
|
||
|
||
---
|
||
|
||
## 5. LA is not "SF but more data"
|
||
|
||
SF is 0.20° × 0.36°. At the 45 m cells the SF heightfield uses, that is 336k
|
||
lattice points and a 1.0 s build.
|
||
|
||
LA + Orange County + Riverside is roughly 0.5° × 1.9° — about **14× the area**.
|
||
The same approach would be 4.6M lattice points and a mesh nobody can load. This
|
||
is the one place where the engine as written does not simply extend, and it is
|
||
better to know that before writing `cities/la.ts` than after.
|
||
|
||
Two changes make it work, and both are cheap now:
|
||
|
||
1. **Cell size is per-city, not a constant.** LA's basin gets coarse cells;
|
||
nothing is lost, because LA's relief is mountains at the edges rather than
|
||
SF's hills every four blocks.
|
||
2. **Cities declare `focusRegions`** — a handful of boxes (DTLA, Santa Monica,
|
||
Culver, Irvine, Pasadena) rendered at fine resolution, with the basin between
|
||
them coarse. This is the LOD story, and it is a per-city data declaration
|
||
rather than engine machinery.
|
||
|
||
SF gets one focus region covering the whole city and behaves exactly as it does
|
||
now. LA gets six. NYC, later, gets Manhattan plus the inner boroughs.
|
||
|
||
---
|
||
|
||
## 6. How Workie feeds it
|
||
|
||
Workie stays the system of record for companies. Tera never gets a database.
|
||
|
||
```
|
||
Workie GET /api/live/markers (private, tailnet) -> status colours
|
||
Workie GET /api/public/markers (public, allowlist) -> sector colours
|
||
|
|
||
v
|
||
adapters/workie.ts -> Marker[]
|
||
|
|
||
v
|
||
engine/markers.ts
|
||
```
|
||
|
||
The public endpoint goes through the **same `export-site.ts` field allowlist**
|
||
that already guards radar.karti.ai and work.karti.ai — the one that is
|
||
fail-closed and aborts a deploy rather than shipping an unknown field. No second
|
||
implementation of that gate. That was the argument for keeping `/live` inside
|
||
Workie originally, and it still holds — but only for the *data path*. The
|
||
renderer left; the gate stayed where it was.
|
||
|
||
Self-hosters get neither endpoint and do not need one: `setMarkers()` takes an
|
||
array, and where it comes from is the deployment's business.
|
||
|
||
`lumbridgecorp.com/live` is a static page, so it calls a small
|
||
`lumbridge-simulate` service behind the existing Caddy `handle /api/*` — the
|
||
same pattern `lumbridge-intake.service` already uses on cloud-2 — for flights
|
||
and for proxying the public marker feed.
|
||
|
||
---
|
||
|
||
## 7. Interiors — and why the asset library is the real product
|
||
|
||
The existing Lumbridge world is a Phaser 4 2D office, and Karti's read is right:
|
||
its tileset is the thing that stops it being open-sourceable. Redrawing that in
|
||
3D with original assets is not a graphics exercise, it is a **licensing
|
||
unlock** — it is what lets the walkable-office half of Lumbridge ship under
|
||
Apache 2.0 at all.
|
||
|
||
That makes `src/assets` the highest-leverage part of this repo over time: an
|
||
original, consistent, Apache-2.0 library of desks, chairs, partitions, screens,
|
||
doors, lighting rigs, floor and wall materials. Built once, used by every
|
||
Lumbridge world and by anyone else who wants one.
|
||
|
||
Interiors reuse the engine's renderer, camera machinery and render loop, and
|
||
swap the city layer for a floorplan layer. The city view and the office view are
|
||
the outside and the inside of one world, and moving between them is a scene swap
|
||
rather than a different application.
|
||
|
||
Two claims that stood here earlier were wrong, and CONTRACT.md carries the
|
||
resolutions:
|
||
|
||
- **They do not share a `THREE.Scene`.** They cannot. San Francisco's `latScale`
|
||
puts one scene unit at ~94 m with 3.6× vertical exaggeration; an office renders
|
||
at 1 unit = 1 m. There are two scenes over one renderer, and the city scene is
|
||
*retained and paused* when you step inside rather than disposed — rebuilding
|
||
its 336,864-point heightfield costs about a second on the way back out.
|
||
- **A person at a desk is not a `Marker`.** A marker is geographic — it has a
|
||
lat/lng. Presence binds to a **seat id** and never to a coordinate, which is
|
||
what lets the office geometry be public and open-source while who is sitting
|
||
in `eng-04` stays private data behind an API. Same shape as the marker rule,
|
||
one level in.
|
||
|
||
Reference point: the per-office spatial products in this space (Simile and
|
||
friends) are the shape to aim at. The difference Spaces is going for is that this
|
||
one is Apache 2.0 and self-hostable — you can run your own world on your own
|
||
hardware, which is the same claim `lumbridge-compute` makes about compute.
|
||
|
||
This is the next phase after the city lands, and it is where `src/assets` starts
|
||
earning its keep.
|
||
|
||
---
|
||
|
||
## 8. Order of work
|
||
|
||
1. **Port** SF out of Workie into `engine` + `cities/sf`, parameterised by city;
|
||
delete Workie's `/live`. *(this commit)*
|
||
2. **Markers + simulated flights.** A demo worth showing, with no data pipeline
|
||
and no licence questions. *(this commit)*
|
||
3. **Mount at `lumbridgecorp.com/live`**, with a small `lumbridge-simulate`
|
||
service behind Caddy for flights.
|
||
4. **Interiors + the asset library** — the office view, original assets, and
|
||
World 1 moving off Phaser. This is the one that unlocks the platform.
|
||
5. **Workie adapter**, once its geocoding pipeline lands. Workie gets an API,
|
||
not a renderer.
|
||
6. **`cities/la`** — LA / OC / Riverside. Needs `focusRegions` from §5 first.
|
||
7. **NYC.**
|
||
|
||
## 9. Open questions
|
||
|
||
1. **Package name.** `@lumbridge/simulate` implies an npm publish; consuming
|
||
straight from Gitea is simpler until someone outside asks for a registry.
|
||
2. **Licence for the asset library specifically.** Apache 2.0 covers code
|
||
cleanly; art is sometimes better served by CC0 or CC-BY so it can be reused
|
||
outside software. Worth deciding before the first mesh lands, because
|
||
relicensing art after contributors exist is painful.
|
||
3. **How much of World 1 moves at once.** The Phaser world is live and has real
|
||
tenants; the city view can ship at `/live` well before any office does.
|