Lane B R39 (Fable's ruling): the magpie takes E's GLB use-if-ready, and the fallback's white moves

+1 draw / +894 tris with the bird on screen, +0 with it off — measured same page, same frame, by
toggling the InstancedMesh count. ?noassets keeps the 182-tri procedural bird (+1 / +182), so this is
furniture.js's use-if-ready SECOND path, not a substitution. state.asset reports which bird is bound.

THREE THINGS MEASURED BEFORE THIS WAS SAFE, AND ALL THREE WOULD HAVE SHIPPED WRONG:
 · E's bird faces +Z and this file flies -Z (Matrix4.lookAt's -Z column is the travel direction).
   Sliced the GLB's 894 tris along Z in ten bins: +Z end mean y 0.31, half-width 0.009 (head + bill);
   -Z end mean y 0.028 (tail). Without rotateY(PI) the magpie swoops TAIL-FIRST. This is the FIFTH
   -Z/+Z instance and the first caught BEFORE it shipped — because R39's lineage table said to point
   a measurement at it. Verified in pictures: perched head-leading, and a swoop frame taken head-on.
 · Its origin is at the FEET (GLB law minY=0) while PERCH_Y is a body-centre height: dropped 0.20 m
   so it stands on the lamp arm (4.12) instead of hovering. `folded` is 1 for the GLB — it IS the perch.
 · THE FETCH IS ARMED BY createMagpie(), NOT BY THE IMPORT. index.html imports this module
   unconditionally and gates only the CONSTRUCTION, so a module-scope loadGLB (furniture.js's shape)
   would have put a brand-new GLB request on the ?classic=1 boot — a fetch delta on the one boot that
   is not allowed one. Asserted on the network log, not on the code: default 1 request (asset 'glb',
   894 tris) · ?classic=1 0 · ?magpie=0 0 · ?noassets=1 0 (asset 'primitive', 182).

AND ONE MY EDIT INTRODUCED INTO ANOTHER LANE'S TOOL: a module-scope loadGLB also fires GLTFLoader's
fetch path under node, so Lane E's pipeline/dump_bird.mjs (which imports this module unmodified) died
with "ProgressEvent is not defined". Gated on IN_BROWSER as well as ?noassets; E's tool then ran
against my edit and reported 182 triangles / 216 verts — the placement fixes are exactly +0 tris.

THE FALLBACK, FIXED AND THEN MEASURED — a half-win, stated as one. E is right that the white bars sat
3 mm ABOVE horizontal wing planes, so from below (the player is always below: perch 4.32 m, EYE 1.62)
the black wing occluded its own flash: bar y 0.048 -> 0.042. Same class of fix, my own arithmetic, for
the other two marks: the nape was BURIED IN THE HEAD (head-sphere surface at z=-0.10 is y 0.0728, the
box topped out at 0.067) so it moved behind the head to z -0.07; nape and rump thickened 0.014->0.030
and 0.02->0.034, because from the flank a 14 mm slab on a 166 mm bird is 1.3 px at 64 px. All +0 tris.
Measured in the same crop of the same pose: pale area 180 -> 399 px, dark 3,017 -> 2,755 — but TRUE
white 33 -> 28, because a downward-facing surface gets no sun. E's GLB reads 755. The fallback cannot
be made pied with primitives at zero cost; the problem is photometric as well as geometric.

Stale comments corrected: magpie.js:9 and :123 said 154 triangles; it is 182 (E's ask 1).

FILED, NOT FIXED: the arcade's awning posts land +/-0.50 m from the centreline — two rows 1.00 m
apart, 34 posts down a 5.00 m lane (my own measurement from the plan, confirming E's). AWNING_DEPTH
2.2 is tuned for a 9 m footpath. One clamp fixes it, but it changes default-boot geometry for a
district whose whole treatment is a scheduled item, and the post line should be designed with the roof.

Lane E's transmission recall checked, not assumed: longbench, streetlight and magpie as staged in
web/assets/models/ carry no KHR_materials_transmission. My lane wires neither of the first two today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-08-03 21:35:05 +10:00
parent a982f3ce67
commit f0b9a1d30f
9 changed files with 240 additions and 27 deletions

View File

@ -34,11 +34,29 @@ first frame; it now draws what has been walked past and **frames itself to it**.
test**. `?classic=1` / `?fog=0` / `?game=0` map canvases are **hash-identical to HEAD's**. test**. `?classic=1` / `?fog=0` / `?game=0` map canvases are **hash-identical to HEAD's**.
- **Lane A's address layer consumed** — street names along walked streets, deduped by name; the - **Lane A's address layer consumed** — street names along walked streets, deduped by name; the
synthetic gets district labels from the same line. No town-type branch anywhere. synthetic gets district labels from the same line. No town-type branch anywhere.
- **FILED, NOT FIXED: `index.html`'s front normal is inverted** at `:384` (Lane D patronage door points) - **THE SIGN, landed on Fable's ruling (`a982f3c`)**`index.html:428` (Lane D patronage door points)
and `:450` (the R32 spawn). `buildings.js`, `dbg.js` and `minimap.js` all use local +Z; measured, the and `:500` (the R32 spawn) computed the shop front as `(sin ry, cos ry)`, the **back** of the
shell's point sits **15.25 m behind the facade** and **0 of the shops within 25 m of the shipped spawn building. Patronage door points **on the footpath: 0/493 → 427/493 synthetic, 0/72 → 67/72 katoomba,
are in front of the player** on all four towns tested. Two characters, but it moves the default boot's 0/139 → 139/139 fitzroy, 0/30 → 28/30 bowral**. At spawn, shops within 25 m that are in front of you:
opening pose mid-round — Fable's call. **0 → 4 on both towns tested.** Katoomba's shipped boot opened *standing in a paddock facing a blank
back wall* — the R31 playtest's own complaint, which R32 was written to fix and never did. **No
golden moves** (runtime pose; selfcheck `0x5f76e76` either side). **F: the R32 spawn gate and the R35
first-five-minutes smoke photograph this pose and need re-baselining.** Written up as the **FOURTH
instance of the Z/+Z confusion** (R13 `RY_FLIP` · R15 the poster on the back wall · R27 every
real-town shopfront facing the paddock · R39 these two) with the lineage table in LANE_B_NOTES §39.
- **THE MAGPIE, on Fable's second ruling** — E's 894-tri GLB wired as `furniture.js`'s **use-if-ready**
path (`+1 draw / +894 tris` with the bird on screen, `+0` off; `?noassets` stays on the 182-tri
procedural bird). **Two things measured before wiring, both would have shipped wrong:** E's bird
faces **+Z** and this file flies **Z** (proved by slicing the GLB along Z — head end mean y 0.31,
tail end 0.028), so without `rotateY(π)` it swoops **tail-first** — *the fifth Z/+Z instance, and
the first caught before it shipped*; and its origin is at the **feet**, dropped 0.20 m onto the lamp
arm. The fallback's white is re-placed (+0 tris, verified by E's own dump: still **182 tris**) and
**measured**: pale area 180 → 399 px but true white 33 → 28, because a downward-facing surface gets
no sun — the GLB reads 755. My edit also broke E's `dump_bird.mjs` (module-scope `loadGLB` under
node); gated on `IN_BROWSER` and re-run green. Two stale `154 tris` comments corrected to **182**.
- **FILED:** the arcade's awning posts land **±0.50 m from the centreline — two rows 1.00 m apart, 34
posts down a 5.00 m lane** (my own measurement, confirming E's). One clamp fixes it, but it belongs
with the scheduled arcade treatment, not ahead of it.
## Round 38 (v8 WAVE 1 — INHABITATION) — all four runtime items, **+1 walked draw for the lot** ## Round 38 (v8 WAVE 1 — INHABITATION) — all four runtime items, **+1 walked draw for the lot**

View File

@ -125,7 +125,7 @@ synthetic, 40.3% katoomba, 43.4% bowral, 43.5% fitzroy** — and on a shopper's
refuses **02**, which is exactly the shape you want: free where a shopper stands, decisive behind the refuses **02**, which is exactly the shape you want: free where a shopper stands, decisive behind the
buildings. buildings.
### THE SIGN WAS WRONG — the FOURTH instance of the Z/+Z confusion *(Fable ruled: landed, `1b7fa5f`)* ### THE SIGN WAS WRONG — the FOURTH instance of the Z/+Z confusion *(Fable ruled: landed, `a982f3c`)*
**The lineage, because the next lane to touch a heading should find it.** Every time, the rule is the **The lineage, because the next lane to touch a heading should find it.** Every time, the rule is the
same: **the rig/pose convention is local Z, the visible front is local +Z**, and every time it same: **the rig/pose convention is local Z, the visible front is local +Z**, and every time it
@ -249,6 +249,80 @@ argument: the map canvas FNV hash at a fixed pose is **identical to HEAD's on al
because classic also turns the gig layer and the yards off, which is the control proving the hash is because classic also turns the gig layer and the yards off, which is the control proving the hash is
sensitive to the map's content at all). sensitive to the map's content at all).
### THE MAGPIE, SETTLED ON E'S PICTURE — the GLB is the path, the fallback is fixed anyway
*(Fable's second R39 ruling. Lane E's sheet: `docs/shots/laneE/r39_magpie_ab.png`.)*
**Wired as `furniture.js`'s use-if-ready second path, not a substitution** — `?noassets` and `?classic`
still get the procedural bird, so it had to be fixed too. `loadGLB('depot:procity_street_magpie_01.glb')`
once at module init, a synchronous ready-check in `createMagpie`, and the geometry/material swapped
into **the one InstancedMesh**. `state.asset` reports which bird is bound so a gate reads it instead of
inferring it from a fetch.
| same page, same frame, bird on vs off | draws | tris |
|---|---|---|
| GLB (default boot) | **+1** | **+894** |
| procedural (`?noassets=1`) | **+1** | **+182** |
| procedural on HEAD (before the re-placement) | +1 | +182 — **the placement fix is +0 tris, in-engine** |
**TWO THINGS I HAD TO MEASURE BEFORE WIRING IT, AND BOTH WOULD HAVE SHIPPED WRONG.**
1. **E's bird faces +Z; this file flies Z** (`Matrix4.lookAt`'s Z column is the travel direction, and
the primitive's nose is modelled along Z). Sliced the GLB's 894 tris along Z, ten bins: the **+Z**
end is high and narrow (mean y **0.31**, half-width **0.009** — head and bill), the **Z** end low
and thin (mean y **0.028** — tail). Without `rotateY(π)` the magpie swoops **tail-first**. *This is
the FIFTH Z/+Z instance — the first one caught before it shipped, and only because the lineage
table above told me to point a measurement at it.* Verified in a picture both ways: perched with
the head leading, and a swoop frame taken **head-on**.
2. **Its origin is at the feet** (GLB law `minY = 0`) while `PERCH_Y = 4.32` is a body-centre height —
dropped 0.20 m so the feet land on the lamp arm at 4.12 (`furniture.js`: pole 4.2, arm 4.1) instead
of hovering over it. And `folded` (the 0.42× X squash that makes the primitive's flight geometry
sit) is **1 for the GLB**, which is already the perch pose.
**A defect my own edit introduced into ANOTHER LANE'S TOOL, caught by running it.** A module-scope
`loadGLB` fires GLTFLoader's fetch path under node, and Lane E's `pipeline/dump_bird.mjs` imports this
module unmodified — `ReferenceError: ProgressEvent is not defined`, E's A/B tool dead, my fault. The
load is now gated on `IN_BROWSER` as well as `?noassets`. E's tool then ran against my edited file and
reported **182 triangles, 216 verts** — the placement changes are exactly +0 tris, confirmed by the
lane that filed them.
**THE FALLBACK FIX, AND ITS MEASURED CEILING — which is not the answer E hoped for.** E's finding is
right: the white bars sat **3 mm above** horizontal wing planes, so from below (perch 4.32 m, EYE
1.62 m — the player is *always* below) the black wing occluded its own flash. I moved the bar under
the wing (y 0.048 → **0.042**), and did the same class of fix for the other two marks with my own
arithmetic: the **nape was buried in the head** (head-sphere surface at z = 0.10 is y **0.0728**, the
box topped out at **0.067**) so it moved back to z 0.07 where the body's back is at 0.0675, and both
nape and rump were **thickened** (0.014 → 0.030, 0.02 → 0.034) because from the flank — 80% of viewing
— a 14 mm slab on a 166 mm bird is **1.3 px at 64 px** and a slab you can see the *side* of is not.
All +0 triangles.
**Then I measured the result instead of trusting it, and it is a half-win:** counting pixels in the
same crop of the same pose, pale area (lum 110150) went **180 → 399**, dark (<70) **3,017 → 2,755**
but *true* white (>150) went **33 → 28**. **A downward-facing surface gets no sun**, so the white I
moved into view reads **grey**. E's GLB in the same crop: **755 px above 150.** The fallback is
measurably better and it cannot be made pied with primitives at zero cost, because the problem is
photometric as well as geometric. Worth recording as the reason the GLB is the path rather than a
preference.
**Stale comments corrected:** `magpie.js:9` and `:123` both said **154 triangles**; it is **182**
(sphere(8,6) 80 + sphere(6,5) 48 + cone(5) 10 + 3 boxes 36 + 4 planes 8), and E's dump of my own
`birdGeometry()` agrees — 216 verts, index length 546.
### → LANE E / a later round: THE ARCADE'S AWNING POSTS — filed, measured, NOT fixed
Confirmed E's number independently from the plan: the arcade lane is **5.00 m** (facades at ±2.50 m
from the centreline) and `buildings.js:643`'s post line lands at **±0.50 m — two rows 1.00 m apart,
34 posts down a 42 m lane.** `AWNING_DEPTH = 2.2` is tuned for a main street with a ~9 m footpath; in
a 2.5 m half-lane it reaches nearly to the middle, and it reads as a colonnade standing in its own
doorway (no collider, so it walks). **The fix is one clamp** — awning depth ≤ a fraction of the
front clearance, which needs a `lot.block → block.kind` lookup `buildings.js` does not do today.
**Not this round:** it changes shipped geometry on the default boot for a district whose whole
treatment (roof lights, tiled floor, reverb, E's 5.86 m `longbench`, a mouth) is a scheduled item, and
the post line should be designed with the roof rather than ahead of it.
**Lane E's transmission recall, checked rather than assumed:** the three republished GLBs staged in
`web/assets/models/` carry **no `KHR_materials_transmission`** (only `KHR_materials_specular`/`ior`,
benign) — `longbench`, `streetlight` and `magpie` all clean on disk. My lane wires none of the first
two today (`furniture.js` GLB_FILES is bench / food_cart / bin / bus_shelter), so there was nothing of
mine to re-fetch; the magpie I *am* wiring is clean.
### Held — for Fable / for John ### Held — for Fable / for John
1. **The inverted front normal at `index.html:384` and `:450`** (above). Two characters, measured to 1. **The inverted front normal at `index.html:384` and `:450`** (above). Two characters, measured to
death, not applied: it moves the default boot's opening pose mid-round. death, not applied: it moves the default boot's opening pose mid-round.

View File

@ -16,6 +16,14 @@ both sides. Every number behind these pictures is in `docs/LANES/LANE_B_NOTES.md
| `sign-*-BEFORE.png` | the default boot's spawn **as shipped since R32**: synthetic — a footpath facing a plaza with not one shopfront in frame; katoomba — **standing in a paddock facing the blank back wall of a building**. 3 shops within 25 m, **0 of them in front of you**, 0 discovered | | `sign-*-BEFORE.png` | the default boot's spawn **as shipped since R32**: synthetic — a footpath facing a plaza with not one shopfront in frame; katoomba — **standing in a paddock facing the blank back wall of a building**. 3 shops within 25 m, **0 of them in front of you**, 0 discovered |
| `sign-*-AFTER.png` | the same code with the front normal corrected (`+sin`): **on the footpath under the awnings, signage down the strip**, 4 shops within 25 m, **4 in front**, 4 discovered | | `sign-*-AFTER.png` | the same code with the front normal corrected (`+sin`): **on the footpath under the awnings, signage down the strip**, 4 shops within 25 m, **4 in front**, 4 discovered |
| `magpie-AFTER-glb-zoom.png` | Lane E's 894-tri GLB **in the engine**, perched on the lamp arm, `asset:'glb'` — pied, legged, unmistakable. **755 px of true white** in the crop |
| `magpie-BEFORE-primitive-zoom.png` / `magpie-AFTER-primitive-zoom.png` | the procedural fallback (`?noassets=1`) before and after the re-placed white: pale area **213 → 427 px**, but true white **33 → 28** — the underside gets no sun, so the fix reads grey. The measured ceiling on a primitives-only fallback, and the reason the GLB is the answer |
| `magpie-AFTER-glb-swoop.png` | the swoop, **head-on** — proof the `rotateY(π)` is right in flight, not just on the perch |
| `magpie-AFTER-glb-flank.png` | the honest in-situ frame (70° FOV, 14 m): the bird is a speck. E's A/B sheet owns the size question; the `-zoom` frames are 12° FOV **inspection** shots |
*(All three magpie arms measured `+1 draw` with the bird on screen and `+0` with it off, same page, same
frame: GLB +894 tris, primitive +182, and the placement fix is +0 tris in-engine.)*
The `sign-*` pair is **booted code on both sides** — two port-isolated no-store servers, `:8702` The `sign-*` pair is **booted code on both sides** — two port-isolated no-store servers, `:8702`
serving the fog commit (old sign) and `:8701` the fix. Nothing is monkey-patched. serving the fog commit (old sign) and `:8701` the fix. Nothing is monkey-patched.

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -6,8 +6,11 @@
// ONE town-wide `InstancedMesh`, `count` 0 or 1 ⇒ **+1 draw while a bird is on screen, exactly 0 // ONE town-wide `InstancedMesh`, `count` 0 or 1 ⇒ **+1 draw while a bird is on screen, exactly 0
// otherwise** (three.module.js:2193-2199 returns before the GL call at count 0; `render.calls++` is // otherwise** (three.module.js:2193-2199 returns before the GL call at count 0; `render.calls++` is
// downstream at :4094 — the charter's own vendored-line verification). Geometry is a merged // downstream at :4094 — the charter's own vendored-line verification). Geometry is a merged
// primitive bird, **154 triangles** against the ≤900 allowance. There is no atlas, no GLB, no fetch: // primitive bird, **182 triangles** against the ≤900 allowance — sphere(8,6) 80 + sphere(6,5) 48 +
// procedural by construction, so `?noassets` is the same code path as everything else. // cone(5) 10 + 3 boxes 36 + 4 planes 8. *(This comment and `:123` both said 154 for two rounds; Lane
// E re-measured it three ways in R39, including a dump of this file's own `birdGeometry()` — 216
// verts, index length 546. Corrected.)* The fallback bird needs no atlas, no GLB and no fetch, so
// `?noassets` is the same code path as everything else.
// //
// ── TERRITORY SCALES WITH THE ROAD GRAPH (Fable's R38 binding correction 4) ───────────────────── // ── TERRITORY SCALES WITH THE ROAD GRAPH (Fable's R38 binding correction 4) ─────────────────────
// A fixed handful of anchors is the trap: the synthetic default has ~6.8 km of street and // A fixed handful of anchors is the trap: the synthetic default has ~6.8 km of street and
@ -35,19 +38,37 @@
// thing that makes a moving object gate-checkable at all. The only stateful part is *when* a launch // thing that makes a moving object gate-checkable at all. The only stateful part is *when* a launch
// starts, which is recorded as three numbers. // starts, which is recorded as three numbers.
// //
// ── LANE E'S 894-TRI `magpie` GLB: NOT ADOPTED THIS ROUND, AND THE REASON IS THE WHITE ────────── // ── LANE E'S 894-TRI `magpie` GLB: ADOPTED IN R39 AS THE USE-IF-READY PATH (Fable ruled on the
// E shipped it mid-round with a caveat traced (not guessed) to the operator's PBR bake: **it reads // picture, `docs/shots/laneE/r39_magpie_ab.png`) ───────────────────────────────────────────────
// as a CROW** — the white nape, wing bar and tail are gone, and E confirms the loss is already in // R38 refused the GLB because it read as a crow. E re-tinted it, then shot both birds through the
// the raw 500k mesh, so our decimation is exonerated. Measured against what is in this file: // same rig at the size the bird is actually seen and found the thing neither of us had: **the player
// procedural bird **182 tris**, white nape + rump + wing bars baked as vertex colours, 0 fetches // is looking at the PERCH, not the swoop** — simulated on this file's own clock, the bird is drawn in
// E's GLB **894 tris** (4.9×), no white, +1 fetch, +1 material // 54.1% of frames and of those it is **perched 80.5%**, swooping 7.7%. E's asset IS a perch pose (a
// A magpie you see for 1.2 seconds in your peripheral vision at speed IS its white bar — that black // standing bird with legs); this file's perch is the flight geometry squashed 0.42× in X.
// -and-white flash is the entire read. A uniformly dark bird is a currawong or a crow, and the joke // And E found the mechanism behind "reads as a dark aeroplane", which is geometric, not a tint:
// this item exists for does not land. So **§1.3 does not hold for the tint; the GLB does.** When E // **the white wing bars sat 3 mm ABOVE horizontal wing planes**, so from underneath — and the player
// hand-tints the 512² atlas, the swap is `birdGeometry()` / `birdMaterial()` below — two factory // is ALWAYS underneath, perch 4.32 m against EYE 1.62 m — the black wing occluded its own white bar.
// singletons, one call site each, the same use-if-ready shape `furniture.js` uses for the depot // The flash was painted on the roof of the wing. That is fixed below, and it is fixed *whatever* the
// props. Deliberately NO speculative `loadGLB` here: the asset is not in the manifest, and a boot // GLB does, because `?noassets` and `?classic` show the procedural bird forever.
// that probes for a file nobody has shipped is the blind probe R26 spent a round killing. //
// So: **two paths, one draw.** `birdGeometry()`/`birdMaterial()` stay the fallback; the GLB is loaded
// once at module init exactly the way `furniture.js` upgrades the depot props (`loadGLB('depot:…')`
// → a synchronous ready-check, never an async write into a live build) and swapped into the ONE
// InstancedMesh when it lands. **+712 tris at one instance; draws are a wash** — both are the same
// town-wide InstancedMesh at count 0 or 1. It is not a blind probe: `magpie` has been in
// `manifest.json` and on the depot since R38 §7a, which is precisely the thing R26's ruling asked for.
//
// TWO THINGS I HAD TO MEASURE MYSELF BEFORE WIRING IT, and both would have shipped wrong:
// • **E's bird faces +Z; this file flies Z.** `place()` builds its basis with `Matrix4.lookAt`,
// whose Z column is the travel direction, and the primitive bird's nose is modelled along Z.
// Sliced the GLB's 894 tris along Z: the +Z end is high and narrow (mean y 0.31, half-width
// 0.009 — the head and bill) and the Z end is low and thin (mean y 0.028 — the tail). Without
// `rotateY(π)` the magpie would swoop tail-first. *This is the fifth Z/+Z instance and it was
// caught by pointing the measurement at it — see §39's lineage table.*
// • **its origin is at the feet** (GLB law: minY = 0), while `PERCH_Y = 4.32` is a body-centre
// height for the primitive. Dropped by `GLB_FOOT_DROP` so the feet land on the lamp arm (4.12,
// `furniture.js`: pole to 4.2, arm at 4.1) instead of hovering 0.22 m over it.
// The GLB is rigid (no `applyWind`) and is NOT X-squashed when perched — it is already the perch.
// //
// The other two Lane E R38 props are a decided NO, on arithmetic rather than taste: `paling_fence` // The other two Lane E R38 props are a decided NO, on arithmetic rather than taste: `paling_fence`
// is 1,121 tris and this town wants **905 fence runs** (5 × 181 lots) ⇒ **1.01 M triangles** against // is 1,121 tris and this town wants **905 fence runs** (5 × 181 lots) ⇒ **1.01 M triangles** against
@ -62,6 +83,7 @@ import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
import { rng, frange } from '../core/prng.js'; import { rng, frange } from '../core/prng.js';
import { resolveEdges } from './planutil.js'; import { resolveEdges } from './planutil.js';
import { applyWind } from './wind.js'; import { applyWind } from './wind.js';
import { loadGLB } from '../core/loaders.js'; // [R39] Lane E's magpie GLB, use-if-ready
const TERRITORY_SPACING = 1200; // metres of street per swooping bird (see header) const TERRITORY_SPACING = 1200; // metres of street per swooping bird (see header)
const TERRITORY_MAX = 400; // sanity cap on the candidate list (bendigo lands ~157) const TERRITORY_MAX = 400; // sanity cap on the candidate list (bendigo lands ~157)
@ -120,7 +142,22 @@ export function swoopPos(launch, t) {
// ── the bird ──────────────────────────────────────────────────────────────────────────────────── // ── the bird ────────────────────────────────────────────────────────────────────────────────────
// Merged primitives with baked vertex colours: black body/head/wings, white nape, white wing bar, // Merged primitives with baked vertex colours: black body/head/wings, white nape, white wing bar,
// white rump. 154 triangles. One geometry, one material, one InstancedMesh. // white rump. **182 triangles** (was written as 154 here for two rounds — see the header).
// One geometry, one material, one InstancedMesh.
//
// [R39] THE WHITE IS NOW WHERE THE PLAYER IS. Every mark was placed for a bird seen from the side at
// eye level; the bird is seen from BELOW, on a 4.32 m lamp arm, and 80.5% of its drawn frames are the
// perch. Three placements changed, all **+0 triangles** (a box is a box, a plane is a plane):
// 1. the wing bar y 0.048 → **0.042**: under the wing plane (y 0.045) instead of 3 mm over it, so
// the black wing stops occluding its own flash. Both are DoubleSide, so nothing is lost above.
// 2. the nape moved off the head it was buried in. Measured: the head sphere's surface at z = 0.10
// is y **0.0728** and the old box topped out at 0.067 — only its rear ~12 mm was ever proud. It
// now sits at z 0.07, *behind* the head (head spans z 0.195…0.085), where the body's back is
// at y 0.0675, and it is **0.030 thick instead of 0.014** so it has a face you can see edge-on.
// 3. the rump likewise: 0.034 thick at y 0.062, sitting on the body's back (y 0.0566 at z = 0.10)
// rather than half-buried in it.
// Thickness is the point, not height: from the flank — 80% of viewing — a horizontal slab 14 mm
// thick on a 166 mm bird is **1.3 px of white at 64 px**. A slab you can see the side of is not.
const BLACK = new THREE.Color('#191a1c'), WHITE = new THREE.Color('#e6e4dd'), BILL = new THREE.Color('#b9bcc0'); const BLACK = new THREE.Color('#191a1c'), WHITE = new THREE.Color('#e6e4dd'), BILL = new THREE.Color('#b9bcc0');
function paint(geo, color) { function paint(geo, color) {
const n = geo.attributes.position.count; const n = geo.attributes.position.count;
@ -137,15 +174,15 @@ function birdGeometry() {
parts.push(paint(new THREE.SphereGeometry(0.085, 8, 6).scale(1, 0.9, 1.75), BLACK)); parts.push(paint(new THREE.SphereGeometry(0.085, 8, 6).scale(1, 0.9, 1.75), BLACK));
parts.push(paint(new THREE.SphereGeometry(0.055, 6, 5).translate(0, 0.035, -0.14), BLACK)); // head parts.push(paint(new THREE.SphereGeometry(0.055, 6, 5).translate(0, 0.035, -0.14), BLACK)); // head
parts.push(paint(new THREE.ConeGeometry(0.021, 0.08, 5).rotateX(-Math.PI / 2).translate(0, 0.03, -0.22), BILL)); parts.push(paint(new THREE.ConeGeometry(0.021, 0.08, 5).rotateX(-Math.PI / 2).translate(0, 0.03, -0.22), BILL));
parts.push(paint(new THREE.BoxGeometry(0.052, 0.014, 0.055).translate(0, 0.06, -0.10), WHITE)); // nape parts.push(paint(new THREE.BoxGeometry(0.052, 0.030, 0.055).translate(0, 0.0745, -0.07), WHITE)); // nape (R39: off the head, thick enough to read edge-on)
parts.push(paint(new THREE.BoxGeometry(0.11, 0.02, 0.13).translate(0, 0.055, 0.10), WHITE)); // rump/back parts.push(paint(new THREE.BoxGeometry(0.11, 0.034, 0.13).translate(0, 0.062, 0.10), WHITE)); // rump/back (R39: on the back, not in it)
parts.push(paint(new THREE.BoxGeometry(0.05, 0.022, 0.30).translate(0, 0.0, 0.26), BLACK)); // tail parts.push(paint(new THREE.BoxGeometry(0.05, 0.022, 0.30).translate(0, 0.0, 0.26), BLACK)); // tail
// wings: flat, spanning ±WING_SPAN in x — the `wing` wind weight is |x| / WING_SPAN // wings: flat, spanning ±WING_SPAN in x — the `wing` wind weight is |x| / WING_SPAN
for (const s of [-1, 1]) { for (const s of [-1, 1]) {
parts.push(paint(new THREE.PlaneGeometry(WING_SPAN, 0.15).rotateX(-Math.PI / 2) parts.push(paint(new THREE.PlaneGeometry(WING_SPAN, 0.15).rotateX(-Math.PI / 2)
.translate(s * WING_SPAN / 2, 0.045, -0.01), BLACK)); .translate(s * WING_SPAN / 2, 0.045, -0.01), BLACK));
parts.push(paint(new THREE.PlaneGeometry(WING_SPAN * 0.42, 0.05).rotateX(-Math.PI / 2) parts.push(paint(new THREE.PlaneGeometry(WING_SPAN * 0.42, 0.05).rotateX(-Math.PI / 2)
.translate(s * WING_SPAN * 0.72, 0.048, 0.03), WHITE)); // the wing bar .translate(s * WING_SPAN * 0.72, 0.042, 0.03), WHITE)); // [R39] the wing bar — BELOW the wing
} }
for (const p of parts) { p.deleteAttribute('uv'); if (!p.attributes.normal) p.computeVertexNormals(); } for (const p of parts) { p.deleteAttribute('uv'); if (!p.attributes.normal) p.computeVertexNormals(); }
_geo = mergeGeometries(parts, false); _geo = mergeGeometries(parts, false);
@ -161,6 +198,66 @@ function birdMaterial() {
return _mat; return _mat;
} }
// ── [R39] Lane E's GLB, use-if-ready — `furniture.js`'s pattern, not a probe ─────────────────────
// Loaded ONCE at module init (skipped entirely under ?noassets, like furniture.js's depot props);
// `_glb` is checked SYNCHRONOUSLY by createMagpie and again when the promise lands, so a bird built
// before the GLB arrives keeps the primitive and swaps to the GLB the moment it exists. One mesh in,
// one mesh out: this stays ONE InstancedMesh and therefore one draw either way.
const GLB_FILE = 'procity_street_magpie_01.glb'; // manifest.furniture.magpie (depot since R38 §7a)
const GLB_FOOT_DROP = 0.20; // GLB origin is the FEET (minY=0); PERCH_Y is a body-centre height
// NOT-IN-A-BROWSER is its own arm, and it is not pedantry: Lane E's `pipeline/dump_bird.mjs` imports
// THIS MODULE unmodified to dump `birdGeometry()` for the A/B render, and a module-scope `loadGLB`
// fires GLTFLoader's fetch path there — `ProgressEvent is not defined`, E's tool dead, my file's
// fault. Caught by running E's tool against my own edit before committing it.
const IN_BROWSER = typeof window !== 'undefined' && typeof document !== 'undefined';
const NOASSETS = !IN_BROWSER || (() => { try { const p = new URLSearchParams(location.search); return p.has('noassets') && p.get('noassets') !== '0'; } catch { return false; } })();
let _glb = null; // { geo, mat, tris } once (and if) it loads
const _glbSubs = []; // createMagpie instances waiting for it
function extractBird(gltf) {
const geos = []; let material = null;
gltf.scene.updateWorldMatrix(true, true);
gltf.scene.traverse((o) => {
if (!o.isMesh || !o.geometry) return;
const g = o.geometry.clone(); g.applyMatrix4(o.matrixWorld);
for (const name of Object.keys(g.attributes)) if (name !== 'position' && name !== 'normal' && name !== 'uv') g.deleteAttribute(name);
g.morphAttributes = {};
if (!g.attributes.normal) g.computeVertexNormals();
if (!g.attributes.uv) g.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * 2), 2));
geos.push(g);
if (!material) material = Array.isArray(o.material) ? o.material[0] : o.material;
});
if (!geos.length) return null;
let merged = null;
try { merged = mergeGeometries(geos, false); } catch (e) { merged = null; }
geos.forEach((g) => g.dispose());
if (!merged || !material) return null; // heterogeneous → the primitive stays
// E's bird is modelled head-to-+Z and standing on its feet; this file flies Z from a body centre.
merged.rotateY(Math.PI).translate(0, -GLB_FOOT_DROP, 0);
merged.computeBoundingSphere();
const idx = merged.getIndex();
return { geo: merged, mat: material, tris: (idx ? idx.count : merged.attributes.position.count) / 3 };
}
// THE FETCH IS ARMED BY createMagpie(), NOT BY THE IMPORT — and that is the classic covenant, not
// tidiness. `index.html` imports this module unconditionally; only the CONSTRUCTION is gated
// (`?classic=1` / `?magpie=0` never call createMagpie). A module-scope load would therefore have put
// a brand-new GLB request on the classic boot — a fetch delta on the one boot that is not allowed one.
// furniture.js can load at module scope because furniture is built on every boot; this cannot.
let _glbAsked = false;
function ensureGLB() {
if (_glbAsked || NOASSETS) return;
_glbAsked = true;
loadGLB('depot:' + GLB_FILE)
.then((g) => {
const ex = g && extractBird(g);
if (!ex) return; // fail-soft: no file, no merge → primitive forever
_glb = ex;
for (const fn of _glbSubs.splice(0)) { try { fn(ex); } catch (e) { /* one bird's problem */ } }
})
.catch(() => { /* the primitive is the answer, silently */ });
}
/** /**
* createMagpie({ scene, plan, citySeed, camera, chunks, force }) { group, update, dispose, state } * createMagpie({ scene, plan, citySeed, camera, chunks, force }) { group, update, dispose, state }
* `force` (?magpie=1) forces the season open. The shell never constructs this under ?classic=1 or * `force` (?magpie=1) forces the season open. The shell never constructs this under ?classic=1 or
@ -171,6 +268,9 @@ export function createMagpie({ scene, plan, citySeed, townKey = null, camera, ch
const state = { const state = {
territories: 0, roadKm: 0, active: null, mode: 'absent', swoopsToday: 0, territories: 0, roadKm: 0, active: null, mode: 'absent', swoopsToday: 0,
gotYouToday: false, day: 1, dayRolls: 0, inSeason: false, forced: !!force, launches: 0, gotYouToday: false, day: 1, dayRolls: 0, inSeason: false, forced: !!force, launches: 0,
// [R39] which bird is actually bound — 'primitive' (182 tris) or 'glb' (Lane E's 894). F's gate
// reads this rather than guessing from a fetch: ?noassets must stay 'primitive' forever.
asset: 'primitive', tris: 182,
// Diagnostics so "is there a magpie in this town?" is answerable without a 200-day soak. // Diagnostics so "is there a magpie in this town?" is answerable without a 200-day soak.
townKey, seasonOffset: seasonOffset(citySeed >>> 0, townKey), townKey, seasonOffset: seasonOffset(citySeed >>> 0, townKey),
get daysToSeason() { return daysToSeason(this.day, citySeed >>> 0, townKey); }, get daysToSeason() { return daysToSeason(this.day, citySeed >>> 0, townKey); },
@ -226,6 +326,19 @@ export function createMagpie({ scene, plan, citySeed, townKey = null, camera, ch
group.add(mesh); group.add(mesh);
scene.add(group); scene.add(group);
// [R39] the use-if-ready swap. `foldScale` is 0.42 for the primitive (whose perch is the flight
// geometry squashed in X) and 1 for the GLB, which IS the perch pose. Geometry/material are
// swapped on the ONE InstancedMesh — no second mesh, no second draw — and the primitive singletons
// are NOT disposed: they are module-level and a second createMagpie would want them.
let foldScale = 0.42;
function adoptGLB(ex) {
mesh.geometry = ex.geo; mesh.material = ex.mat;
foldScale = 1;
state.asset = 'glb'; state.tris = ex.tris;
}
if (_glb) adoptGLB(_glb);
else if (!NOASSETS) { _glbSubs.push(adoptGLB); ensureGLB(); }
// ── proximity-first territory selection, refreshed on the R37 §0.4 chunk seam ── // ── proximity-first territory selection, refreshed on the R37 §0.4 chunk seam ──
let dirty = true, home = null; let dirty = true, home = null;
const markDirty = () => { dirty = true; }; const markDirty = () => { dirty = true; };
@ -266,7 +379,7 @@ export function createMagpie({ scene, plan, citySeed, townKey = null, camera, ch
// ±0.34 m wing planes to ±0.14 m reads as a bird sitting with its wings in, which is what a // ±0.34 m wing planes to ±0.14 m reads as a bird sitting with its wings in, which is what a
// perched magpie looks like and what the first screenshot showed it emphatically was not. // perched magpie looks like and what the first screenshot showed it emphatically was not.
function place(x, y, z, fx, fz, folded = false) { function place(x, y, z, fx, fz, folded = false) {
_s.set(folded ? 0.42 : 1, 1, 1); _s.set(folded ? foldScale : 1, 1, 1);
_p.set(x, y, z); _p.set(x, y, z);
_fwd.set(fx, 0, fz); _fwd.set(fx, 0, fz);
if (_fwd.lengthSq() < 1e-6) _fwd.set(0, 0, 1); if (_fwd.lengthSq() < 1e-6) _fwd.set(0, 0, 1);