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
|
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
|
needs. Same code path the rAF loop uses; no test-only branch that can drift. Everything I reported
|
||||||
above was measured through them.
|
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",
|
"blender": "5.1.2",
|
||||||
"assets": [
|
"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",
|
"name": "sail_post",
|
||||||
"dims": [
|
"dims": [
|
||||||
@ -203,178 +19,7 @@
|
|||||||
],
|
],
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"problems": []
|
"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": [
|
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@ -829,26 +829,40 @@ def build_sail_post(name):
|
|||||||
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
|
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
|
||||||
H, R = 4.0, 0.048
|
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,
|
join_group([add_cyl(f"{name}_collar", 0.26, 0.14, (0, 0, 0.05), conc,
|
||||||
verts=14),
|
verts=14),
|
||||||
add_cyl(f"{name}_collar_top", 0.22, 0.04, (0, 0, 0.13), conc,
|
add_cyl(f"{name}_collar_top", 0.22, 0.04, (0, 0, 0.13), conc,
|
||||||
verts=14)], "footing", root)
|
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["anchor_type"] = "post"
|
||||||
e["rating_hint"] = 0.9
|
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")
|
stamp(root, name, "hardware")
|
||||||
root["post_height"] = H
|
root["post_height"] = H
|
||||||
root["rake_note"] = "rotate about rake_pivot; rake away from the load"
|
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');
|
'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
|
// 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
|
// it has to carry the arms round. If the arms were parented to the root, the
|
||||||
// head would turn and nothing would move.
|
// head would turn and nothing would move.
|
||||||
|
|||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user