HardYards/tools/garden_bench/probe.html
type-two a495fcdba6 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>
2026-07-18 11:00:09 +10:00

100 lines
5.0 KiB
HTML

<!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>