/**
* What one statewide board would actually cost, measured rather than argued.
*
* node scripts/merge-feasibility.mjs [--no-still] [--out
]
*
* **Nothing here is wired into the product.** No board id, no route, no flag
* that reaches a deploy. It builds the merged pack in node, counts what comes
* out, and takes one photograph — and the photograph is the deliverable, because
* the decision it supports is not an engineering one.
*
* ## The question
*
* "Why are there still three boards" has an answer that costs nothing —
* `cities/reconcile.ts`, four data rules — and an answer that costs a
* rebuild: one pack, one lattice, one lot size, from Yreka to the border.
* The second one is priced here.
*
* A lot is `LOT = 0.42` **scene units** (`blocks.ts`), so how much ground a
* building stands on is decided entirely by the board's metres-per-unit: 40 m on
* the Bay Area, 164 m on Southern California, 806 m on the state. A merged board
* therefore has to pick one, and the pick is the whole decision:
*
* - at Bay Area density the state is **dead** — the anonymous city alone is
* tens of millions of triangles against a 2.6M desktop cap;
* - at Southern California density it **fits**, and San Francisco is drawn the
* way Southern California is drawn now: 164 m lots instead of 40 m.
*
* The second is a real option and it costs the product its best-looking board.
* That trade is the owner's to make against a picture, which is why this ends
* with `--out/merge-sf-40m-vs-164m.png` rather than with a recommendation.
*
* ## What the still actually varies
*
* `LOT`, not `latScale`. Rescaling San Francisco's pack would move its camera,
* its terrain lattice and every chapter pose at once, and the resulting pair
* would differ in a dozen ways of which lot size was one. Changing `LOT` to
* `0.42 × (390.6 / 94.34) = 1.7392` in a **throwaway tree under /tmp** puts
* exactly 164 m of ground under a building and leaves the frame otherwise
* identical — which is the comparison that was asked for. `blocks.ts` in this
* repo is not touched, and `git status` after a run says so.
*/
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { performance } from "node:perf_hooks";
import CALIFORNIA from "../src/cities/california.ts";
import SF from "../src/cities/sf.ts";
import SOCAL from "../src/cities/socal.ts";
import { createBlocks } from "../src/engine/blocks.ts";
import { createTerrain } from "../src/engine/terrain.ts";
import { World } from "../src/engine/world.ts";
const args = process.argv.slice(2);
const flag = (name, fallback) => {
const i = args.indexOf(name);
return i === -1 ? fallback : args[i + 1];
};
const OUT = flag("--out", "/tmp/tera-look/merge");
const STILL = !args.includes("--no-still");
mkdirSync(OUT, { recursive: true });
const REPO = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
const BUDGETS = JSON.parse(readFileSync(`${REPO}/scripts/performance-budgets.json`, "utf8"));
// ---- The merged pack --------------------------------------------------------
/** Does a lat/lng fall inside a board's rectangle? */
const inside = (city, lat, lng) =>
lat >= city.bounds.minLat && lat <= city.bounds.maxLat &&
lng >= city.bounds.minLng && lng <= city.bounds.maxLng;
const centroid = (polygon) => {
let lat = 0;
let lng = 0;
for (const [a, b] of polygon) {
lat += a;
lng += b;
}
return [lat / polygon.length, lng / polygon.length];
};
/**
* One California, at whatever density the caller asks for.
*
* The two metro rectangles do not intersect and California contains both, so
* merging the districts is a union with one subtraction: a state district whose
* centre falls inside a metro board is dropped, because the metro pack draws
* that ground in far more detail. That is 116 authored districts before the
* subtraction — which is the number that kills a per-axis focus lattice, since
* two focus regions at opposite corners refine nearly the whole board.
*/
function merged(metresPerUnit, dropCovered = true) {
const latScale = 111_320 / metresPerUnit;
const metros = [SF, SOCAL];
const covered = (polygon) => {
const [lat, lng] = centroid(polygon);
return metros.some((city) => inside(city, lat, lng));
};
const districts = [
...CALIFORNIA.districts.filter((d) => !dropCovered || !covered(d.polygon)),
...SF.districts,
...SOCAL.districts,
];
return {
...CALIFORNIA,
id: "california-merged",
name: "California",
latScale,
// California's own lattice, unchanged: it is the only ground cell that
// covers 989,000 km² at all. Southern California's coarse cell over the
// state is 1.6M lattice points and 21 s of single-threaded field.
landmasses: [...CALIFORNIA.landmasses, ...SF.landmasses, ...SOCAL.landmasses],
inlandWater: [...CALIFORNIA.inlandWater, ...SF.inlandWater, ...SOCAL.inlandWater],
parks: [...CALIFORNIA.parks, ...SF.parks, ...SOCAL.parks],
hills: [...CALIFORNIA.hills, ...SF.hills, ...SOCAL.hills],
districts,
roads: [...CALIFORNIA.roads, ...SF.roads, ...SOCAL.roads],
landmarks: [...CALIFORNIA.landmarks, ...SF.landmarks, ...SOCAL.landmarks],
bridges: [...SF.bridges, ...SOCAL.bridges],
airports: [...(SF.airports ?? []), ...(SOCAL.airports ?? [])],
ports: [...(SF.ports ?? []), ...(SOCAL.ports ?? [])],
reconciled: true,
};
}
const triangles = (mesh) => {
const geometry = mesh.geometry;
const perInstance = geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3;
return perInstance * (mesh.isInstancedMesh ? mesh.count : 1);
};
function measure(label, metresPerUnit, dropCovered = true) {
const city = merged(metresPerUnit, dropCovered);
const world = new World(city);
// `createBlocks` samples the land and park masks, so the field is built here,
// lazily, on this thread — which is also where the placement time below is
// measured from, and is why the field build is outside the timer.
world.lattice();
const t0 = performance.now();
const blocks = createBlocks(world);
const placementMs = performance.now() - t0;
const lat = (city.bounds.maxLat - city.bounds.minLat) * city.latScale;
const lng = (city.bounds.maxLng - city.bounds.minLng) * world.lngScale;
return {
label,
metresPerUnit,
lotMetres: 0.42 * metresPerUnit,
extent: [lng, lat],
districts: city.districts.length,
instances: blocks.count,
triangles: triangles(blocks),
placementMs,
world,
};
}
console.log("=== the merged pack, built and counted ===\n");
console.log("A lot is 0.42 scene units, so lot size in metres is the board's scale and nothing else.");
console.log("These are the ANONYMOUS CITY only — `createBlocks`, one instanced mesh — which is the");
console.log("layer that scales with density and the layer that decides the answer.\n");
const socalDensity = measure("SoCal density", 111_320 / 285);
const bayDensity = measure("Bay Area density", 111_320 / 1180);
/**
* The same two boards without the subtraction, because the subtraction is a
* judgement and the reader should be able to see what it is worth.
*
* Keeping all 116 authored districts means the state pack's coarse Bay Area and
* Los Angeles polygons place a second, 164 m city on top of the metro packs'
* own. It is the wrong merge — but it is the honest upper bound, and quoting a
* number without saying which of the two it is has already produced one 1.8x
* disagreement in this round's own brief.
*/
const socalDensityAll = measure("SoCal density, all 116 districts", 111_320 / 285, false);
const bayDensityAll = measure("Bay Area density, all 116 districts", 111_320 / 1180, false);
const cap = BUDGETS.scenes["bay-area"].desktop.maxTriangles;
for (const row of [socalDensity, socalDensityAll, bayDensity, bayDensityAll]) {
console.log(`${row.label}`);
console.log(` metres per unit ${row.metresPerUnit.toFixed(2)} lot ${row.lotMetres.toFixed(0)} m`);
console.log(` board extent ${row.extent[0].toFixed(0)} x ${row.extent[1].toFixed(0)} units`);
console.log(` districts ${row.districts}`);
console.log(` instances ${row.instances.toLocaleString("en-US")}`);
console.log(` triangles ${row.triangles.toLocaleString("en-US")}` +
` (${(row.triangles / cap).toFixed(2)}x the bay-area desktop cap of ${cap.toLocaleString("en-US")})`);
console.log(` placement ${row.placementMs.toFixed(0)} ms on the main thread (NOISY — see below)\n`);
}
/**
* Read the counts, not the clock.
*
* Instances and triangles are a function of the pack and the seeded RNG and come
* back identical to the digit on every run. The placement millisecond does not:
* five runs of the *same* build on this box gave 323, 554, 617, 909 and 1,989 ms
* for the same 93,253 lots — a factor of six, on a box whose GPU never leaves
* 500 MHz of a possible 2,725 and which usually has two other agents on it. The
* brief's 432 ms is inside that spread and so is almost anything else. Quote the
* counts; treat the milliseconds as an order of magnitude.
*/
console.log("Placement time on this box spans ~320-2,000 ms for the same 93,253 lots across runs.");
console.log("The instance and triangle counts are deterministic to the digit. Trust those.\n");
// The terrain the merged board would carry, on California's own lattice. Held
// back until after the block counts because it is the cheap half and saying so
// in the wrong order invites the wrong conclusion.
{
const t0 = performance.now();
const terrain = createTerrain(socalDensity.world);
const ms = performance.now() - t0;
let tris = 0;
terrain.traverse((node) => {
if (node.isMesh) tris += triangles(node);
});
console.log(`terrain, on California's 3,473 m cell: ${tris.toLocaleString("en-US")} triangles, ${ms.toFixed(0)} ms`);
console.log(`whole merged board at SoCal density: ${(tris + socalDensity.triangles).toLocaleString("en-US")} triangles\n`);
}
console.log("Today, for comparison — measured by scripts/performance-budget.mjs at bcac6aa:");
console.log(" california 375,351 tri / 374 draws socal 1,429,993 / 218 bay-area 2,264,956 / 209\n");
// ---- The picture ------------------------------------------------------------
if (!STILL) {
console.log("merge-feasibility: --no-still, stopping before the photograph");
process.exit(0);
}
/**
* Two builds of the same commit, differing in one constant.
*
* `git archive` rather than a copy of the working tree: the point of the
* picture is what a lot size does, and a working tree with three agents in it
* is not a controlled variable.
*/
const SCRATCH = `${OUT}/scratch`;
const COARSE_LOT = (0.42 * (111_320 / 285)) / (111_320 / 1180);
const trees = [
{ name: "sf-40m", lot: null },
{ name: "sf-164m", lot: COARSE_LOT },
];
for (const tree of trees) {
const root = `${SCRATCH}/${tree.name}`;
rmSync(root, { recursive: true, force: true });
mkdirSync(root, { recursive: true });
execFileSync("bash", ["-c", `cd ${REPO} && git archive HEAD | tar -x -C ${root}`]);
execFileSync("ln", ["-sfn", `${REPO}/node_modules`, `${root}/node_modules`]);
if (tree.lot !== null) {
const path = `${root}/src/engine/blocks.ts`;
const source = readFileSync(path, "utf8");
const next = source.replace(
/^const LOT = 0\.42;.*$/m,
`const LOT = ${tree.lot.toFixed(6)}; // 164 m at SF's scale — merge-feasibility.mjs, throwaway tree`,
);
if (next === source) throw new Error("merge-feasibility: could not find LOT in blocks.ts");
writeFileSync(path, next);
}
execFileSync("npx", ["vite", "build", "--outDir", `${OUT}/dist-${tree.name}`, "--emptyOutDir"], {
cwd: root,
stdio: "ignore",
});
console.log(`merge-feasibility: built ${tree.name}${tree.lot === null ? "" : ` (LOT ${tree.lot.toFixed(4)})`}`);
}
const { chromium } = await import("playwright");
const { spawn } = await import("node:child_process");
const { createServer } = await import("node:net");
async function shoot(dist, out) {
const port = await new Promise((resolve, reject) => {
const probe = createServer();
probe.once("error", reject);
probe.listen(0, "127.0.0.1", () => {
const { port: p } = probe.address();
probe.close(() => resolve(p));
});
});
const server = spawn(`${REPO}/node_modules/.bin/vite`,
["preview", "--outDir", dist, "--port", String(port), "--strictPort"],
{ detached: true, stdio: ["ignore", "pipe", "pipe"], cwd: REPO });
await new Promise((resolve) => {
const settle = setTimeout(resolve, 30000);
const read = (chunk) => {
if (/http:\/\/(?:localhost|127\.0\.0\.1):\d+/.test(String(chunk))) {
clearTimeout(settle);
resolve();
}
};
server.stdout.on("data", read);
server.stderr.on("data", read);
});
const browser = await chromium.launch({
channel: "chrome",
args: ["--use-gl=angle", "--use-angle=vulkan", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"],
});
const context = await browser.newContext({
viewport: { width: 1200, height: 900 },
deviceScaleFactor: 1,
timezoneId: "America/Los_Angeles",
reducedMotion: "reduce",
});
await context.clock.setFixedTime(new Date("2026-08-21T20:00:00Z"));
const page = await context.newPage();
await page.goto(`http://localhost:${port}/?city=sf`, { waitUntil: "networkidle", timeout: 60000 });
await page.waitForTimeout(11000);
try {
await page.getByText(/^Skip$/).first().click({ timeout: 2000 });
await page.waitForTimeout(1200);
} catch {
/* not shown */
}
// Downtown, by identity rather than by position in the list.
await page.locator('.chapter[data-view="fidi"]').first().click({ timeout: 5000 });
await page.waitForTimeout(7000);
await page.screenshot({ path: out, clip: { x: 340, y: 0, width: 860, height: 900 } });
await browser.close();
try {
process.kill(-server.pid, "SIGTERM");
} catch {
/* already gone */
}
}
const halves = [];
for (const tree of trees) {
const path = `${OUT}/${tree.name}.png`;
await shoot(`${OUT}/dist-${tree.name}`, path);
halves.push(path);
console.log(`merge-feasibility: shot ${path}`);
}
const still = `${OUT}/merge-sf-40m-vs-164m.png`;
execFileSync("montage", [...halves, "-tile", "2x1", "-geometry", "+4+4", "-background", "#101418", still]);
console.log(`\nmerge-feasibility: ${still}`);
console.log("Left: San Francisco today, 40 m lots. Right: the same view at 164 m lots — what");
console.log("San Francisco looks like on a statewide board that fits. That picture is the decision.");
if (!existsSync(still)) process.exit(1);