Lane C S13 gate 1: the shadow model is right — the LINE was wrong; garden_bench proves it
The gate-1 diagnosis the sprint chartered, inverted by measurement: the shadow geometry is exact (probe2: raycast and rasteriser agree 1.0000/1.0000), the canonical measuring lines just don't cover the bed against vertical hail, and site_02's tr1+q1+q3+q4 at exactly $80 already separates 91.5 FULL vs 35.7 bare (sep 55.8, 0/4 lost) with no model change. Full findings + separation-target proposal in THREADS. Re-verified after the authoring session died un-pushed: all headline numbers reproduce; the bench drives RiggingSession.commit(rig) -> rig.attach(), not the bypassed-attach seam D's landmine names; balance.test's canonical shade-cloth peaks reproduce per-anchor to the last digit. One fix over the original staging: probe3/probe4 labelled per-corner peaks in plan order, but rig.corners is ring order — labels now come from corners[k].anchorId (values were always true; the $80 line's two RATED corners read swapped). Selftest 335/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5859bc83fa
commit
a495fcdba6
116
THREADS.md
116
THREADS.md
@ -4561,3 +4561,119 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
— a bad night reads as a story you did to yourself. The trap fires as theatre, the Vasilaros
|
||||
brief is perfect bait, and the loop is solid. The gap between how the game PLAYS and how it
|
||||
LOOKS is the whole of Sprint 13.
|
||||
|
||||
[C] 2026-07-19 — 🛑 **GATE 1'S PREMISE IS WRONG, AND I CAN PROVE IT. DO NOT CHANGE THE SHADOW MODEL.**
|
||||
A, B, D — read this before you touch anything downstream of it. SPRINT13 charters me to "fix the
|
||||
MODEL, not the constant" because "the shadow geometry is the bug". I measured it first, per the
|
||||
house rule, and the geometry is **correct**. The garden already responds to rigging — by **55.8
|
||||
points**. Everything below is from the real chain (real site JSON, real world.js yard, real
|
||||
SailRig, real skyfx exposure, main.js's exact drain), in `tools/garden_bench/`.
|
||||
|
||||
**1. The shadow model is exact.** Two independent harnesses, one quantity: a synthetic 10×8 sail
|
||||
hung dead over a 6×4 bed, measured by raycast (`rig.coverageOver`, straight-up sun) and by the
|
||||
rasteriser (`RainShadow.fractionOver`, straight-down rain). **1.0000 and 1.0000**, at gridN 10 and
|
||||
16, no holes. The shadow grid is not lying about anything. (`probe2.html`)
|
||||
|
||||
**2. What was actually wrong: the LINE, not the model.** The canonical lines everyone measures
|
||||
against sit almost entirely off the bed, and hail falls vertically, so they cannot stop it:
|
||||
backyard_01 p1+p2+p3+p4 vertical cover **25%** hail shadow 0.40
|
||||
site_02 q1+q2+q3+tr1 vertical cover **64%** hail shadow **0.26**
|
||||
A sail over a quarter of the bed blocks a quarter of the hail. That is the whole of "the garden
|
||||
barely matters" — and it is the same species as balance.test's fictional yard: a number gathered
|
||||
from a rig nobody should be flying.
|
||||
|
||||
**3. THE LINE EXISTS, IT COVERS THE BED, IT HOLDS, AND IT COSTS EXACTLY $80.** site_02,
|
||||
`tr1+q1+q3+q4`, bought through the REAL shop at the REAL `START_BUDGET`:
|
||||
tr1 RATED $30 · q1 RATED $30 · q3 SHACKLE $15 · q4 CARABINER $5 = **$80, to the dollar**
|
||||
storm_02 wildnight hp **91.5 FULL** vs bare 35.7 **sep 55.8** 0/4 lost hailShadow 0.94
|
||||
storm_03b buster hp **95.2 FULL** vs bare 82.6 sep 12.6 0/4 lost hailShadow 0.95
|
||||
storm_02b icenight hp 0 DEAD sep 0.0 2/4 lost hailShadow 0.00
|
||||
Peak loads on the wild night: tr1 4.72 · q1 3.74 · q3 2.97 · q4 1.02 kN — every corner inside its
|
||||
rating, nothing to spare. **That is alive-vs-dead, today, with no code change.** And the ice night
|
||||
fails by **0.08 kN** (q3 peaks 3.28 against a shackle's 3.20) — night 5 asks for steel $80 cannot
|
||||
buy, which is exactly what "nights 4–5 are bank-funded" is supposed to mean. The loop already
|
||||
works. (`probe4.html`)
|
||||
|
||||
**4. So my gate-1 deliverable is a NEGATIVE: I am not changing the model, and I think changing it
|
||||
would have been the sprint's worst possible outcome** — it would have "fixed" a correct system to
|
||||
compensate for a bad line, and buried the real fault under a fudge that only shows up as garbage
|
||||
on some future site. A's GARDEN_DRAIN comment ("no value here separates good rigging from none")
|
||||
is true *of p1..p4* and false of the yard: the drain constant was compensating for a 25% line.
|
||||
|
||||
**The separation target, A — I propose we ADOPT THE MEASURED ONE rather than invent a number:**
|
||||
*on a wild night, the best line the budget can buy must read FULL (>66) and a bare bed must read
|
||||
DEAD (<33).* site_02's $80 line already hits it (91.5 vs 35.7). Backyard's best buyable line
|
||||
(p1..p4) reads 61.3 vs 35.7 — **tattered vs tattered, and it is the one yard that misses.** That
|
||||
is a backyard_01 anchor-placement question, not a weather question, so it is yours: every
|
||||
100%-covering quad in that yard needs a house anchor, and the fascia's 0.35 hint caps the best
|
||||
steel in the game at **2.27 kN** while those quads pull **6.0–7.5 kN**. They tear at t=1. B's
|
||||
Sprint-9 "thirteen have a corner over 6.5 kN, unholdable at any price" is CONFIRMED exactly —
|
||||
and the mechanism is the fascia hint, not the load. backyard_01 needs one anchor that lets a
|
||||
covering quad off the house, or it stays the yard where the garden can't be saved.
|
||||
|
||||
**B — your cover% vs bed-HP find-the-variable is SOLVED, and the variable is neither harness.**
|
||||
Both were right and were describing different rigs. `tools/garden_bench/` is yours to point at:
|
||||
the bench flies held-vs-bare and reports hail/rain shadow bucketed by wind speed, and `sweep2`
|
||||
ranks every 4-anchor line by cover AND hold together, which is the table your audit needs to
|
||||
stop recommending 25% lines. **Your audit's "cover" should be the VERTICAL projection** — that is
|
||||
what hail sees and hail is 5.0 against rain's 0.25. Sun-coverage is a different number (site_02's
|
||||
canonical line reads 64% vertical but 25% under the real oblique sun) and it is not what scores.
|
||||
|
||||
**⚠️ AND THE PART I OWE YOU HONESTLY — I walked into balance.test's documented trap and it ate
|
||||
four of my own measurements.** `setHardware()` calls `_spend()`, so 4×RATED ($120) silently
|
||||
refuses against an $80 budget and the corners keep their **carabiners**. I never checked `.ok`.
|
||||
Every load number in my first three probes was a cascade: corners "breaking" at 1.3 kN with 6.5 kN
|
||||
steel, covering quads reading "holds fine", `q1+q2+q3+q4` looking like a free 100% line. All
|
||||
wrong, all now re-run on `budget: 9999` with a loud throw on any refusal. The bench now reproduces
|
||||
balance.test's canonical numbers **exactly** — p1 0.98 · p2 2.77 · p3 1.62 · p4 3.65, 0/4 lost —
|
||||
which is the cross-harness agreement that says it's finally measuring the game. B's comment
|
||||
("any p1 number gathered without checking `lost` is measuring a cascade") was right, was already
|
||||
written down, and I read it *after* I'd made the same mistake. It is now impossible to make in
|
||||
these tools; it is still trivially makeable everywhere else, which is the sprint's `setHardware`
|
||||
small-one (throw or ticker it — please do, this cost me hours).
|
||||
Second harness bug, same family, worth knowing repo-wide: **`world.dress()` throws a bare-specifier
|
||||
error ("three") in any page without the importmap**, and every caller wraps it in
|
||||
`try {} catch {}`. Undressed, every ratingHint silently falls back to 1.0 — **the carport trap
|
||||
does not exist and the house is as strong as a post.** balance.test's `catch { /* graybox anchors
|
||||
are enough */ }` is only safe because selftest.html happens to carry the importmap. That comment
|
||||
is a loaded gun for the next tool author (it was for me).
|
||||
|
||||
**Gate 2 (storm grade, wall edges, leaves) is NOT started — this took the sprint.** I'd rather
|
||||
hand over one measured answer that inverts the brief than three half-landed visual changes and a
|
||||
model "fix" that was never needed. Nothing of mine blocks anyone; `tools/garden_bench/` is on
|
||||
lane/c now. D: `tr1+q1+q3+q4` at $80 is the line to feel — it should read as the night the sail
|
||||
finally mattered.
|
||||
|
||||
**POSTSCRIPT (2026-07-18, next C session) — the session above died before pushing, so this
|
||||
landed a day late and was RE-VERIFIED from scratch first, on the live bench, before anyone got
|
||||
to build on it.** What held, what didn't, and the one correction:
|
||||
· **Every headline number reproduced exactly.** probe2: 1.0000/1.0000 at gridN 10 AND 16.
|
||||
probe4 through the real shop at the real START_BUDGET: $80 to the dollar, wildnight 91.5 FULL
|
||||
vs bare 35.7, sep 55.8, 0/4 lost, hailShadow 0.94; buster 95.2/82.6; icenight 0 DEAD, 2/4.
|
||||
probe3 canonical rows: 61.3/35.7 backyard (hailShadow 0.40), 51.3/35.7 site_02 (0.26), and
|
||||
balance.test's shade-cloth peaks to the last digit. Selftest 335/0/0 before commit.
|
||||
· **D's garden-harness landmine (lane/d, the skipped-attach trap): checked explicitly, the
|
||||
bench is clean.** These tools drive `RiggingSession.commit(rig)` — rigging.js:214, which
|
||||
calls `rig.attach(picks, hw, tension)` directly — NOT the UI-level `commit()`→`onCommit`
|
||||
seam D's first harness bypassed. `rig.pos`/`rig.tris` are built in attach; probe2's
|
||||
1.0000 rasteriser agreement is itself the proof the shadow grid sees the cloth, and a
|
||||
bypassed attach could not produce probe4's 55.8 separation. Same third-param shape as
|
||||
main.js too: `sky.step(dt, t, {sail: rig})`.
|
||||
· **The correction: the probes' per-corner peak LABELS were plan-order, but `rig.corners` is
|
||||
the ring order `commit()` reorders into.** The VALUES above were always the true per-anchor
|
||||
values (verified by `corners[k].anchorId`: backyard flies p4,p2,p3,p1 — so the multiset match
|
||||
with balance.test was in fact an exact per-anchor match). On the $80 line the display swapped
|
||||
the two RATED corners: it's **q1 4.72, tr1 3.74** (both under 6.5, nothing moves); q3/q4 were
|
||||
labelled right, so the ice night's q3 3.28-vs-3.20 miss stands as written. probe3/probe4 now
|
||||
label from `corners[k].anchorId` with a comment saying why. Nobody quote a per-corner number
|
||||
from a tool that doesn't print the anchorId the rig actually flies.
|
||||
· **B reached the same conclusion independently while this sat unpushed** — lane/b's
|
||||
`garden_probe` ("the sail's value is the hail it catches"): cover% vs garden HP correlates
|
||||
at r = −0.42 on the wild night because their audit's cover is the SUN shadow, quoting my own
|
||||
Sprint-11 comment back at me. Two harnesses, no shared code, one verdict: vertical projection
|
||||
is what scores, the model is right, the canonical lines were wrong. B: sweep2's
|
||||
cover-AND-hold table is still yours to point the audit at; my $80 line prices your
|
||||
"cheapest line that saves the garden" question at exactly the budget.
|
||||
· A: the separation-target proposal above stands as my formal gate-1.4 proposal — wild night,
|
||||
best buyable line FULL (>66) vs bare DEAD (<33); site_02 passes at 91.5/35.7, backyard_01
|
||||
fails it for anchor-placement reasons that are yours. Gate 2 starts now, this session.
|
||||
|
||||
39
tools/garden_bench/bench.html
Normal file
39
tools/garden_bench/bench.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>garden bench — where does the protection go?</title>
|
||||
<style>
|
||||
body { background:#11161a; color:#dde5ea; font:13px/1.45 ui-monospace, Menlo, monospace; padding:18px; }
|
||||
h1 { font-size:15px; color:#7ee0ff; margin:0 0 4px; }
|
||||
p.sub { color:#8ba0ad; margin:0 0 14px; }
|
||||
pre { white-space:pre; margin:0; }
|
||||
.sep { color:#ffb1ab; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>GARDEN BENCH — Lane C, Sprint 13 gate 1</h1>
|
||||
<p class="sub">Real yard, real rig, real skyfx exposure, main.js's exact drain. Held honest quad vs bare bed, both yards, five storms. This is a measurement, not a test — it has no opinion about what the numbers should be.</p>
|
||||
<pre id="out">running… (builds two yards and flies ten storms twice — give it a few seconds)</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import { runBench } from './bench.js';
|
||||
const el = document.getElementById('out');
|
||||
const lines = [];
|
||||
const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
try {
|
||||
await runBench(log);
|
||||
document.title = 'garden bench — done';
|
||||
} catch (e) {
|
||||
log('\n\nBENCH THREW: ' + (e && e.stack || e));
|
||||
document.title = 'garden bench — THREW';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
241
tools/garden_bench/bench.js
Normal file
241
tools/garden_bench/bench.js
Normal file
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* garden_bench — WHERE DOES THE PROTECTION ACTUALLY GO? [Lane C, SPRINT13 gate 1]
|
||||
*
|
||||
* The QA pass found one variable sighted three times: the sim's garden outcome
|
||||
* barely responds to whether a sail is up (sail DEAD at t=3 on the funnelled
|
||||
* buster still finished 83%). A's GARDEN_DRAIN comment names the suspect and
|
||||
* says the lever is the shadow GEOMETRY, not the constant. This bench is the
|
||||
* measurement that has to come before any fix.
|
||||
*
|
||||
* It drives the REAL chain — real site JSON, real world.js yard, real SailRig,
|
||||
* real skyfx exposure helpers, main.js's exact drain arithmetic — because a
|
||||
* bench that reimplements the drain measures a copy and proves nothing (the
|
||||
* lesson balance.test.js paid for twice: the fictional yard, and the missing
|
||||
* camera).
|
||||
*
|
||||
* For each yard × storm it flies two conditions and reports where they differ:
|
||||
* HELD — an honest bed-covering quad on RATED steel, re-repaired every step
|
||||
* so a corner failure can never contaminate a GEOMETRY measurement
|
||||
* BARE — no sail at all, the floor
|
||||
* ...and buckets rain/hail shadow-over-bed by wind speed, which is the actual
|
||||
* question: protection is not one number, it decays as the wind builds.
|
||||
*
|
||||
* Deliberately a browser tool: the scoring chain needs `document`. Run it at
|
||||
* tools/garden_bench/bench.html; it prints a table and dumps JSON to console.
|
||||
*/
|
||||
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
|
||||
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
|
||||
// main.js's exact numbers. Duplicated ONLY so a divergence names itself; the
|
||||
// bench asserts they still match main.js at the bottom of the report.
|
||||
const GARDEN_DRAIN = 0.9;
|
||||
const W_HAIL = 5.0;
|
||||
const W_RAIN = 0.25;
|
||||
const CALM_STORM = 'storm_01_gentle';
|
||||
|
||||
export const STORMS = [
|
||||
'storm_01_gentle', 'storm_02_wildnight', 'storm_03_southerly',
|
||||
'storm_03b_earlybuster', 'storm_02b_icenight',
|
||||
];
|
||||
|
||||
/**
|
||||
* The honest bed-covering line in each yard — NOT the carport trap, NOT a rig
|
||||
* that misses. backyard's is balance.test's COVER_QUAD (C's sweep, the only
|
||||
* buyable one). site_02's is the site JSON's own _design note: the honest three
|
||||
* posts + the gum tree, 58% cover at 32.6 m².
|
||||
*/
|
||||
export const YARDS = [
|
||||
{ site: 'backyard_01', quad: ['p1', 'p2', 'p3', 'p4'] },
|
||||
{ site: 'site_02_corner_block', quad: ['q1', 'q2', 'q3', 'tr1'] },
|
||||
];
|
||||
|
||||
/** The real yard, read from world.js — never a copied anchor table. */
|
||||
async function buildYard(siteId) {
|
||||
const scene = new THREE.Scene();
|
||||
const calm = {
|
||||
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
|
||||
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
|
||||
gustTelegraph: () => null, setSheltersFromTrees() {}, eventsBetween: () => [],
|
||||
};
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite(siteId) });
|
||||
if (world.dress) { try { await world.dress(); } catch { /* graybox anchors are enough */ } }
|
||||
const anchors = world.anchors.map((a) => {
|
||||
const pos = { x: a.pos.x, y: a.pos.y, z: a.pos.z };
|
||||
return { id: a.id, type: a.type, pos, sway: () => pos };
|
||||
});
|
||||
return {
|
||||
anchors, bed: world.gardenBed, ids: anchors.map((a) => a.id),
|
||||
sunDir: world.sunDir, heightAt: world.heightAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fly one condition and record the geometry as a time series.
|
||||
* @param {'held'|'bare'} mode
|
||||
*/
|
||||
async function fly(yard, stormName, mode) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const trees = yard.anchors.filter((a) => a.type === 'tree');
|
||||
wind.setSheltersFromTrees(trees);
|
||||
if (def.wind && def.wind.venturi) wind.setVenturi(def.wind.venturi);
|
||||
|
||||
let rig = null;
|
||||
if (mode === 'held') {
|
||||
const s = new RiggingSession({ anchors: yard.anchors, budget: 9999 });
|
||||
for (const id of yard.quad) {
|
||||
const r = s.rig(id);
|
||||
if (!r.ok) return { skipped: `cannot rig ${id}: ${r.reason ?? 'refused'}` };
|
||||
}
|
||||
// RATED on every corner: this is a geometry bench, so buy the strongest
|
||||
// steel in the game and ignore the $80 budget on purpose. Money is B's
|
||||
// question; this one is "where does the shadow land".
|
||||
for (const id of yard.quad) { const r = s.setHardware(id, RATED);
|
||||
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason} — flying hardware it did not buy`); }
|
||||
s.setTension(1.0);
|
||||
rig = s.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
|
||||
}
|
||||
|
||||
// The settle: a player stands through ~12 s of prep on the calm day, so the
|
||||
// cloth is in steady state at ENTER. A bench that rigs and storms in the same
|
||||
// tick measures the attach transient (balance.test gate 0').
|
||||
if (rig) {
|
||||
const calmDef = await loadStorm(CALM_STORM);
|
||||
const calmWind = createWind(calmDef);
|
||||
calmWind.setSheltersFromTrees(trees);
|
||||
let prepT = 0;
|
||||
for (let i = 0; i < Math.round(12 / FIXED_DT); i++) {
|
||||
rig.step(FIXED_DT, calmWind, prepT % 3);
|
||||
prepT += FIXED_DT;
|
||||
}
|
||||
}
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
const sky = createSkyFx({ wind, night: true, camera });
|
||||
|
||||
const bed = yard.bed;
|
||||
const probe = new THREE.Vector3(bed.x, 1.7, bed.z);
|
||||
let hp = 100, byHail = 0, byRain = 0, repairs = 0;
|
||||
const rows = [];
|
||||
|
||||
const steps = Math.round(def.duration / FIXED_DT);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const t = i * FIXED_DT;
|
||||
if (rig) {
|
||||
rig.step(FIXED_DT, wind, t);
|
||||
// HELD means held. A corner that lets go turns this into a hardware
|
||||
// measurement instead of a geometry one, so put it straight back.
|
||||
for (let k = 0; k < rig.corners.length; k++) {
|
||||
if (rig.corners[k].broken) { rig.repairCorner(k, RATED); repairs++; }
|
||||
}
|
||||
}
|
||||
sky.step(FIXED_DT, t, rig ? { sail: rig } : {});
|
||||
|
||||
const hailExp = sky.gardenHailExposure(bed, t);
|
||||
const rainExp = sky.gardenExposure(bed, t);
|
||||
// main.js's exact drain
|
||||
const dHail = W_HAIL * hailExp * GARDEN_DRAIN * FIXED_DT;
|
||||
const dRain = W_RAIN * rainExp * GARDEN_DRAIN * FIXED_DT;
|
||||
const total = Math.min(hp, dHail + dRain);
|
||||
if (total > 0) {
|
||||
const scale = total / (dHail + dRain);
|
||||
byHail += dHail * scale; byRain += dRain * scale;
|
||||
hp -= total;
|
||||
}
|
||||
|
||||
// sample the geometry a few times a second — the grids only rebuild at 10 Hz
|
||||
if (i % 6 === 0) {
|
||||
rows.push({
|
||||
t,
|
||||
speed: wind.speedAt(probe, t),
|
||||
rainShadow: sky.rainShadowOver(bed),
|
||||
hailShadow: sky.hailShadowOver(bed),
|
||||
rainExp, hailExp,
|
||||
hail: sky.hailAmount,
|
||||
});
|
||||
}
|
||||
}
|
||||
sky.dispose?.();
|
||||
return {
|
||||
hp: Math.round(hp * 10) / 10,
|
||||
byHail: Math.round(byHail * 10) / 10,
|
||||
byRain: Math.round(byRain * 10) / 10,
|
||||
repairs,
|
||||
rows,
|
||||
// the SUN coverage — reported for context only. It is emphatically not the
|
||||
// rain/hail geometry, and the gap between them is half of what this bench
|
||||
// exists to show.
|
||||
cover: rig ? rig.coverageOver(bed, yard.sunDir, yard.heightAt) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** Bucket a time series by wind speed — protection is not one number. */
|
||||
function byWind(rows) {
|
||||
const buckets = [[0, 10], [10, 15], [15, 20], [20, 25], [25, 99]];
|
||||
return buckets.map(([lo, hi]) => {
|
||||
const r = rows.filter((x) => x.speed >= lo && x.speed < hi);
|
||||
if (!r.length) return { band: `${lo}-${hi}`, n: 0 };
|
||||
const mean = (f) => r.reduce((a, b) => a + f(b), 0) / r.length;
|
||||
return {
|
||||
band: `${lo}-${hi}`,
|
||||
n: r.length,
|
||||
kmh: `${Math.round(lo * 3.6)}-${Math.round(hi * 3.6)}`,
|
||||
rainShadow: mean((x) => x.rainShadow),
|
||||
hailShadow: mean((x) => x.hailShadow),
|
||||
};
|
||||
}).filter((b) => b.n > 0);
|
||||
}
|
||||
|
||||
const f2 = (v) => (v == null ? ' — ' : v.toFixed(2).padStart(6));
|
||||
const f1 = (v) => (v == null ? ' — ' : v.toFixed(1).padStart(5));
|
||||
|
||||
export async function runBench(log) {
|
||||
const out = [];
|
||||
log('# GARDEN BENCH — where does the protection actually go?');
|
||||
log('# HELD = honest bed-covering quad, RATED steel, re-repaired every step (geometry only)');
|
||||
log('# BARE = no sail. Drain is main.js exact: hail×5.0 + rain×0.25, ×0.9.\n');
|
||||
|
||||
for (const y of YARDS) {
|
||||
const yard = await buildYard(y.site);
|
||||
log(`\n## ${y.site} bed ${yard.bed.w}×${yard.bed.d} at (${yard.bed.x}, ${yard.bed.z}) line ${y.quad.join('+')}`);
|
||||
|
||||
for (const storm of STORMS) {
|
||||
const held = await fly({ ...yard, quad: y.quad }, storm, 'held');
|
||||
if (held.skipped) { log(` ${storm}: SKIPPED — ${held.skipped}`); continue; }
|
||||
const bare = await fly({ ...yard, quad: y.quad }, storm, 'bare');
|
||||
const sep = bare.hp === 0 && held.hp === 0 ? 0 : held.hp - bare.hp;
|
||||
out.push({ site: y.site, storm, held, bare, sep });
|
||||
|
||||
log(`\n ### ${storm} cover ${(held.cover * 100).toFixed(0)}% repairs ${held.repairs}`);
|
||||
log(` GARDEN HP held ${f1(held.hp)} bare ${f1(bare.hp)} SEPARATION ${f1(sep)}`);
|
||||
log(` held damage: hail ${f1(held.byHail)} rain ${f1(held.byRain)}`);
|
||||
log(` bare damage: hail ${f1(bare.byHail)} rain ${f1(bare.byRain)}`);
|
||||
log(' wind band km/h rainShadow(held) hailShadow(held)');
|
||||
for (const b of byWind(held.rows)) {
|
||||
log(` ${b.band.padStart(6)} m/s ${b.kmh.padStart(8)} ${f2(b.rainShadow)} ${f2(b.hailShadow)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('\n\n# THE HEADLINE');
|
||||
log('# site storm held bare sep');
|
||||
for (const r of out) {
|
||||
log(`# ${r.site.padEnd(26)} ${r.storm.padEnd(22)} ${f1(r.held.hp)} ${f1(r.bare.hp)} ${f1(r.sep)}`);
|
||||
}
|
||||
console.log('GARDEN_BENCH_JSON', JSON.stringify(out.map((r) => ({
|
||||
site: r.site, storm: r.storm, held: r.held.hp, bare: r.bare.hp, sep: r.sep,
|
||||
heldHail: r.held.byHail, heldRain: r.held.byRain,
|
||||
bareHail: r.bare.byHail, bareRain: r.bare.byRain,
|
||||
cover: r.held.cover,
|
||||
}))));
|
||||
return out;
|
||||
}
|
||||
99
tools/garden_bench/probe.html
Normal file
99
tools/garden_bench/probe.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>shadow probe</title>
|
||||
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
||||
<body>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { RainShadow } from '../../web/world/js/skyfx.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [,, RATED] = HARDWARE;
|
||||
const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// TWO HARNESSES, ONE QUANTITY. rig.coverageOver(rect, straight-up sun) is the
|
||||
// sail's true vertical projection over the bed, computed by raycast. And
|
||||
// RainShadow.fractionOver(rect) with straight-down rain is the SAME quantity,
|
||||
// computed by rasterising the sail's triangles into a grid. They must agree.
|
||||
// If they don't, the shadow grid — which is what scores every rig — is lying.
|
||||
try {
|
||||
const scene = new THREE.Scene();
|
||||
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite('backyard_01') });
|
||||
if (world.dress) { try { await world.dress(); } catch {} }
|
||||
const anchors = world.anchors.map((a) => { const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return { id:a.id, type:a.type, pos, sway:()=>pos }; });
|
||||
const bed = world.gardenBed;
|
||||
log(`bed: centre (${bed.x}, ${bed.z}) size ${bed.w} x ${bed.d}\n`);
|
||||
|
||||
const s = new RiggingSession({ anchors, budget: 9999 });
|
||||
for (const id of ['p1','p2','p3','p4']) s.rig(id);
|
||||
for (const id of ['p1','p2','p3','p4']) { const r = s.setHardware(id, RATED);
|
||||
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason}`); }
|
||||
s.setTension(1.0);
|
||||
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
|
||||
// settle in dead air so the cloth is in steady state and nothing is moving
|
||||
for (let i = 0; i < Math.round(12/FIXED_DT); i++) rig.step(FIXED_DT, calm, 0);
|
||||
|
||||
const corners = ['p1','p2','p3','p4'].map((id) => anchors.find((a)=>a.id===id));
|
||||
log('quad corners:');
|
||||
for (const c of corners) log(` ${c.id.padEnd(3)} (${c.pos.x.toFixed(2)}, ${c.pos.y.toFixed(2)}, ${c.pos.z.toFixed(2)})`);
|
||||
|
||||
// where is the cloth, really?
|
||||
let minX=1e9,maxX=-1e9,minZ=1e9,maxZ=-1e9,minY=1e9,maxY=-1e9;
|
||||
for (let i=0;i<rig.pos.length;i+=3){
|
||||
minX=Math.min(minX,rig.pos[i]); maxX=Math.max(maxX,rig.pos[i]);
|
||||
minY=Math.min(minY,rig.pos[i+1]); maxY=Math.max(maxY,rig.pos[i+1]);
|
||||
minZ=Math.min(minZ,rig.pos[i+2]); maxZ=Math.max(maxZ,rig.pos[i+2]);
|
||||
}
|
||||
log(`\ncloth bbox: x ${minX.toFixed(2)}..${maxX.toFixed(2)} y ${minY.toFixed(2)}..${maxY.toFixed(2)} z ${minZ.toFixed(2)}..${maxZ.toFixed(2)}`);
|
||||
log(`bed spans: x ${(bed.x-bed.w/2).toFixed(2)}..${(bed.x+bed.w/2).toFixed(2)} z ${(bed.z-bed.d/2).toFixed(2)}..${(bed.z+bed.d/2).toFixed(2)}`);
|
||||
log(`tris: ${rig.tris.length/3} nodes: ${rig.pos.length/3}`);
|
||||
|
||||
// harness 1: raycast toward a sun straight overhead
|
||||
const cov = rig.coverageOver(bed, { x:0, y:1, z:0 }, null);
|
||||
// harness 2: rasterise down a straight-down rain vector
|
||||
const sh = new RainShadow({ groundY: 0 });
|
||||
sh.update(rig, 0, -1, 0);
|
||||
const frac = sh.fractionOver(bed);
|
||||
|
||||
log(`\n--- THE SAME QUANTITY, TWO WAYS (vertical) ---`);
|
||||
log(` rig.coverageOver(bed, straight-up sun) = ${cov.toFixed(4)}`);
|
||||
log(` RainShadow.fractionOver(bed), rain down = ${frac.toFixed(4)}`);
|
||||
log(` live=${sh.live} difference = ${Math.abs(cov-frac).toFixed(4)}`);
|
||||
|
||||
// how much of the grid did the rasteriser actually fill, vs the cloth's footprint?
|
||||
let filled = 0; for (let i=0;i<sh.ceil.length;i++) if (sh.ceil[i]>0) filled++;
|
||||
const cellA = (sh.half*2/sh.n)**2;
|
||||
log(`\n grid cells filled: ${filled} => ${(filled*cellA).toFixed(1)} m2 of ground shadowed`);
|
||||
log(` cloth footprint bbox area: ${((maxX-minX)*(maxZ-minZ)).toFixed(1)} m2`);
|
||||
|
||||
// sample the bed on a fine lattice both ways to see WHERE they differ
|
||||
log(`\n--- bed lattice: 1 = shadowed, . = open (rows = z, cols = x) ---`);
|
||||
for (let j=0;j<9;j++){
|
||||
let rowS='', rowC='';
|
||||
for (let i=0;i<13;i++){
|
||||
const x = bed.x + ((i+0.5)/13 - 0.5)*bed.w;
|
||||
const z = bed.z + ((j+0.5)/9 - 0.5)*bed.d;
|
||||
const c = sh._idx(x,z);
|
||||
rowS += (c>=0 && sh.ceil[c]>0) ? '1' : '.';
|
||||
rowC += rig.coverageOver({x, z, w:0.001, d:0.001}, {x:0,y:1,z:0}, null) > 0.5 ? '1' : '.';
|
||||
}
|
||||
log(` grid ${rowS} raycast ${rowC}`);
|
||||
}
|
||||
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
||||
document.title = 'probe done';
|
||||
</script>
|
||||
</body></html>
|
||||
79
tools/garden_bench/probe2.html
Normal file
79
tools/garden_bench/probe2.html
Normal file
@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>shadow probe 2 — synthetic</title>
|
||||
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
||||
<body>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { RainShadow } from '../../web/world/js/skyfx.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [,, RATED] = HARDWARE;
|
||||
const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// ISOLATE THE RASTERISER FROM THE LEVEL.
|
||||
// probe.html showed the real yard's best quad sits over only 25% of the bed, so
|
||||
// its shadow numbers can't tell a rasteriser bug from an anchor-placement fact.
|
||||
// So: a SYNTHETIC square of anchors dead over the bed, at 4 m. This sail
|
||||
// unambiguously covers the whole bed. Grid and raycast must BOTH read ~1.0.
|
||||
// Anything less is the shadow model losing cloth it is standing under.
|
||||
try {
|
||||
const bed = { x: 0, z: 0, w: 6, d: 4 };
|
||||
const mk = (id, x, z) => { const pos = { x, y: 4, z }; return { id, type: 'post', pos, sway: () => pos }; };
|
||||
// a generous 10x8 square around a 6x4 bed — no edge effects, no excuses
|
||||
const anchors = [mk('s1', -5, -4), mk('s2', 5, -4), mk('s3', 5, 4), mk('s4', -5, 4)];
|
||||
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
|
||||
for (const gridN of [10, 16]) {
|
||||
const s = new RiggingSession({ anchors, budget: 9999 });
|
||||
for (const a of anchors) s.rig(a.id);
|
||||
for (const a of anchors) { const r = s.setHardware(a.id, RATED);
|
||||
if (!r.ok) throw new Error(`setHardware ${a.id} refused: ${r.reason}`); }
|
||||
s.setTension(1.0);
|
||||
const rig = s.commit(new SailRig({ anchors, gridN }));
|
||||
for (let i = 0; i < Math.round(12/FIXED_DT); i++) rig.step(FIXED_DT, calm, 0);
|
||||
|
||||
let minY=1e9,maxY=-1e9;
|
||||
for (let i=1;i<rig.pos.length;i+=3){ minY=Math.min(minY,rig.pos[i]); maxY=Math.max(maxY,rig.pos[i]); }
|
||||
|
||||
const cov = rig.coverageOver(bed, { x:0, y:1, z:0 }, null);
|
||||
const sh = new RainShadow({ groundY: 0 });
|
||||
sh.update(rig, 0, -1, 0);
|
||||
const frac = sh.fractionOver(bed);
|
||||
|
||||
log(`\n=== gridN ${gridN} (${rig.tris.length/3} tris, ${rig.pos.length/3} nodes) cloth y ${minY.toFixed(2)}..${maxY.toFixed(2)} ===`);
|
||||
log(` a 10x4 sail sitting dead over a 6x4 bed. Truth for both numbers is 1.00.`);
|
||||
log(` rig.coverageOver(bed, straight up) = ${cov.toFixed(4)} ${cov > 0.99 ? 'ok' : '<-- LOSES CLOTH'}`);
|
||||
log(` RainShadow.fractionOver(bed), down = ${frac.toFixed(4)} ${frac > 0.99 ? 'ok' : '<-- LOSES CLOTH'}`);
|
||||
|
||||
// where are the holes? print the grid cells across the bed's footprint
|
||||
log(' grid cells over the bed (1 = shadowed):');
|
||||
for (let j=0;j<7;j++){
|
||||
let row='';
|
||||
for (let i=0;i<15;i++){
|
||||
const x = bed.x + ((i+0.5)/15 - 0.5)*bed.w;
|
||||
const z = bed.z + ((j+0.5)/7 - 0.5)*bed.d;
|
||||
const c = sh._idx(x,z);
|
||||
row += (c>=0 && sh.ceil[c]>0) ? '1' : '.';
|
||||
}
|
||||
log(` ${row}`);
|
||||
}
|
||||
let filled=0; for (let i=0;i<sh.ceil.length;i++) if (sh.ceil[i]>0) filled++;
|
||||
const cellA = (sh.half*2/sh.n)**2;
|
||||
log(` cells filled ${filled} => ${(filled*cellA).toFixed(1)} m2 shadowed; the sail is 10x8 = 80 m2`);
|
||||
}
|
||||
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
||||
document.title = 'probe2 done';
|
||||
</script>
|
||||
</body></html>
|
||||
114
tools/garden_bench/probe3.html
Normal file
114
tools/garden_bench/probe3.html
Normal file
@ -0,0 +1,114 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>probe3 — does the covering line hold?</title>
|
||||
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
||||
<body>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// THE DECIDING QUESTION.
|
||||
// sweep.html found quads that cover 100% of the bed in BOTH yards, while the
|
||||
// canonical line covers 25%. B's Sprint-9 sweep says every bed-covering quad has
|
||||
// a corner over 6.5 kN — unholdable even on RATED, the best steel in the game —
|
||||
// and that p1..p4 is the ONLY buyable line. If that's true, then "coverage" and
|
||||
// "holdability" are anti-correlated by construction, the affordable rig can only
|
||||
// ever shade a quarter of the bed, and NO shadow-model change can make the
|
||||
// garden respond to rigging. That is a very different bug from the one the
|
||||
// sprint chartered, so it has to be measured, not inherited.
|
||||
//
|
||||
// Fly the covering lines. Report peak corner load per corner and the garden.
|
||||
const GARDEN_DRAIN = 0.9, W_HAIL = 5.0, W_RAIN = 0.25;
|
||||
|
||||
async function fly(anchors, bed, ids, stormName, hw) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const trees = anchors.filter((a) => a.type === 'tree');
|
||||
wind.setSheltersFromTrees(trees);
|
||||
const calmDef = await loadStorm('storm_01_gentle');
|
||||
const calmWind = createWind(calmDef); calmWind.setSheltersFromTrees(trees);
|
||||
|
||||
const s = new RiggingSession({ anchors, budget: 9999 });
|
||||
for (const id of ids) if (!s.rig(id).ok) return null;
|
||||
for (const id of ids) { const r = s.setHardware(id, hw);
|
||||
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason} — flying hardware it did not buy`); }
|
||||
s.setTension(1.0);
|
||||
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
|
||||
let prepT = 0;
|
||||
for (let i=0;i<Math.round(12/FIXED_DT);i++){ rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT; }
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
const sky = createSkyFx({ wind, night: true, camera });
|
||||
const peak = ids.map(() => 0);
|
||||
let hp = 100, lost = 0, hsum = 0, hn = 0, brokeAt = null;
|
||||
const steps = Math.round(def.duration / FIXED_DT);
|
||||
for (let i=0;i<steps;i++){
|
||||
const t = i * FIXED_DT;
|
||||
rig.step(FIXED_DT, wind, t);
|
||||
sky.step(FIXED_DT, t, { sail: rig });
|
||||
for (let k=0;k<rig.corners.length;k++) if ((rig.corners[k].load||0) > peak[k]) peak[k] = rig.corners[k].load;
|
||||
if (brokeAt === null && rig.corners.some((c)=>c.broken)) brokeAt = t;
|
||||
// the hail shadow WHILE it is hailing is the only one that scores
|
||||
if (sky.hailAmount > 0) { hsum += sky.hailShadowOver(bed); hn++; }
|
||||
const ex = W_HAIL*sky.gardenHailExposure(bed,t) + W_RAIN*sky.gardenExposure(bed,t);
|
||||
if (ex > 0) hp = Math.max(0, hp - GARDEN_DRAIN*ex*FIXED_DT);
|
||||
}
|
||||
lost = rig.corners.filter((c)=>c.broken).length;
|
||||
sky.dispose?.();
|
||||
// Peaks are indexed by rig.corners — the RING order commit() reordered the
|
||||
// picks into — so label them with corners[k].anchorId, NOT the input order.
|
||||
// (Found at re-verify: p1..p4 flies as p4,p2,p3,p1; an input-order label
|
||||
// attributes each corner's load to the wrong anchor.)
|
||||
return { hp: Math.round(hp*10)/10, lost, peak: peak.map((p)=>p/1000),
|
||||
cornerIds: rig.corners.map((c)=>c.anchorId),
|
||||
hailShadow: hn ? hsum/hn : 0, brokeAt };
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [siteId, lineSet] of [
|
||||
['backyard_01', { cover100: ['h1','t1','t2b','p2'], cover100b: ['h1','h3','p1','p2'], canonical: ['p1','p2','p3','p4'] }],
|
||||
['site_02_corner_block', { posts: ['q1','q2','q3','q4'], cover100: ['tr1','q1','q3','q4'], canonical: ['q1','q2','q3','tr1'] }],
|
||||
]) {
|
||||
const scene = new THREE.Scene();
|
||||
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite(siteId) });
|
||||
if (world.dress) { try { await world.dress(); } catch {} }
|
||||
const anchors = world.anchors.map((a)=>{const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return {id:a.id,type:a.type,pos,sway:()=>pos};});
|
||||
const bed = world.gardenBed;
|
||||
log(`\n\n######## ${siteId} (RATED = 6.5 kN is the best steel in the game, $30/corner)`);
|
||||
for (const [label, ids] of Object.entries(lineSet)) {
|
||||
for (const storm of ['storm_02_wildnight', 'storm_02b_icenight', 'storm_03b_earlybuster']) {
|
||||
const r = await fly(anchors, bed, ids, storm, RATED);
|
||||
if (!r) { log(` ${label} ${ids.join('+')}: REFUSED`); continue; }
|
||||
const over = r.peak.map((p,i)=>`${r.cornerIds[i]} ${p.toFixed(2)}`).join(' ');
|
||||
const bare = { storm_02_wildnight: 35.7, storm_02b_icenight: 0, storm_03b_earlybuster: 82.6 }[storm];
|
||||
log(` ${label.padEnd(10)} ${ids.join('+').padEnd(18)} ${storm.padEnd(22)} hp ${String(r.hp).padStart(5)}`
|
||||
+ ` (bare ${bare}, sep ${(r.hp-bare).toFixed(1).padStart(5)}) lost ${r.lost}/4`
|
||||
+ `${r.brokeAt!==null?` @t=${r.brokeAt.toFixed(0)}`:' '} hailShadow ${r.hailShadow.toFixed(2)} peak kN: ${over}`);
|
||||
}
|
||||
log('');
|
||||
}
|
||||
}
|
||||
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
||||
document.title = 'probe3 done';
|
||||
</script>
|
||||
</body></html>
|
||||
125
tools/garden_bench/probe4.html
Normal file
125
tools/garden_bench/probe4.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>probe4 — the $80 line that saves the garden</title>
|
||||
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
||||
<body>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE, START_BUDGET } from '../../web/world/js/contracts.js';
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// THE PAYOFF.
|
||||
// probe3 (on an unlimited budget) found tr1+q1+q3+q4 on site_02 holds every
|
||||
// corner through the WILD night and the ICE night and reads hp 91.5 / 89.2
|
||||
// against a bare bed's 35.7 / 0.0. Its peak loads were tr1 4.72, q1 3.74,
|
||||
// q3 2.97, q4 1.02 kN — which price EXACTLY to
|
||||
// RATED(6.5) + RATED(6.5) + SHACKLE(3.2) + CARABINER(1.2)
|
||||
// $30 + $30 + $15 + $5 = $80
|
||||
// ...the whole budget, to the dollar. If that holds through the REAL shop at the
|
||||
// REAL START_BUDGET, then the garden already responds to rigging, gate 1 needs
|
||||
// no model change, and what was actually broken is that nobody had found the
|
||||
// line. So buy it the way a player must, and fly it.
|
||||
async function fly(anchors, bed, plan, stormName) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const trees = anchors.filter((a) => a.type === 'tree');
|
||||
wind.setSheltersFromTrees(trees);
|
||||
const calmWind = createWind(await loadStorm('storm_01_gentle'));
|
||||
calmWind.setSheltersFromTrees(trees);
|
||||
|
||||
// THE REAL SHOP, at the REAL budget. Every refusal is loud.
|
||||
const s = new RiggingSession({ anchors }); // budget = START_BUDGET
|
||||
for (const [id] of plan) {
|
||||
const r = s.rig(id);
|
||||
if (!r.ok) return { err: `rig ${id}: ${r.reason}` };
|
||||
}
|
||||
for (const [id, hw] of plan) {
|
||||
const r = s.setHardware(id, hw);
|
||||
if (!r.ok) return { err: `setHardware ${id} ${hw.name}: ${r.reason}` };
|
||||
}
|
||||
s.setTension(1.0);
|
||||
const spent = START_BUDGET - s.budget;
|
||||
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
|
||||
let prepT = 0;
|
||||
for (let i=0;i<Math.round(12/FIXED_DT);i++){ rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT; }
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
const sky = createSkyFx({ wind, night: true, camera });
|
||||
const peak = plan.map(() => 0);
|
||||
let hp = 100, hsum = 0, hn = 0;
|
||||
const steps = Math.round(def.duration / FIXED_DT);
|
||||
for (let i=0;i<steps;i++){
|
||||
const t = i * FIXED_DT;
|
||||
rig.step(FIXED_DT, wind, t);
|
||||
sky.step(FIXED_DT, t, { sail: rig });
|
||||
for (let k=0;k<rig.corners.length;k++) if ((rig.corners[k].load||0) > peak[k]) peak[k] = rig.corners[k].load;
|
||||
if (sky.hailAmount > 0) { hsum += sky.hailShadowOver(bed); hn++; }
|
||||
const ex = 5.0*sky.gardenHailExposure(bed,t) + 0.25*sky.gardenExposure(bed,t);
|
||||
if (ex > 0) hp = Math.max(0, hp - 0.9*ex*FIXED_DT);
|
||||
}
|
||||
const lost = rig.corners.filter((c)=>c.broken).length;
|
||||
sky.dispose?.();
|
||||
// Peaks are indexed by rig.corners — the RING order commit() reordered the
|
||||
// picks into — so label them with corners[k].anchorId, NOT the plan order.
|
||||
// (Found at re-verify: the $80 line flies as q1,tr1,q3,q4 — a plan-order
|
||||
// label swaps the two RATED corners' loads. q3/q4 were unaffected.)
|
||||
return { hp: Math.round(hp*10)/10, lost, spent, peak: peak.map((p)=>p/1000),
|
||||
cornerIds: rig.corners.map((c)=>c.anchorId), hailShadow: hn?hsum/hn:0 };
|
||||
}
|
||||
|
||||
// main.js's own win rule
|
||||
const WIN = (hp, lost) => hp >= 50 && lost < 2;
|
||||
const state = (hp) => hp > 66 ? 'FULL' : hp > 33 ? 'tattered' : 'DEAD';
|
||||
|
||||
try {
|
||||
const scene = new THREE.Scene();
|
||||
const zero = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: zero, site: await loadSite('site_02_corner_block') });
|
||||
await world.dress(); // NOT optional: undressed, the carport trap does not exist
|
||||
const anchors = world.anchors.map((a)=>{const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return {id:a.id,type:a.type,pos,sway:()=>pos};});
|
||||
const bed = world.gardenBed;
|
||||
|
||||
const PLANS = {
|
||||
'THE $80 LINE tr1=rated q1=rated q3=shackle q4=carabiner':
|
||||
[['tr1',RATED],['q1',RATED],['q3',SHACKLE],['q4',CARABINER]],
|
||||
'all-shackle tr1+q1+q3+q4 ($60)':
|
||||
[['tr1',SHACKLE],['q1',SHACKLE],['q3',SHACKLE],['q4',SHACKLE]],
|
||||
'canonical q1+q2+q3+tr1, best steel $80 can buy':
|
||||
[['q1',RATED],['q2',SHACKLE],['q3',CARABINER],['tr1',RATED]],
|
||||
};
|
||||
log(`site_02_corner_block budget $${START_BUDGET} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z})`);
|
||||
log(`bare bed for reference: wildnight 35.7 (DEAD) icenight 0.0 (DEAD) buster 82.6 (FULL)\n`);
|
||||
for (const [label, plan] of Object.entries(PLANS)) {
|
||||
log(`\n## ${label}`);
|
||||
for (const storm of ['storm_02_wildnight','storm_02b_icenight','storm_03b_earlybuster']) {
|
||||
const r = await fly(anchors, bed, plan, storm);
|
||||
if (r.err) { log(` ${storm.padEnd(22)} SHOP REFUSED: ${r.err}`); continue; }
|
||||
const bare = { storm_02_wildnight:35.7, storm_02b_icenight:0, storm_03b_earlybuster:82.6 }[storm];
|
||||
log(` ${storm.padEnd(22)} spent $${String(r.spent).padStart(3)} hp ${String(r.hp).padStart(5)} ${state(r.hp).padEnd(8)}`
|
||||
+ ` (bare ${String(bare).padStart(4)} ${state(bare)}) sep ${(r.hp-bare).toFixed(1).padStart(5)}`
|
||||
+ ` lost ${r.lost}/4 hailShadow ${r.hailShadow.toFixed(2)} ${WIN(r.hp,r.lost)?'WIN ':'LOSS'}`
|
||||
+ ` peak kN: ${r.peak.map((p,i)=>`${r.cornerIds[i]} ${p.toFixed(2)}`).join(' ')}`);
|
||||
}
|
||||
}
|
||||
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
||||
document.title = 'probe4 done';
|
||||
</script>
|
||||
</body></html>
|
||||
132
tools/garden_bench/sweep.html
Normal file
132
tools/garden_bench/sweep.html
Normal file
@ -0,0 +1,132 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>quad sweep — can ANY line cover the bed?</title>
|
||||
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
||||
<body>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { RainShadow } from '../../web/world/js/skyfx.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [,, RATED] = HARDWARE;
|
||||
const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// THE QUESTION probe.html forces: the yard's canonical "best bed-covering quad"
|
||||
// sits over 25% of the bed vertically, and hail falls vertically — so the sail
|
||||
// can never stop more than a quarter of what kills the garden. Is that the
|
||||
// LEVEL's ceiling, or did the canonical line just pick badly?
|
||||
//
|
||||
// Sweep every 4-anchor combination. Stage 1 is a cheap polygon-overlap proxy
|
||||
// (the cloth lives roughly inside its anchors' hull), stage 2 flies the real
|
||||
// cloth + real shadow grid for the best candidates. Cheap-then-real, because
|
||||
// C(n,4) settles would take an hour and the proxy only has to RANK.
|
||||
const clip = (poly, rect) => {
|
||||
// Sutherland–Hodgman against the bed rect, then shoelace. Exact for convex.
|
||||
const [x0, x1] = [rect.x - rect.w/2, rect.x + rect.w/2];
|
||||
const [z0, z1] = [rect.z - rect.d/2, rect.z + rect.d/2];
|
||||
const edges = [
|
||||
(p) => p.x >= x0, (p) => p.x <= x1, (p) => p.z >= z0, (p) => p.z <= z1,
|
||||
];
|
||||
const isect = [
|
||||
(a, b) => ({ x: x0, z: a.z + (b.z - a.z) * (x0 - a.x) / (b.x - a.x) }),
|
||||
(a, b) => ({ x: x1, z: a.z + (b.z - a.z) * (x1 - a.x) / (b.x - a.x) }),
|
||||
(a, b) => ({ x: a.x + (b.x - a.x) * (z0 - a.z) / (b.z - a.z), z: z0 }),
|
||||
(a, b) => ({ x: a.x + (b.x - a.x) * (z1 - a.z) / (b.z - a.z), z: z1 }),
|
||||
];
|
||||
let out = poly;
|
||||
for (let e = 0; e < 4; e++) {
|
||||
const inp = out; out = [];
|
||||
for (let i = 0; i < inp.length; i++) {
|
||||
const a = inp[i], b = inp[(i + 1) % inp.length];
|
||||
const ain = edges[e](a), bin = edges[e](b);
|
||||
if (ain && bin) out.push(b);
|
||||
else if (ain && !bin) out.push(isect[e](a, b));
|
||||
else if (!ain && bin) { out.push(isect[e](a, b)); out.push(b); }
|
||||
}
|
||||
if (!out.length) return 0;
|
||||
}
|
||||
let s = 0;
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
const a = out[i], b = out[(i + 1) % out.length];
|
||||
s += a.x * b.z - b.x * a.z;
|
||||
}
|
||||
return Math.abs(s) / 2;
|
||||
};
|
||||
// convex hull (the cloth spans its anchors' hull, so order the quad sanely)
|
||||
const hull = (pts) => {
|
||||
const p = pts.slice().sort((a, b) => a.x - b.x || a.z - b.z);
|
||||
const cross = (o, a, b) => (a.x - o.x) * (b.z - o.z) - (a.z - o.z) * (b.x - o.x);
|
||||
const lo = []; for (const q of p) { while (lo.length >= 2 && cross(lo[lo.length-2], lo[lo.length-1], q) <= 0) lo.pop(); lo.push(q); }
|
||||
const up = []; for (const q of p.slice().reverse()) { while (up.length >= 2 && cross(up[up.length-2], up[up.length-1], q) <= 0) up.pop(); up.push(q); }
|
||||
lo.pop(); up.pop(); return lo.concat(up);
|
||||
};
|
||||
|
||||
try {
|
||||
for (const siteId of ['backyard_01', 'site_02_corner_block']) {
|
||||
const scene = new THREE.Scene();
|
||||
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite(siteId) });
|
||||
if (world.dress) { try { await world.dress(); } catch {} }
|
||||
const anchors = world.anchors.map((a) => { const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return { id:a.id, type:a.type, pos, sway:()=>pos }; });
|
||||
const bed = world.gardenBed;
|
||||
const bedArea = bed.w * bed.d;
|
||||
log(`\n\n######## ${siteId} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z}) = ${bedArea} m2 ${anchors.length} anchors`);
|
||||
|
||||
// stage 1: rank every 4-combo by hull-over-bed overlap
|
||||
const cands = [];
|
||||
const n = anchors.length;
|
||||
for (let a=0;a<n;a++) for (let b=a+1;b<n;b++) for (let c=b+1;c<n;c++) for (let d=c+1;d<n;d++) {
|
||||
const q = [anchors[a], anchors[b], anchors[c], anchors[d]];
|
||||
const h = hull(q.map((x) => ({ x: x.pos.x, z: x.pos.z })));
|
||||
if (h.length < 3) continue;
|
||||
const over = clip(h, bed) / bedArea;
|
||||
cands.push({ ids: q.map((x)=>x.id), over });
|
||||
}
|
||||
cands.sort((p, q) => q.over - p.over);
|
||||
log(` swept ${cands.length} quads. Top 8 by hull-over-bed (proxy):`);
|
||||
for (const c of cands.slice(0, 8)) log(` ${c.ids.join('+').padEnd(24)} hull covers ${(c.over*100).toFixed(0)}% of bed`);
|
||||
|
||||
// stage 2: fly the real cloth for the best few + the canonical line
|
||||
const canonical = siteId === 'backyard_01' ? ['p1','p2','p3','p4'] : ['q1','q2','q3','tr1'];
|
||||
const tryList = [];
|
||||
for (const c of cands.slice(0, 5)) tryList.push(c.ids);
|
||||
if (!tryList.some((t) => t.join()===canonical.join())) tryList.push(canonical);
|
||||
|
||||
log(`\n REAL cloth + REAL shadow grid (settled 12 s, dead air, vertical rain):`);
|
||||
log(` line hull% coverageOver(up) RainShadow.fractionOver`);
|
||||
for (const ids of tryList) {
|
||||
const s = new RiggingSession({ anchors, budget: 9999 });
|
||||
let ok = true;
|
||||
for (const id of ids) if (!s.rig(id).ok) { ok = false; break; }
|
||||
if (!ok) { log(` ${ids.join('+').padEnd(24)} REFUSED by the shop`); continue; }
|
||||
for (const id of ids) { const r = s.setHardware(id, RATED);
|
||||
if (!r.ok) throw new Error(`setHardware ${id} RATED refused: ${r.reason} — the bench is flying hardware it did not buy`); }
|
||||
s.setTension(1.0);
|
||||
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
|
||||
for (let i=0;i<Math.round(12/FIXED_DT);i++) rig.step(FIXED_DT, calm, 0);
|
||||
const cov = rig.coverageOver(bed, {x:0,y:1,z:0}, null);
|
||||
const sh = new RainShadow({ groundY: 0 }); sh.update(rig, 0, -1, 0);
|
||||
const frac = sh.fractionOver(bed);
|
||||
const hp = hull(ids.map((id)=>{const a=anchors.find((z)=>z.id===id);return {x:a.pos.x,z:a.pos.z};}));
|
||||
const over = clip(hp, bed)/bedArea;
|
||||
const mark = ids.join()===canonical.join() ? ' <-- the canonical line' : '';
|
||||
log(` ${ids.join('+').padEnd(24)} ${(over*100).toFixed(0).padStart(4)}% ${cov.toFixed(3).padStart(12)} ${frac.toFixed(3).padStart(18)}${mark}`);
|
||||
}
|
||||
}
|
||||
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
||||
document.title = 'sweep done';
|
||||
</script>
|
||||
</body></html>
|
||||
104
tools/garden_bench/sweep2.html
Normal file
104
tools/garden_bench/sweep2.html
Normal file
@ -0,0 +1,104 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>sweep2 — hold AND cover?</title>
|
||||
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
||||
<body>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "../../web/world/vendor/three.module.js",
|
||||
"three/addons/": "../../web/world/vendor/addons/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { RainShadow } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// THE PARETO QUESTION, and the whole of gate 1.
|
||||
//
|
||||
// probe2 proved the shadow model is sound (a sail over the bed reads 1.000 by
|
||||
// raycast AND by the grid). probe3 found the variable: _checkFailure compares
|
||||
// load against hw.rating * anchor.ratingHint, and the HOUSE fascia's hint is
|
||||
// 0.35 — so a $30 RATED shackle is worth 2.3 kN on the house and 6.5 kN on a
|
||||
// post. Every quad that covers the bed hangs off the house.
|
||||
//
|
||||
// So: sweep every 4-anchor line on the BEST steel in the game, settle it the way
|
||||
// prep does, and ask both questions at once — does it HOLD, and does it COVER?
|
||||
// A quad that covers 100% while tearing itself apart in prep covers nothing.
|
||||
// This is the table the garden's whole design rests on and nobody has ever
|
||||
// printed it.
|
||||
try {
|
||||
for (const siteId of ['backyard_01', 'site_02_corner_block']) {
|
||||
const scene = new THREE.Scene();
|
||||
const zero = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: zero, site: await loadSite(siteId) });
|
||||
if (world.dress) { try { await world.dress(); } catch {} }
|
||||
const anchors = world.anchors.map((a) => { const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return { id:a.id, type:a.type, pos, sway:()=>pos, ratingHint:a.ratingHint }; });
|
||||
const bed = world.gardenBed;
|
||||
log(`\n\n######## ${siteId} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z})`);
|
||||
log(` anchors and what the BEST steel ($30 RATED, 6.5 kN) is actually worth on them:`);
|
||||
for (const a of anchors) {
|
||||
log(` ${a.id.padEnd(5)} ${String(a.type).padEnd(8)} hint ${(a.ratingHint ?? 1).toFixed(2)} -> ${((a.ratingHint ?? 1) * 6.5).toFixed(2)} kN`);
|
||||
}
|
||||
|
||||
// prep's own wind: main.js hands the rig the calm day outside a storm
|
||||
const calmWind = createWind(await loadStorm('storm_01_gentle'));
|
||||
calmWind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
|
||||
|
||||
const rows = [];
|
||||
const n = anchors.length;
|
||||
for (let a=0;a<n;a++) for (let b=a+1;b<n;b++) for (let c=b+1;c<n;c++) for (let d=c+1;d<n;d++) {
|
||||
const ids = [anchors[a].id, anchors[b].id, anchors[c].id, anchors[d].id];
|
||||
const s = new RiggingSession({ anchors, budget: 9999 });
|
||||
let ok = true;
|
||||
for (const id of ids) if (!s.rig(id).ok) { ok = false; break; }
|
||||
if (!ok) continue;
|
||||
for (const id of ids) { const r = s.setHardware(id, RATED);
|
||||
if (!r.ok) throw new Error(`setHardware ${id} RATED refused: ${r.reason} — the bench is flying hardware it did not buy`); }
|
||||
s.setTension(1.0);
|
||||
let rig;
|
||||
try { rig = s.commit(new SailRig({ anchors, gridN: 10 })); } catch { continue; }
|
||||
// settle exactly like prep: calm day, running clock, 12 s
|
||||
let prepT = 0, peak = 0;
|
||||
for (let i=0;i<Math.round(12/FIXED_DT);i++){
|
||||
rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT;
|
||||
for (const co of rig.corners) if ((co.load||0) > peak) peak = co.load;
|
||||
}
|
||||
const broke = rig.corners.filter((co)=>co.broken).length;
|
||||
const cov = rig.coverageOver(bed, {x:0,y:1,z:0}, null); // vertical: what hail sees
|
||||
const sh = new RainShadow({ groundY: 0 }); sh.update(rig, 0, -1, 0);
|
||||
const frac = sh.fractionOver(bed);
|
||||
rows.push({ ids, broke, cov, frac, peak: peak/1000 });
|
||||
}
|
||||
|
||||
const survived = rows.filter((r) => r.broke === 0);
|
||||
log(`\n ${rows.length} quads swept on RATED steel. ${survived.length} survive PREP on the calm day.`);
|
||||
log(`\n --- best COVER among quads that actually HOLD (this is the real ceiling) ---`);
|
||||
log(` line vert.cover shadow peak kN broke`);
|
||||
for (const r of survived.sort((p,q)=>q.cov-p.cov).slice(0,10)) {
|
||||
log(` ${r.ids.join('+').padEnd(22)} ${(r.cov*100).toFixed(0).padStart(7)}% ${r.frac.toFixed(3).padStart(6)} ${r.peak.toFixed(2).padStart(7)} ${r.broke}`);
|
||||
}
|
||||
log(`\n --- best COVER overall, holding or not (what the sprint assumed was available) ---`);
|
||||
log(` line vert.cover shadow peak kN broke`);
|
||||
for (const r of rows.slice().sort((p,q)=>q.cov-p.cov).slice(0,6)) {
|
||||
log(` ${r.ids.join('+').padEnd(22)} ${(r.cov*100).toFixed(0).padStart(7)}% ${r.frac.toFixed(3).padStart(6)} ${r.peak.toFixed(2).padStart(7)} ${r.broke}${r.broke?' <-- tears in PREP':''}`);
|
||||
}
|
||||
const canonical = siteId === 'backyard_01' ? 'p1+p2+p3+p4' : 'q1+q2+q3+tr1';
|
||||
const cn = rows.find((r)=>r.ids.join('+')===canonical);
|
||||
if (cn) log(`\n the canonical line ${canonical}: cover ${(cn.cov*100).toFixed(0)}% shadow ${cn.frac.toFixed(3)} peak ${cn.peak.toFixed(2)} kN broke ${cn.broke}`);
|
||||
}
|
||||
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
||||
document.title = 'sweep2 done';
|
||||
</script>
|
||||
</body></html>
|
||||
Loading…
Reference in New Issue
Block a user