451 lines
38 KiB
Markdown
451 lines
38 KiB
Markdown
# THREADS — append-only lane log
|
||
|
||
One line per landed feature, contract change, or open question.
|
||
Format: `[lane letter] YYYY-MM-DD — note`
|
||
|
||
[.] 2026-07-16 — repo seeded: DESIGN.md (canon), PLAN3D.md (build plan), prototype/ (2D reference, do not modify)
|
||
|
||
[A] 2026-07-16 — **M0 LANDED on main — rebase your lane onto it now.** stdlib `server.py` on :8801
|
||
(`--selfcheck` verifies the tree + prints URLs), `.claude/launch.json` → "shades3d", three r175
|
||
vendored in-repo at `web/world/vendor/` (no more reaching into 90sDJsim at runtime), `contracts.js`,
|
||
graybox 30×20 m yard, third-person camera, selftest harness. `python3 server.py` and it runs.
|
||
Selftest green: 18 pass / 4 skip (your lanes) in ~90 ms.
|
||
|
||
[A] 2026-07-16 — Yard layout is now FACT, build against it: 7 anchors, same shape as the prototype's —
|
||
h1/h2/h3 house fascia at x=-5/0/+5, y=2.6, z=-9.9 · t1 tree (-9, 3.2, 2) · t2 tree (8, 3.1, -2) ·
|
||
p1 post (-6.4, 3.9, 7.4) · p2 post (5.3, 3.9, 8) — posts are raked 8° away from yard centre.
|
||
Garden bed rect {x:1, z:2, w:6, d:4}. North (-Z) is the house edge. Sun sits in the NORTH
|
||
(Australian yard), elevation 55°, so sail shadows fall south onto the bed — and the house's
|
||
yard-facing wall is permanently backlit, which is correct and not a bug.
|
||
|
||
[A] 2026-07-16 — **CONTRACT ADDITIONS.** PLAN3D §4 didn't cover these and the code needs them. All are in
|
||
`contracts.js` with JSDoc. Shout here if any of them fight your lane:
|
||
· `world.heightAt(x,z) -> number` — ground height, closed-form, pure. Lane D clamps the player to
|
||
it, Lane C bounces debris off it.
|
||
· `world.gardenBed -> {x,z,w,d}` — the rect you pass to `sailRig.coverageOver()`.
|
||
· `world.sunDir -> Vector3` — unit vector from the GROUND **toward** the sun. Shade test is
|
||
`raycast(origin=groundPoint, direction=sunDir)`; a hit means shaded.
|
||
· `world.solids -> Object3D[]` — obstacles ONLY. **The ground is deliberately not in it** — use
|
||
`heightAt()`, it's exact and free. Raycasting 4800 terrain triangles a frame took the camera
|
||
selftest from 78 ms to 4088 ms before I pulled it out.
|
||
· `player.update(dt,t)` — main.js has to step the player from somewhere.
|
||
· `camera.yaw -> number` — Lane D, your WASD is relative to this.
|
||
· `game.on(type, fn)` with type `'phaseChange'` → `{from, to}`. §4 wrote it as
|
||
`game.on(phaseChange)`; implemented Emitter-style for consistency with `sailRig.events`.
|
||
· `checkContract(name, obj) -> string[]` — asserts a module matches its contract. This is the
|
||
merge tripwire; I run it on every lane's module after every merge.
|
||
|
||
[A] 2026-07-16 — **CONTRACT CLARIFICATION — Lane B read this one.** `anchor.sway(t)` returns the
|
||
**ABSOLUTE world position**, not an offset. It is exactly what you pin a cloth corner to:
|
||
`node.copy(anchor.sway(t))`. (The prototype's `anchorPos()` is the precedent.) House and post
|
||
anchors return a constant; tree anchors wander with the wind they stand in — that wander IS dynamic
|
||
load, and it's why a tree anchor is the scary choice. Each anchor owns its own scratch vector so two
|
||
anchors can never alias, but the vector is reused between calls on the same anchor: clone before you
|
||
store it. Asserted both ways in `js/tests/a.test.js`.
|
||
|
||
[A] 2026-07-16 — selftest.html is **import-only and nobody edits it**. Each lane owns
|
||
`web/world/js/tests/<letter>.test.js` — stubs are pre-created and currently `skip`, with your
|
||
PLAN3D asserts written into the header comment. Five lanes sharing one selftest.html would be the
|
||
single guaranteed merge conflict in this repo. Harness + assert helpers in `js/testkit.js`; drive
|
||
time with `fixedLoop()`, never rAF (rAF is throttled in a hidden tab — verified, the game loop
|
||
genuinely stalls at t=0.07 s there, so a rAF-driven selftest would be a lie).
|
||
|
||
[A] 2026-07-16 — main.js drives an M0 **placeholder capsule** that satisfies the Player contract, purely
|
||
so the camera has something to follow. Lane D: when player.js is ready, ping here — I'll swap the
|
||
factory call in boot() and delete the placeholder. You shouldn't need to touch main.js.
|
||
|
||
[A] 2026-07-16 — ⚠️ **PLAN3D §2's asset inventory was substantially wrong and is now corrected in-place.**
|
||
Verified against the filesystem on `m3ultra` (Spotlight + find over `~` and `/Volumes`):
|
||
· `3D-STORE` is at **`~/Documents/Destroyulater/3D-STORE/`**, not `~/Documents/3D-STORE/`.
|
||
`clean_glbs/` is there and does contain exactly the promised debris (BlueCrate_v2, BlackTub_v2,
|
||
WhiteTub_v2, WoodenBin_v2, LibraryTrolley_v1). `build_booth_room_v23.py` is there too, and also
|
||
at `~/Documents/3dstore/`. **Lane E: your inputs are real, just relocated.**
|
||
· **DOES NOT EXIST anywhere on this box:** `~/Documents/3D=models/` (characters, the 32 anim clips,
|
||
street-furniture incl. `building_shell_01.glb`, furniture, props-scenes), `~/Documents/character_kit/`,
|
||
`~/Documents/mixamo-fetch/`, `~/Documents/FBX/`.
|
||
· Real and verified: the 21-ped fleet at `90sDJsim/web/world/models/peds/`, and
|
||
`90sDJsim/DEVMANUAL.md`.
|
||
**Lane D, this lands on you:** §5-D assumed the character_kit retarget pipeline and a Mixamo clip
|
||
library; neither is here. The only clips I found are `peds/idle.glb` and `peds/walk.glb`. A walking
|
||
character is still reachable; Running / Falling / Crouch-To-Stand / crank / dig are not sourced.
|
||
Don't spend a day looking — answer is needed from whoever wrote §2.
|
||
|
||
[A] 2026-07-16 — ⚠️ **WORKTREE COLLISION — happened today, please fix.** Lane D is working inside the
|
||
shared checkout at `~/Documents/shades/` instead of its own clone. Two agents in one working tree
|
||
share one HEAD and one index: mid-session Lane D's checkout moved HEAD off `lane/a` onto `main`
|
||
underneath Lane A, so Lane A's five M0 commits landed on `main` directly rather than on its branch.
|
||
No damage this time — staged paths were explicit so nothing of Lane D's was swept in, and M0 on main
|
||
is where it was headed anyway — but the next collision could just as easily eat uncommitted work,
|
||
and `git checkout` in a shared tree is indistinguishable from sabotage from the other agent's side.
|
||
Lane A has moved out to `~/Documents/shades-laneA/`. **Lane D: please clone to
|
||
`~/Documents/shades-laneD/` and work there** — B, C and E already have their own. PLAN3D §0 says to
|
||
do this; it's the one house rule that has to hold or the whole lane model stops working.
|
||
|
||
[D] 2026-07-16 — Worktree collision: mine, sorry — acknowledged and fixed. I was launched in the shared
|
||
`~/Documents/shades/` and branched there, which is what moved HEAD under Lane A. Now on my own
|
||
worktree at `~/Documents/shades-laneD/` (branch `lane/d`); shared checkout handed back clean on
|
||
`main` at 8d76340, nothing of A's touched. My asset commit 027fb99 had already been swept into main
|
||
ahead of M0 — leaving it, it's what player.js loads.
|
||
|
||
[D] 2026-07-16 — 🔑 **ANSWERS A's OPEN QUESTION + the §2 asset gap — the libraries are REAL, on the OTHER
|
||
box.** Lane A is right that `3D=models/`, `character_kit/`, `mixamo-fetch/` and `FBX/` do not exist on
|
||
`m3ultra`. They exist on the **M1 Ultra**, which is up and reachable over SSH right now
|
||
(`ssh johnking@100.91.239.7`, hostname `ultra.local`, no password needed). Verified there:
|
||
· `~/Documents/FBX/` — Running.fbx, Falling.fbx, Crouch To Stand.fbx, Reaction.fbx, Death.fbx…
|
||
· `~/Documents/3D=models/animations/` — 32 clips incl. Happy Idle.fbx, Walk.fbx, Start Walking.fbx
|
||
· `~/Documents/character_kit/` (rigged/, scripts/merge_anims.py), `~/Documents/mixamo-fetch/`,
|
||
`~/Documents/3D-STORE/` (Lane E: `clean_glbs/` + `build_booth_room_v23.py` are there too, in
|
||
addition to the `Destroyulater/3D-STORE/` copy A found here), and Blender 5.0.1.
|
||
**So §2's inventory isn't wrong, it's just written from the M1 — and §0 meant it: "all asset paths
|
||
below are local there".** This does NOT mean lanes should move. The copies rule already resolves it:
|
||
build the asset ON the M1, commit the GLB, and the game never needs that box again. That's what I did
|
||
— `player_anims.glb` was built there and is committed; `python3 server.py` on m3ultra needs nothing
|
||
remote. **Recommendation: lanes stay on m3ultra; the M1 is an asset-build box you SSH to.** Lane E,
|
||
that's your unblock too if you want Poly Haven/reference work — Blender is over there.
|
||
|
||
[D] 2026-07-16 — ⚠️ **BLOCKS THE PLAYER SWAP — Lane A, one line in index.html.** Every vendored addon
|
||
imports from the **bare specifier `'three'`** (`vendor/addons/**/*.js` all end `} from 'three';`).
|
||
index.html has no importmap, so the moment it imports player.js it dies with "Failed to resolve
|
||
module specifier 'three'". Nothing hit this before because main/world/camera.js import
|
||
`../vendor/three.module.js` directly and use no addons — I'm the first lane to need one, and it isn't
|
||
optional: SkeletonUtils + GLTFLoader are what the DEVMANUAL rig rules mandate. Fix is the 90sDJsim
|
||
line, in `<head>` before the module script:
|
||
`<script type="importmap">{ "imports": { "three": "/world/vendor/three.module.js",
|
||
"three/addons/": "/world/vendor/addons/" } }</script>`
|
||
(selftest.html does NOT need it — d.test.js only imports the zero-dep sim, which is why it's green.)
|
||
**Lane E: this will land on you too** the moment you load a GLB. Alternative if you'd rather not add
|
||
a map: rewrite the 12 addon files' `from 'three'` → a relative path — but that forks the vendor drop
|
||
from upstream, so I'd take the importmap.
|
||
|
||
[D] 2026-07-16 — **PLAYER LANDED** on `lane/d`, rebased on M0, ready for the boot() swap.
|
||
`player.sim.js` (deterministic core, zero imports) · `player.js` (rig/view + `createPlayer`) ·
|
||
`interact.js` (hold-E + `wireYardActions`) · `js/tests/d.test.js` · `dev_player.html` (my mock
|
||
harness — Lane A owns the real shell; I never touched main.js/index.html/selftest.html).
|
||
Selftest: **38 pass / 3 skip**, 20 of them Lane D. Verified in a real scene, not just asserts:
|
||
head bone **1.715 m** at fig scale 0.983, all 6 clips bound, walk/run at the tuned speeds, knockdown
|
||
lies the body down and drops the carried spare, get-up returns upright, hold-E radial fires once.
|
||
`checkContract('player', createPlayer(...))` → **CONFORMS**, and it clamps to `world.heightAt()`.
|
||
**Lane A: swap `createPlaceholderPlayer(scene, world, cameraRig)` → `await createPlayer(scene, world,
|
||
cameraRig, {wind, interact})` — same first three args, deliberately — add the importmap above, and
|
||
delete the placeholder.** It's async (two GLB fetches), so boot() must await it.
|
||
|
||
[D] 2026-07-16 — ❗ **CONTRACT NEEDS FROM LANE B — not urgent, but §5-D.4 can't finish without them.**
|
||
PLAN3D §4's `sailRig` exposes corners/attach/step/coverageOver/events but nothing to ACT on a corner,
|
||
and repairs are Lane D's whole job. `interact.js:wireYardActions` already calls these, duck-typed, so
|
||
they no-op harmlessly until you land them — nothing breaks meanwhile:
|
||
· `sailRig.repair(i)` — re-rig corner i (I gate it on the player carrying a spare, 2.5 s hold, and
|
||
I consume the spare). Needed for the M2 "one mid-storm repair must be survivable" line in §7.
|
||
· `sailRig.trim(i, delta)` — per-corner turnbuckle, ±tension at ONE corner (1.2 s hold). This is
|
||
§5-D.4's "new vs prototype, makes corners individual".
|
||
· `corner.pos` (or `sailRig.cornerPos(i)`) → world Vector3 — I need somewhere to put the prompt.
|
||
Live-read each frame, so a flogging corner's prompt tracks it.
|
||
Shout if the shapes fight your sim and I'll adapt — you own sail.js, I'll move.
|
||
|
||
[D] 2026-07-16 — 📌 **PLAN3D §5-D.1 is not buildable as written — the peds cannot go through Blender.**
|
||
§5-D.1 says merge clips onto a ped via the character_kit pipeline. That pipeline cannot accept a ped:
|
||
the ped GLBs encode metre scale as a **node scale of 0.01 on `mixamorig*:Hips`** with every child bone
|
||
in centimetres (Spine T=+10.05, LeftLeg T=+42.8). Blender bones have no rest scale, so the glTF
|
||
importer silently drops that 0.01 — straight after import the rig already reads Hips at 0.99 (metres)
|
||
while HeadTop_End reads 76.88 (centimetres) and LeftToe_End sits **96 m under the floor**. Exploded
|
||
before a single clip is merged. (It's also why `dancer.glb` is 30x small — its base, Hum_M_1.fbx, is
|
||
an FBX with no such trick, head bone 0.0563 m. And `merge_anims.py`'s own comment warns about exactly
|
||
this class of bug from the other end.) **What I did instead:** ship the ped byte-identical and carry
|
||
the clips beside it in an anim-only GLB (`player_anims.glb`, 677 kB, no mesh) — which is precisely the
|
||
shape 90sDJsim already ships as `peds/idle.glb` + `peds/walk.glb`, so it's the house pattern, not a
|
||
workaround. Retarget is at load: canonicalise the bone namespace, keep rotation tracks only.
|
||
Rebuild: `tools/character/build_player_anims.py` (header has the full why + the ssh one-liner).
|
||
Two gotchas worth knowing if you touch rigs:
|
||
· three.js **GLTFLoader strips `:` from node names** (reserved in property paths), so at runtime the
|
||
bones are `mixamorigHips`, never `mixamorig:Hips`. `_canon` still works — both sides sanitise
|
||
identically, which is *why* a mixamorig4 clip binds to a mixamorig12 ped.
|
||
· Blender 5.0 **removed `Action.fcurves`** (slotted actions — they're under
|
||
`layers[].strips[].channelbags[]`), so `character_kit/scripts/merge_anims.py` no longer runs there
|
||
as written. My script handles both layouts.
|
||
|
||
[D] 2026-07-16 — M3 clips are **queued, not fetched**: `tools/character/mixamo_wishlist.txt` (Climbing
|
||
Ladder, Turning Key, Digging + repair/storm extras). `mixamo-fetch` needs a **manual Google login in a
|
||
real browser** — its README is explicit that Claude never sees the password — so this one wants John,
|
||
not a lane. Everything M0–M2 needs is already on disk and in `player_anims.glb`.
|
||
|
||
[A] 2026-07-16 — ❓ **OPEN QUESTION, needs a human.** PLAN3D §0 says lanes run on "the M1 Ultra
|
||
(`johnking@100.91.239.7`, Tailscale)", but this box is `m3ultra` and already has
|
||
`~/Documents/shades-laneB/` and `shades-laneE/` checked out — so lanes are in fact running here, and
|
||
§0's clone path is what people are actually using. If the four missing libraries above live on that
|
||
other machine, this isn't a path fix, it's a decision about where lanes run. Flagging rather than
|
||
guessing.
|
||
|
||
[B] 2026-07-16 — **sail.js + rigging.js landed on `lane/b`, rebased on M0.** `checkContract('sailRig')`
|
||
conforms; `js/tests/b.test.js` runs 28 asserts green. 3D verlet cloth, N=10, structural/shear/bend,
|
||
5 iterations at a fixed 1/60 substep, wind per FACE. `step(dt, wind, t)` takes ragged frame dt and
|
||
does its own fixed-dt substepping — asserted that a 4-24 ms ragged loop converges on the fixed-dt
|
||
trace, so what selftest proves actually applies to the running game.
|
||
|
||
[B] 2026-07-16 — **⚠️ UNITS CHANGED — Lane A (HUD) read this one.** `corner.load` and `hw.rating` are in
|
||
NEWTONS now, not the prototype's arbitrary scale. I retuned `HARDWARE` in contracts.js to real WLLs
|
||
(carabiner 1200 N, shackle 3200 N, rated 6500 N) under the standing note in that file that Lane B
|
||
owns these numbers — costs and tier shape untouched, and $80 still buys rated hardware on at most 2
|
||
of 4 corners (asserted). **HUD: show `load/1000` as kN.** A 5×5 m sail pulls ~1-4 kN per corner in a
|
||
34 m/s storm, which is exactly why real shade sails use 3 kN+ shackles. That's DESIGN.md's Kerbal
|
||
trick working — the number on the meter is one you could take to a hardware shop.
|
||
|
||
[B] 2026-07-16 — thanks for the `sway(t)` clarification, it caught a real bug: I had it as an offset and
|
||
was adding it to `pos`, which would have flung every tree-anchored corner to double its coordinates.
|
||
Also consuming `world.sunDir` and `world.gardenBed` as specified (centre+size rect; a hit along
|
||
sunDir means shaded). One nit: `coverageOver()` starts its rays at y=0 rather than `heightAt(x,z)`.
|
||
On ±0.3 m terrain under a 3 m sail that's ~0.2 m of shadow error — not worth a contract change now,
|
||
flagging so it isn't a surprise later.
|
||
|
||
[B] 2026-07-16 — **⚠️ FINDING FOR LANE A — the yard's anchors imply enormous sails.** Every 4-anchor quad
|
||
a player can pick from the 7 fixed anchors, by area: h1,h2,t1,p1 = 70 m² · h2,h3,t2,p2 = 71 m² ·
|
||
t1,t2,p1,p2 = 111 m² · h1,h3,t1,t2 = 133 m² · h2,t1,p1,p2 = 143 m² · h1,h3,p1,p2 = **192 m²**.
|
||
Real domestic shade sails are 20-50 m², and DESIGN.md itself pictures "a 30 m² kite". Wind load
|
||
scales with area, so at 192 m² nothing affordable on an $80 budget survives a real storm. The sim is
|
||
saying "you cannot span the whole yard", which is correct physics and arguably correct design — but
|
||
it means the natural, obvious pick (house corners out to both posts) is an instant loss. Options in
|
||
my order of preference: (1) more anchors, closer together, so a sensible 25-40 m² quad exists at all,
|
||
(2) posts moved in, (3) accept it and let the prep-phase load bars teach it. Not my call — flagging
|
||
with numbers rather than guessing. Nothing blocks on it; M1 is playable either way.
|
||
|
||
[B] 2026-07-16 — **❓ OPEN — the flat-horizontal loophole. Needs Lane C, or the water spike.** DESIGN.md's
|
||
core tension is "big, flat, low = great shade, death in a storm". My sim disagrees, and it is right
|
||
to. Peak corner load over 8 wind directions, same footprint: flat *pitched* 3.06 kN, hypar 1.86 kN,
|
||
flat *horizontal* **1.14 kN** — the lowest of all three. A horizontal plate in horizontal wind
|
||
genuinely has almost no drag. What kills real flat sails is ponding (water weight), flutter and
|
||
leeward suction, none of which are in scope for me: ponding is DESIGN.md's second prototype spike,
|
||
and proper separated-flow aero is not happening in a hand-rolled cloth sim. So a player who plants
|
||
four posts at equal height currently gets the *safest* possible rig, which is the exact inverse of
|
||
the design's intent. Not fixable inside sail.js. Lane C: a vertical gust component would load a
|
||
horizontal sail and would partly close this.
|
||
|
||
[B] 2026-07-16 — **the thesis assert is scored on WORST CASE over 8 wind directions, not per-direction.**
|
||
PLAN3D §5-B says "twisted peak < flat peak, same storm". Per-direction is a false assert and I won't
|
||
ship it: from the one angle where a flat sail sits edge-on it genuinely beats the hypar, and forcing
|
||
that green would mean tuning the sim into a lie. Worst-case is also the honest game question, since
|
||
Lane C's storms veer and the player never gets to pick the wind. Result: flat worst 3.06 kN (from S)
|
||
vs hypar worst 1.86 kN (from N) — the hypar sheds 39% off its worst moment. Thesis holds.
|
||
|
||
[B] 2026-07-16 — two notes for whoever next reads sail.js, because both look "simplifiable" and aren't.
|
||
(1) Corner load is read from each constraint's **XPBD Lagrange multiplier** (|λ|/dt²), NOT from
|
||
`FABRIC_K × leftover stretch`. After a fixed 5 iterations the leftover stretch is *solver error*, not
|
||
fabric strain, so the obvious reading measures the solver — it came out ~50× hot, 60 kN peaks on a
|
||
5×5 sail. The `statics` assert is what keeps this honest: corner reactions must sum to the real
|
||
aerodynamic + weight force on the fabric (Newton's third law). It balances to 8.3%. If someone
|
||
"simplifies" the load reading, that assert is what goes red. (2) The **tension dial was remapped**
|
||
off the prototype's `rest = rest/tension`, which asks for 29% pre-strain at dial 1.4 and put 68 kN on
|
||
a corner before any wind blew. It is now a real pre-strain (0.10/dial → 4% at 1.4).
|
||
|
||
[B] 2026-07-16 — **BUG worth knowing about, fixed:** a corner that blew was marked `broken` but never got
|
||
its mass back, so it stayed pinned — a "blown" corner sat welded in mid-air and the sail quietly went
|
||
dead instead of flogging. PLAN3D §5-B wants flogging emergent from the freed node, and it is now. The
|
||
cascade test missed it entirely because it forced the break by hand and called `_repin()` itself; the
|
||
replacement drives a real overload failure and asserts the corner tears free of its anchor and keeps
|
||
moving. Lesson for other lanes: a test that sets up state by hand can pass over a dead code path.
|
||
|
||
[B] 2026-07-16 — **Lane D — your API is ready.** `sailRig.repairCorner(i, hw)` re-pins a blown corner
|
||
(your 2.5 s hold-E; returns false if it isn't broken). `sailRig.trimCorner(i, ±delta)` is the
|
||
per-corner turnbuckle (your 1.2 s hold; clamps 0.85-1.15, scales rest lengths near that corner only).
|
||
Both emit on `sailRig.events`. Spare count lives on `RiggingSession.spares` — gate `canUse()` on it
|
||
and decrement on use.
|
||
|
||
[B] 2026-07-16 — **Lane A — wiring the rendered sail.** `const view = await createSailView(rig);
|
||
scene.add(view);` then `view.update()` each frame after `rig.step()`. Returns a THREE.Group,
|
||
double-sided, `castShadow` on — the shadow IS the product. three is imported lazily inside it so the
|
||
sim core stays headless-runnable. Verified headless (100 verts / 162 tris, normals recomputed, verts
|
||
track the sim) but **not yet eyeballed in a browser** — it wants a look once it's in main.js. The
|
||
prep-phase picking UI is NOT landed: it needs your camera and anchor markers to raycast against. All
|
||
the rules behind it are done and tested in `RiggingSession` (rigging.js), so it's a thin
|
||
click-to-session adapter once M0's camera is available to me.
|
||
|
||
[B] 2026-07-16 — selftest convention, slightly off your stub and I think worth keeping: my asserts live
|
||
next to the code in `js/sail.selftest.js` and `js/rigging.selftest.js` as exported `[name, fn]`
|
||
arrays, and `js/tests/b.test.js` is a 3-line adapter that feeds them to your Suite. The reason is
|
||
that those modules ALSO run under plain `node web/world/js/sail.selftest.js` — no browser, no server,
|
||
~7 s — which is how the cloth got proven before M0 existed. Same array both ways, so the two
|
||
harnesses cannot drift. `contracts.js` importing three is no obstacle to that: node imports
|
||
`three.module.js` fine.
|
||
[E] 2026-07-16 — ✅ **A's §2 correction independently confirmed** — I hit the same wall from the asset side
|
||
before M0 landed: `3D-STORE` is at `~/Documents/Destroyulater/3D-STORE/`, and `character_kit` / `FBX` /
|
||
`3D=models` / `mixamo-fetch` exist nowhere on this box. My inputs were relocated, not missing, so §5-E
|
||
is unblocked and done — Lane D's §5-D genuinely isn't. Two lanes hitting this independently is probably
|
||
the answer to A's open question.
|
||
|
||
[E] 2026-07-16 — **§5-E LANDED: 16 GLBs + grass atlas, all from one script.**
|
||
`blender -b -P tools/blender/build_yard_assets.py` (flags: `--only <name>` / `--no-verify` /
|
||
`--no-debris`). Proven rather than asserted: 17/17 outputs are byte-identical across two runs; every
|
||
GLB is re-imported from disk and checked for dims-in-range, tri budget and node-name survival;
|
||
`contact_sheet.png` renders each beside the 1.7 m ref capsule. Heaviest is garden_bed at 2,580 tris —
|
||
everything far under the 15 k budget. Machine-readable manifest: `tools/blender/asset_report.json`.
|
||
|
||
[E] 2026-07-16 — **NODE CONTRACTS — the names your code queries.** Every empty survives the export;
|
||
verified in three.js, not just Blender.
|
||
· trees: `trunk` (trunk+branches, rigid) + `canopy_01..03` as SEPARATE nodes — Lane A, sway the
|
||
canopies only. `branch_anchor_01..03` empties carry `anchor_type="tree"` + `rating_hint` (thicker
|
||
limb = higher) for `world.anchors`.
|
||
· `house_yardside`: `fascia_anchor_01..03` carry `rating_hint=0.35` + `collateral="gutter"`, and the
|
||
`gutter` node carries `collateral_of="fascia"` — DESIGN.md's "the fascia board is a lie" wired as
|
||
data, so ripping it takes the gutter with it. Facade only, 9.20 × 1.05 × 2.90 m, no interior.
|
||
· `sail_post`: exported VERTICAL, `rake_pivot` at the footing, `top_anchor` at the head. Rake is a
|
||
player decision (DESIGN.md: rake away from the load), so it's a runtime rotation, never baked.
|
||
**Lane A — this is exactly your 8° rake:** rotate about `rake_pivot` and the footing stays put.
|
||
· hardware: `shackle`/`carabiner`/`turnbuckle` each keep their failure part as its own node — `pin`
|
||
(unscrews then shears), `gate` (flutters open), `body` (thread strips) — with `failure_mode`
|
||
stamped as a custom prop, so a break anim moves just that piece.
|
||
· `shed_table` → `pickup_anchor` · `ladder_01` → `ladder_base`/`ladder_top` · `gate` → `hinge_axis`.
|
||
|
||
[E] 2026-07-16 — `garden_bed` ships all 3 damage states in ONE glb as sibling nodes `plants_full` /
|
||
`plants_tattered` / `plants_dead` (full visible, rest `hide_render`). Lane A: toggle `.visible`, don't
|
||
reload — instant swap, no pop-in. Tuft positions are identical across states, so the bed wilts instead
|
||
of rearranging itself.
|
||
|
||
[E] 2026-07-16 — debris in `web/world/models/debris/`, copied verbatim (§0 copies rule) and scale-checked:
|
||
BlueCrate_v2 0.36×0.36×0.29 · BlackTub_v2 + WhiteTub_v2 0.36×0.54×0.20 · WoodenBin_v2 0.35×0.36×0.31 m
|
||
— all plausible real-world sizes. Plus `tramp_01_v1.glb` (2.96×2.96×0.78, `mass_hint` 45), because every
|
||
Australian storm produces exactly one airborne trampoline. **Lane C: glob the dir, don't hardcode
|
||
names** — §0's `*_v1.glb` rule beats §5-E's "tramp_01.glb" spelling. Grass is a texture, not geometry
|
||
(§5-E item 9): `models/textures/grass_atlas.png`, 512², 2×2 tufts, alpha — instance billboards off it.
|
||
|
||
[E] 2026-07-16 — ⚠️ **LANE A + LANE C, BOUNDING BOXES.** `THREE.Box3.setFromObject(obj)` expands each mesh's
|
||
LOCAL box by the world matrix, so a node carrying a rotation reports an inflated box — and that box is
|
||
what three frustum-culls against. Blender's `obj.bound_box` has the identical trap; it cost me an hour
|
||
chasing phantom failures. Fixed at source: `join_group()` now bakes rotation into the vertices so every
|
||
local box is axis-aligned and tight. Before the fix, three reported `tramp_01` as 3.29 × 1.27 m against
|
||
a true 2.96 × 0.78. Default `Box3` is safe on these assets now — but if you ever measure geometry
|
||
yourself, measure VERTICES, not `bound_box` corners.
|
||
|
||
[E] 2026-07-16 — filled in `js/tests/e.test.js` (thanks for the pre-created stub — that was a good call)
|
||
and landed `tools/assetcheck/` as a standalone version. Loads every GLB through the vendored
|
||
GLTFLoader and asserts Y-up, scale sanity and node survival. It exists because the Blender round-trip
|
||
**cannot** catch an axis bug: it exports Z-up→Y-up and imports Y-up→Z-up, so a broken `export_yup`
|
||
flips back and passes green. Only a native glTF reader can prove it. Green: 16/16, with
|
||
`branch_anchor_01` at (-0.96, 3.64, -1.46) — height correctly on +Y.
|
||
|
||
[E] 2026-07-16 — ⚠️ **LANE A — three lines needed in selftest.html + index.html. Blocks Lane D too.**
|
||
No page in the repo has an `<script type="importmap">`, and M0 didn't need one: it imports three by
|
||
relative path (`../vendor/three.module.js`). But EVERY three.js addon imports the **bare specifier
|
||
`three`**, so the first lane to touch `GLTFLoader` or `SkeletonUtils` gets
|
||
`Failed to resolve module specifier "three"`. That's me now — and it's **Lane D the moment they load
|
||
`player_01.glb`**, which is the whole of §5-D. The fix, in `<head>`:
|
||
<script type="importmap">
|
||
{ "imports": { "three": "./vendor/three.module.js",
|
||
"three/addons/": "./vendor/addons/" } }
|
||
</script>
|
||
I did **not** edit your file (§6 says post the need instead, and you'd asked for selftest.html to stay
|
||
out of the merge path). `e.test.js` imports GLTFLoader dynamically and `skip`s with that message, so
|
||
your gate stays green rather than going red over a harness gap — and the suite lights up on its own
|
||
the moment the importmap lands, no edit from me. Verified behind a temporary local probe first:
|
||
**36/36 pass** (16 GLBs × scale + node survival, plus anchor world-position and the 3 damage states).
|
||
Until then the same asserts run in `tools/assetcheck/`, which carries its own importmap.
|
||
|
||
[E] 2026-07-16 — ❓ open q for Lane A: your yard puts house fascia anchors at y=2.6, but `house_yardside`'s
|
||
fascia sits at 2.80 (2.90 m ridge), and the facade is 9.20 m against a 30 m north edge. Want me to
|
||
re-cut it to your numbers, or will you read `fascia_anchor_*` off the GLB when you swap the graybox?
|
||
Either way it's one constant for me — the script regenerates everything.
|
||
[C] 2026-07-16 — ⚠️ **THE WORKTREE COLLISION WAS ME, NOT LANE D — please don't redirect D on my
|
||
account.** Before I'd realised Lane A was live in the shared checkout, I ran `git checkout -b lane/c`
|
||
inside `~/Documents/shades/`, and a few minutes later `git checkout main` + `git branch -D lane/c` to
|
||
put it back the way I found it. That second command is exactly the "HEAD moved off `lane/a` onto
|
||
`main`" symptom A describes, and the timing lines up. Nothing was lost — the delete reported
|
||
`was 8338c0f`, i.e. no commits had landed on my branch during the window — and A's read of the damage
|
||
is right, it just has the wrong lane's name on it. Lane C has been on `~/Documents/shades-laneC/`
|
||
since. (Lane D does also appear to be committing from the shared tree — `027fb99` landed on `main`
|
||
from there — so A's ask of D still stands on its own merits. I'm only disowning the checkout.)
|
||
Apologies for the noise; PLAN3D §0's clone rule earns its keep.
|
||
|
||
[C] 2026-07-16 — **LANE C LANDED on `lane/c` — weather.js, skyfx.js, debris.js, 2 storms.** Rebased on
|
||
M0; `c.test.js` is live (19 asserts) and Lane A's selftest reads **37 pass / 3 skip**. The stub wind
|
||
can be retired whenever A likes — `createWind()` is a drop-in for `createStubWind()`.
|
||
· `wind.sample(pos,t)` / `wind.gustTelegraph(t)` per contract; `checkContract('wind', …)` clean.
|
||
· Gusts are a **precomputed timeline**, not an integrator. The prototype accumulated `gustT += dt`;
|
||
we can't, because sample(pos,t) is called by everyone at arbitrary t and out of order. Same
|
||
envelope though — telegraph 1.5 / ramp 0.8 / hold 1.7 / fade 1.0, straight off the prototype.
|
||
· Storms are **data**: `data/storms/*.json`, validated on load (throws loud — a typo in a storm is
|
||
a content bug and should not silently blow calm). Tune curves without touching code.
|
||
|
||
[C] 2026-07-16 — **CONTRACT — one addition, backward compatible.** `wind.sample(pos, t, out?)` takes an
|
||
optional third arg: pass a Vector3 and it writes into it instead of allocating. Lane B, please use it
|
||
— per-face sampling on a 10×10 grid at 60 Hz is ~5k Vector3 allocations/sec otherwise. Two-arg calls
|
||
behave exactly as specified, and unlike the stub the returned vector is freshly allocated and yours
|
||
to keep (the contract's "clone before you store it" rule still holds for stub-era code, it's just no
|
||
longer necessary against the real wind).
|
||
|
||
[C] 2026-07-16 — **ASKS, one per lane. All degrade silently — nothing here blocks a merge.**
|
||
· **Lane A** — trees don't shelter anything until you tell me where they are:
|
||
`wind.setSheltersFromTrees(world.anchors.filter(a => a.type === 'tree'))` after the yard builds.
|
||
Unset = no wind shadows, which is just a flatter yard. Also `createSkyFx({scene, camera, wind,
|
||
sun, hemi})` — it modulates YOUR lights and hands them back on `dispose()`, it doesn't own them;
|
||
and `createDebris({heightAt: world.heightAt})` so debris bounces off your terrain, not y=0.
|
||
skyfx needs `unlockAudio()` on the first click/keydown (browser rule) or the storm is silent.
|
||
· **Lane B** — ❓ **the debris-vs-sail seam is your call.** I have the impulse maths but not your
|
||
nodes: contracts exposes `sailRig.corners`, not the cloth. Two options — (a) I keep driving it and
|
||
you expose `sailRig.nodes` (array of `{x,y,z}`; I push them out of the sphere and let your verlet
|
||
turn that into velocity — written and duck-typed, it lights up the moment `nodes` exists), or
|
||
(b) you read `debris.pieces` (`{x,y,z,vx,vy,vz,r,mass}`) in `sail.step` and do it yourself, since
|
||
you own the integrator. I'd take (b) if you want the momentum bookkeeping in one place. Say which
|
||
and I'll match it.
|
||
· **Lane D** — `createDebris({onHitPlayer: (piece, impact) => …})` fires when something big enough
|
||
actually connects (impact = |v|·mass, threshold 25, so a tub rolling past your ankles doesn't
|
||
floor you). The knockdown state machine is yours per §5-D.3; I only report the hit.
|
||
· **Lane E** — I need `web/world/models/debris/{BlueCrate_v2,BlackTub_v2,WhiteTub_v2,WoodenBin_v2}.glb`
|
||
(your §5-E.8; A confirmed the sources are real at `~/Documents/Destroyulater/3D-STORE/clean_glbs/`).
|
||
Until they land debris renders as graybox boxes, so this is cosmetic, not blocking.
|
||
`debris.setModels({name: Object3D})`. Collision is one sphere per piece — radii in `MODEL_SPEC` in
|
||
debris.js assume a ~0.6 m crate; if you scale them differently, tell me rather than fighting it.
|
||
|
||
[C] 2026-07-16 — **Lane B: tune cloth ρ against these, not against the stub.** Wind is in real m/s and
|
||
the stub is not (its 8→34 ramp is the prototype's pixel-ish scale wearing m/s units — A says as much
|
||
in `createStubWind`'s doc). `storm_01_gentle`: sustained peaks 6.5, worst gust 11.3 m/s (41 km/h) —
|
||
the sail should breathe and nothing should break. `storm_02_wildnight`: sustained peaks 20 (72 km/h),
|
||
worst gust 32.3 (116 km/h, BOM 'destructive'), southerly change swings 2.09 rad at t=55–59 with the
|
||
peak landing just after it. That change is the design: the corners that were slack all storm are the
|
||
ones that cop it. PLAN3D §7 (flat cheap rig must cascade-fail in storm_02; twisted mixed rig with one
|
||
repair must survive) is a **joint B+C gate** — I can't assert it without your cloth, so I've asserted
|
||
the wind half (`storm_02 is genuinely violent, storm_01 is not`). Ping me when sail.js lands and we'll
|
||
tune together; if storm_02 can't break a carabiner rig I'll raise the curve — that's a data edit.
|
||
|
||
[C] 2026-07-16 — Notes on my own files, so nobody trips over them:
|
||
· `weather.core.js` imports **nothing** — no THREE, no DOM, no Date.now. Deliberate: it makes the §4
|
||
determinism rule structural rather than a promise, and it means the whole suite also runs headless
|
||
via `node web/world/js/tests/run-node.mjs` (~1 s, no browser, no server). Tuning a storm curve
|
||
through a browser round trip is miserable. `weather.js` is the thin THREE adapter over it.
|
||
· Cost of that: `weather.core.js` carries its own copy of mulberry32 rather than importing
|
||
`contracts.rng` — identical algorithm and output, it just can't import a file that pulls in THREE.
|
||
`debris.js` and `skyfx.js` do use `contracts.rng`. Not thrilled about the duplication; the
|
||
alternative was giving up node-side testing of the one module everything else depends on.
|
||
· Asserts live in `js/tests/weather.selftest.js` as a plain case list; `c.test.js` and the node
|
||
runner are two harnesses over the same list, so they can't drift.
|
||
· `weather_demo.html` is a Lane C bench (mock sail, storm scrub, 4×, throw-a-crate) on its own URL —
|
||
it touches nothing of yours. Delete it whenever it stops earning its place.
|
||
· **Lane A:** a.test.js's 'gust telegraph always gives at least 1.2 s of warning' is now also
|
||
asserted against the real wind in c.test.js, per your note in the stub. Yours to drop when the
|
||
stub goes.
|
||
|
||
[C] 2026-07-16 — Three bugs worth knowing about, because the shapes recur:
|
||
· Advected noise: I had `drift = U(t)·advect·t`, which is not an integral — when U or dir moved it
|
||
yanked the whole accumulated field sideways: a **6.8 m/s jump in one frame** at the southerly
|
||
change. Now integrated once at build time into a table. If you ever advect anything by time,
|
||
integrate it.
|
||
· Debris friction: `v *= 0.86` per frame while grounded is `0.86^60` per second — glue, not scrape.
|
||
It pinned a 9 kg crate at 0.7 m/s in a 19 m/s wind. Anything per-frame that should be per-second
|
||
needs dt.
|
||
· `storm_02`'s southerly change blew **north** in its first draft: the wind vector blows toward
|
||
`(cos d, sin d)` and contracts puts north at -Z, so a southerly needs `sin(d) < 0`. Worth a second
|
||
look at anything that reasons about wind direction.
|
||
All three were caught by an assert or the bench rather than by reading, which is the argument for both.
|
||
|
||
[I] 2026-07-16 — **INTEGRATION PASS (main).** All four lane branches merged to main (b → e → c → d;
|
||
THREADS conflicts resolved keep-both). Added the importmap D+E asked for to index.html AND
|
||
selftest.html (relative form: `./vendor/…` — D's `/world/…` spelling 404s on the repo-root server).
|
||
Same absolute-path bug fixed in dev_player.html and player.js GLB URLs (`/world/models/…` →
|
||
`./models/…`) — the ped never loaded under `server.py`; it does now, verified in dev_player.html.
|
||
Selftest on merged main: **121 pass / 0 skip / 0 fail** (E's suite lit up as promised).
|
||
launch.json now runs `--port 8809` (8801 was held by another session). Next work: SPRINT2.md.
|
||
|
||
[I] 2026-07-16 — **M3 CLIP PACK LANDED — the mixamo wishlist is fetched and baked.** John supplied a
|
||
logged-in Mixamo session; 11 clips downloaded Without Skin @30fps (subs where Mixamo has no such
|
||
clip: Turning Key→Pulling Lever, Standing Up Ready→Standing Up, Covering Head→Taking Cover; bonus
|
||
find: Dig And Plant Seeds. Hammering/Sweeping/Bracing don't exist — skipped). FBXs now canonical in
|
||
the M1's ~/Documents/FBX/; CLIPS extended in build_player_anims.py (names: ClimbLadder, Crank, Dig,
|
||
PickUp, Carry, CarryTurn, CarryIdle, StandUp, TakeCover, StumbleBack, PlantSeeds); rebuilt on the M1
|
||
(Blender 5.0.1, 17 NLA tracks, 2.3 MB) and committed. Verified: GLTFLoader reads all 17 clips with
|
||
contract names; selftest still 121/0/0. Lane D: your M3 verbs are on disk — wire when ready.
|