Second harness on B's post-ratingHint audit: measures what the STORM delivers at every dressed anchor — peak speedAt, tPeak, dynamic pressure (downdraft folded back in), dose, and Pa/hint (the wind-side failure-order prediction under load > rating x ratingHint). No cloth, no rig, no tension: independent by construction. Browser front-end dresses the yard the way the game does (createWorld + dress) and wires venturi + tree shelters exactly as main.js:446-447 does. envelope.selftest.js wired into c.test.js, every assert written to fail if a wiring line is deleted (sweep.selftest's pattern). Selftest 320/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
6.3 KiB
HTML
141 lines
6.3 KiB
HTML
<!doctype html>
|
||
<!--
|
||
storm_envelope, browser front-end. [Lane C, SPRINT12 — gate 2.4's second harness]
|
||
|
||
B's site_audit measures the failure envelope through the CLOTH (peak corner
|
||
loads, kN). This measures the same envelope through the STORM: what the wind
|
||
actually delivers at every dressed anchor, per night — peak local speed, when,
|
||
dynamic pressure, dose, and the pressure/hint ranking that predicts failure
|
||
ORDER under B's `load > rating × ratingHint` wiring. Two harnesses, one
|
||
envelope; if they disagree, find the variable before anyone retunes E's hints.
|
||
|
||
Dressed positions the way the game gets them — createWorld(await loadSite(name))
|
||
then dress() — because a graybox envelope measures a yard that does not ship
|
||
(site_audit's own lesson, kept).
|
||
|
||
tools/storm_envelope/envelope.html ← the whole week
|
||
tools/storm_envelope/envelope.html?site=site_02_corner_block&storm=storm_03b_earlybuster
|
||
|
||
Served from the repo root (server.py).
|
||
-->
|
||
<meta charset="utf-8">
|
||
<title>storm_envelope</title>
|
||
<style>
|
||
body { background:#111; color:#ddd; font:13px/1.5 ui-monospace,Menlo,monospace; margin:0; padding:20px; }
|
||
h1 { font-size:15px; color:#fff; margin:0 0 2px; }
|
||
h2 { font-size:13px; color:#9cf; margin:18px 0 2px; }
|
||
.sub { color:#888; margin-bottom:10px; white-space:pre-wrap; }
|
||
table { border-collapse:collapse; margin:6px 0; }
|
||
th { text-align:right; color:#789; font-weight:normal; padding:1px 12px 1px 0; }
|
||
th:first-child, td:first-child { text-align:left; }
|
||
td { padding:1px 12px 1px 0; white-space:nowrap; text-align:right; }
|
||
.weak { color:#e96; } .probe { color:#678; font-style:italic; }
|
||
</style>
|
||
<h1>storm_envelope — the wind side of the failure envelope</h1>
|
||
<div class="sub" id="sub">loading…</div>
|
||
<div id="out"></div>
|
||
|
||
<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 { createWorld, loadSite } from '../../web/world/js/world.js';
|
||
import { NIGHTS } from '../../web/world/js/week.js';
|
||
import { stormEnvelope } from './envelope.js';
|
||
|
||
const q = new URLSearchParams(location.search);
|
||
const el = (id) => document.getElementById(id);
|
||
const loadJSON = async (path) => (await fetch(path)).json();
|
||
|
||
// One dressed world per site, cached — the yard doesn't change between storms.
|
||
const siteCache = new Map();
|
||
async function dressedSite(name) {
|
||
if (siteCache.has(name)) return siteCache.get(name);
|
||
const site = await loadSite(name);
|
||
const calmStub = {
|
||
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(new THREE.Scene(), { wind: calmStub, site });
|
||
let dressed = false;
|
||
if (world.dress) { try { await world.dress(); dressed = true; } catch (e) { /* flagged below */ } }
|
||
const anchors = world.anchors.map((a) => ({
|
||
id: a.id, type: a.type, ratingHint: a.ratingHint,
|
||
pos: { x: a.pos.x, y: a.pos.y, z: a.pos.z },
|
||
}));
|
||
const out = { site, anchors, dressed, bed: world.gardenBed };
|
||
siteCache.set(name, out);
|
||
return out;
|
||
}
|
||
|
||
function render(title, siteName, stormName, res, dressed) {
|
||
const h = document.createElement('h2');
|
||
h.textContent = title;
|
||
el('out').appendChild(h);
|
||
const sub = document.createElement('div');
|
||
sub.className = 'sub';
|
||
sub.textContent = `${siteName} × ${stormName} — downdraftOfTotal ${res.downFrac}, ${res.duration}s`
|
||
+ (dressed ? '' : ' ⚠ dress() FAILED — graybox positions, not what ships');
|
||
el('out').appendChild(sub);
|
||
|
||
const tbl = document.createElement('table');
|
||
const head = tbl.insertRow();
|
||
for (const c of ['anchor', 'type', 'hint', 'peak m/s', 'tPeak s', 'peak Pa', 'Pa/hint', 'dose m²/s', 'eff. cara/shackle/rated N']) {
|
||
const th = document.createElement('th'); th.textContent = c; head.appendChild(th);
|
||
}
|
||
for (const r of res.rows) {
|
||
const tr = tbl.insertRow();
|
||
if (r.probe) tr.className = 'probe';
|
||
else if (r.hint < 0.5) tr.className = 'weak';
|
||
tr.insertCell().textContent = r.id;
|
||
tr.insertCell().textContent = r.type;
|
||
tr.insertCell().textContent = r.hint.toFixed(2);
|
||
tr.insertCell().textContent = r.peak.toFixed(2);
|
||
tr.insertCell().textContent = r.tPeak.toFixed(1);
|
||
tr.insertCell().textContent = r.peakPa.toFixed(0);
|
||
tr.insertCell().textContent = r.paPerHint.toFixed(0);
|
||
tr.insertCell().textContent = r.dose.toFixed(0);
|
||
tr.insertCell().textContent = r.probe ? '—' : r.effN.map((n) => n.toFixed(0)).join(' / ');
|
||
}
|
||
el('out').appendChild(tbl);
|
||
}
|
||
|
||
async function runPair(siteName, stormName, title) {
|
||
const { site, anchors, dressed, bed } = await dressedSite(siteName);
|
||
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
|
||
const venturi = site.wind?.venturi ?? [];
|
||
const probes = [{ id: 'bed centre', pos: { x: bed.x, z: bed.z } }];
|
||
if (venturi.length) probes.push({ id: 'throat', pos: { x: venturi[0].x, z: venturi[0].z } });
|
||
const res = stormEnvelope({ anchors, stormDef, venturi, probes });
|
||
render(title, siteName, stormName, res, dressed);
|
||
return { site: siteName, storm: stormName, dressed,
|
||
rows: res.rows.map(({ id, type, hint, peak, tPeak, peakPa, paPerHint, dose }) =>
|
||
({ id, type, hint, peak, tPeak, peakPa, paPerHint, dose })) };
|
||
}
|
||
|
||
async function run() {
|
||
const single = q.get('site') || q.get('storm');
|
||
const pairs = single
|
||
? [{ site: q.get('site') || 'backyard_01', storm: q.get('storm') || 'storm_02_wildnight', title: 'requested pair' }]
|
||
: NIGHTS.map((n, i) => ({ site: n.site, storm: n.storm, title: `night ${i + 1}` }));
|
||
el('sub').textContent = `${pairs.length} pair(s) · sampling at FIXED_DT over each storm · `
|
||
+ `venturi + tree shelters wired the way main.js wires them`;
|
||
const results = [];
|
||
for (const p of pairs) results.push(await runPair(p.site, p.storm, p.title));
|
||
// machine-readable, so the page can be driven headless-in-browser
|
||
window.__envelope = results;
|
||
document.title = 'storm_envelope — done';
|
||
}
|
||
|
||
run().catch((e) => {
|
||
el('sub').textContent = `storm_envelope crashed: ${e.message}\n${e.stack || ''}`;
|
||
window.__envelope = { error: e.message };
|
||
document.title = 'storm_envelope — ERROR';
|
||
});
|
||
</script>
|