The --only runs during the sprint left a partial sheet committed. Also fills the
empty slots in a partly-filled last row with the background sampled from a
tile's corner, so 19 assets in a 4x5 grid no longer leaves a black hole. The
world colour can't be reused for this — the render is sRGB-encoded, the scene
value is linear.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rake_pivot shipped as a childless empty, so rotating it moved nothing — and
rotating the whole GLB instead, which is the only alternative, tips the concrete
footing out of the ground along with the post. DESIGN.md makes raking away from
the load a player decision, so the handle has to actually work.
It now holds post + pad_eye + top_anchor, with footing left on the root: rake it
8° and the post leans while the concrete stays planted. Asserted both directions
in e.test.js — the head must travel over 0.3 m and the footing under 0.01.
Same class as the canopy bug, found the same way: by driving the handle in a
test instead of eyeballing the model. Exported dims unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Textures (SPRINT2 §Lane E-2). sail_weave.png is a seamless 512² knitted
shade-cloth weave with the stripe banding real cloth has; the script proves the
wrap by evaluating a second tile and requiring an exact match, because Lane B is
being told to set RepeatWrapping and a bad wrap would seam across the whole
sail. Luminance rides in a narrow band so it multiplies the base colour instead
of replacing it — high contrast reads as burlap, not HDPE. Dropped the per-pixel
noise: invisible at ±0.012 and incompressible, it cost 305 KB of the 323.
sail_tears.png is a strip of 4 escalating rips. Each is a lens, not a slit —
fabric under tension parts widest in the middle and tapers to a point, and a
constant-width gap reads as a drawn line. Threads bridge the gap, scaled to the
local width; without them a dark lens is a hole rather than a tear.
Dressing (§Lane E-3): wheelie_bin_01 (240 L, 12 kg, `lid` on its own node so it
flaps before the bin goes over), washing_line_01 (a Hills Hoist — the `head`
freewheels, giving a second wind tell at head height), garden_gnome_01
(collateral bait: a smashed gnome reads where a damage number doesn't).
Bin and tramp land in models/debris/ for Lane C to glob. All 19 assets pass, 28
output files byte-identical across two runs, selftest 129/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
world.js sways a tree by rotating a `canopy` group whose origin sits at the
trunk top, so the blobs swing about the trunk. The trees shipped canopy_01..03
as siblings of `trunk` with each origin at its own blob centre — rotating one
spins a sphere in place, which is visually nothing. Lane A could not have swayed
these trees, and the canopy lean is the gust telegraph the player reads a beat
before it hits the sail, so the tell would have gone missing rather than looked
wrong.
Adds the `canopy` empty at the trunk top with the blobs parented under it, so
A's existing code works unchanged, plus sway_amp / sway_phase / sway_pivot_y
for the per-tree tuning SPRINT2 §Lane E asks for (gum_01 is big and leans less
at 0.85; gum_02 is whippy at 1.20 and shows a gust first).
sway_phase draws from its own RNG stream: taking it from the shared one would
consume a value and shift every blob draw after it, resilhouetting a tree the
other lanes have already tuned against. Exported dims are identical to Sprint 1.
e.test.js asserts the pivot by rotating the handle and measuring that a blob
actually travels — red before this change, green after.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the GLBs in three.js (not just Blender) showed four assets reporting
inflated bounds: tramp_01 came back 3.29 x 1.27 m against a true 2.96 x 0.78.
Box3.setFromObject expands each mesh's LOCAL box by the world matrix, so a node
carrying a rotation over-reports — the same trap Blender's obj.bound_box sets,
and what three uses for frustum culling. Joined nodes inherited parts[0]'s
rotation, which for an arc is half a segment step off-axis.
Applying rotation at join time makes every local box axis-aligned, so the
default Box3 path is now correct for consumers and culling is tight. World
geometry is unchanged; the exported dims are identical.
Adds tools/assetcheck/, the three.js harness that caught this. It's the only
check that can: a Blender round-trip exports Z-up->Y-up and imports Y-up->Z-up,
so a broken export_yup flips back and passes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One script regenerates every nature/hardware asset in PLAN3D §5-E, following
the house idiom from 3D-STORE/racks_to_glb.py: reset per asset, build under a
root empty at the origin, join by group, stamp props, export Y-up GLB.
Groups are joined per sway-unit rather than per-asset, so trees keep trunk and
canopy_* as separate nodes for Lane A to animate. Paths resolve from __file__
instead of a hardcoded home dir, since the library lives elsewhere on this box.
Verification re-imports each exported GLB from disk and asserts dims, tri
budget, and node-name survival, then renders it against the 1.7 m ref capsule.
Checking the file rather than the in-memory scene is what makes it a real test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>