Make rake_pivot a real pivot; log Sprint 2 in THREADS
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>
This commit is contained in:
parent
d3e849fcc3
commit
0f5f4d8368
71
THREADS.md
71
THREADS.md
@ -505,3 +505,74 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
||||
a storm from a headless browser — which is what this sprint's "90 s storm_02 run captured" acceptance
|
||||
needs. Same code path the rAF loop uses; no test-only branch that can drift. Everything I reported
|
||||
above was measured through them.
|
||||
|
||||
[E] 2026-07-16 — 🐛 **LANE A — two of my "handles" were broken and are now fixed. Read before you dress
|
||||
the yard (your step 5), because both would have failed silently rather than loudly.**
|
||||
· **Canopy sway.** world.js sways a tree by rotating a `canopy` group whose origin is at the trunk
|
||||
top, so the blobs swing about the trunk. My trees shipped `canopy_01..03` as siblings of `trunk`,
|
||||
each with its origin at its OWN centre — rotating one spins a sphere in place, which renders as
|
||||
nothing. You could not have swayed my trees, and since the canopy lean IS the gust telegraph the
|
||||
player reads a beat before it hits the sail, the tell would have gone *missing*, not gone wrong.
|
||||
Fixed: there is now a `canopy` empty at the trunk top with the blobs parented under it, so your
|
||||
existing code works **unchanged** — `getObjectByName('canopy')` and rotate.
|
||||
· **`rake_pivot`.** Same trap, worse. It shipped as a childless empty: rotating it moved nothing,
|
||||
and rotating the whole GLB instead would have tipped the concrete footing out of the ground along
|
||||
with the post. Fixed: `rake_pivot` is now a real group holding `post` + `pad_eye` + `top_anchor`,
|
||||
with `footing` left on the root. Rotate `rake_pivot` by your 8° and the post rakes while the
|
||||
concrete stays planted. Asserted both ways in e.test.js (head must move >0.3 m, footing <0.01 m).
|
||||
Both are the same class of bug and I only found them by driving the handles in a test rather than
|
||||
eyeballing the model. If you add a handle to anything, rotate it in an assert.
|
||||
|
||||
[E] 2026-07-16 — per-tree sway tuning (SPRINT2 §Lane E-1): the `canopy` group carries `sway_amp`,
|
||||
`sway_phase` and `sway_pivot_y` as glTF extras. gum_01 is big and heavy-limbed at amp 0.85; gum_02 is
|
||||
whippy at 1.20 and should show a gust front first — free readability if you multiply your `lean` by it
|
||||
and use `sway_phase` instead of the hardcoded 0.7 / 2.9. Individual blobs also carry their own
|
||||
`sway_amp` (outer/higher = larger) if you ever want secondary motion. Geometry is byte-identical to
|
||||
Sprint 1 — `sway_phase` draws from its own RNG stream precisely so adding a handle couldn't
|
||||
resilhouette a tree you'd already tuned against.
|
||||
|
||||
[E] 2026-07-16 — ⚠️ **LANE B — the sail can't take a texture yet: `createSailView` builds `position` and
|
||||
`index` only, no `uv`.** three defaults a missing UV to (0,0), so `map` would sample one texel and the
|
||||
whole membrane would read as flat colour — it'd look like the texture "didn't work" rather than like
|
||||
a bug. `sail_weave.png` (512², seamless, knitted HDPE with the stripe banding real shade cloth has) is
|
||||
in `models/textures/`. The recipe, against your `N*N` grid:
|
||||
const N = rig.N, uv = new Float32Array(N * N * 2);
|
||||
for (let j = 0, k = 0; j < N; j++)
|
||||
for (let i = 0; i < N; i++, k += 2) { uv[k] = i / (N - 1); uv[k + 1] = j / (N - 1); }
|
||||
geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
|
||||
const tex = await new THREE.TextureLoader().loadAsync('/world/models/textures/sail_weave.png');
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
tex.repeat.set(6, 6); // ~6 tiles across a 5 m sail
|
||||
tex.colorSpace = THREE.SRGBColorSpace; // r175: colorSpace, not encoding
|
||||
mat.map = tex; // keep mat.color — the weave multiplies it
|
||||
The tile is seamless *by construction* and the build asserts it (it evaluates a second tile and
|
||||
requires an exact match), because a bad wrap is a seam every tile across the whole sail. Shout if you'd
|
||||
rather I ship it at a different density. `sail_tears.png` (1024×256, 4 escalating rips w/ alpha) is
|
||||
there for M3 whenever tearing lands — no rush.
|
||||
|
||||
[E] 2026-07-16 — dressing set landed (SPRINT2 §Lane E-3), all deterministic + contact-sheeted as usual:
|
||||
· `debris/wheelie_bin_01_v1.glb` — 240 L kerbside bin, 0.58×0.68×1.12 m, `mass_hint` 12 (empty; a
|
||||
full one doesn't blow over). `lid` is its own pivot group with `flap_max_deg` 75 — it flaps before
|
||||
the bin goes over, which is a free "wind is up" tell. Lane C: it's in debris/, so your glob has it.
|
||||
· `washing_line_01_v1.glb` — a Hills Hoist, 2.84×2.84×2.28 m. `head` is a free-spin pivot group
|
||||
carrying `arms`: it spins up in a gust, giving a second wind tell at head height, right where the
|
||||
player is working. Not debris — it's cemented in.
|
||||
· `garden_gnome_01_v1.glb` — 0.36 m, `mass_hint` 4.5, `breakable`, `collateral_value` 25. Scoring
|
||||
bait per DESIGN.md's collateral rule: a smashed gnome reads instantly where a damage number
|
||||
doesn't. Lane A, he wants to be somewhere a flogging sail can reach him.
|
||||
|
||||
[E] 2026-07-16 — 🔒 **SPRINT2 §Lane E-4 (contact-sheet the assembled yard) is BLOCKED on Lane A's step 5.**
|
||||
Checked main at de86aa1: `world.js` and `main.js` contain zero `_v1.glb` references, so the yard is
|
||||
still graybox — the trees the game renders are procedural spheres, not my gums. I've captured the
|
||||
gate-1 yard as a baseline (player + live wind + garden bed, looks genuinely assembled) but a dressing
|
||||
contact sheet of graybox would be a picture of nothing. **Ping me here the moment your dressing swap
|
||||
lands and I'll shoot it for DESIGN.md same session.** Everything you need is above; nothing of mine is
|
||||
blocking you.
|
||||
|
||||
[E] 2026-07-16 — 👀 art note for whoever owns lighting (A?), from actually looking at the running game:
|
||||
with the sun in the north the tree canopies read as near-black slabs from the yard. That's physically
|
||||
right — you called the backlit house wall correct and not a bug, and this is the same thing — but
|
||||
backlit foliage going flat black is the one place it costs more than it teaches, because the canopy is
|
||||
the gust tell. Cheap fixes in your court: lift the hemisphere light's ground colour, or I can bake a
|
||||
little emissive into the leaf material so gums stay readable from underneath. Say the word and it's
|
||||
one constant in my palette — not touching it unprompted since lighting is yours.
|
||||
|
||||
@ -1,190 +1,6 @@
|
||||
{
|
||||
"blender": "5.1.2",
|
||||
"assets": [
|
||||
{
|
||||
"name": "ref_capsule",
|
||||
"dims": [
|
||||
0.4,
|
||||
0.4,
|
||||
1.7
|
||||
],
|
||||
"tris": 220,
|
||||
"nodes": [
|
||||
"head_height",
|
||||
"ref_capsule",
|
||||
"ref_capsule_mesh"
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
{
|
||||
"name": "tree_gum_01",
|
||||
"dims": [
|
||||
4.5522,
|
||||
4.956,
|
||||
7.9702
|
||||
],
|
||||
"tris": 396,
|
||||
"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": 288,
|
||||
"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": 200,
|
||||
"nodes": [
|
||||
"door",
|
||||
"fascia",
|
||||
"fascia_anchor_01",
|
||||
"fascia_anchor_02",
|
||||
"fascia_anchor_03",
|
||||
"gutter",
|
||||
"house_yardside",
|
||||
"roof",
|
||||
"wall",
|
||||
"window"
|
||||
],
|
||||
"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": [
|
||||
@ -203,178 +19,7 @@
|
||||
],
|
||||
"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": []
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
]
|
||||
"debris": []
|
||||
}
|
||||
@ -829,26 +829,40 @@ def build_sail_post(name):
|
||||
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
|
||||
H, R = 4.0, 0.048
|
||||
|
||||
# The footing is cast into the ground and stays put — only the post rakes.
|
||||
join_group([add_cyl(f"{name}_collar", 0.26, 0.14, (0, 0, 0.05), conc,
|
||||
verts=14),
|
||||
add_cyl(f"{name}_collar_top", 0.22, 0.04, (0, 0, 0.13), conc,
|
||||
verts=14)], "footing", root)
|
||||
join_group([add_cyl(f"{name}_shaft", R, H, (0, 0, H / 2), steel, verts=12),
|
||||
add_cyl(f"{name}_base_plate", 0.11, 0.02, (0, 0, 0.13), dark,
|
||||
verts=12),
|
||||
add_cyl(f"{name}_cap", R * 1.15, 0.02, (0, 0, H), dark,
|
||||
verts=12)], "post", root)
|
||||
# Pad eye at the head — where the corner chain actually clips on.
|
||||
join_group([add_box(f"{name}_padeye", (0.012, 0.07, 0.09),
|
||||
(0, 0, H - 0.10), dark),
|
||||
add_arc_tube(f"{name}_eye", 0.026, 0.008, 0, math.tau, dark,
|
||||
segs=10, center=(0, 0, H - 0.02), plane='XZ')],
|
||||
"pad_eye", root)
|
||||
|
||||
e = add_empty("top_anchor", (0, 0, H - 0.02), root, size=0.2)
|
||||
# rake_pivot is a GROUP, not a marker. Everything above the footing hangs off
|
||||
# it, so rotating it rakes the post while the concrete stays level in the
|
||||
# ground. Shipping it as a childless empty (as Sprint 1 did) means rotating
|
||||
# it moves nothing, and rotating the whole GLB instead tips the footing out
|
||||
# of the dirt with it. Same trap as the canopy handle. Asserted in e.test.js.
|
||||
rake = add_empty("rake_pivot", (0, 0, 0.12), root, size=0.25)
|
||||
rake["rake_axis"] = "x/z — rake AWAY from the load (DESIGN.md)"
|
||||
rake["rake_default_deg"] = 8
|
||||
|
||||
above = []
|
||||
above.append(join_group([
|
||||
add_cyl(f"{name}_shaft", R, H, (0, 0, H / 2), steel, verts=12),
|
||||
add_cyl(f"{name}_base_plate", 0.11, 0.02, (0, 0, 0.13), dark, verts=12),
|
||||
add_cyl(f"{name}_cap", R * 1.15, 0.02, (0, 0, H), dark, verts=12),
|
||||
], "post"))
|
||||
# Pad eye at the head — where the corner chain actually clips on.
|
||||
above.append(join_group([
|
||||
add_box(f"{name}_padeye", (0.012, 0.07, 0.09), (0, 0, H - 0.10), dark),
|
||||
add_arc_tube(f"{name}_eye", 0.026, 0.008, 0, math.tau, dark, segs=10,
|
||||
center=(0, 0, H - 0.02), plane='XZ'),
|
||||
], "pad_eye"))
|
||||
|
||||
e = add_empty("top_anchor", (0, 0, H - 0.02), size=0.2)
|
||||
e["anchor_type"] = "post"
|
||||
e["rating_hint"] = 0.9
|
||||
add_empty("rake_pivot", (0, 0, 0.12), root, size=0.25)
|
||||
above.append(e)
|
||||
for o in above:
|
||||
parent_keep_transform(o, rake)
|
||||
stamp(root, name, "hardware")
|
||||
root["post_height"] = H
|
||||
root["rake_note"] = "rotate about rake_pivot; rake away from the load"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 191 KiB |
@ -180,6 +180,36 @@ export default async function run(t) {
|
||||
'the blob centre, not the trunk top, so the tree cannot visibly sway');
|
||||
});
|
||||
|
||||
// DESIGN.md: rake the post away from the load — so rake is a runtime rotation,
|
||||
// not baked. Rotating rake_pivot must carry the post and its top_anchor over
|
||||
// while the concrete footing stays level in the ground. A post whose footing
|
||||
// tips out of the dirt with it isn't raked, it's falling.
|
||||
t.test('sail_post rakes about rake_pivot with the footing left planted', () => {
|
||||
const g = loaded.get('sail_post');
|
||||
assert(g, 'sail_post did not load');
|
||||
const rake = g.scene.getObjectByName('rake_pivot');
|
||||
const anchor = g.scene.getObjectByName('top_anchor');
|
||||
const footing = g.scene.getObjectByName('footing');
|
||||
assert(rake && anchor && footing, 'sail_post needs rake_pivot, top_anchor and footing');
|
||||
|
||||
const at = () => {
|
||||
g.scene.updateWorldMatrix(true, true);
|
||||
return [new THREE.Vector3().setFromMatrixPosition(anchor.matrixWorld),
|
||||
new THREE.Vector3().setFromMatrixPosition(footing.matrixWorld)];
|
||||
};
|
||||
const [a0, f0] = at();
|
||||
rake.rotation.x += (8 * Math.PI) / 180; // Lane A rakes 8°
|
||||
const [a1, f1] = at();
|
||||
rake.rotation.x -= (8 * Math.PI) / 180;
|
||||
g.scene.updateWorldMatrix(true, true);
|
||||
|
||||
const head = a0.distanceTo(a1), foot = f0.distanceTo(f1);
|
||||
assert(head > 0.3,
|
||||
`an 8° rake moved the head only ${head.toFixed(3)} m — rake_pivot has no children`);
|
||||
assert(foot < 0.01,
|
||||
`the footing moved ${foot.toFixed(3)} m — concrete should stay planted`);
|
||||
});
|
||||
|
||||
// Same pivot class as the canopy: the Hills Hoist head freewheels, so spinning
|
||||
// it has to carry the arms round. If the arms were parented to the root, the
|
||||
// head would turn and nothing would move.
|
||||
|
||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user