diff --git a/THREADS.md b/THREADS.md index a58ba7d..d94aec2 100644 --- a/THREADS.md +++ b/THREADS.md @@ -2177,3 +2177,52 @@ Format: `[lane letter] YYYY-MM-DD โ€” note` D calibrated the douse + fixed the sagging-belly reach; C ruled hail-porosity honestly (stone size is the only defensible fabric-hail difference) and shipped storm_02b_icenight; E shipped night dressing + end-card diptych + the hide_render fix. All carried to SPRINT7. + +[E] 2026-07-18 โ€” ๐ŸŒณ **took my own standing offer: the tree limbs no longer read as coat hooks.** Each limb + was one straight uniform tube โ€” a peg poking out of a pole, and the most visible art problem in + `docs/yard_day.jpg`. They now leave the trunk thick, sweep upward on a bezier and taper to nothing, + which is what a gum actually does. +96 tris on gum_01 (396 โ†’ 492), silhouette bounds unchanged. + **Nothing physical moved and I can prove it.** The tip is a bezier ENDPOINT, and the four rng draws per + branch are the same four in the same order, so every `branch_anchor_*` is bit-for-bit where it was โ€” + measured before and after, all five identical to 6 dp. **Lane A: your winning line rigs off t2 and it + is untouched. Lane B/C: no balance number can have moved.** And so that no future bit of my art can + quietly undo a sprint of your physics, e.test.js now hard-codes those five world positions as a + tripwire: if it ever goes red, the art moved an anchor โ€” fix the art, don't touch the numbers. + (Light sprint, this is what I spent it on. Offered since Sprint 4; nobody had to ask.) + +[E] 2026-07-18 โ€” **LANE A โ€” the diptych's words (SPRINT7 ยงE). Cards are on disk, left third is yours.** + My pick, in the voice DESIGN.md set โ€” the trade talking, dry, no triumph: + ยท **WIN** (`card_win.jpg`) โ€” headline **"THE WEEK HELD"** ยท sub *"Five nights. Everything's still + where you put it."* ยท kicker *"Nobody thanks you for the storm that did nothing. That's the job."* + ยท **GAME OVER** (`card_gameover.jpg`) โ€” headline **"OFF THE JOB"** ยท sub *"Broke, with the week + still running."* ยท kicker *"The wind never sent an invoice. Everyone else did."* + Alternates if those miss: win โ†’ "FIVE NIGHTS, FIVE MORNINGS" / *"You made the least wrong call five + times running."* (leans on DESIGN.md's own line); game over โ†’ "THE BANK WENT FIRST" / *"Not every job + is lost to weather."* Take, edit or bin any of it โ€” you own the screen, I'm just handing over words so + the art isn't waiting on them. + +[E] 2026-07-18 โ€” **dawn tint for the morning-after aftermath (ยงE) โ€” and it's a snippet, not an asset, + deliberately.** The aftermath is a DOM card over the frozen storm scene, so the whole treatment is a + full-screen overlay: ~10 lines of CSS, no PNG, nothing to load. Shipping a gradient as a texture would + be me adding a ninth file to a pile where two of eight get used โ€” my own audit says so. Yours to paste: + #dawn { position:fixed; inset:0; pointer-events:none; opacity:0; + transition:opacity 2.2s ease; mix-blend-mode:screen; + background:linear-gradient(to top, + rgba(255,178,102,.30) 0%, /* the sun that finally turned up */ + rgba(255,150,96,.16) 22%, + rgba(120,132,168,.10) 55%, + rgba(38,48,84,.16) 100%); } /* night still up there */ + #dawn.on { opacity:1; } + `screen` is the point โ€” it lifts the black storm scene into morning without touching the card on top. + Add `.on` when you open the aftermath, drop it on forecast. The 2.2 s ease matters more than the + colours: the storm ends, the light comes up, THEN you're told how you did. Sell the survival before + the scoreboard. + +[E] 2026-07-18 โ€” housekeeping: `window_glow` is in world.js โ€” thanks, that was quick. Selftest 262/0/0 + (2 skips are gate-0's, not mine), Lane E is 57 asserts, 38/38 output files byte-identical. + Still open and still yours to close either way, Lane A (SPRINT7 says close or write "won't do"): + **`grass_atlas` โ€” six sprints, 0 refs** (recipe's in my Sprint 5 entry โ€” I'd honestly just bin it), and + **`moon.png`** is on disk unused if the night sky wants it. `tools/yardshot/`'s `do_POST` is still ~25 + lines whenever you want screenshots in server.py; it works standalone meanwhile, so it isn't urgent. + **When the week lands, ping me and I'll reshoot `docs/yard_day/night` with a winnable rig in frame** โ€” + one minute now, and it's the picture DESIGN.md has wanted since Sprint 2. diff --git a/tools/blender/asset_report.json b/tools/blender/asset_report.json index 64ef1eb..f52e9df 100644 --- a/tools/blender/asset_report.json +++ b/tools/blender/asset_report.json @@ -16,7 +16,436 @@ ], "status": "PASS", "problems": [] + }, + { + "name": "tree_gum_01", + "dims": [ + 4.5522, + 4.956, + 7.9702 + ], + "tris": 492, + "nodes": [ + "branch_anchor_01", + "branch_anchor_02", + "branch_anchor_03", + "canopy", + "canopy_01", + "canopy_02", + "canopy_03", + "tree_gum_01", + "trunk" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "tree_gum_02", + "dims": [ + 3.8871, + 2.7787, + 5.4972 + ], + "tris": 352, + "nodes": [ + "branch_anchor_01", + "branch_anchor_02", + "canopy", + "canopy_01", + "canopy_02", + "tree_gum_02", + "trunk" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "fence_post", + "dims": [ + 0.13, + 0.13, + 2.03 + ], + "tris": 24, + "nodes": [ + "fence_post", + "post" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "fence_panel", + "dims": [ + 2.4, + 0.054, + 1.8194 + ], + "tris": 324, + "nodes": [ + "fence_panel", + "palings", + "rails" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "gate", + "dims": [ + 1.045, + 0.0615, + 1.75 + ], + "tris": 220, + "nodes": [ + "gate", + "gate_frame", + "gate_palings", + "hinge_axis", + "hinges" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "house_yardside", + "dims": [ + 9.2, + 1.0547, + 2.9 + ], + "tris": 212, + "nodes": [ + "door", + "fascia", + "fascia_anchor_01", + "fascia_anchor_02", + "fascia_anchor_03", + "gutter", + "house_yardside", + "roof", + "wall", + "window", + "window_glow", + "window_light_anchor" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "shed_01", + "dims": [ + 2.58, + 1.9708, + 2.2224 + ], + "tris": 96, + "nodes": [ + "door_anchor", + "doors", + "roof", + "shed_01", + "shell" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "shed_table", + "dims": [ + 1.6, + 0.6, + 0.9 + ], + "tris": 72, + "nodes": [ + "pickup_anchor", + "shed_table", + "table_frame", + "table_top" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "garden_bed", + "dims": [ + 3.0, + 1.2, + 0.8609 + ], + "tris": 2580, + "nodes": [ + "bed", + "garden_bed", + "plants_dead", + "plants_full", + "plants_tattered", + "soil" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "sail_post", + "dims": [ + 0.507, + 0.52, + 4.0327 + ], + "tris": 528, + "nodes": [ + "footing", + "pad_eye", + "post", + "rake_pivot", + "sail_post", + "top_anchor" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "ladder_01", + "dims": [ + 0.455, + 0.075, + 3.0 + ], + "tris": 276, + "nodes": [ + "ladder", + "ladder_01", + "ladder_base", + "ladder_top" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "shackle", + "dims": [ + 0.0569, + 0.019, + 0.0744 + ], + "tris": 560, + "nodes": [ + "bow", + "pin", + "shackle" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "carabiner", + "dims": [ + 0.049, + 0.009, + 0.1027 + ], + "tris": 476, + "nodes": [ + "body", + "carabiner", + "gate" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "turnbuckle", + "dims": [ + 0.0292, + 0.0341, + 0.1955 + ], + "tris": 728, + "nodes": [ + "body", + "eye_a", + "eye_b", + "turnbuckle" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "tramp_01", + "dims": [ + 2.9555, + 2.9555, + 0.78 + ], + "tris": 976, + "nodes": [ + "legs", + "mat", + "pad", + "rim", + "tramp_01" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "wheelie_bin_01", + "dims": [ + 0.58, + 0.6808, + 1.1188 + ], + "tris": 120, + "nodes": [ + "bin_body", + "lid", + "lid_plate", + "wheelie_bin_01", + "wheels" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "washing_line_01", + "dims": [ + 2.8441, + 2.8441, + 2.2777 + ], + "tris": 336, + "nodes": [ + "arms", + "head", + "mast", + "washing_line_01" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "garden_gnome_01", + "dims": [ + 0.1427, + 0.15, + 0.365 + ], + "tris": 236, + "nodes": [ + "garden_gnome_01", + "gnome" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "garden_gnome_01_broken", + "dims": [ + 0.3947, + 0.3519, + 0.106 + ], + "tris": 344, + "nodes": [ + "garden_gnome_01_broken", + "hat", + "head", + "shards", + "stump" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "hail_stone_01", + "dims": [ + 0.0198, + 0.0176, + 0.0197 + ], + "tris": 20, + "nodes": [ + "hail_stone_01", + "stone" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "broom_01", + "dims": [ + 0.3891, + 0.055, + 1.4213 + ], + "tris": 200, + "nodes": [ + "bristles", + "broom_01", + "grip_anchor", + "handle", + "head", + "poke_tip" + ], + "status": "PASS", + "problems": [] + }, + { + "name": "fence_panel_snapped", + "dims": [ + 2.4125, + 1.0144, + 1.8181 + ], + "tris": 312, + "nodes": [ + "debris_palings", + "fence_panel_snapped", + "palings", + "rails" + ], + "status": "PASS", + "problems": [] } ], - "debris": [] + "debris": [ + { + "file": "BlueCrate_v2.glb", + "dims": [ + 0.36, + 0.36, + 0.29 + ], + "sane": true + }, + { + "file": "BlackTub_v2.glb", + "dims": [ + 0.36, + 0.54, + 0.2 + ], + "sane": true + }, + { + "file": "WhiteTub_v2.glb", + "dims": [ + 0.36, + 0.54, + 0.2 + ], + "sane": true + }, + { + "file": "WoodenBin_v2.glb", + "dims": [ + 0.35, + 0.36, + 0.31 + ], + "sane": true + } + ] } \ No newline at end of file diff --git a/tools/blender/build_yard_assets.py b/tools/blender/build_yard_assets.py index f67ad96..b486060 100644 --- a/tools/blender/build_yard_assets.py +++ b/tools/blender/build_yard_assets.py @@ -467,6 +467,30 @@ def build_ref_capsule(name): return root +def _limb(name, base, tip, r_base, r_tip, mat, segs=3, sweep=0.30): + """A tapered, up-swept limb from base to tip. + + A single straight uniform tube is what made these read as coat hooks in + docs/yard_day.jpg โ€” a peg poking out of a pole. Real gum limbs leave the + trunk thick, sweep upward, and thin to nothing. + + The tip is a quadratic-bezier ENDPOINT, so it lands on `tip` exactly. That + matters more than the look: branch_anchor_* sit on these tips, Lane A's + winning line rigs off t2, and every balance number in the repo assumes those + points don't move. Consumes NO rng โ€” the caller's draw order is untouched. + """ + b, t = Vector(base), Vector(tip) + ctrl = (b + t) / 2 + Vector((0, 0, (t.z - b.z) * sweep + 0.12)) + parts, prev = [], b + for i in range(1, segs + 1): + u = i / segs + pt = (1 - u) ** 2 * b + 2 * (1 - u) * u * ctrl + u ** 2 * t # u=1 -> exactly t + r = r_base + (r_tip - r_base) * ((i - 0.5) / segs) + parts.append(add_tube_between(f"{name}_s{i}", prev, pt, r, mat, verts=6)) + prev = pt + return parts + + def _gum_tree(name, height, canopy_blobs, spread, anchor_heights, seed_name, sway_amp=1.0): """Eucalypt: pale chalky trunk, sparse olive canopy, low branches that a @@ -500,9 +524,11 @@ def _gum_tree(name, height, canopy_blobs, spread, anchor_heights, seed_name, tip = (base[0] + math.cos(ang) * reach, base[1] + math.sin(ang) * reach, z0 + reach * rng.uniform(0.35, 0.6)) - trunk_parts.append(add_tube_between( - f"{name}_branch_{i:02d}", base, tip, r_top * rng.uniform(0.5, 0.7), - bark, verts=8)) + # Same four rng draws, same order, so every tip is bit-for-bit where it + # was โ€” only the geometry hanging off them changed. + r_limb = r_top * rng.uniform(0.5, 0.7) + trunk_parts.extend(_limb(f"{name}_branch_{i:02d}", base, tip, + r_limb * 1.9, r_limb * 0.42, bark)) anchors.append(tip) join_group(trunk_parts, "trunk", root) diff --git a/tools/blender/contact_sheet.png b/tools/blender/contact_sheet.png index 9f72654..9c0cf4f 100644 Binary files a/tools/blender/contact_sheet.png and b/tools/blender/contact_sheet.png differ diff --git a/web/world/js/tests/e.test.js b/web/world/js/tests/e.test.js index 1833470..f2914c6 100644 --- a/web/world/js/tests/e.test.js +++ b/web/world/js/tests/e.test.js @@ -144,6 +144,42 @@ export default async function run(t) { }); } + // These five numbers are load-bearing for the whole repo: Lane A's winning + // line rigs off t2, and every balance figure B and C measured assumes the + // branch anchors sit exactly here. I reshaped the limbs in Sprint 7 for looks + // and pinned the tips bit-for-bit to do it; this is the tripwire that keeps + // the next bit of art from quietly invalidating a sprint of physics. + // If it fails, the art moved an anchor โ€” fix the art, don't touch the numbers. + t.test('branch anchors have not moved โ€” the balance numbers depend on them', () => { + const EXPECT = { + tree_gum_01: { + branch_anchor_01: [-0.960, 3.636, -1.460], // Blender (x,y,z) -> glTF (x,z,-y) + branch_anchor_02: [-0.941, 4.145, 0.777], + branch_anchor_03: [-1.382, 5.227, 0.978], + }, + tree_gum_02: { + branch_anchor_01: [-0.167, 2.950, 1.154], + branch_anchor_02: [0.142, 3.722, 1.043], + }, + }; + for (const [tree, anchors] of Object.entries(EXPECT)) { + const g = loaded.get(tree); + assert(g, `${tree} did not load`); + g.scene.updateWorldMatrix(true, true); + for (const [n, want] of Object.entries(anchors)) { + const o = g.scene.getObjectByName(n); + assert(o, `${tree}/${n} missing`); + const p = new THREE.Vector3().setFromMatrixPosition(o.matrixWorld); + const got = [p.x, p.y, p.z]; + for (let i = 0; i < 3; i++) { + assert(Math.abs(got[i] - want[i]) < 0.005, + `${tree}/${n} moved on ${'xyz'[i]}: ${got[i].toFixed(3)} vs ${want[i]} ` + + `(full ${got.map((v) => v.toFixed(3)).join(', ')})`); + } + } + } + }); + // Anchors are the actual product here: Lane B pins cloth corners to them and // Lane A builds world.anchors from them. A surviving name isn't enough โ€” the // position has to be usable. diff --git a/web/world/models/tree_gum_01_v1.glb b/web/world/models/tree_gum_01_v1.glb index 159f9b2..6475479 100644 Binary files a/web/world/models/tree_gum_01_v1.glb and b/web/world/models/tree_gum_01_v1.glb differ diff --git a/web/world/models/tree_gum_02_v1.glb b/web/world/models/tree_gum_02_v1.glb index c52ec85..39c8448 100644 Binary files a/web/world/models/tree_gum_02_v1.glb and b/web/world/models/tree_gum_02_v1.glb differ