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>
133 lines
6.7 KiB
HTML
133 lines
6.7 KiB
HTML
<!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>
|