48ddfcdaec1b196b9ae27e78c4b94a4430983a1c
36 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
48ddfcdaec |
perf: the terrain is a grid of chunks, so three can cull it
`createTerrain` returned one mesh covering 32.50-42.05 N and -124.50 to
-114.00 W, and three frustum-culls per *object* — so at every pose, from
400 km and from 2.5, the merged board submitted all 86,400 of its visible
triangles. Standing over San Francisco it paid for the Mojave.
It now returns a `THREE.Group` named `terrain`, holding a 5 x 5 grid over
`city.bounds` — **17 non-empty chunk meshes**, eight cells being Pacific,
Nevada and Arizona — plus one un-chunked shadow caster. The geometry is
not touched: same patches, same resolution, same board-wide normals. Only
the index is cut up, which is what makes this culling and not level of
detail, and what sidesteps placement drift entirely — no ground is
re-derived, so nothing that was placed against it can move.
All 18 geometries share **one** `position`/`color`/`normal` attribute
triple: no duplicated vertices, one upload of exactly today's bytes, and
no lighting crease down any of the sixteen seams, since the normals were
averaged before the partition existed. The bounding volumes are therefore
assigned by hand — `computeBoundingSphere` walks the position attribute
and not the index, so a chunk left to compute its own would get
California's, cull nothing, and restore the whole cost with seventeen
extra draw calls and no visible symptom. `terrainLod.test.ts` pins that.
5 x 5 because a draw call is the scarce resource here, not a triangle.
The sweep is in the constant's own comment: 4x4 -> 5x5 buys 1,246
triangles per added draw, 5x5 -> 6x6 buys 573, 6x6 -> 8x8 buys 61. Five is
the last grid where a draw is worth more than a thousand triangles.
The caster stays one object with `frustumCulled = false`, deliberately:
`WebGLShadowMap` gates the depth pass on the same test as the colour pass,
so a chunked caster would drop a ridge that legitimately shadows into
frame the moment the camera turned. It sits at `drawRange(0, 0)` with the
hooks inverted, and `renderBufferDirect` early-returns on `< 0` and
`Infinity` but not on 0 — one zero-triangle draw call per frame, forever.
Gated on `city.districts.some(d => d.detail === true)`, which
`cities/unify.ts` is the only place in the repo that writes. `?city=sf`,
`?city=socal`, both `?one=0` California cells and `office` take the
unchunked branch and get today's single mesh inside a group: measured, the
socal board is identical to the draw call at three poses, and `?one=0`
identical to the digit at five.
Measured with `scripts/cost-at.mjs` at 37.7897, -122.3972, merged board,
before -> after (triangles / draws):
400 km 355,759/414 -> 355,759/431 the whole board, all 17 drawn
120 km 351,019/327 -> 321,057/340
45 km 332,969/265 -> 270,373/273
7.7 km 330,578/340 -> 222,482/343
2.5 km 329,544/335 -> 215,598/338 -34.6%
and over Los Angeles at 2.5 km, 761,648 -> 335,186: a breach of the
400,000 cap that no budget cell poses at, now under it. The +17 draws at
the whole-board pose is the price, it is a bound and not a measurement
(there are 17 objects), and it lands at 437/433 against caps of 460/455.
No cap moved; the budgets file gains one sentence saying that geometry
submitted is now a function of the camera, and only ever downward.
What it does not fix: it is culling, not level of detail — a chunk 1% on
screen draws 100% of its triangles, and the ground under San Francisco is
still `UNIFIED_FINE_METRES` at 300 m against that metro's own 45. Nothing
streams; build time and resident memory are unchanged. ~157,000 triangles
at the close pose are sky, and after this they are three quarters of what
is left. `minimap.ts` still samples 160 x 160 across the whole board.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
eeedfd7d6d |
feat: one California is the product, not a flag
Behind `?one=1` the merged board was something a visitor could not find and did not get: the site still landed on the Bay Area, the left column still offered two other boards, and clicking one still tore the board down and built another. The board existed and the product did not change. A bare URL is now one California. `?city=sf` and `?city=socal` still get the dedicated boards, unchanged and on purpose — twenty-nine capture guards and the `bay-area` and `socal` budget cells aim at those boards by name and assert the `data-board` they land on, and a metro id falling back to the merged board would not degrade them, it would make them silently measure something else. `?one=0` forces the old arrangement, and both `california` budget cells now pass it: their caps are the plain state board's 440,000, and without it they would measure the merged board that `california-one` already covers. Also `CALIFORNIA.md`: what a single cohesive California is still missing, measured rather than listed. Every district on the board by the latitude of its centre is 1 / 23 / 26 / 1 / 2 / 54 across 32-37 N and **zero above 38 N** — 107 districts, 103 of them in two clusters four hundred kilometres apart, and four hundred and fifty kilometres of northern California with not one building on it. Eight airports, all Bay or LA. Two ports, both in San Pedro Bay. Two freeways of twelve vertices each across a thousand kilometres of state. The scale is not the problem and the file says so first: the board is 32.50-42.05 N and -124.50 to -114.00 W at 1,919 m to the unit, every feature on its real coordinate, nothing schematic. It is not too small. It is empty. |
||
|
|
d6dc307d9a |
feat: relief that ramps with the camera, and an instrument to cost a pose
`verticalExaggeration` is derived per board so the tallest blended peak
fills a fixed fraction of the board framed whole — 15.00 for California,
5.78 for the Bay Area, 3.41 for the Southland. Each is right, and while a
board could only be looked at from its own stand-off one number was all a
board could want. The merged California is the first that can be flown
from 1,551 km to 1.9 km, and 15x is what put the camera inside Twin Peaks
at the FiDi rung: 2.19 units of drawn hill against a 2.26-unit stand-off.
So a pack may now declare `nearVerticalExaggeration` — what it wants when
the camera is *in* it — and `unify.ts` sets it to the largest figure
either merged metro asked for, which is San Francisco's 5.78. Largest
rather than mean or smallest: both metros are known to read well at their
own number, and picking the smaller would flatten the Bay to suit a basin
four hundred kilometres away.
It is applied as one `scale.y` on a new `ground` group rather than by
rebuilding anything. Every height in that group came from the same
exaggeration, so multiplying restates all of them consistently — a lot
placed on a hillside is still on it, a bridge deck still clears the
water, a berth is still at the quay. That is the property the alternative
does not have: re-deriving placements against new ground is exactly the
blocker TODO.md names for the terrain quadtree, and this sidesteps it
rather than solving it. The quadtree will still have to solve it.
The sky is deliberately outside the group — clouds, precipitation,
migration, live aircraft, satellites, Starlink. Their altitudes are facts
about the atmosphere, not about the terrain, and an aeroplane that sinks
when the hills flatten draws the wrong thing. The visible consequence is
that traffic stands further off the ground as the ramp flattens it,
because it always was that far off in true metres.
The ramp is driven from stand-off, not altitude. Altitude is measured
against the ground and this moves the ground, so an altitude trigger is a
feedback loop; stand-off belongs to the controls alone. Log-interpolated,
because the rungs are spaced logarithmically — 1,551, 501, 256, 95, 45,
24, 8.3, 7.7 km — and a linear ramp spends its whole travel between the
two widest and none across the eight that matter.
Also: `scripts/cost-at.mjs`, which costs an arbitrary pose with
`performance-budget.mjs`'s own patched GL counters, so its numbers are
comparable rather than nearly comparable. The budget harness only ever
visits the poses a board is judged on, which is the right instrument for
"did this regress" and useless for "how much room is there at the bottom
of the descent". First run, merged board, at SF HQ:
400 km 355,759 tris / 414 draws
120 km 351,019 / 327
45 km 332,969 / 265
7.7km 330,426 / 339
2.5km 329,392 / 334
The Bay Area board at that last pose draws 2,307,964. Ours is flat across
three orders of magnitude of stand-off, which is not a budget being spent
carefully — it is a board with no frustum culling doing any work, paying
for the whole state's heightfield while looking at one city block.
|
||
|
|
285b5b19e7 |
fix: the merged board's live feeds, and two instruments it could not be judged without
An ultracode investigation mapped the single-board work across five parallel readers and three adversarial reviewers. It found things this session would have walked into, and two of them are fixed here. **THE MERGED BOARD WAS SHIPPING WITH THREE FEEDS SILENTLY OFF.** Live ADS-B and live weather were gated on `id !== "california"` — "not the coarse statewide board", correct the day it was written, since live aircraft over a board at 1,919 m to the unit are a glyph problem and one station cannot speak for a thousand kilometres of coast. `cities/unify.ts` then built a board that is the whole state AND metro-detailed, keeping the `california` id deliberately so the fire gate, the ladder's region table and every `?city=` deep link keep working. It inherited a gate meant for something else. Measured before the fix: the `#source` badge read `""` on the merged board and `live weather · live traffic` on the Bay Area's. A defect that reads as "it feels less alive" and never as an error. `carriesMetroDetail(city)` asks the pack instead: `focusRegions` is the honest predicate and needs no new field, because the coarse state pack declares none and every pack with ground worth drawing at metro resolution declares one. **AND ASKING WAS NOT ENOUGH, BECAUSE THE FEEDS ARE PER-METRO.** With the gate fixed the board asked — and was refused: `GET /flights?lat=37.30&lng=-119.25& radiusNm=402 → 400 bad_request, "Nothing this deployment serves is near 37.3,-119.25"`. Correctly: `regionOf` derives its circle from the board's bounds, which on a statewide board is 402 nautical miles centred on the middle of the state, and what the deployment serves is San Francisco and the Southland, five hundred and sixty kilometres apart. `mergedTraffic` asks for both. In `adapters/http.ts` and not `engine/flights.ts` for the reason `TrafficSource` itself lives there: the engine draws darts at coordinates and has no business with provenance, and every interesting part of this merge is provenance. De-duplicated by id, because overlapping circles both see the aircraft between them and `flights.ts` measures a track's span from repeated observations — a duplicate is not merely a double image. `live()` is `some` and not `every`, so one dark metro does not make the other's observed traffic claim to be simulated. Verified: `?lat=37.77` → 200 live, 24 aircraft; `?lat=33.82` → 200 live, 15 aircraft; `#source` now reads `live traffic` on the merged board and stays `""` on the coarse one. **TWO INSTRUMENTS, BOTH BECAUSE THIS SESSION KEPT FAILING WITHOUT THEM.** `scripts/performance-budget.mjs` gains a `california-one` cell. Until now the only way to measure the merged board was to hand-edit the `california` cell's query, run, and edit it back — done six times in one session, which is exactly the procedure that gets half-done. Its `ready` asserts `#sea-section`, not just the signature chapter: `california-overview` is on the coarse board too, so a cell whose `?one=1` quietly stopped working would measure the coarse board and pass. No ports, no section, no readiness. Caps are RECORDED from its first run with headroom, in the same spirit as bay-area and socal — they were briefly copied from `california` and that is wrong for the same reason that cell's numbers are wrong for this board. **No existing cap was raised.** `scripts/look.mjs` gains `--lat/--lng/--standoff/--height`. Every aim in this harness goes through a control a reader also uses, which is right and stays the default — but it means a board can only be photographed where a chapter already points, and the merged board carries the state pack's six: the whole state, the north, two corridors, two doors. None is near a city. The board exists to put cities on the state and there was no way to photograph one; three attempts by clicking the minimap and guessing wheel notches landed in open ocean twice and on empty coast once. The seek moves the camera and nothing else. **A regression the new cell caught within one run.** The first version of the marker fix gated on bounds alone, like the office doors. The coarse state board's rectangle contains San Francisco, so it picked up forty-four company markers it has no business drawing at 1,919 m to the unit: 373 draw calls → 417. Now gated on `carriesMetroDetail` as well, and `california` measures 372,415 triangles / 373 draws — identical to before this commit. All twelve budget cells pass. 1,705 tests pass. Recorded for the next round, from the review: **SoCal's two focus rectangles overlap by 1.7 x 4.6 km** (verified: lat 34.075–34.090, lng −118.300–−118.250), so any per-rectangle terrain tier must clip them to a disjoint cover first or it draws that ground twice. Today's per-axis lattice is what hides it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6fc0b2b60e |
feat: land on a real place, and let a wheel notch cross the seam
The owner has now asked four times why there are three boards, and the last
answer missed the point: reconciling the packs made them draw one California,
but you still *arrived* on the coarsest tier the product owns and still changed
boards by picking a name off a list. This changes both.
**The behaviour was already built and behind a second flag defaulted off.**
`handover()` in `ladder.ts` — promote/demote by camera stand-off, hysteresis at
0.9/1.15, a drag guard so a board never swaps under a finger — was pure, tested
and shipped a round ago, with `?handover=1` as the only way to see it. It is on
now, `?handover=0` turns it off. Measured on the deployed bundle first:
promotion into the Bay Area fired at the sixteenth wheel notch in from the state
pose, arrival clean, no boot card, no tab, no click on a name.
**And it did not work, for a reason worth writing down.** Landing on the Bay
with the flag on bounced straight back to the state board with no input at all.
Two wrong diagnoses on the way, both from reasoning instead of measuring:
1. "sf's ceiling equals its own widest pose, so the band is too tight." It is
not — `chapterStandoffMetres` puts the resting pose at 71.2 km against a
demote threshold of 81.9 km.
2. "the arrival flight carries the camera through the threshold, so guard on
`arriving()`." Right about the cause, wrong about the mechanism: the guard
went in and the bounce survived it.
A trajectory log settled it in one run. The handover tick arrived *before* the
first `arriving=true` sample, at 108,316 m — 1.5x sf's resting stand-off, which
is `arrivalStart`'s own offset. `arrive()` read:
kit.setPose(from);
arrival = { from, to: rest, elapsed: 0 };
`setPose` drives `OrbitControls`, which fires `change` **synchronously**, and
`main.ts` listens on that event. So the listener ran on the line *between* those
two statements: camera already 1.5x out, `arriving()` still false. A one-frame
ordering race, and the guard could not fire because the flag it reads was set one
statement too late. The assignment now goes first.
The guard stays, because the inequality behind it is structural rather than
incidental: `ARRIVAL_STANDOFF` is 1.5 and `DEMOTE` is 1.15, both global, so the
opening frame of *every* board sits outside that board's own retention band.
`handoverArrivalGuard.test.ts` asserts that relation and drives the real rule
through the opening stand-off to watch it demote, so neither the guard nor either
constant can be quietly simplified.
**The landing board is `DEFAULT_CITY_ID`, and it is the Bay Area.** It was
`CITIES[0]` in three places, which meant the state tier: seventeen districts over
1063x930 km at 1,919 m per unit, no city legible, and a left column whose first
offer is somewhere else to go. The detailed boards carry 52 and 47 districts at
94 and 391 m per unit. With free handover on, the state tier stops being a
destination and becomes what you get when you pull back — the role it is good at,
since it is the only board drawing 97.4% of California. A named constant rather
than reordering `CITIES`, because that array's order is the `?city=` fallback and
is read positionally by other consumers.
**Which caught a silent break in the capture harness, and this is the part that
would have cost a week.** `shots.mjs` and `films.mjs` built `?city=` only when a
shot declared one — and 4 of 21 shots and 1 of 4 films declare none, so they
inherited the app's default. Moving that default would have re-pointed five
pieces of marketing imagery at a different place while every filename, caption
and alt text stayed as it was. Both harnesses now name `california` themselves.
`performance-budget.mjs`'s `california` and `california-drive` cells had no query
at all for the same reason; `signature` would have failed them loudly rather than
mismeasuring, which is the harness working, but a harness that depends on an app
default reports someone else's change as its own flake.
Every harness also pins `handover=0`. A planted pose wider than a board's
retention band would otherwise demote to the coarser tier while the shutter is
open, and the frame that comes back is a real photograph of the wrong board.
Verified: bare URL lands on the Bay Area and stays there; `?city=california` and
`?city=socal` still deep-link; `?handover=0` stays put; zooming out from the Bay
demotes to the state tier at the second notch. 1,694 tests pass. All ten budget
cells pass with no cap raised, and every cell now measures the board it names.
Still true and now a decision rather than a doubt: 97.4% of California has no
board below 242 km of stand-off, so zooming into the middle of the state lands on
coarse ground. That picture has been looked at. Authoring Sacramento, Fresno and
the Central Valley is what retires it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4bd8481be1 |
feat: the boards stop being three products
The owner asked twice why there are still three separate boards. The honest answer, and what this round executes: **it feels like three boards, but not because the scale jumps 20x — because the three packs draw three different Californias, and the loudest difference is that the mountains are four times taller on one of them.** **THE 20x HORIZONTAL SCALE JUMP IS INVISIBLE**, and measuring that collapsed the cost of this whole round. `World.project` is a uniform scale in x/z with no vertical term, and a uniform scale leaves a perspective image identical — so a camera carried across the seam on matched true-metre offsets draws a pixel-identical horizontal frame. 1,919 -> 94 m/unit costs nothing to look at. Rescaling was never the problem. A boot card, a tab strip and a 4.17x vertical deflation were. **THE PAUSE WAS MOSTLY FAKE.** A switch covered the screen for 1,715 ms but only 608 ms blocked the main thread; the page drew 46 of 69 frames with nothing to show, because the outgoing board had already been disposed. `mountCity` now retains it: the incoming board builds BEHIND a live, interactive picture, and `stage.setScene` fires only on completion. Measured across all six directions, three runs each — boot card yes -> **no**, opaque cover 726-1,415 ms -> **0**, blank frames 21-46 -> **exactly 1**, wall clock down 12-29%, blocked main thread down 15-47%. A return to a board already seen links **zero** shader programs and blocks **zero** milliseconds: 298-312 ms of camera flight where it was ~1,600 ms behind a card. Disposal had been throwing away the shader cache too — linkProgram ran 38, 59, 78, 109, 127 across five mounts and never reused one. **The transition is a fog dip, not a crossfade**, through the `setAerialFog` seam built last round. Every both-boards-live crossfade breaks a budget — ca+sf is 2,640,307 triangles against bay-area's 2,600,000 cap — and a fade never lands inside the harness's sample window, which is the "a cap you do not measure is a cap you do not have" failure this repo already argues against. The dip costs zero triangles and zero draw calls, and it hides the 4.17x deflation, the 4,025 m projection disagreement and the vanishing 2 km freeway symbols at once, because all three happen at maximum obscuration. It is also diegetic: a descent through haze. The first dip was wrong and the photograph caught it: collapsing to 6% of board SPAN turned the whole night frame into one flat field — the exact "turns the map off" failure the risk list named. Re-anchored to 70% of camera STAND-OFF, so the coastline survives and only the relief melts. **One ladder, one places list.** 26 authored chapters become 24 rungs sorted descending by STAND-OFF, not altitude — by altitude they interleave badly and altitude cannot tell a low oblique from a high plan. The three-board tab strip is off by default; the left column is now one scrolling list of all 24 rungs under three region headings that does not change when the board does. Only which row is lit changes. Label collisions are resolved in the ladder and never in a pack, so the 29 index-aimed capture guards are untouched. The minimap stops turning through 90 degrees between boards: every board is pinned to a rectangle with California's proportions. **SF and SoCal are not regressed**, and that was the acceptance that mattered: 95.9-98.8% of board pixels are delta-0 against a baseline hash-verified identical to what the live site serves, and every one of the 34-70 surviving pixels per frame is an aircraft or a hull. **A real defect found only by photograph:** `minimap.setMarkers()` had zero call sites. Every marker on every board was gone — the LA studio's door dot, the Bay Area's eight company markers — dropped when the minimap went per-board. Typecheck, tests, budgets and the console were all green with that bug in. Also fixed: two capture presets that lied. `look.mjs`'s `glyph-la` and `glyph-sf` claimed California chapter closeups and returned SoCal and Bay Area frames, because they aimed by chapter index and the indices had moved. Aiming is now by identity, with a guard test. NOT SHIPPED, DELIBERATELY: the pack merge. At Bay density it is 34.04M triangles, 13x the highest budget — dead, not a trade. At SoCal density it is 1.99M and fits today, and the price is San Francisco rendering at 164 m lots instead of 40 m, i.e. SF looking the way SoCal looks now. SF and SoCal carry every marketing still on the site. That is the owner's decision and it is worthless as an argument and decisive as a photograph, so it ships as a measurement artifact with a side-by-side still and is wired into nothing. The four data reconciliations that would make one world honest — one exaggeration rule, roads in metres, one projection centre, one coastline convention — are behind TERA_RECONCILE, default OFF. Tests 1,570 -> 1,651, server 295. All ten budget cells pass, no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bcac6aa41a |
feat: the crane grows a mast, the harbour works a shift, and the site is re-shot
**The Asset Factory verdict, and it mostly went against the vote.** Nine candidates were thumbed up. One was taken. TOOK the STS crane. Rebuilt in `ports.ts` from 5 unit boxes to 11 — an A-frame mast and apex cap, a forestay to the boom, a backstay to the tail, a sill, the truck-lane portal beam, a machinery house — still exactly ONE InstancedMesh. What was missing is the thing that makes a gantry a gantry: on a real STS the tallest part of a WORKING crane is the A-frame apex, not the boom, and a parked raised boom clears its own apex by only 15-25%. Before, 56 gantries read from altitude as 56 crosses — two coincident verticals with one bar through them and nothing above it — so a berth flattened into a picket fence. Proportions came from both upvoted candidates agreeing independently (hinge ~58 m under an apex at 99-104 m), taken conservatively because Tera's packs already author an 82 m hinge against a real 55-60. The apex beacon came across as EMISSION: `craneLights()` returns bare positions, `nightlights.ts` turns them into one additive Points cloud, 56 points, one draw call, night only, no THREE.Light anywhere. 0.09 units was invisible against the port's own cream emissive; 0.17 — half a bridge head light — is right, and the screenshot at 0.09 is what condemned it. REJECTED all three bridges, city-lights and both aircraft: the incumbents won on the picture, decisively for the bridge. TWO PARTS WERE BUILT FROM THE APPROVED CANDIDATES, PHOTOGRAPHED, AND CUT. Four legs: 14 m of quay spacing is 0.036 units at 391 m/unit against a 0.032 member floor, so 90% overlap. A portal X-brace: the bay is 0.115 wide by 0.38 tall, so both diagonals come out near-vertical and add a lump at mid-leg. Both are among the best things about the factory cranes AT THE FACTORY'S FRAMING. Neither survives at board scale. That gap is the whole reason a factory asset is reference geometry and not a drop-in. Fixed a defect the rebuild exposed: the backreach started a full rail-gauge behind the hinge, leaving a gap over the portal with the beam floating below it. One unbroken girder now. And every inclined member goes through a `strut()` that takes two points in the (distance-along-boom, height) plane, so the vertical-exaggeration bug the module header warns about is no longer reachable — it needs a length and an angle, and there is now no way to start from those. **The harbour works a shift.** It was a frozen tableau: 19 hulls placed from the pack's berths that never changed. Vessels now arrive through the channel, are met by a tug, berth, work and depart — seeded, so two people see the same harbour and a capture script shoots the same frame twice. A ship loses its wake when it ties up, because the wake is the information. **Every still and film re-shot.** The site was showing a Tera that no longer existed — SHOTS_COMMIT |
||
|
|
acf4d1a510 |
feat: the state becomes California, and the port fills with ships
**Stage 1 of one California.** The owner's complaint had two halves and this is
the first: the state board was a CROPPED SLAB. `california.ts` stopped at 38.05 N,
so the board disagreed with its own minimap about the shape of California in a
single frame, and Bug Fire's 93,733 acres burned off-frame while the panel said
all clear. Bounds now run 32.50-42.05 N / -124.50 to -114.0 W — Cape Mendocino,
the ruled Oregon parallel, the 120th-meridian corner into the Nevada diagonal.
**And it got cheaper.** 391,169 triangles to 375,351, while gaining the North
Coast, the Sacramento Valley, the Klamath knot, the Cascade arc, Shasta at 4,320 m
and Lassen at 3,190 m. Extending the bounds alone would have doubled the lattice
to 168,813 points and blown the mobile cap; coarsening cellLat 0.022 -> 0.0312 and
cellLng 0.027 -> 0.0383 holds it at ~83,800. The cell as a FRACTION of the board
moves 0.0030 -> 0.0033 — unchanged in frame — because the camera retreats to frame
whatever it is given. That argument was already written in the pack's own comment.
The second half — three boards becoming one world you zoom through — is NOT here.
Merging at Bay density would be 34.04M triangles, 13x the highest budget, and
merging at SoCal density would downgrade San Francisco from 40 m lots to 164 m.
Both delete the board every marketing still is shot from. `sf.ts` and `socal.ts`
are untouched by design.
**Aerial perspective, which the state board could not have had before.** The old
fog started at 1.15 board spans = 944 km, on a board whose longest diagonal is
820 km — so no pixel could ever be fogged. Fog now responds to camera altitude,
clamped to the authored pair as a ceiling.
`Atmosphere.aerial(env, view)` is a second pure method returning `{ near, far }`
and **deliberately no colour**. That is structural, not stylistic: it is why a
future camera-dependent term cannot reach `environmentKey()`'s colour fingerprint
and start rebuilding the PMREM cubemap on every camera step. Coarsening the
fingerprint instead would have hidden one instance and armed the mechanism. A
mutation-tested seam guard fails if anyone merges the two paths back together.
**The port.** Terminal Island rendered as a bare tan polygon with generic white
blocks while the chapter text called it the busiest port complex in the
hemisphere. Now six container yards drawn as canvas atlases, 56 gantry cranes at
varied boom angles, the 13 km San Pedro breakwater, the dredged channel. Five
buckets merging ACROSS ports the way airports.ts merges across fields, so a
second complex costs no extra draws: +11 draws and +4,377 triangles for all of it.
At vertical exaggeration 3.4 a 130 m gantry is 1.132 units tall against a 400 m
ship's 1.024 long — the crane is the taller object, and it is what makes a port
read as a port from altitude.
**Ships, and the wake carries the information.** Moored hulls have no foam,
verified at three terminals; a tug under way in the Main Channel trails a clean
Kelvin V. One hull geometry, one InstancedMesh, orientation from the BERTH rather
than the wire. The AIS gate strips sog 102.3, heading 511 and cog 360 — all mean
"not available" — with an explicit test that cog 358.7 SURVIVES, because a naive
range check on cog eats real headings near north.
"Empty or full" is not in AIS position reports and is not invented per ship. The
honest answer is at port level and is a better story: 348,691 of 460,467 boxes
left Los Angeles empty in July 2026, corroborated by FBX01 $7,491 inbound against
FBX02 $347 outbound.
**Radar and birds ship dark, and say why.** California is 0.47% wet and migration
is nocturnal and seasonal, so both layers have nothing to say on most days. The
panel reads "No radar feed is configured, so this board draws no weather. That is
a fact about this box, not about the sky."
Also recorded, and it matters beyond this commit: **the GPU on amd-server never
leaves 500 MHz of a possible 2725**, traced across 80 seconds of sustained load.
`bay-area/desktop` is fragment-bound at that clock and sits on the vsync deadline,
so a trivial change in fragment work flips it between 16.8 and 33.3 with geometry
identical to the digit. Every frame-time number measured on this box is a floor.
Two investigations reached two different wrong conclusions from single-run
comparisons before this was traced. Geometry is the gate; frame time is advisory.
No cap was raised.
Tests 1,340 -> 1,540, server 280 -> 295.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b25f217e3e |
feat: real fire on the boards, the LA office as a twin, and a night sky worth reading
The world stops being a simulation of California and starts being California. **THE PROMOTION GATE WAS THE FIRST COMMIT, BEFORE ANY ORANGE PIXEL EXISTED.** On today's live store the SoCal board contains 22 incidents. Every one has NULL acreage and fifteen are nameless LA County dispatch numbers. Drawn naively that is 22 orange marks over Los Angeles on a day nothing is burning — in a frame that contains no other warm colour, so one glyph would be the most salient object on the board and twenty-two would spend its credibility permanently. `acres >= 10 AND contained < 80 AND type != 'RX' AND last_seen = max(last_seen)` returns 0 on SoCal, exactly 5 on California, 0 on the Bay — same body, same day, three correct answers. The empty board is a deliverable, not a fallback: it says "No active fire on this board — CAL FIRE and WFIGS, just now", states that 21 records were gated and why, lists the largest fires burning OUTSIDE the frame with distances, and counts the hot pixels it is deliberately not drawing. **The privacy leak is structurally impossible rather than carefully avoided.** cloud-1 serves a projection; the four home-relative columns never leave that box. `observations.threat` was the one that nearly got through — it is `(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, so with acreage and containment public it inverts to a distance circle around a house and three fires give an intersection. A grep of the built bundle for distance_km, bearing_deg, threat, 7762 and the street name returns nothing. **Deliberately not used, and both would have produced a confident wrong answer:** the store's `air` table retains only the last parameter of each poll, so all 78 rows read "Good" while the live feed reports ozone 101 "Unhealthy for Sensitive Groups" — haze driven off it would clear the sky during a smoke event. And `weather` is written only inside the NWS alerts loop, so a quiet day stores no wind at all. Tera's own per-region NWS wind is already correct and already what the clouds drift on. Satellite detections are drawn as evidence and never as incidents. The permanent industrial heat source 4.7 km from the owner's house is flagged persistent and dropped, asserted by a test that first proves it is present in the fixture. MODIS integer confidence and VIIRS string confidence are branched on `sat`. **The LA office is a twin.** Its entire authored second storey — Model Loft, Model Bay, The Materials Room, 430 lines nobody had ever stood in — is reachable on foot: a walker crosses level-1 to level-2 in 73 fixed steps, floorY 0 to 5, verified against the real pack rather than a synthetic plan. Its two studio devices read real hardware through a field-allowlisted bridge: mute, volume and reachability only. Never level, because there is no passive level upstream and obtaining one would record a room with people in it. Never dB, because upstream is gainPct across four different native scales. The bridge refuses all writes. Fixed at its root: an anonymous visitor was getting permanently at-rest instruments backing off against a 401. The tier moves into `createDeviceSource`, so anon gets the living simulator three file headers already promised. **Item 8 is closed, not fixed, and the correction is the point.** The Bay Area "stutter" was GPU power management — the card sat at 500 MHz of 2725 through every run that reproduced it, 4096/2048/1024/256 shadow maps all render in 1.21-1.31 ms, and two consecutive runs over a byte-identical dist gave 33.4 then 16.7. The allowance is removed and the cell is back to 16.7. Geometry is the gate; frame time is advisory. Item 7 was re-scoped after measuring: 1,069,006 of the Bay Area's 2,265,056 triangles were the second submission of the same buildings into the shadow pass. Mobile now has its own triangle caps and bay-area mobile draws 1,266,096. Also: bridges and the freeway corridor light up at night as emission, not lights — 1,614 deck lamps and 18 tower heads on the Bay in two draw calls. The single change that made US-101 legible was moving its edge lines from the lit material to the unlit one: retroreflective paint, the argument the SFO night frame already makes. California went 21,991 lamps to 4,051, clustered at the 17 town districts, because a rural interurban corridor genuinely is unlit. Tests 1137 -> 1340, server 280. All ten budget cells pass on first attempt with no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b7f5c41da5 |
feat(brand): a shot can take a body, so the last hand-made stills are generated
Three pictures on /arena — the chase camera behind the EV, the crow in flight, the
aircraft over the board — were the only imagery on the site with no generator
script anywhere in either repo. They were made by hand, and so they were a single
daylight frame served to dark-mode readers too, with no pair to choose from.
`shots.mjs` gains a `play` capability beside `aim`: a shot names a control mode,
the script clicks the app's own mode dock, and only then shoots — with the HUD
deliberately left in frame, because on these three the body and its readout ARE
the subject. `#mode-dock` and `#play-hud` are in FURNITURE.BARE rather than
CLUTTER for that reason, and there is a comment at the `hide()` call so a later
tidy-up does not crop the subject out.
Three guards, each verified by deliberately making it fire:
- the mode must be OFFERED, not merely present — asking for `drive` on the Bay
Area board now says `"drive" is not offered here — the dock is showing
[overview, actor]`;
- `aria-pressed="true"` AND `#play-hud` unhidden, the same pair
`performance-budget.mjs` waits on;
- the HUD's own body chip, so asking for `Explore` when an anonymous visitor
gets a crow says `the play HUD says "Crow", not "Explore"`.
The body guard runs twice, at the click and again at the shutter, because a
completed route or a bound ends a mode mid-wait and hands back an orbit wide shot
under the right filename.
`untilProgress` exists because the first delivery drifted: `run: 9000` gave 55% of
the leg in a 1x preview and 69% in the 2x render — a different stretch of road,
with a town's edge across the top of the frame. Road traffic runs from page load,
so a duration is really "boot plus a bit". Waiting on the app's own route
percentage lands preview and delivery on the same frame; overshoot throws rather
than shooting. The crow and the aircraft are built inert, so a duration is exact
for them and only the car needed the gate — measured, not assumed.
The run log now echoes the HUD (`[Drive] 56 mph · US-101 | assisted · 55% ·
chase`), because "shot drive-101-day" is equally true of a parked car, a finished
route and a correct frame.
The crow moved from the state board to the Bay Area. On California a one-metre
bird stands against ground drawn at 1,919 m to the unit with an oak the size of a
hill behind it, and the night frame would have been a black rectangle with a bird
in it. `crow-nav-v1`'s `source` said "Tera / California" and that was wrong on the
environment's own terms as well — `crowNav.ts` runs a ±120 m box with no terrain
at all.
One frame is honestly weak and is recorded rather than dressed up: `drive-101`
at night. From behind, on an unlit corridor, the EV is two tail lamps and a roof
strip. Four hours were shot between 21:15 and 20:05 and the corridor's ground is
equally black at all of them, so the hour is not the lever; both real fixes are in
the engine and are in TODO.md. It is still the honest night frame.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2aa4049258 |
feat(brand): re-shoot everything at the new engine, and make it one command
**Seven new shots**, because the world grew the most photogenic things in it
after the last pass: `sfo`, `lax`, `golden-gate`, `bay-bridge`, `freeway`,
`california-relief` and `pacific-sea`. All nine existing ids are unchanged — the
manifest emits a `ShotId` union that lumbridge-v4 imports, so ids are added and
never renamed.
**A shot can now aim itself.** The chapter list has no camera for SFO, LAX, the
bridges, the freeway or the open sea, and the engine has no `?pose=` back door,
so a shot points itself by driving the app's own inputs: a click on the plan view
slides the orbit target to a lat/lng while keeping the chapter's stance, wheel
notches set the standoff, and a drag sets azimuth and elevation. The plan view's
pixel-to-coordinate map is solved at runtime from three hovers of
`#minimap-readout` rather than hard-coded, so it survives a board resize or a
restyle of the widget.
**The tera share card was a picture of the wrong thing, and had been.** Its art
came from `keyboard.press("2")`, which had landed on the California board's drive
mode once the default board changed — so the card under the headline "Cities from
above." was a chase camera on US-101, showing metre-scale cars driving between
kilometre-wide buildings, with the DRIVE readout and the mode pill baked into the
art. It renders, it looks deliberate, and it is why an unguarded key press has no
place in a capture script. `capture.mjs` now clicks an indexed chapter and
asserts its `shortLabel` the way `shots.mjs` does, waits on `#boot` and
`#chapters` instead of sleeping twenty seconds, and gives each card its own hour.
**`npm run refresh` is the durable half.** One command: build, stills, cards,
films, both manifests, and a hashed before/after diff of every deliverable. It
fails loudly and specifically on the two conditions that otherwise produce
confident wrong output — the renderer coming up as SwiftShader, and a chapter
`expect` guard firing. `--stills-only` / `--cards-only` / `--films-only` compose,
`--dry-run` lists the plan without opening a browser, and
`shots.mjs --list` prints the whole shot plan — board, chapter, expect, aim, both
hours — which is what to run first when a guard does fire.
It also re-stamps `PROVENANCE.json`, narrowly: only entries whose origin is
`repository-generated` and whose `generator` names a script the run actually
executed, by literal hash substitution rather than re-serialising the file.
Without that, every legitimate card re-shoot leaves `npm run provenance` red.
**Every film re-shot.** They were at `9c9e78f`, captured 2026-08-07, and predated
the tone mapping, the reflective sea, the sky dome, terrain shadows, the rebuilt
California board, SFO, LAX, both bridges and the moving aircraft.
Tests 1137, typecheck, build, eight budget cells and every provenance and licence
check pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f81d5218d4 |
feat: SFO, LAX, both bridges, a road that reads as a road, and aeroplanes that move
**The aeroplanes were stuck because the wire could not describe motion.** `WireAircraft` carried position, altitude and heading and nothing else, so the client could only interpolate between the last two observations: every aircraft replayed a segment it had already flown, arrived at the newest known point, and sat still until the next poll landed five to fifteen seconds later. The feed had the missing numbers the whole time and the server threw them away. Sampled live from `api.adsb.lol/v2/point` while writing this — `gs` ground speed, `track`, `baro_rate`, plus `r` registration and `t` type designator. They are on the wire now in SI, aircraft dead-reckon along their own track and correct toward the truth when a fix lands, and the click card an anonymous visitor gets says "B739 · N68834". That last part is the enrichment FR24 was wanted for, obtained from an ODbL feed we may actually republish. **SFO and LAX exist.** A new `engine/airports.ts` composes an airport from runways, taxiways, aprons and terminal masses, with markings drawn on a canvas rather than modelled; the pattern of the runways is what the eye recognises from altitude, long before any building does. SFO is the two crossing pairs on the bay fill; LAX is the four parallels either side of the terminal horseshoe, plus the Southland fields under the traffic that actually flies there. **The Golden Gate and the Bay Bridge are those bridges.** One kit in `engine/bridges.ts`, because a suspension bridge is a repeated tower, a catenary main cable, a series of hangers and a deck — so both are configurations rather than two private implementations. The Bay Bridge carries the real 2013 topology: two suspension towers west of Yerba Buena, one east, then the piered causeway. The freeway stopped being a wireframe overlay and became a road, with shoulders, a median, and lane markings as texture. **And the board got faster while all of that landed.** California went from 728,744 triangles and 562 draw calls to 391,169 and 371 — headroom from 2.8% to 47.8%. The Bay Area board is 506,550 triangles lighter than before this work. Two things paid for it: - `transmission: 0.08` on the aircraft cockpit glass. three.js runs a full transmission backdrop pass whenever any rendered material has transmission above zero, re-drawing the entire opaque scene into a second target every frame — so the city was rendering terrain, every block and every freeway piece TWICE. Measured by patching only that number in a copy of the built bundle: 703,267 tris / 562 draws with it, 398,608 / 371 without. The material was already `transparent: true, opacity: 0.86`, so it was buying nothing. - Flatness-adaptive terrain LOD, which collapses runs of lattice cells wherever the height and colour agree with the quad replacing them. The coastline is provably untouched — a patch collapses only when every point is on land and agrees about `park` — and a test asserts the drawn footprint matches the cell-by-cell area to 1e-6. `createTerrain` got *faster*: the vertices it stops emitting cost more than the flatness scan costs to run. **The budget now watches the boards this was built on.** There was no `bay-area` or `socal` cell — so SFO, LAX and both bridges all landed in frames nothing measured, which is how a cap you do not have looks from the inside. Both are in the matrix now with caps set from measurement, and the rationale lives in the harness because JSON cannot hold a comment. Two known defects ship with this, both recorded in TODO.md rather than hidden: - `bay-area.desktop` drops about one frame in twenty (p50 16.7, p95 33.3). It is desktop-only and not fill rate — mobile runs the same 2.26 M triangles at a comparable pixel count and holds 16.7 flat — which points at the 2048 shadow map desktop uses against handheld's 1024. Measured at the commit before this work with the same harness: identical p95 33.3. Pre-existing, and invisible until the cell existed. - The aeroplane glyph is still about 1.5x the Golden Gate's main span at chapter zoom, down from 2.5x. `GLYPH_MAX_SCALE` is 52 because the raw scale at the far end of the California orbit is 51.0 at a 60-degree field of view, and 26 — tried first — put the glyph at 0.0123 of the frame against the 0.012 where the wings stop resolving. The real fix is to clamp against the camera's focus distance rather than the aircraft's, which is a signature change. Tests 1020 -> 1137. Typecheck, build, eight budget cells, no-binaries, provenance, zero-config boot, dependency licences, arena source hashes and the UI smoke across two viewports and two access tiers all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2b2dd5cb64 |
chore(brand): re-shoot the cards, and stop describing a building that is gone
The share cards were stale in the two ways that survive a code fix, because both of them end up baked into a picture. **"Two buildings" was wrong in two files.** The words on the card live in `og.html`; the words a crawler reads live in `vite.config.ts`. Fixing either one alone leaves the other saying something else — and there are three packs now, one of which is a courtyard block in Los Angeles that neither file mentioned. Both now say three, name the LA block, and mention the microphone on the desk. Lumbridge HQ also stopped being "a tower floor" some hours ago and is a live/work studio. **The tera card was cropping around a defect that no longer exists.** Its framing was pinned to `56% 88%` with a comment explaining that low was necessary to keep the far edge of the board — where the terrain stopped and the sky began in a hard diagonal — out of shot, because a card with that seam in it read as a rendering fault rather than as a map. The sea now runs eighteen board spans and ends past the fog's far plane, and the sky is a world-space dome, so there is no seam to hide from. `56% 62%` puts the framing where the picture is best instead. Also a per-shot `hide` in `shots.mjs`. The studio-hardware card is a `.panel-group` inside a scrolling card, so when it is taller than the viewport the last control is below the fold — correct on a screen, where a reader scrolls, and wrong in a photograph, where the frame edge cuts a gain slider in half and anyone who does not know the card scrolls reads it as a broken render. It is also a question of subject: `office-desks` is a picture *of* that hardware and keeps it, while the LA courtyard is a picture of a courtyard and does not want a column of audio controls down one side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c4649d078 |
fix: make the app boot on an insecure origin, and re-aim the capture harness
Three things, all found by trying to re-shoot the product imagery and failing.
**`crypto.randomUUID` is secure-context only.** `main.ts` called it at module top
level for its three wire identities, so on any origin that is not HTTPS and not
`localhost` the call threw before the scene was built and the app stopped at
"Starting up" with one TypeError and no other symptom. Every way a developer
normally opens this app is a secure context — `vite dev` and `vite preview` serve
localhost, the deployed site is HTTPS — which is why this survived since
|
||
|
|
8fb85cd2e5 |
feat: give the boards a horizon, a sea that reflects, and a state worth flying over
The wide shot, which is what an anonymous visitor actually lands on. **The sea was `MeshLambertMaterial`** — a material with no specular term at all, by construction — on a board where water is half the frame. It is now a low-roughness dielectric that reads `scene.environment`, with a runtime-generated tiling swell normal map sampled twice per fragment at two scales and two headings, so the sun breaks into a moving glitter path instead of a mirror point. An `onBeforeCompile` patch takes the body colour toward the deep value looking straight down and leaves it to the reflection at grazing, and walks roughness up past 1.6 board spans so the far water cannot shimmer. The swell spectrum is 1/k^2 and not 1/k because the first attempt was photographed: at 1/k every component carries the same slope, the shortest wave wins, and the sea renders as hard diagonal corduroy. A test holds it now. **The board no longer ends in a diamond.** The sea plane went from 1.8 board spans to 18, past the fog's far plane from anywhere the orbit reaches, and the sky is a world-space dome rather than a screen-space gradient. That gradient was wrong in a way dusk made obvious: the sunset band was painted along the *bottom* of the picture, under the board, while the true horizon at the top of frame stayed zenith blue. `daylight.ts` pinning the horizon stop to the fog colour to hide the seam was a symptom of it. **Terrain casts shadows.** Left off before because double-sided terrain against a ~16 m-per-texel shadow map gives acne; `shadowSide = BackSide` is the cure, shot at four sun elevations down to +0.0 degrees to confirm no stippling. The caster is a stride-2 decimation appended to the same index buffer and swapped in by `onBeforeShadow`/`onAfterShadow` via `drawRange`: no extra draw call, a quarter of the depth cost, and indistinguishable from the full-resolution caster in a side-by-side crop. Stride 1 was measured at +65,566 triangles and would have missed the budget by ~47,000, so it was not shipped. **California reads as California.** It was a beige kite: the eastern edge one ruled line for five degrees of latitude, the south closing in a diagonal V, the whole south-east a featureless tan wedge. Now the coast runs to the Mexican border with San Diego on it, the eastern edge follows the Colorado and the Nevada diagonal, and the south-east is the Basin and Range — forty parallel desert ridges throwing shadows east, Death Valley as a white pan between the Panamints and the Black Mountains, the Salton Sea the one cool value for two hundred kilometres. The opening pose is retuned to the bigger board; the old 452/392 stand-off left a slab of empty ocean where the state should be. **The aircraft were six pixels.** Measured, by enlarging a screenshot 200% to find one at all — indistinguishable from a dead pixel, on a board whose entire claim is that the sky is live. They are airliners now, with planform and trail, and clicking one raises its card for a signed-out visitor. **The Model X is off the wall.** It stood at floor level outside a studio 188 m up a Transbay tower, reading as a car balanced on a parapet. The apron is now chosen from `site.elevation`, which the pack already carries — not from an office id, which is the bug class this repo already hit once when a door marker gated on `id === "sf"` and would have pinned the Los Angeles building to San Francisco. Also fixed, and nearly shipped: sea z-fighting dithered every flat piece of ground on the Bay Area and SoCal boards. And one test asserted an exact source line for the water material, so the better multi-line implementation failed it — it now asserts the property (dielectric, metalness 0, low roughness) rather than the author's first guess at formatting. Tests 964 -> 1015. California desktop 562/650 draw calls and 728,744/750,000 triangles — 2.8% of triangle headroom left, which is the number the next person should check first. No budget was raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
db074e9cf7 |
feat: tone-mapped render rig, studio devices, LA fidelity pass, UI overhaul
The build the studios needed, across eight workstreams and one strict file partition. **The render rig was the quality ceiling.** The renderer ran three's NoToneMapping default while atmosphere drove the sun to 2.35 and assets set emissives to 3.2, so every value above 1.0 hard-clipped to flat white — which is why walls blew out and every fitting looked like a white rectangle. ACES filmic tone mapping and an explicit output colour space land in `stage.ts`, and the atmosphere intensity table and palette headroom are re-tuned against the new curve rather than left tuned for the clipping we removed. `engine/environmentRig.ts` builds a PMREM environment at runtime, procedurally, so nothing binary is committed. There was no environment map anywhere before, so every `metalness > 0` role had nothing to reflect and rendered dull grey — a defect the code already documented against itself in `office/optimus.ts`, where a whole material role was abandoned over it, and worked around in `modelX.ts` with a fake emissive that this change deletes. Atmosphere remains the sole light owner; the rig derives from the `LightingState` it already produced. **Studio hardware exists.** There was no device concept anywhere in the product: no type, no route, no state. `devices/types.ts` fixes a declaration/state/ capability/command contract that a smart light, a thermostat, a door sensor and a charger all fit without a schema change, and both studios now carry a desk mic and a computer speaker with deterministic simulated behaviour behind an adapter seam a real API can occupy later. Reads are the demo and are open; commands are a signed-in action and are kept off the read body entirely, because a shared cache replaying a GET that turned a microphone on is exactly what the fail-closed cache default exists to prevent. **The ADS-B licence hole is closed.** `TERA_ADSB_ENDPOINT` accepted any URL, the response was served publicly cacheable, and the attribution hardcoded adsb.lol regardless of where the endpoint pointed — one env var away from republishing non-redistributable data under an open-terms credit. The host is now allowlisted, the credit is derived from the host actually configured, public cacheability is conditional on redistributability, and a refused endpoint demotes to simulated flights and says so in `degraded[]`. The gate is on the source, not the feature: live aircraft and their detail cards stay open to anonymous visitors. **The LA studio was never the smaller pack** — 16 rooms and 248 props against SF's 4 and 28. Its deficit was fidelity per square metre: 98 of those props were ceiling troffers, it bound no props to seats, placed none of the habitat kit, and 12 of its 16 rooms had no viewpoint. Density comes from new asset kinds rather than more instances, because `furnish.ts` draws once per kind and folds colour into the batch key, so repeat instances add nothing the eye can read. **The interface stops being forty imperative mutations.** Every visibility decision moves into a pure, tested `ui/chromeState.ts` and one applier, so the chrome has coverage for the first time. Deleted: ~100 lines of CSS and two bindings targeting elements that no longer exist, and a `body:has()` rule that shifted the desktop layout by 160px for touch controls hidden there. Fixed: the office picker tabs that drew their label and their badge on top of each other. Added: a first-run flow, because the product is two verbs and neither was ever stated on screen. Mobile is designed on its own terms instead of being the desktop with things hidden — the plan view comes back, and the keyboard-only shortcuts button is replaced by touch controls. `arena/studioOps.ts` frames the whole thing as the multi-variable environment it is, wrapping the same simulators the renderer drives rather than a headless copy. Also removed `input/vehicle.ts`, which nothing but its own test imported. Tests 385 -> 961, all passing. Typecheck, build, performance budgets across six matrix cells, no-binaries, provenance, dependency licences, zero-config boot and arena source hashes all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
264daaab61 | feat: unify life-sim play controls | ||
|
|
e378a03740 | feat: add deterministic office robot jobs | ||
|
|
747744fa6c | feat(arena): add deterministic headless RL environments | ||
|
|
3326d2e6d0 | feat: complete actor handoff and piloted aircraft presence | ||
|
|
655c383061 | test: enforce simulation and render budgets | ||
|
|
fe58290728 | California gets roads, traffic, and a car to follow | ||
|
|
9c9e78f6f9 |
The Southland gets a door, and the people in it move
**Mateo Court**, a courtyard block in the Arts District — a third pack and a third *kind* of building. Two office floors and a shed already existed, so this is the plan the format had not been shown: a ring of rooms round an open-air yard with **no corridor anywhere**. Every door opens onto the yard, and the yard does the job a corridor does in the other two. It is also the first pack whose `ceiling: null` means there is genuinely no ceiling at any height rather than "take the lid off so the shot can see in", and the first sited off the Bay Area board at all. That last part needed a fix, not just a coordinate. `OFFICE_MARKERS` was gated on `id === "sf"`, which was correct for exactly as long as every office was in the Bay Area — it would have kept the Los Angeles building off the Los Angeles board and pinned it to San Francisco's. The gate is the board's own bounds now, which is the same question asked honestly. Every door through the 0.25 m courtyard skin is 1.2 m rather than the usual 0.9, and that is not a style choice: `blocked` inflates by the walker's radius *and* the wall's thickness, so a 0.9 m leaf through a wall that thick seals the room behind it while rendering perfectly. Two rooms were sealed exactly that way on the first pass. The test says so, because a well-meaning edit back to 0.9 for consistency would do it again. **People move.** `samplePresenceAt` thinned the roster by hour, which fixed a building that was full at 1 a.m., but everybody was still pinned to their own desk all day. Occupancy is a hand-written booking table now: meetings fill a room for a plausible length, the kitchen island fills at lunch, and a `Presence` binds to a seat id — so "in a meeting" means occupying a meeting-room seat, and the whole thing is choosing seat ids rather than inventing positions. The flood fill that checks a pack is walkable existed three times over. One copy now, since the third is where a divergence lives. **Films.** A new `office-dusk` reel — six hours over Lumbridge HQ catching the moment the house lights take over from the sun, which is the one thing only a time-lapse can show and which did not exist. The three existing reels are re-rendered rather than re-captioned: `films.ts` in v4 named a commit eleven behind HEAD, so the published reels were shot before there were any clouds and before the city drew office pins. Reviewers caught roughly a dozen false statements in the new prose across these files — a room census that did not add up, a wall-thickness count, a claim that every room has daylight when one does not, and a cost figure saying `updateSun` runs once a second when it runs once a minute. The consequential ones are fixed. In a codebase where the comments are the design record, a confident wrong number is a defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
df9641cddd |
The share cards are photographs of the building that exists
Both cards re-rendered off the current engine, and the office card's own copy fixed at its source: `og.html` said "one floor plan" while the artwork behind it showed two storeys held apart around a commons. The meta description in `vite.config.ts` and the words baked into the picture are two different strings and only one of them had been corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
effa077cc7 |
The shot list catches up with the building it is photographing
The two office stills were captioned for a building that no longer exists: "thirty-four metres by eighteen, one storey" against a plate that is now forty-eight by eighteen over two storeys, and a night note promising the office "keeps its own light after the sun has gone" — which was aspirational when it was written and is only now true. Two new frames. `office-commons` is the double-height wing, which is the one view that shows why the storeys are held fourteen metres apart. `office-hangar` is Frontier Valley, and it needed a new capability: the harness presses the office picker before choosing a chapter, because switching buildings disposes the scene and rebuilds it, so a chapter click issued first would land against a legend that no longer exists. Selected by visible label and asserted, for the same reason `chapter()` asserts its `shortLabel` — a reordered `OFFICES` table would otherwise photograph one building and caption it with the other's name. The office share card said "Lumbridge HQ, one floor: the rooms, the desks and who is at them". It is the first thing a stranger sees and it was wrong in three ways. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
18dadda917 |
The surface library can be photographed
`/simulate/assets` on lumbridgecorp.com argues that the art in this repository is code — eight textures drawn on a 2D canvas from seeded value noise, thirty-five surface roles pairing one of those with a colour and a roughness, and no binary art anywhere, enforced by a CI job. It made that argument in about four thousand pixels of prose with nothing on it to look at, which is a strange way to talk about the appearance of things. `textures.mjs` writes the library out: one tile per texture kind drawn by `TextureBin`, multiplied by the colour of the first role that carries it, and a neutral tile beside it so a reader can see for themselves that the map has no hue of its own. The role table goes with them as data, so the site sets it in its own type rather than baking labels into an image. It is the one capture here that runs against Vite dev rather than `dist/`, and the reason is in the header of both files. `shots.mjs` and `films.mjs` photograph the *application*, and the application a visitor gets is the built bundle. This photographs a *module*. `TextureBin` is not reachable from the bundle — it exposes no names — and making it reachable would mean a third Vite entry, which would ship a texture-sheet page to tera.lumbridgecorp.com so that a script could screenshot it. Vite dev transforms `/src/assets/*.ts` on request, so the page imports the same files a reader opens on the repository. Nothing in it re-implements the tables in `materials.ts`: it asks `MaterialRegistry` for every role in `DEFAULT_INTERIOR_PALETTE` and reports what comes back, so a role that changes its texture or its roughness changes the sheet on the website with it. No GPU, no WebGL, no renderer string to check — Canvas2D and the browser's own WebP encoder, about two seconds for all sixteen tiles at 97 kB total. `check-no-binaries` still passes: these live under `scripts/`, and the tiles are written into the lumbridge-v4 checkout, not this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
0cc2126e85 |
The capture harness uses the graphics card that was there all along
`launch()` asks for `--use-angle=vulkan` and gets the Radeon RX 6700 XT. On the real film loop at 1440x900 that is 7.56 s a frame to 0.47, and about twelve cores pegged down to less than one. A 180-frame film goes from twenty-two minutes to ninety seconds; the three shipped reels go from over an hour to four minutes. The SwiftShader flag was inherited from the share-card script and its comment said "a box with no display and no GPU". Half of that was never true. The card has been in this machine the whole time, with the amdgpu driver and a working RADV ICD, and Chrome reaches it with **no display server at all** — no DISPLAY, no XDG_RUNTIME_DIR, nothing but read access to /dev/dri/renderD128, which group `render` already grants. So the portability the flag was protecting is intact: this still runs over ssh and under cron. Naming the backend is the whole trick, and most of the plausible spellings are traps. `--use-angle=gl`, `--use-gl=egl`, `--use-gl=desktop`, `--enable-features=Vulkan` and passing no GL flags at all were each measured on this box, and all five land on SwiftShader while reporting success. Only `--use-angle=vulkan` and `--use-angle=gl-egl` reach the card. So `launch()` verifies instead of assuming. `--use-angle=vulkan` is a demand, not a preference: with the driver hidden it yields no WebGL context at all rather than falling back, the boot curtain never lifts, and the caller sits in `waitForFunction` until a three-minute timeout before failing with something that looks nothing like "there is no GPU here". Reading `UNMASKED_RENDERER_WEBGL` once per run costs about half a second and turns that into a printed line and a slow, correct render. Both branches are tested; the sabotaged one was verified with `VK_LOADER_DRIVERS_DISABLE`. The pictures are the same pictures: mean absolute difference of half a level out of 255, confined to MSAA edges and to cloud shadow that already drifts between any two runs of the same renderer. Also corrects every comment across the four scripts that asserted three frames a second or twenty-two minutes a reel. Those were true when written and are not now, and the `reducedMotion` rationale in two of them was resting on the number: the real reason a chapter flight cannot be waited out is that it is ~40 frames however fast they are drawn, which was twenty seconds under software GL and is a shorter race on the GPU — still a race. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
68a5165d6f |
The hero gets its own film, shot bare and encoded for a page that autoplays it
`hero-soma` is the landing backdrop: SoMa, midnight to midnight, 180 frames at eight minutes each so the loop closes without repeating a minute. It is the only film shot `chrome: "bare"` — the figures keep the app's panel because its clock is their caption, but a hero has a headline on top and a second interface under that is two interfaces arguing. It is also the only one that runs a full 24 hours, because the page seeks it to the reader's local hour and a film starting at 04:40 has no frame for somebody at two in the morning. Whole-board was the first camera and was wrong: at that standoff the edge of the terrain plate stands against the sky across the top of the frame, and midday is a lot of pale sand. Encoding is two-stage now — frames to a CRF 18 master, deliverables derived from it. A time-lapse is near the worst case for inter-frame compression, because the camera never moves but every pixel changes as the light does, and at the figure default of CRF 22 this reel came out at 2.5 MB. Deriving from a master turned 'try CRF 28' into ten seconds instead of a twenty-two minute re-shoot, and it keeps what ships identical to what this script makes rather than something hand-rolled with ffmpeg the day the size became a problem. It lands at 1440w CRF 28 (1.0 MB) plus a 960w CRF 30 (402 kB) for small screens, because <video> has no srcset and a phone has no use for the wide encode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ff23f5d4bd |
Films reach the site the same way stills do
`films.mjs` now writes into the sibling lumbridge-v4 checkout — the MP4s and their posters into `apps/web/public/films/`, and a generated `films.ts` beside the shots manifest, carrying each reel's caption, alt text and running time. Same shape as `shots.mjs`, including the `FilmId` union that turns a page naming a deleted reel into a typecheck failure rather than a dead <video>. `--publish <dir>` puts an existing render in front of the site without re-shooting it. Twenty-two minutes a reel is long enough that the alternative would have been `cp`, and a hand-copied artefact is the thing this pipeline exists to not have. `--frames` refuses to publish at all: a 24-frame stutter is a rough cut, not something to put on a website by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b4eb2fa1a8 |
The engine can be filmed, by stepping its clock rather than recording it
`films.mjs` renders time-lapses of the running dist/: a locked camera watching eighteen hours pass, shadow sweeping the city and the windows coming on as the computed sun goes down. Two ship — the Financial District and the whole board. Real-time capture was never on the table and would have been the wrong idea anyway. SwiftShader draws about three frames a second with no GPU, so a screen recording is a slideshow; and a sunset takes an hour, which is not a length of video anybody watches. So it is rendered offline the way films always have been: set the clock, let the frame settle, expose, advance. The output is smooth 30fps regardless of what the renderer managed while being photographed. Two shims in `filmClock()` get that done without a reload per frame, which matters because a reload is fifteen seconds and would put a 180-frame film at three quarters of an hour. The skew is a live global read on every `Date` call rather than baked in at load; and `setInterval(…, 60_000)` is compressed to 120ms, which is the load-bearing half — `main.ts` recomputes the sun on a once-a-minute wall-clock tick, so a shifted clock otherwise sits unrendered. Only the 60-second interval is touched, by value. Both are capture-harness lies told to the page. The deployed bundle has no idea this file exists. Not wired into the site. This is the capability and two reels to look at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
138a342c17 |
The frame matches the page's light rather than opposing it
Correcting the pairing this script documents: the daylit frame goes on the light page and the night frame on the dark one. A reader in dark mode has asked for a dark room, and a bright noon render is a hole punched in it; the hairline frame around each figure is what keeps a night shot from dissolving into the page, which is the border's job rather than the sun's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d04a75b9bd |
Every shot is taken twice, and the site shows you the other one
Each of the seven cameras now fires at two clocks — one daylight frame, one around sixteen degrees below the horizon — and lumbridgecorp.com loads whichever is the opposite of the reader's theme. A night render on that site's dark page is a dark rectangle on a dark page: the edges dissolve and the picture stops being an object. A daylit one on the white page has the same problem in reverse, and the hazier frames wash out entirely. It is also the cheapest demonstration available of the claim those pages lean on hardest. The sun is computed from a real time rather than themed, and a reader who doubts it can hit the light/dark toggle and watch the same city change hour with nothing else about the geometry moving. Shot ids lose their time of day — `soma-afternoon` is `soma`, `fidi-night` is `fidi`, `peninsula-morning` is `peninsula` — because none of them names one hour any more. The manifest gains a `Frame` type: `at`, `src`, `alt` and a `note` belong to an hour, while `place` and `caption` are true of both, so a caption cannot end up describing light that is not in the frame under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
37d5320664 |
Both doors get a share card, and they are not the same card
A link to tera.lumbridgecorp.com or office.lumbridgecorp.com unfurled as a bare
blue URL. No picture, no sentence, and a title — "Lumbridge Simulate — San
Francisco" — that was wrong at one of the two doors and stale at the other.
There were no `og:` or `twitter:` tags in the document at all. For a project
whose entire pitch is that you should look at it, that is the most expensive
missing markup in the repo.
Both cards are screenshots of the running app, not drawings of it, and that is
the load-bearing decision rather than a shortcut. `scripts/brand-assets/` builds
them in two passes: shoot the city and the office out of `dist/`, then render
`og.html` over those shots at exactly 1200x630. The thing is worth looking at,
and a drawing of it goes stale in silence — which is not hypothetical. The card
currently live on lumbridgecorp.com is a viewport screenshot of a marketing page
that has since been rewritten, so that preview advertises a positioning the site
no longer uses, and it has been doing so for a month with nobody noticing. A
card regenerated from `dist/` by one command is a card that can be kept true by
running the command.
The clock is shifted to midday for the capture, because the sun is real —
`observe()` computes it from `new Date()` — and a card regenerated at two in the
morning is an honest photograph of a black rectangle. Shifted rather than
frozen: everything else runs off `requestAnimationFrame`, and a stopped clock
stalls the frame loop the screenshot is waiting on.
Every string on the cards is the project's own. The headlines are what
`README.md` already says each half is; "Clone it and it works" is CONTRACT.md
§0's acceptance test in the words `main.ts` uses for it; the licence in the
corner is the one in the repo root. A share card is the most-read and least
reviewed sentence a project has, which is exactly why it should not be where new
claims get invented.
The harder half was that the two doors are one bundle. The app sorts out which
door it is by reading its own hostname; a crawler cannot, because it reads the
HTML and nothing else — so one `index.html` means both doors unfurl as the same
place, and being a different place is the office door's whole reason to exist. A
second hand-written shell is what the Caddy config already argues against for the
static root ("sharing the root rather than copying it means a deploy cannot leave
the two doors on different builds"), and two 900-line files each carrying the
inline stylesheet would drift on the first CSS change with nothing to notice.
So the build emits both. Everything outside the `ogc:` markers is copied byte for
byte — verified: `office.html` and `index.html` are identical below `</head>` and
point at the same bundle hash — and only the head block differs. A change to the
interface reaches both doors by construction. It runs in `writeBundle` rather
than `transformIndexHtml` because it needs the finished document, after Vite has
rewritten the asset URLs, and it errors rather than no-oping if the markers go
missing: a card that is quietly the wrong one is the failure the plugin exists to
prevent.
`deploy/Caddyfile.snippet` documents the one line that turns it on — the office
door's `try_files` fallback pointing at `/office.html` off the same shared root.
A deployment that skips it is not broken; the office door keeps working and
unfurls with the city's card, which is what it did before.
Not deployed. The live Caddyfile still falls back to /index.html for the office
host, so this needs that one-line change on cloud-2 before office.
lumbridgecorp.com unfurls as the office.
Both shells boot clean in Chrome with zero console errors, 31 client tests and
the no-binary gate still pass — `public/` is exempt from it, which is where the
two PNGs live.
|
||
|
|
44c5a79424 |
SoCal, the whole bay, a moon, and gates that actually run
Six agents in parallel, and the two city packs independently reported the same blocker: `focusRegions` and `coarseFactor` existed on the `City` type and nothing implemented them. Uniform lattices would have been 2.9M points for Southern California and 3.7M for the expanded bay. Both packs were unloadable as written. `buildAxis` is the answer, and it is honest about its limits: refinement is per axis, not per rectangle, so a focus region sharpens its whole row *and* its whole column. Two regions at opposite corners refine nearly everything between them. Measured, not guessed — the bay went 0.53M points with one region and 1.64M with three, for detail nobody is looking at from a board this wide. One region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s. Then three things that were only ever right because San Francisco was the only city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a 230-unit board; the bay is 1003 units across and the camera physically could not retreat far enough to frame it. Fog distances were scene units pinned to the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest weather, which over ninety-four kilometres of bay correctly hides three quarters of it — the night view was a black rectangle for a completely reasonable reason. All three now derive from the board. The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a physical ratio of one to four hundred thousand. What is being reproduced is what a moonlit night looks like on a screen in a lit room. The CI gate caught itself, which is the part worth keeping. Port 8431 was already held by a server from an earlier session, so the boot check polled a healthy stranger while the process it started died on EADDRINUSE. It now refuses to run rather than pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |