PROCITY/pipeline/arcade_measure.mjs
m3ultra 30b86a9ef7 Lane E R39 (2/n): THE ARCADE, MEASURED THEN DRESSED — 3 skins live ($0, on-device, green), and the instance count taken BEFORE generating
MEASURED FIRST (pipeline/arcade_measure.mjs, over 5 seeds, off Lane A's own generatePlan):
  the arcade is ONE 42.00 m x 5 m edge, 2 blocks, 16-17 shops (17 at the default seed), frontage
  3.00-4.94 m (mean 3.87-4.05), depth 5.1-8.0 m, 4-11 of them two-storey. Walkable 5.00 m between
  lot faces. AWNING_DEPTH 2.2 from each face means the two slabs already meet 0.60 m apart over the
  centreline — the arcade IS roofed, and the roof is an accident.
  ⚠ AND THE POSTS ARE IN THE WRONG PLACE: buildings.js:643 puts them 2.00 m out from each lot face,
  i.e. +/-0.50 m from the centreline — two rows of posts 1.00 m apart down a 5.00 m lane. No
  collider, so it walks; it just reads as a colonnade planted in its own doorway. Lane B ask filed.

SHIPPED (all riding existing shared materials or stating their draw):
  facade-shuttered   768x595 (the 1.29:1 arcade lot aspect, not the pool's 1.78:1) — the dead
                     tenant. +0 draws, +0 tris, 1 of the 14 FREE facade-atlas slots (22 of 36 used).
  ground-arcade-floor 512^2 terrazzo — +1 draw TOWN-WIDE, +0 tris (the 504 m2 lane quad moves out
                     of footGeos). use:'arcade-floor' names a slot that does not exist yet, on purpose.
  ground-arcade-roof  512^2 pressed-metal + wired glass — +1 draw in the 1-2 arcade chunks, +2 tris.

pipeline/seamless_tile.py: cut a regular-pattern skin to a whole number of its own periods instead
of feathering the joint. It has a guard, because on this pair the naive version made things WORSE:
the roof's x seam went 26.7 -> 8.3 (kept), the diffusion-drawn terrazzo floor is not periodic enough
to cut on (autocorr r=0.26) and 27.9 -> 31.9 was rejected and passed through. Recorded, not hidden.

manifest: facades 48->49, grounds 14->16. validate_manifest.py 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:46:43 +10:00

104 lines
7.4 KiB
JavaScript
Raw 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.

#!/usr/bin/env node
// PROCITY Lane E — arcade_measure.mjs (R39, item 39.4)
//
// THE SIZING LAW, MADE RUNNABLE. R38 paid for the law with `paling_fence`: a perfectly good
// 1,121-tri panel became a REJECTED asset the moment Lane B counted the placement rule (5 runs ×
// 181 lots = 905 instances = 1.01 M triangles). The law is therefore: an asset's budget is
// `tris × the instance count the consuming lane's placement rule implies`, and you get that number
// BEFORE you generate. This script is that number for the arcade, so no later round has to trust a
// prose figure.
//
// node pipeline/arcade_measure.mjs [seed ...]
//
// Everything here is read out of Lane A's own `generatePlan` and Lane B's own constants; nothing
// is estimated. Constants mirrored from web/js/world/buildings.js are named at the point of use —
// if they move, this file is the second place to change (the R27 same-literal-in-two-files trap,
// declared rather than hidden).
import { generatePlan } from '../web/js/citygen/plan.js';
const AWNING_DEPTH = 2.2, AWNING_Y = 2.95, POST_INSET = 0.2; // buildings.js:21-22, :643
const FOOT = 3.5; // ground.js:14
const seeds = process.argv.slice(2).length ? process.argv.slice(2).map(Number)
: [20261990, 7, 123456, 999, 20260101];
const rows = [];
for (const s of seeds) {
const plan = generatePlan(s);
const arc = plan.districts.find((d) => d.kind === 'arcade');
const nodeById = new Map(plan.streets.nodes.map((n) => [n.id, n]));
const aEdges = plan.streets.edges.filter((e) => e.kind === 'arcade');
const len = aEdges.reduce((t, e) => {
const a = nodeById.get(e.a), b = nodeById.get(e.b);
return t + Math.hypot(b.x - a.x, b.z - a.z);
}, 0);
const width = aEdges[0].width;
const aBlocks = plan.blocks.filter((b) => b.district === arc.id);
const bIds = new Set(aBlocks.map((b) => b.id));
const lots = plan.lots.filter((l) => bIds.has(l.block));
const lotIds = new Set(lots.map((l) => l.id));
const shops = plan.shops.filter((sh) => lotIds.has(sh.lot));
const byType = {};
for (const sh of shops) byType[sh.type] = (byType[sh.type] || 0) + 1;
const skins = new Set(plan.shops.map((sh) => String(sh.facadeSkin).replace(/\.jpe?g$/, '').replace(/^facade-/, '')));
rows.push({
seed: s, len: +len.toFixed(2), width, blocks: aBlocks.length, lots: lots.length,
shops: shops.length, byType,
frontage: [Math.min(...lots.map((l) => l.w)), Math.max(...lots.map((l) => l.w))],
meanFrontage: lots.reduce((t, l) => t + l.w, 0) / lots.length,
depth: [Math.min(...lots.map((l) => l.d)), Math.max(...lots.map((l) => l.d))],
twoStorey: shops.filter((sh) => sh.storeys >= 2).length,
townShops: plan.shops.length, townLots: plan.lots.length,
distinctSkins: skins.size,
unmarked: Math.max(2, Math.min(12, Math.round(plan.shops.length * 0.06))),
});
}
const f = (n, d = 2) => Number(n).toFixed(d);
console.log('THE ARCADE, MEASURED (synthetic only — plan_osm.js:305 is one unconditional');
console.log('addDistrict("mainstreet"), so no cache town has an arcade DISTRICT. Real towns do have');
console.log('1,102 arcade-KIND edges = 39 km of ordinary suburban footpath: key any arcade dressing');
console.log('on district.kind, never on edge.kind. That is the v9 cut list as a code rule.\n');
console.log('seed lane blocks lots shops 2-storey frontage(m) depth(m) town shops skins unmarked');
for (const r of rows) {
console.log(`${String(r.seed).padEnd(11)} ${f(r.len)} m × ${r.width} m ${r.blocks} ${String(r.lots).padStart(2)} ${String(r.shops).padStart(2)} ${String(r.twoStorey).padStart(2)} `
+ `${f(r.frontage[0])}${f(r.frontage[1])}${f(r.meanFrontage)}) ${f(r.depth[0])}${f(r.depth[1])} ${String(r.townShops).padStart(4)} ${r.distinctSkins} ${r.unmarked}`);
}
const sh = rows.map((r) => r.shops);
console.log(`\nSHOPS PER ARCADE over ${rows.length} seeds: ${Math.min(...sh)}${Math.max(...sh)} (charter says 17; seed 20261990 = ${rows[0].shops})`);
console.log('types seen:', JSON.stringify(rows.reduce((a, r) => { for (const [k, v] of Object.entries(r.byType)) a[k] = (a[k] || 0) + v; return a; }, {})));
// ── the geometry the dressing has to live inside ────────────────────────────────────────────────
const half = rows[0].width / 2;
console.log('\nTHE LANE, from Lane B\'s own constants:');
console.log(` lot faces stand at ±${f(half)} m from the centreline ⇒ WALKABLE ${f(half * 2)} m between shopfronts`);
console.log(` buildings.js gives every use:'shop' lot an awning ${AWNING_DEPTH} m deep at y=${AWNING_Y} m, so the two`);
console.log(` slabs reach to ±${f(half - AWNING_DEPTH)} m ⇒ THE ARCADE IS ALREADY ROOFED EXCEPT A ${f((half - AWNING_DEPTH) * 2)} m SLOT`);
console.log(` down the centreline. Ceiling height is ${AWNING_Y} m — a hanging sign must clear EYE 1.62 and sit under it.`);
console.log(` ⚠ THE POSTS ARE IN THE WRONG PLACE. buildings.js:643 plants the two awning posts at`);
console.log(` d/2 + ${AWNING_DEPTH} ${POST_INSET} = ${f(AWNING_DEPTH - POST_INSET)} m out from the lot face, i.e. ±${f(half - (AWNING_DEPTH - POST_INSET))} m from the centreline —`);
console.log(` TWO ROWS OF POSTS ${f((half - (AWNING_DEPTH - POST_INSET)) * 2)} m APART DOWN THE MIDDLE OF A ${f(half * 2)} m LANE. They carry no collider`);
console.log(' (colliders.push(lotCollider(lot)) only), so the lane is walkable and the charter\'s 4.99 m');
console.log(' stands — but visually the arcade is a colonnade planted in its own doorway. AWNING_DEPTH');
console.log(' 2.2 was sized for a 3.5 m FOOT band, not a 2.5 m half-lane. → Lane B ask, see LANE_E_NOTES.');
console.log(` ground.js pave: the arcade branch emits ONE footpath quad ${f(rows[0].len)} × ${f(rows[0].width + FOOT * 2)} m`);
console.log(` = ${f(rows[0].len * (rows[0].width + FOOT * 2), 0)} m² — the single biggest surface in the district, and it wears footSkin today.`);
// ── the budget line every arcade asset has to answer ────────────────────────────────────────────
console.log('\nINSTANCE COUNTS THE CONSUMING LANE IMPLIES (the number to generate against):');
const n = rows[0].shops;
const rows2 = [
['hanging blade sign', 'one per arcade shop', n, 'synthetic only'],
['shuttered facade skin', 'the dead tenant(s)', 1, '12 of the arcade\'s shops'],
['arcade floor skin', 'one merged ground class', 1, 'one town-wide mesh'],
['arcade roof/ceiling', 'one quad over the lane', 1, '12 chunks hold arcade lots'],
['A-frame board', 'arcade ~4 + v9 unmarked cue', 4 + rows[0].unmarked, `${4 + rows[0].unmarked}/town (unmarked = clamp(round(shops×0.06),2,12))`],
['key-cutter sign', 'the arcade\'s one trade sign', 1, 'landmark, not a repeat'],
];
for (const [what, rule, cnt, note] of rows2) {
console.log(` ${what.padEnd(22)} ${String(cnt).padStart(2)} × ${rule.padEnd(28)} ${note}`);
}
console.log('\nA GLB CANNOT RIDE boxMats (BoxGeometry + one shared untextured shell material), so each');
console.log('distinct GLB geometry is +1 draw. Ride it as ONE TOWN-WIDE InstancedMesh (magpie.js\'s');
console.log('pattern, count 0 or 1..N) and it is +1 total; make it a per-chunk furniture class and it is');
console.log('+1 × up to 25 live chunks, which at 8 draws of margin is a breach, not a cost.');