From 9b8aabe0db4c683f0191008d0427e114f523988c Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 01:29:22 +1000 Subject: [PATCH 1/2] Add sail UVs and Lane E's weave texture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E's recipe verbatim: grid (i,j) -> (u,v), repeat 6x6, sRGB. Without the uv attribute three defaults every vertex to (0,0), the map samples one texel and the membrane reads as flat colour — which looks like the texture failing rather than like a bug, so E flagged it ahead of time. Texture URL resolves against import.meta.url rather than the server root, same as weather.js's STORM_DIR and the same thing the integrator's /world/ -> relative pass was fixing. A missing texture warns and falls back to flat colour instead of throwing: the cloth is the game, the weave is a finish, and it must not be able to take the sail down. Added anisotropy 4 — the sail is mostly viewed at a raking angle from underneath, which is exactly where an unfiltered weave turns to moire. Co-Authored-By: Claude Opus 4.8 --- web/world/js/sail.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/web/world/js/sail.js b/web/world/js/sail.js index 8a0b972..becc223 100644 --- a/web/world/js/sail.js +++ b/web/world/js/sail.js @@ -755,9 +755,37 @@ export async function createSailView(rig, { color = 0xd8c48a } = {}) { geo.setAttribute('position', new THREE.BufferAttribute(verts, 3)); geo.setIndex(new THREE.BufferAttribute(new Uint16Array(rig.tris), 1)); + // UVs: the grid IS the UV space, so (i, j) maps straight to (u, v). Without + // this three defaults every vertex to (0,0), the map samples one texel, and + // the membrane reads as flat colour — which looks like the texture failing + // rather than like a bug. (Lane E's recipe, THREADS.) + const N = rig.N; + const 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 mat = new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide, roughness: 0.92, metalness: 0.0, }); + + // Resolved against this module rather than the server root: the same reason + // weather.js builds STORM_DIR this way, and it's what the integrator's + // /world/ -> relative pass was fixing. A missing texture must not take the + // sail down — the cloth is the game, the weave is a finish. + try { + const tex = await new THREE.TextureLoader().loadAsync( + new URL('../models/textures/sail_weave.png', import.meta.url).href, + ); + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + tex.repeat.set(6, 6); // ~6 tiles across a 5 m sail (E's density) + tex.colorSpace = THREE.SRGBColorSpace; // r175 spelling — `encoding` is gone + tex.anisotropy = 4; // it's viewed at a raking angle from underneath + mat.map = tex; // keep mat.color: the weave multiplies it + } catch (err) { + console.warn('[sail] weave texture missing, falling back to flat colour:', err.message); + } const mesh = new THREE.Mesh(geo, mat); mesh.castShadow = true; // the shadow IS the product mesh.receiveShadow = true; From 9f83fd4f20e8b925eb5cfe4fcce6c0c973e1ab5d Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 01:30:54 +1000 Subject: [PATCH 2/2] Report that decisions 3 and 8 are unachievable, with the algebra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented and swept fraction-of-total downdraft: it pincers exactly like gust-only did. The cause is not the semantics, it's the bar. A pitched sail's normal is still 96% vertical, so a downdraft loads it too, and on its worst heading pitch and downdraft add: |d| = 0.287 + 0.958f against the horizontal sail's f. The ratio asymptotes at 109% and needs f=0.86 (28 m/s of falling air) to reach 60%. No value works under any semantics. Recommend retiring it as a wind problem. DESIGN.md's own answer is ponding, and the arithmetic dwarfs wind (1250 kg vs 8 kg of fabric vs 1 kN of wind) — but it can't bite in 90 s without a ~40x time-compression fiat, so it's an M4 item with an owner, not a Sprint 3 fix. Co-Authored-By: Claude Opus 4.8 --- THREADS.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/THREADS.md b/THREADS.md index 1803f6f..f82735b 100644 --- a/THREADS.md +++ b/THREADS.md @@ -836,3 +836,72 @@ Format: `[lane letter] YYYY-MM-DD — note` Selftest on merged main: **169 pass / 0 fail**. Hand-driven check via SHADES.step: storm_02 with the default rig loses p1 (carabiner) + p2 by t=40 with downdraft live — cascade is earlier and meaner than A's pre-downdraft run, as C's numbers predicted. Screenshot of the merged storm going to DESIGN.md. + +[B] 2026-07-17 — 🛑 **DECISION 8 CANNOT WORK, AND NOR CAN DECISION 3. C + INTEGRATOR — read before you + spend the sprint on it.** I implemented fraction-of-TOTAL downdraft and swept it. (Modelled exactly, + without touching C's file: `vecAt` already returns the full horizontal speed, so `out.y = + -hypot(x,z)*frac` IS decision 8.) It pincers the same way gust-only did: + ``` + frac (of total) flat-h / flat-pitched twisted rig + 0.05 21% 4/4 holds + 0.10 27% 4/4 holds + 0.15 32% loses one <- gate B breaks here + 0.30 48% loses one + ``` + The integrator was right that data can't settle it. But the reason isn't the semantics — **it's the + bar.** This is algebra, not a measurement: + · flat-pitched (my 16.7° reference rig) has normal `(0, 0.958, 0.287)` — still 96% vertical. + · So a downdraft loads the PITCHED sail too, and on its worst heading pitch and downdraft ADD: + `|d| = 0.287 + 0.958·f`, against the horizontal sail's `|d| = f`. + · ratio = `(f / (0.287 + 0.958·f))²`. Solve for 60% → **f = 0.86**, i.e. at storm_02's 32.6 m/s peak, + **28 m/s of falling air**. The asymptote at INFINITE downdraft is **109%**. + No downdraft value satisfies the 60% bar under any semantics. It was never a tuning problem. + I also respecified the bar the way I think it was meant — flat-horizontal vs the **hypar**, since + flat-vs-flat was never the design claim (the player's choice is flat or twisted) — and it fails too: + 40% at f=0, 68% at f=0.25, twisted rig dead from f=0.15. **A flat horizontal sail is the lowest-load + rig in this sim at every downdraft value, because a horizontal plate in a mostly-horizontal flow + catches less than any tilted surface, and adding vertical flow raises everyone.** + ➜ **Recommendation: retire decision 3/8 as a WIND problem and drop the sprint item.** It costs nothing + to ship gate 3 with the loophole open: flat-horizontal is only reachable by planting four posts at + identical heights, every real quad in the yard is pitched, and §7's three legs pass today on C's + landed 0.3 without any of this. C — your downdraft is worth keeping regardless: it's what made the + cascade meaner and it's real weather. It just can't carry this bar. + +[B] 2026-07-17 — **the flat-sail loophole has an answer and DESIGN.md already wrote it: PONDING, not + wind.** §"Rain → ponding": *"Flat sails collect water; water is heavy; the belly collects more + (positive feedback) until sudden dump, tear, or corner failure."* The arithmetic isn't close: + ``` + 5 cm of water on a 25 m² flat sail = 1250 kg = 3.1 kN/corner + the fabric itself, 25 m² @ 0.32 = 8 kg + measured storm_02 WIND on that sail = 0.2-1.1 kN/corner + ``` + Ponding is 3-15× the entire quantity we've spent two sprints trying to tune; it only loads sails that + can HOLD water, so unlike a downdraft it CANNOT pincer the twisted rig — a hypar has no flat to pool + in, so the feedback loop never starts. It needs nothing new from C (`wind.rainAt(t)` exists) and it + gives DESIGN.md's broom — "the funniest correct mechanic in the game" — somewhere to live. + ⚠️ **But it cannot bite in 90 seconds, and that's worth knowing now.** Real heavy rain (50 mm/hr) + delivers 1.25 mm over a 90 s storm = 31 kg = 0.08 kN/corner — **2.5%** of what's needed. Ponding wants + ~40 min of rain. storm_02 is 90 s of wall clock but a whole night of story ("southerly change around + the hour mark"), so making it bite means ruling that game-time rain runs ~40× real. That's a design + fiat, not physics, and above my lane. I prototyped it (~50 lines: `rainAt` × per-node flatness → water + mass → weight, plus `pondMass()` for the HUD) and **reverted it** — default-off code tuned by a + constant I invented is worse than the finding. Clean M4 item the moment someone owns the + time-compression call; it's about a day. + +[B] 2026-07-17 — Sprint 3 §B-3 done: **sail UVs + E's weave.** E's recipe verbatim (grid i,j → u,v, + repeat 6×6, sRGB), plus anisotropy 4 — the sail is mostly seen at a raking angle from underneath, + which is exactly where an unfiltered weave moirés. E: took your density as shipped, and the + seam-by-construction assert is a good idea. A missing texture warns and falls back to flat colour + rather than throwing: the cloth is the game, the weave is a finish, and it shouldn't be able to take + the sail down. The URL resolves against `import.meta.url` like weather.js's STORM_DIR, so it survives + whatever root server.py runs on — same class of bug as the `/world/` paths the integrator fixed. + `sail_tears.png` noted for M3, not this sprint. + +[B] 2026-07-17 — **A — the preview-rig offer stands, and it's ~10 lines my side.** For prep force arrows: + build a second `SailRig` over the session's current picks, `step()` it against the calm wind during + prep, and read `corners[i].loadVec` — it's already there and it's the reaction VECTOR, not just the + magnitude, so it points the arrow for you; `.load` gives you the length. That also closes the real gap + I flagged last sprint: prep can't show loads at all today because nothing is attached until commit, so + the player commits blind to the one number the whole game is about. If you want the cheap version of + the same lesson instead, `riggingUI.summary.area` is already live and picking the obvious quad reads + "191 m2" before you commit to it.