1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/scripts/merge-feasibility.mjs
T
karti 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>
2026-08-23 06:20:18 -07:00

339 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* What one statewide board would actually cost, measured rather than argued.
*
* node scripts/merge-feasibility.mjs [--no-still] [--out <dir>]
*
* **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);