1
0
Commit Graph

7 Commits

Author SHA1 Message Date
karti fe58290728 California gets roads, traffic, and a car to follow 2026-08-11 18:24:53 -07:00
karti a229fb2721 The sky gets the things above the aeroplanes
Satellites, end to end: CelesTrak element sets behind the same TTL cache
the weather and the flights use, served as TLEs rather than as positions,
and propagated in the browser with SGP4.

Sending elements is the same trick `flights/plan.ts` plays and it has a
better excuse here — a TLE *is* the closed form, valid for days either
side of its epoch, so one cacheable fetch every six hours replaces a poll
and every viewer agrees about where everything is.

Two things are worth knowing about the shape of it:

  - There is no region parameter. An aeroplane at 10,000 m is local and
    a satellite at 550 km is above the horizon for a circle two thousand
    kilometres across, so one catalogue serves both boards and the client
    decides what is above its own horizon. Only the observer is per-city,
    which is why `main.ts` shares the elements and rebuilds the catalogue.
  - The layer draws on a dome, because it cannot draw anywhere else.
    `world.metres(550_000)` is 21,000 scene units against a far plane at
    3,000. Azimuth and elevation are real; the radius carries nothing.

Off by default: a clone that started pulling CelesTrak on `npm run dev`
would have volunteered somebody else's bandwidth for its onboarding.

Godmode gets the two dials that point at the sky rather than at the
light — fabricated traffic, which composes with a live ADS-B feed instead
of replacing it, and a switch for the satellite layer with a count beside
it. Both are god-only lies about the inputs, in the manner of the weather
override.

`satellite.js` is the second runtime dependency this package has taken.
Its entry point star-exports an Emscripten build that cannot be shaken
out, so `noWasmPropagator` in the Vite config cuts it: 308 kB of WASM
loader for a bulk propagator nothing calls, against 26 kB for the SGP4
that does the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:57:14 -07:00
karti d71e844703 The engine gets photographed, and the camera stops moving while the shutter is open
`shots.mjs` renders the product imagery for lumbridgecorp.com/simulate: seven
frames of the running `dist/`, at chapters and times chosen rather than
defaulted, written as WebP into the sibling site checkout together with a
generated manifest that carries each picture's caption and alt text. The pages
there had described a renderer in prose for their whole life without ever
showing one, which is a strange way to sell a renderer.

The captions live next to the camera poses in this repo, not on the site, so
re-aiming a camera cannot leave a caption behind describing the old view. The
manifest emits a `ShotId` union, so a page asking for a picture that has been
renamed fails the site's typecheck instead of rendering a hole.

Along the way: the share cards were never reproducible. `scenekit.ts` eases a
chapter change over about two seconds of scene time, `stage.ts` clamps `dt` to
50 ms a frame, and SwiftShader here draws about three frames a second — so the
flight takes twenty seconds of wall clock and `capture.mjs` waited four. Every
run caught the camera at a different point over the bay, and none of them at
the chapter the key press asked for. Both scripts now open the page with
reduced motion, which is the app's own answer to "somebody clicked a name in a
list": `flyTo` sets the pose outright. `og-tera.png` is regenerated and is
Hayes Valley for the first time. The bytes still differ run to run, because
aircraft are crossing and cloud shadow is drifting; the framing no longer does.

`harness.mjs` is the static server, the SwiftShader flags, the two-hostnames-
one-dist trick and the clock shim, extracted because there are two consumers
now and two copies would have drifted apart while both claimed to photograph
the same app.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:03:50 -07:00
karti aab58a1c24 The client gets tests, starting with the two files that most needed them
Nine test files on the server, none on the client, and no test script in the
root package at all — so CI's only gate on the half of this project that runs in
a stranger's browser was `tsc --noEmit`, which will tell you the types line up
and nothing about whether an anonymous visitor is handed the private office.

Both files here were built to be tested and never were.

`access.ts` is the only module in the bundle whose output is a set of decisions
about what a stranger may see, and its own header carries the reason a test is
owed: a shipped line read `canEnterOffice = s.authenticated || !s.passwordLogin`,
which is true for `auth: none` and dangerously false for `sso` — where `POST
/session` answers 404 precisely *because* credentials are issued elsewhere — so
on an SSO deployment every anonymous visitor got the private view while the
config still said the box was private. There is now a test named after that bug.
The assertions are weighted toward the closed direction on purpose: one that a
member gets what a member is owed, and half a dozen that nobody gets more than
nothing, because showing a member the public office is a bad afternoon and
showing a stranger the private one is what the tiers exist to prevent. The 5xx
case is in there too — a box mid-restart is `anon`, not `member` — and so is the
`javascript:` entry URL, which a CSP of `script-src 'self' 'unsafe-inline'` does
not stop from navigating.

`plan.ts` says in its own header that it imports no three.js "so the splitting
pass is testable without a WebGL context", and that it "drops rather than
throws … every one of those is reported through `problems`" — an array whose
whole purpose is to be asserted on, which nothing asserted on. So: a pack wrong
in five ways still builds and files five reports; a zero-length wall does not
put a NaN in the bounds; an office with no levels stays finite so nothing
downstream divides by it; a pack with the required arrays missing is taken,
because HTTP will send one. And the wall pass gets the check CONTRACT.md §2's
argument deserves — one decomposition, two products — by walking a walker
through the door and into the wall beside it, and through a window and being
stopped. Also the yaw convention, which nothing stated and `officeMinimap.ts`
draws straight from: get the sign wrong and every wall mirrors about its own
centre, invisible on a square and obvious on anything else.

No new dependency. The server already runs `node --test` over `.ts` on native
type stripping, so the client does the same — which matters here, because this
repo's "no surprise dependencies" check is an allowlist naming why each one is
permitted, and a test runner would have needed an entry and an argument.

One source change was needed to make any of it possible. `session.ts` read
`import.meta.env.VITE_IDENTITY_URL` at module scope, and `access.ts` imports
`authFetch` from it — so one property access made the file that decides what an
anonymous visitor sees unreachable from a plain test runner, which is most of
why it had no tests. It now reads the way `plan.ts` already reads `DEV`, by the
idiom that file documents as being there "so this module stays importable from a
plain test runner".

31 tests, 9 suites, all passing, wired into `npm test` and into the CI job beside
the server's. `vite build` is unchanged and no test code reaches the bundle.
2026-08-06 03:46:17 -07:00
Karti Tripathi d464459838 Spaces: the inside of the world, and a sun that is actually where it should be
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>
2026-08-05 00:11:01 -07:00
Karti Tripathi 36471bbad7 Tera, because Simulate was already spoken for
Two names in this repo were quietly colliding with things that already ship.

`lumbridge-simulate` is a Rust crate in the private monorepo — the Governor's
memory-admission simulator, with a public product page — and in its vocabulary
a "Scene" is a set of models, not a three.js scene. Naming a 3D engine the same
thing would have put two different meanings on both words at once.

Meanwhile world1.lumbridgecorp.com has been serving <title>Tera — Lumbridge</title>
since before any of this: the world already had a name.

So the map view is Tera, the interiors product is Spaces, an Office is one
building's interior and a Space is a room inside it. ARCHITECTURE.md opens with
that table now, because the next few thousand lines all depend on it.

The Phaser 2D world comes down rather than being migrated. Its tileset is
precisely what stopped it being open-sourceable, and there are no real users to
strand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:09:44 -07:00
Karti Tripathi 67f8df8d53 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>
2026-08-04 21:49:08 -07:00