/** * One California, from three packs — as **one pack**, not three boards. * * ## Why this exists, and why it is not `reconcile.ts` * * `reconcile.ts` makes the three packs *agree*: one projection, one relief-in- * frame, one road width. It leaves three boards that draw the same California * and hands the camera between them. That was the answer to "the boards feel * like three products". It is not the answer to "I want one full California", * which is this file: a single `City` carrying the state's landform and both * metros' detail, so the engine builds **one world, one lattice and one terrain * mesh** and there is no seam anywhere to stitch. * * ## Why one pack rather than three scenes nested in each other * * The first attempt nested each metro's built scene inside the state board's * under a similarity transform. The transform was right — measured, both metros * landed where they belong at the size they should be — but two independently * built terrains then occupied the same ground and interpenetrated: a block sat * neatly on a coarse hill in one place and was half-buried in it three hundred * metres away. That is nested-grid stitching, and the way to not have the * problem is to not have two grids. One pack has one. * * ## The lattice is affordable, and the number that says so was measured * * `TODO.md` records a statewide lattice at metro fidelity as 23.7M points and * dead. That is true at San Francisco's own 45 m cell, because `buildAxis` * refines per **axis** — a focus region refines its whole row *and* its whole * column, a plus rather than a box — so two metros at opposite corners refine * most of the state. The blowup is a function of the fine cell, and it is * gentle until 45 m: * * | fine cell | coarse | lattice | points | memory | * | --- | --- | --- | --- | --- | * | 45 m | x16 | 6002 x 7600 | 45.6M | 411 MB | * | 100 m | x8 | 1676 x 2002 | 3.36M | 30 MB | * | **150 m** | **x8** | **1120 x 1349** | **1.51M** | **14 MB** | * | 200 m | x8 | 850 x 1013 | 0.86M | 8 MB | * * All three of today's boards together are 0.75M points. So 150 m costs about * twice the whole product's current terrain and buys ground **23 times finer * than the state board's 3,473 m cell** everywhere a city stands. No quadtree, * no per-rectangle refinement, no change to `buildAxis` at all. * * ## What this file must do beyond concatenating arrays * * Everything in a `City` that describes a place is absolute — hills, districts, * landmarks, bridges, ports and airports are all lat/lng — so most of the merge * is a union. Two things are not: * * - **Hills sum.** `World.elevationAt` blends every hill that covers a point * (tallest, plus 35% of the rest), so leaving the state's coarse hills under * a metro's fine ones *adds* their heights and lifts the whole Bay. The * state's hills are dropped where a metro replaces them. * - **Districts overlap.** The state's seventeen districts include coarse * stand-ins for Los Angeles and San Francisco; the metros carry 99 real * ones. Both drawn is two cities in one place, so the coarse stand-ins go. * * Both filters are by centre-in-bounds, which is honest but not exact: a state * hill centred outside a metro with a radius reaching into it still contributes. * At the state's hill radii that is a gentle lift at the edge of a metro rather * than a doubled mountain, and it is recorded here rather than hidden. */ import type { City, District, FocusRegion, Hill, LatLng } from "../engine/types.ts"; import { reconciledCity } from "./reconcile.ts"; import CALIFORNIA from "./california.ts"; import SAN_FRANCISCO from "./sf.ts"; import SOCAL from "./socal.ts"; /** * The fine cell, in metres, and the factor the rest of the state is drawn at. * * **300 m, and it was measured down to that from 150.** The first pass used * 150 m / x8, which is 1.23M lattice points, and the merged board then rendered * **1,114,226 triangles at the whole-board pose against a 440,000 cap** — with * every metro building already hidden by the detail LOD. That number is almost * entirely *terrain*: hiding 99 districts' worth of lots moved it by 43,000 * triangles and moved draw calls by 111, which is how it was established that * the buildings were never the problem. * * So the fine cell is what a statewide board can afford, and the honest ceiling * on this engine — with no terrain level of detail, which is the piece that does * not exist yet — is around 300 m / x16, or 0.18M points. That is **11.6x finer * than the state pack's 3,473 m cell** and 6.7x coarser than San Francisco's own * 45 m board. * * Closing that last gap is terrain LOD, and `TODO.md` already names its three * blockers: placement drift (everything is placed once against `groundAt`), the * shadow caster (15x triangles if selected independently), and determinism (the * budget harness assumes geometry is not a function of camera path). None of * them is in this file's way, and all of them are in that one's. */ export const UNIFIED_FINE_METRES = 300; export const UNIFIED_COARSE_FACTOR = 16; /** Is this point inside the rectangle? */ function within(lat: number, lng: number, r: FocusRegion): boolean { return lat >= r.minLat && lat <= r.maxLat && lng >= r.minLng && lng <= r.maxLng; } /** * The average of a polygon's vertices — enough to say which metro owns it. * * `LatLng` is a `[number, number]` tuple in this codebase and not an object, so * the destructuring below is the whole of the type story. */ function centroid(polygon: readonly LatLng[]): { lat: number; lng: number } { let lat = 0; let lng = 0; for (const [pLat, pLng] of polygon) { lat += pLat; lng += pLng; } const n = Math.max(1, polygon.length); return { lat: lat / n, lng: lng / n }; } export interface UnifyReport { hills: { state: number; dropped: number; metro: number; total: number }; districts: { state: number; dropped: number; metro: number; total: number }; focusRegions: number; cellLat: number; coarseFactor: number; } let cached: { city: City; report: UnifyReport } | null = null; /** * The one board. Memoised, because `World` builds one per mount and the merge * walks every district polygon in the product. */ export function unifiedCalifornia(): { city: City; report: UnifyReport } { if (cached !== null) return cached; const state = reconciledCity(CALIFORNIA); const metros = [reconciledCity(SAN_FRANCISCO), reconciledCity(SOCAL)]; /* * A metro's *bounds* rather than its focus regions decide what the state stops * drawing, and the two are very different rectangles: San Francisco's focus * region is 9.2% of its own board and its bounds are all of it. The state's * coarse stand-in for a city covers the whole metro, so the whole metro is * what has to be cleared — clearing only the focus region would leave a coarse * Oakland beside a fine San Francisco. */ const claimed: FocusRegion[] = metros.map((m) => ({ ...m.bounds })); const claims = (lat: number, lng: number): boolean => claimed.some((r) => within(lat, lng, r)); const stateHills = state.hills.filter((h: Hill) => !claims(h.lat, h.lng)); const metroHills = metros.flatMap((m) => m.hills); const stateDistricts = state.districts.filter((d: District) => { const c = centroid(d.polygon); return !claims(c.lat, c.lng); }); /* * Marked `detail`, which is the flag `createBlocks` orders on and `scene.ts` * reveals by camera stand-off. Without it the merged board measured 1,157,802 * triangles at the whole-board pose against a 440,000 cap, for buildings that * are sub-pixel at 1,919 m to the unit. The lots exist either way; the flag * decides when they are drawn. */ const metroDistricts: District[] = metros.flatMap((m) => m.districts.map((d) => ({ ...d, detail: true as const })), ); /* * ---- Chapters: the state's, and only the state's ------------------------- * * **Tried, photographed, reverted.** Merging all twenty-four rungs onto this * board is arithmetically easy — `focus.lat/lng` is absolute, `distance` is a * horizontal length in the owning board's units and `height` has already been * through that board's exaggeration, so each converts by keeping its true * metres. That was done, and the camera arrives exactly where it should. * * The frame it arrives at is the problem, and it is a fact about the board * rather than about the conversion. Flying to FiDi puts the camera about two * scene units from its target on a board that is 551 units across, and what * is at that range is *state-scale content*: `createFreewayWorld` draws the * corridor as a deliberate 4.7 km-wide atlas glyph — `main.ts` says so where * it sets the corridor altitude, because DRIVE mode has to be able to drive * down it on a board where a real freeway is a fifth of a pixel — and the * terrain under it is 300 m cells. The delivered picture is a black slab * across San Francisco. * * So a rung that flies you into that is worse than not having the rung. The * metro chapters come back when the corridor is drawn at true width on this * board, which is `createFreewayWorld`'s hard-coded scene units and the * `roads` reconciliation rule between them — not this file. */ const mergedChapters = state.chapters; const fine = UNIFIED_FINE_METRES / 111_320; const city: City = { ...state, /* * The state's own id, on purpose. Every consumer that keys off a board — the * fire gate, the ladder's region table, the budget harness's `data-board` * assertion, `?city=` — already knows "california", and this *is* California. * A new id would mean teaching all of them about a fourth board, which is * the opposite of the point. */ id: state.id, name: state.name, cellLat: fine, // The same lat:lng cell ratio the state pack authored, so cells stay square // on the ground at this latitude rather than becoming letterboxes. cellLng: fine * (CALIFORNIA.cellLng / CALIFORNIA.cellLat), coarseFactor: UNIFIED_COARSE_FACTOR, focusRegions: metros.flatMap((m) => m.focusRegions ?? []), hills: [...stateHills, ...metroHills], districts: [...stateDistricts, ...metroDistricts], landmarks: [ ...state.landmarks, ...metros.flatMap((m) => m.landmarks.map((l) => ({ ...l, detail: true as const }))), ], bridges: [...state.bridges, ...metros.flatMap((m) => m.bridges)], parks: [...state.parks, ...metros.flatMap((m) => m.parks)], inlandWater: [...state.inlandWater, ...metros.flatMap((m) => m.inlandWater)], airports: [...(state.airports ?? []), ...metros.flatMap((m) => m.airports ?? [])], ports: [...(state.ports ?? []), ...metros.flatMap((m) => m.ports ?? [])], chapters: mergedChapters, /* * Landmasses are NOT unioned. `isLand` is a point-in-any-polygon test, so a * metro's finer coastline can only ever add land the state's outline already * claimed — while costing a polygon test per sample over a 1.5M-point * lattice. The state's coastline is the state's coastline. */ landmasses: state.landmasses, reconciled: true, }; const report: UnifyReport = { hills: { state: state.hills.length, dropped: state.hills.length - stateHills.length, metro: metroHills.length, total: city.hills.length, }, districts: { state: state.districts.length, dropped: state.districts.length - stateDistricts.length, metro: metroDistricts.length, total: city.districts.length, }, focusRegions: city.focusRegions?.length ?? 0, cellLat: fine, coarseFactor: UNIFIED_COARSE_FACTOR, }; cached = { city, report }; return cached; }