Sweep and taper the tree limbs; pin the anchors that ride them
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 up on a bezier and taper to nothing, which is what a gum does. +96 tris on gum_01, silhouette bounds unchanged. Nothing physical moved. 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 — verified before and after, all five identical to 6dp. Lane A's winning line rigs off t2 and is untouched; no balance number can shift. e.test.js now hard-codes those five world positions as a tripwire, so no future art change of mine can quietly invalidate a sprint of someone else's physics. If it goes red, the art moved an anchor — fix the art, not the numbers. Card text and the dawn-tint treatment go to Lane A in THREADS. The tint is a CSS snippet rather than a ninth texture: only two of my eight are consumed, and a full-screen gradient needs no asset at all. Selftest 262/0/0, 38/38 output files byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
09e5083515
commit
f8710b56bc
49
THREADS.md
49
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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 3.0 MiB |
@ -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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user