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>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* The before/after pair, at every authored chapter of all three packs.
|
||||
*
|
||||
* `TERA_RECONCILE` exists so that four data changes can be *looked at* rather
|
||||
* than argued about, and the acceptance for every one of them is a photograph:
|
||||
* a board that is worse in the picture means that rule reverts. Twenty-six
|
||||
* chapters times two flag states is fifty-two frames, which is more than anyone
|
||||
* will take one `look.mjs` invocation at a time — so this is one command, one
|
||||
* preview and one browser, and it names its output after the chapter's identity
|
||||
* rather than after its position in a list.
|
||||
*
|
||||
* node scripts/reconcile-shots.mjs [--dist <dir>] [--out <dir>]
|
||||
* [--rules <a,b>] [--only <board>]
|
||||
* [--at <iso>] [--wait <ms>]
|
||||
*
|
||||
* `--rules` is passed through as `?reconcile=<rules>`, so a single rule can be
|
||||
* photographed alone — which is the point of the four being independently
|
||||
* switchable in the first place.
|
||||
*
|
||||
* The port and build checks are `look.mjs`'s, for `look.mjs`'s reason: this box
|
||||
* once accumulated a hundred and forty-seven abandoned previews in an afternoon
|
||||
* and photographed somebody else's dist through one of them.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { createServer } from "node:net";
|
||||
import { chromium } from "playwright";
|
||||
import CALIFORNIA from "../src/cities/california.ts";
|
||||
import SF from "../src/cities/sf.ts";
|
||||
import SOCAL from "../src/cities/socal.ts";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, fallback) => {
|
||||
const i = args.indexOf(name);
|
||||
return i === -1 ? fallback : args[i + 1];
|
||||
};
|
||||
|
||||
const DIST = flag("--dist", "dist");
|
||||
const OUT = flag("--out", "/tmp/tera-look/reconcile");
|
||||
const RULES = flag("--rules", "1");
|
||||
const ONLY = flag("--only", null);
|
||||
const AT = flag("--at", "2026-08-21T20:00:00Z");
|
||||
const WAIT = Number(flag("--wait", "6000"));
|
||||
const FIRST_WAIT = Number(flag("--first-wait", "11000"));
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
/**
|
||||
* The two California chapters that are doors, not poses.
|
||||
*
|
||||
* `main.ts` matches them against `CALIFORNIA_DESTINATIONS` and calls
|
||||
* `switchCity()` rather than `flyTo()`, so clicking one leaves the board — which
|
||||
* is what made `look.mjs`'s `glyph-la` and `glyph-sf` presets photographs of the
|
||||
* wrong board for as long as they existed. They are shot anyway, because a
|
||||
* before/after pair at *every* authored chapter is what was asked for and
|
||||
* because the frame they produce is a real frame of the product; the page is
|
||||
* reloaded afterwards so the next chapter is aimed from California again.
|
||||
*/
|
||||
const DOORS = new Set(["los-angeles", "san-francisco"]);
|
||||
|
||||
const BOARDS = [CALIFORNIA, SF, SOCAL].filter((city) => ONLY === null || city.id === ONLY);
|
||||
|
||||
const PORT = await new Promise((resolve, reject) => {
|
||||
const probe = createServer();
|
||||
probe.once("error", reject);
|
||||
probe.listen(0, "127.0.0.1", () => {
|
||||
const { port } = probe.address();
|
||||
probe.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
|
||||
const server = spawn(
|
||||
new URL("../node_modules/.bin/vite", import.meta.url).pathname,
|
||||
["preview", "--outDir", DIST, "--port", String(PORT), "--strictPort"],
|
||||
{ detached: true, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
const shutdown = () => {
|
||||
try {
|
||||
process.kill(-server.pid, "SIGTERM");
|
||||
} catch {
|
||||
/* already gone */
|
||||
}
|
||||
};
|
||||
process.on("exit", shutdown);
|
||||
const bound = await new Promise((resolve) => {
|
||||
let seen = "";
|
||||
const settle = setTimeout(() => resolve(null), 30000);
|
||||
const read = (chunk) => {
|
||||
seen += String(chunk);
|
||||
const match = /http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/.exec(seen);
|
||||
if (match) {
|
||||
clearTimeout(settle);
|
||||
resolve(Number(match[1]));
|
||||
}
|
||||
};
|
||||
server.stdout.on("data", read);
|
||||
server.stderr.on("data", read);
|
||||
});
|
||||
if (bound !== PORT) {
|
||||
console.error(`reconcile-shots: preview bound ${bound}, not ${PORT} — refusing to photograph it`);
|
||||
shutdown();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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: 1600, height: 1000 },
|
||||
deviceScaleFactor: 1,
|
||||
timezoneId: "America/Los_Angeles",
|
||||
// The opening arrival collapses to a cut under this, which is the only way
|
||||
// fifty-two frames taken over half an hour are the same fifty-two frames when
|
||||
// the box is busy.
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
await context.clock.setFixedTime(new Date(AT));
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") errors.push(m.text());
|
||||
});
|
||||
|
||||
async function land(url) {
|
||||
await page.goto(`http://localhost:${PORT}${url}`, { waitUntil: "networkidle", timeout: 60000 });
|
||||
await page.waitForTimeout(FIRST_WAIT);
|
||||
try {
|
||||
await page.getByText(/^Skip$/).first().click({ timeout: 2000 });
|
||||
await page.waitForTimeout(1200);
|
||||
} catch {
|
||||
/* already dismissed, or not shown */
|
||||
}
|
||||
}
|
||||
|
||||
let taken = 0;
|
||||
for (const city of BOARDS) {
|
||||
for (const state of ["off", "on"]) {
|
||||
const url = `/?city=${city.id}${state === "on" ? `&reconcile=${RULES}` : ""}`;
|
||||
await land(url);
|
||||
for (const chapter of city.chapters) {
|
||||
// Aim by identity. `data-view` is the chapter's own id, so a pack that is
|
||||
// reordered under this script still photographs the chapter it names —
|
||||
// the failure `look.mjs`'s two broken presets were an instance of.
|
||||
const button = page.locator(`.chapter[data-view="${chapter.id}"]`).first();
|
||||
try {
|
||||
await button.click({ timeout: 5000 });
|
||||
} catch {
|
||||
console.log(`reconcile-shots: could not click ${city.id}/${chapter.id}`);
|
||||
await land(url);
|
||||
continue;
|
||||
}
|
||||
await page.waitForTimeout(WAIT);
|
||||
const path = `${OUT}/${city.id}-${chapter.id}-${state}.png`;
|
||||
await page.screenshot({ path });
|
||||
taken += 1;
|
||||
console.log(`reconcile-shots: ${path}`);
|
||||
if (DOORS.has(chapter.id)) await land(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const real = errors.filter((e) => !/404|Failed to load resource/.test(e));
|
||||
console.log(`reconcile-shots: ${taken} frames into ${OUT}`);
|
||||
console.log(real.length === 0 ? "reconcile-shots: no console errors" : `reconcile-shots: ERRORS ${JSON.stringify([...new Set(real)].slice(0, 5))}`);
|
||||
|
||||
await browser.close();
|
||||
shutdown();
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user