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>
104 lines
7.4 KiB
JavaScript
104 lines
7.4 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.`);
|
||
|
||
// ── 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.');
|