- The magpie's white, restated exactly. The wing-bar occlusion claim is the strong one and it now carries its footprint proof: the bar's (x 0.174-0.316, z 0.005-0.055) lies ENTIRELY inside the wing's (x 0-0.34, z -0.085-0.065) with the bar 3 mm above it, so from below the wing hides it. The nape box tops out at 0.067 against a head surface at 0.0728 (I had written 0.065, and had the sliver on the wrong side). And the flank argument is now the honest one: nape and rump are 14 mm and 20 mm slabs on a 166 mm bird = 1.3 px and 2 px of white at 64 px. - THE CLASSIC ARGUMENT FOR THE ARCADE IS INVERTED relative to R38 and I nearly carried the wrong one over. R38's ground palette is classic-safe BY ABSENCE (character.js returns frozen literals for a null town key; classic has none). The arcade district exists ONLY on the synthetic, and the synthetic IS the classic boot — so the kit lands squarely on the covenanted town and needs an EXPLICIT flag test, ground.js's VERGE shape, not the by-absence one. - arcade_measure.mjs now also counts chunks: arcade lots span exactly 2 (0,0 and 0,1) on all five seeds, so a per-chunk arcade class is +1 draw in 2 chunks and +0 in the other 23. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
8.1 KiB
JavaScript
117 lines
8.1 KiB
JavaScript
#!/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.`);
|
||
|
||
// ── how many CHUNKS hold arcade lots (a per-chunk class costs +1 draw in each) ───────────────────
|
||
{
|
||
const CHUNK = 64; // planutil.js CHUNK (CITY_SPEC law)
|
||
for (const s of seeds) {
|
||
const plan = generatePlan(s);
|
||
const arc = plan.districts.find((d) => d.kind === 'arcade');
|
||
const bIds = new Set(plan.blocks.filter((b) => b.district === arc.id).map((b) => b.id));
|
||
const keys = new Set(plan.lots.filter((l) => bIds.has(l.block))
|
||
.map((l) => `${Math.floor(l.x / CHUNK)},${Math.floor(l.z / CHUNK)}`));
|
||
console.log(` seed ${String(s).padEnd(9)} arcade lots span ${keys.size} chunk(s): ${[...keys].join(' ')}`);
|
||
}
|
||
}
|
||
|
||
// ── 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, '1–2 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, '1–2 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.');
|