#!/usr/bin/env node // PROCITY Lane E — dump_bird.mjs (R39, the magpie A/B) // // Dump Lane B's PROCEDURAL magpie geometry, by RUNNING LANE B'S OWN MODULE — not by re-implementing // it. `web/js/world/magpie.js` is imported unmodified; the bare `three` / `three/addons/` specifiers // its imports use are resolved to the repo's OWN vendored build through a node resolve hook, so the // vertices dumped here are byte-for-byte the vertices the browser gets. That matters: an A/B render // against a hand-ported strawman proves nothing, and the whole point of R39 item 1 is that Fable // rules on the picture. // // node pipeline/dump_bird.mjs [OUT.json] // // Writes { tris, verts, position[], normal[], color[], index[]|null } in the mesh's own metric frame // (metres, three's +Y up, nose along −Z). `pipeline/bird_to_glb.py` turns it into a GLB so the same // `render_views.py` rig that shot E's tinted GLB can shoot B's bird from the same cameras. import { registerHooks } from 'node:module'; import { pathToFileURL } from 'node:url'; import { writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, '..'); const VENDOR = pathToFileURL(resolve(ROOT, 'web/vendor/three.module.js')).href; const ADDONS = pathToFileURL(resolve(ROOT, 'web/vendor/addons')).href + '/'; // The repo's importmap, as a node resolver: "three" and "three/addons/*" only. registerHooks({ resolve(spec, ctx, next) { if (spec === 'three') return { url: VENDOR, shortCircuit: true }; if (spec.startsWith('three/addons/')) { return { url: ADDONS + spec.slice('three/addons/'.length), shortCircuit: true }; } return next(spec, ctx); }, }); const { generatePlan } = await import(pathToFileURL(resolve(ROOT, 'web/js/citygen/plan.js')).href); const { createMagpie } = await import(pathToFileURL(resolve(ROOT, 'web/js/world/magpie.js')).href); const plan = generatePlan(20261990); const scene = { add() {}, remove() {} }; const camera = { position: { x: 0, y: 1.6, z: 0 } }; const m = createMagpie({ scene, plan, citySeed: 20261990, townKey: null, camera, chunks: null, lighting: null, force: true }); const mesh = m.group.children.find((c) => c.isInstancedMesh); const g = mesh.geometry; const idx = g.index ? Array.from(g.index.array) : null; const pos = Array.from(g.attributes.position.array); const nrm = g.attributes.normal ? Array.from(g.attributes.normal.array) : null; const col = g.attributes.color ? Array.from(g.attributes.color.array) : null; const tris = idx ? idx.length / 3 : pos.length / 9; const out = { source: 'web/js/world/magpie.js :: birdGeometry() via createMagpie()', tris, verts: pos.length / 3, material: { vertexColors: true, roughness: 0.62, metalness: 0, side: 'DoubleSide', wind: 'wing' }, bbox: (() => { g.computeBoundingBox(); const b = g.boundingBox; return { min: [b.min.x, b.min.y, b.min.z], max: [b.max.x, b.max.y, b.max.z], size: [b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z] }; })(), // the perch pose is the same geometry squashed in X (magpie.js `place(..., folded)`) foldedScaleX: 0.42, index: idx, position: pos, normal: nrm, color: col, }; const dst = process.argv.find((a) => a.endsWith('.json')) || resolve(HERE, '_bird_b.json'); writeFileSync(dst, JSON.stringify(out)); console.log(`bird: ${tris} triangles, ${out.verts} verts, bbox size ${out.bbox.size.map((v) => v.toFixed(3)).join(' × ')} m → ${dst}`); // ── --sim: WHICH POSE IS THE PLAYER ACTUALLY LOOKING AT? ──────────────────────────────────────── // The A/B is usually argued as "the swoop is the whole point", but magpie.js's own clock says the // swoop is the minority state: COOLDOWN 5.5 s perched against SWOOP_T 1.25 + RETURN_T 1.9 = 3.15 s // in the air, and the mesh is drawn out to DEFEND_R × 2.2 = 74.8 m where it can only be perched. // So walk a player down the street past a territory at WALK speed and COUNT the frames. if (process.argv.includes('--sim')) { const t = m.territories[0] || { x: 0, z: 0 }; const DT = 1 / 60, SPEED = 4.6, OFFSET = 4.0; // WALK m/s, and how far off the perch you pass globalThis.window = { PROCITY: { game: { day: 1 } } }; const tally = {}; let frames = 0; for (let i = 0; i < 60 * 60; i++) { // 60 s of walking const s = -140 + i * DT * SPEED; // straight past the perch, 140 m either side camera.position.x = t.x + s; camera.position.z = t.z + OFFSET; m.update(DT); tally[m.state.mode] = (tally[m.state.mode] || 0) + 1; if (m.count > 0) frames++; } const drawn = Object.entries(tally).filter(([k]) => k === 'perched' || k === 'swooping' || k === 'returning'); const total = drawn.reduce((a, [, v]) => a + v, 0); console.log(`sim: 60 s walk at ${SPEED} m/s, ${OFFSET} m off the perch — modes ${JSON.stringify(tally)}`); console.log(`sim: bird DRAWN in ${frames} of 3600 frames (${(frames / 36).toFixed(1)}%)`); for (const [k, v] of drawn) console.log(`sim: ${k.padEnd(10)} ${v} frames = ${(100 * v / total).toFixed(1)}% of the frames it is on screen`); }