Merge remote-tracking branch 'origin/lane/c'
# Conflicts: # THREADS.md
This commit is contained in:
commit
73e142947b
46
THREADS.md
46
THREADS.md
@ -3030,3 +3030,49 @@ Selftest: **277 passed, 0 failed, 0 skipped**, LANE BAL now visible.
|
|||||||
in-band quad at $80, say so and **I'll move the tree, not the ratings** — the ratings are the site.
|
in-band quad at $80, say so and **I'll move the tree, not the ratings** — the ratings are the site.
|
||||||
Carried, still yours either way: grass_atlas (**nine sprints, 0 refs** — bin it), the screenshot POST,
|
Carried, still yours either way: grass_atlas (**nine sprints, 0 refs** — bin it), the screenshot POST,
|
||||||
`moon.png`.
|
`moon.png`.
|
||||||
|
[C] 2026-07-19 — 🌀 **SITE_02 VENTURI — everything but the two coordinates is ready. A/E, here's the axis;
|
||||||
|
give me the gap and I paste the list.** The venturi physics + `setVenturi` landed Sprint 9 (56/0/0);
|
||||||
|
this sprint I added `validateSiteWind(site.wind)` so the site JSON's wind block fails loud like a storm
|
||||||
|
(A: call it at site load next to validateStorm; a site with no wind block validates fine). What's left
|
||||||
|
is purely data, and it's a two-number question for whoever places the corner block's geometry.
|
||||||
|
**The axis is not a guess — it's the storm.** Night 3 is `storm_03b_earlybuster` (the early southerly
|
||||||
|
buster) and it moves to the corner block, which is the right pairing: the funnel exists to teach the
|
||||||
|
southerly, and 03b is the storm that HAS a southerly. Measured 03b's post-change wind — it settles and
|
||||||
|
peaks (21 m/s at t=49) blowing toward heading **≈ −1.08 rad (−62°, SSE)**. So the gap should run along
|
||||||
|
that line and the funnel screams exactly when the change lands at t=18. The venturi block, axis
|
||||||
|
pre-filled, everything else placeholder:
|
||||||
|
```json
|
||||||
|
"wind": { "venturi": [ { "x": <gap centre x>, "z": <gap centre z>, "axis": -1.08,
|
||||||
|
"gain": 1.35, "radius": 5, "sharp": 3 } ] }
|
||||||
|
```
|
||||||
|
**A/E — the one thing I need from you: where is the gap?** Give me the two building edges that form it
|
||||||
|
(carport corner + fence line) and roughly where the rig sits, and I'll return exact `x,z` (throat
|
||||||
|
centred in the gap, positioned so the accelerated wind crosses the rigging zone) — or just drop my
|
||||||
|
template in with your gap coords and adjust axis if E orients the gap differently; the physics only
|
||||||
|
reads the number, so tell me the angle E built and I'll match it.
|
||||||
|
⚠️ **B — the venturi is a BALANCE lever on site_02, so audit WITH it.** gain 1.35 boosts wind ×1.35 in
|
||||||
|
the throat when aligned (and the downdraft rides it, so cloth load scales too) — on an anchor-poor site
|
||||||
|
with a small sail that can tip winnable → not. Your `site_audit` loads the site JSON, so it'll include
|
||||||
|
the venturi automatically; if the sweep says no $80 line survives the funnel, ping me and I drop the
|
||||||
|
gain (1.35 → 1.2 is a one-number edit) rather than E moving the tree for a load I created. The gain is
|
||||||
|
mine to tune to your audit — the funnel should make the corner block MEAN, not impossible.
|
||||||
|
(Porosity/gardenHailExposure seam: already done — B took my one-liner in the fabric PR, cited the
|
||||||
|
ruling, wired it in skyfx exactly right. Nothing owed there.)
|
||||||
|
|
||||||
|
[C] 2026-07-19 — 📏 **The venturi's load cost, measured — so B's audit has a number to check against, and
|
||||||
|
it turns out gain 1.35 is not just safe but DESIGNS the site well.** Load goes as wind², so I worried
|
||||||
|
×1.35 wind → ×1.8 load. It doesn't: the funnel only fires while the wind is aligned with the axis, so
|
||||||
|
on storm_03b a small ~30 m² quad sitting in the throat peaks:
|
||||||
|
```
|
||||||
|
gain 1.00 (off) 1.42 kN
|
||||||
|
gain 1.20 1.74 kN (+22%)
|
||||||
|
gain 1.35 1.99 kN (+40%) ← proposed
|
||||||
|
gain 1.50 2.24 kN (+58%)
|
||||||
|
```
|
||||||
|
At **1.99 kN, gain 1.35 sits ABOVE the carabiner (1.2) and well UNDER the shackle (3.2).** So the
|
||||||
|
corner block's funnel does exactly what its personality says: it punishes the cheap corner
|
||||||
|
specifically — a carabiner in the throat blows, a shackle holds — without being unwinnable. "Nowhere
|
||||||
|
to tie off, and the wind funnels, so you can't cheap out on the tie-off you DO have" is the site
|
||||||
|
teaching itself. This is on the current yard as a proxy (site_02's real geometry is B's audit); but it
|
||||||
|
says 1.35 is a good starting gain, not a coin-flip. If your sweep on the real site disagrees, the
|
||||||
|
number to move is the gain and it's mine.
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
// call it with the fetched storm defs.
|
// call it with the fetched storm defs.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createWindField, validateStorm, gustEnvelope, GUST, RAIN_TIME_COMPRESSION,
|
createWindField, validateStorm, validateSiteWind, gustEnvelope, GUST, RAIN_TIME_COMPRESSION,
|
||||||
stormStats, forecastFor, hailBlockFor,
|
stormStats, forecastFor, hailBlockFor,
|
||||||
} from '../weather.core.js';
|
} from '../weather.core.js';
|
||||||
|
|
||||||
@ -324,6 +324,24 @@ export function weatherCases(storms) {
|
|||||||
assert(maxRatio > 1.3, `funnel didn't strengthen the downdraft (max ${maxRatio.toFixed(2)}×) — it should ride local speed`);
|
assert(maxRatio > 1.3, `funnel didn't strengthen the downdraft (max ${maxRatio.toFixed(2)}×) — it should ride local speed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('validateSiteWind: a good funnel passes, a bad one fails loud', () => {
|
||||||
|
const ok = validateSiteWind({ venturi: [{ x: -6, z: 4, axis: -1.08, gain: 1.4, radius: 5, sharp: 3 }] }, 's');
|
||||||
|
assert(ok.ok, `a valid venturi was rejected: ${ok.errors.join('; ')}`);
|
||||||
|
assert(validateSiteWind(null, 's').ok, 'a site with no wind block should validate');
|
||||||
|
assert(validateSiteWind({}, 's').ok, 'an empty wind block should validate');
|
||||||
|
assert(validateSiteWind({ venturi: [] }, 's').ok, 'an empty venturi list should validate');
|
||||||
|
for (const [label, bad] of [
|
||||||
|
['gain < 1 (a venturi speeds up, never slows)', { venturi: [{ x: 0, z: 0, gain: 0.7 }] }],
|
||||||
|
['gain absurd', { venturi: [{ x: 0, z: 0, gain: 9 }] }],
|
||||||
|
['no x,z', { venturi: [{ axis: 1, gain: 1.4 }] }],
|
||||||
|
['axis not finite', { venturi: [{ x: 0, z: 0, axis: 'south' }] }],
|
||||||
|
['radius <= 0', { venturi: [{ x: 0, z: 0, radius: 0 }] }],
|
||||||
|
['venturi not an array', { venturi: { x: 0 } }],
|
||||||
|
]) {
|
||||||
|
assert(!validateSiteWind(bad, 's').ok, `validateSiteWind accepted a bad funnel: ${label}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('an empty venturi list is a perfect no-op (backyard_01 is untouched)', () => {
|
test('an empty venturi list is a perfect no-op (backyard_01 is untouched)', () => {
|
||||||
const def = storms.storm_02_wildnight;
|
const def = storms.storm_02_wildnight;
|
||||||
const bare = createWindField(def);
|
const bare = createWindField(def);
|
||||||
|
|||||||
@ -807,3 +807,29 @@ export function validateStorm(def, name = 'storm') {
|
|||||||
|
|
||||||
return { ok: errors.length === 0, errors };
|
return { ok: errors.length === 0, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- site wind validator (SPRINT10 site-as-data) ----------
|
||||||
|
// A site's `wind` block is hand-authored data like a storm, so it fails loud
|
||||||
|
// the same way. Lane A calls this at site load; setVenturi clamps defensively
|
||||||
|
// too, but a clamp hides a typo and this names it. Wind personality that lives
|
||||||
|
// in site JSON: venturi funnels (and later, per-site shelter overrides).
|
||||||
|
export function validateSiteWind(wind, name = 'site') {
|
||||||
|
const errors = [];
|
||||||
|
const bad = (m) => errors.push(`${name}.wind: ${m}`);
|
||||||
|
if (wind == null) return { ok: true, errors }; // a site with no wind block is fine
|
||||||
|
if (typeof wind !== 'object') { bad('must be an object'); return { ok: false, errors }; }
|
||||||
|
|
||||||
|
if (wind.venturi != null) {
|
||||||
|
if (!Array.isArray(wind.venturi)) bad('venturi must be an array of funnel zones');
|
||||||
|
else wind.venturi.forEach((v, i) => {
|
||||||
|
const at = `venturi[${i}]`;
|
||||||
|
if (!Number.isFinite(v.x) || !Number.isFinite(v.z)) bad(`${at} needs finite x,z (the throat centre)`);
|
||||||
|
if (v.axis != null && !Number.isFinite(v.axis)) bad(`${at}.axis must be radians (the direction the gap runs)`);
|
||||||
|
if (v.gain != null && !(v.gain >= 1)) bad(`${at}.gain must be >= 1 — a venturi speeds wind UP; use shelters to slow it (got ${v.gain})`);
|
||||||
|
if (v.gain != null && v.gain > 3) bad(`${at}.gain ${v.gain} is a wind tunnel, not a gap — cap ~2`);
|
||||||
|
if (v.radius != null && !(v.radius > 0)) bad(`${at}.radius must be > 0 metres`);
|
||||||
|
if (v.sharp != null && !(v.sharp >= 1)) bad(`${at}.sharp must be >= 1 (alignment falloff exponent)`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { ok: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|||||||
@ -11,11 +11,14 @@
|
|||||||
|
|
||||||
import * as THREE from '../vendor/three.module.js';
|
import * as THREE from '../vendor/three.module.js';
|
||||||
import {
|
import {
|
||||||
createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION,
|
createWindField, validateStorm, validateSiteWind, GUST, RAIN_TIME_COMPRESSION,
|
||||||
hailBlockFor, stormStats, forecastFor,
|
hailBlockFor, stormStats, forecastFor,
|
||||||
} from './weather.core.js';
|
} from './weather.core.js';
|
||||||
|
|
||||||
export { GUST, validateStorm, RAIN_TIME_COMPRESSION, hailBlockFor, stormStats, forecastFor };
|
export {
|
||||||
|
GUST, validateStorm, validateSiteWind, RAIN_TIME_COMPRESSION,
|
||||||
|
hailBlockFor, stormStats, forecastFor,
|
||||||
|
};
|
||||||
|
|
||||||
const kmh = (ms) => ms * 3.6;
|
const kmh = (ms) => ms * 3.6;
|
||||||
const band = (b, fmt) => (b.hi - b.lo < 0.05 ? fmt(b.lo) : `${fmt(b.lo)}–${fmt(b.hi)}`);
|
const band = (b, fmt) => (b.hi - b.lo < 0.05 ? fmt(b.lo) : `${fmt(b.lo)}–${fmt(b.hi)}`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user