HardYards/tools/garden_bench/sweep.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

133 lines
6.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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) => {
// SutherlandHodgman 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>