Merge remote-tracking branch 'origin/lane/e'

# Conflicts:
#	THREADS.md
This commit is contained in:
m3ultra 2026-07-16 22:14:09 +10:00
commit 8791eccc08
29 changed files with 2104 additions and 21 deletions

5
.gitignore vendored
View File

@ -6,6 +6,11 @@
*.obj
*.mtl
# Lane E: per-asset verification renders — regenerable, and 3 MB of churn.
# The tiled tools/blender/contact_sheet.png IS committed; it's the acceptance
# evidence for §5-E, and it renders deterministically so it never churns.
tools/blender/thumbs/
# macOS / python noise
.DS_Store
__pycache__/

View File

@ -177,3 +177,80 @@ Format: `[lane letter] YYYY-MM-DD — note`
~7 s — which is how the cloth got proven before M0 existed. Same array both ways, so the two
harnesses cannot drift. `contracts.js` importing three is no obstacle to that: node imports
`three.module.js` fine.
[E] 2026-07-16 — ✅ **A's §2 correction independently confirmed** — I hit the same wall from the asset side
before M0 landed: `3D-STORE` is at `~/Documents/Destroyulater/3D-STORE/`, and `character_kit` / `FBX` /
`3D=models` / `mixamo-fetch` exist nowhere on this box. My inputs were relocated, not missing, so §5-E
is unblocked and done — Lane D's §5-D genuinely isn't. Two lanes hitting this independently is probably
the answer to A's open question.
[E] 2026-07-16 — **§5-E LANDED: 16 GLBs + grass atlas, all from one script.**
`blender -b -P tools/blender/build_yard_assets.py` (flags: `--only <name>` / `--no-verify` /
`--no-debris`). Proven rather than asserted: 17/17 outputs are byte-identical across two runs; every
GLB is re-imported from disk and checked for dims-in-range, tri budget and node-name survival;
`contact_sheet.png` renders each beside the 1.7 m ref capsule. Heaviest is garden_bed at 2,580 tris —
everything far under the 15 k budget. Machine-readable manifest: `tools/blender/asset_report.json`.
[E] 2026-07-16 — **NODE CONTRACTS — the names your code queries.** Every empty survives the export;
verified in three.js, not just Blender.
· trees: `trunk` (trunk+branches, rigid) + `canopy_01..03` as SEPARATE nodes — Lane A, sway the
canopies only. `branch_anchor_01..03` empties carry `anchor_type="tree"` + `rating_hint` (thicker
limb = higher) for `world.anchors`.
· `house_yardside`: `fascia_anchor_01..03` carry `rating_hint=0.35` + `collateral="gutter"`, and the
`gutter` node carries `collateral_of="fascia"` — DESIGN.md's "the fascia board is a lie" wired as
data, so ripping it takes the gutter with it. Facade only, 9.20 × 1.05 × 2.90 m, no interior.
· `sail_post`: exported VERTICAL, `rake_pivot` at the footing, `top_anchor` at the head. Rake is a
player decision (DESIGN.md: rake away from the load), so it's a runtime rotation, never baked.
**Lane A — this is exactly your 8° rake:** rotate about `rake_pivot` and the footing stays put.
· hardware: `shackle`/`carabiner`/`turnbuckle` each keep their failure part as its own node — `pin`
(unscrews then shears), `gate` (flutters open), `body` (thread strips) — with `failure_mode`
stamped as a custom prop, so a break anim moves just that piece.
· `shed_table``pickup_anchor` · `ladder_01``ladder_base`/`ladder_top` · `gate``hinge_axis`.
[E] 2026-07-16 — `garden_bed` ships all 3 damage states in ONE glb as sibling nodes `plants_full` /
`plants_tattered` / `plants_dead` (full visible, rest `hide_render`). Lane A: toggle `.visible`, don't
reload — instant swap, no pop-in. Tuft positions are identical across states, so the bed wilts instead
of rearranging itself.
[E] 2026-07-16 — debris in `web/world/models/debris/`, copied verbatim (§0 copies rule) and scale-checked:
BlueCrate_v2 0.36×0.36×0.29 · BlackTub_v2 + WhiteTub_v2 0.36×0.54×0.20 · WoodenBin_v2 0.35×0.36×0.31 m
— all plausible real-world sizes. Plus `tramp_01_v1.glb` (2.96×2.96×0.78, `mass_hint` 45), because every
Australian storm produces exactly one airborne trampoline. **Lane C: glob the dir, don't hardcode
names** — §0's `*_v1.glb` rule beats §5-E's "tramp_01.glb" spelling. Grass is a texture, not geometry
(§5-E item 9): `models/textures/grass_atlas.png`, 512², 2×2 tufts, alpha — instance billboards off it.
[E] 2026-07-16 — ⚠️ **LANE A + LANE C, BOUNDING BOXES.** `THREE.Box3.setFromObject(obj)` expands each mesh's
LOCAL box by the world matrix, so a node carrying a rotation reports an inflated box — and that box is
what three frustum-culls against. Blender's `obj.bound_box` has the identical trap; it cost me an hour
chasing phantom failures. Fixed at source: `join_group()` now bakes rotation into the vertices so every
local box is axis-aligned and tight. Before the fix, three reported `tramp_01` as 3.29 × 1.27 m against
a true 2.96 × 0.78. Default `Box3` is safe on these assets now — but if you ever measure geometry
yourself, measure VERTICES, not `bound_box` corners.
[E] 2026-07-16 — filled in `js/tests/e.test.js` (thanks for the pre-created stub — that was a good call)
and landed `tools/assetcheck/` as a standalone version. Loads every GLB through the vendored
GLTFLoader and asserts Y-up, scale sanity and node survival. It exists because the Blender round-trip
**cannot** catch an axis bug: it exports Z-up→Y-up and imports Y-up→Z-up, so a broken `export_yup`
flips back and passes green. Only a native glTF reader can prove it. Green: 16/16, with
`branch_anchor_01` at (-0.96, 3.64, -1.46) — height correctly on +Y.
[E] 2026-07-16 — ⚠️ **LANE A — three lines needed in selftest.html + index.html. Blocks Lane D too.**
No page in the repo has an `<script type="importmap">`, and M0 didn't need one: it imports three by
relative path (`../vendor/three.module.js`). But EVERY three.js addon imports the **bare specifier
`three`**, so the first lane to touch `GLTFLoader` or `SkeletonUtils` gets
`Failed to resolve module specifier "three"`. That's me now — and it's **Lane D the moment they load
`player_01.glb`**, which is the whole of §5-D. The fix, in `<head>`:
<script type="importmap">
{ "imports": { "three": "./vendor/three.module.js",
"three/addons/": "./vendor/addons/" } }
</script>
I did **not** edit your file (§6 says post the need instead, and you'd asked for selftest.html to stay
out of the merge path). `e.test.js` imports GLTFLoader dynamically and `skip`s with that message, so
your gate stays green rather than going red over a harness gap — and the suite lights up on its own
the moment the importmap lands, no edit from me. Verified behind a temporary local probe first:
**36/36 pass** (16 GLBs × scale + node survival, plus anchor world-position and the 3 damage states).
Until then the same asserts run in `tools/assetcheck/`, which carries its own importmap.
[E] 2026-07-16 — ❓ open q for Lane A: your yard puts house fascia anchors at y=2.6, but `house_yardside`'s
fascia sits at 2.80 (2.90 m ridge), and the facade is 9.20 m against a 30 m north edge. Want me to
re-cut it to your numbers, or will you read `fascia_anchor_*` off the GLB when you swap the graybox?
Either way it's one constant for me — the script regenerates everything.

View File

@ -0,0 +1,50 @@
# assets_in_three.html — verify the GLBs in the real consumer
`build_yard_assets.py` already re-imports every GLB into Blender and asserts
dims, tri budget, and node names. That is necessary but **structurally cannot
catch an axis error**: Blender exports Z-up→Y-up and imports Y-up→Z-up, so a
broken `export_yup` flips back on the way in and round-trips green. Only
something that reads glTF natively can prove the file is right.
This page is that check. It loads each GLB with three.js r175's `GLTFLoader` and
asserts:
- **Y-up**: a Blender asset measuring `(dx, dy, dz)` must arrive as `(dx, dz, dy)`.
- **node survival**: `branch_anchor_*`, `fascia_anchor_*`, `plants_*` etc. still
exist after the export — glTF has no "empty", anchors arrive as bare
`Object3D`, and exporters have been known to prune childless nodes.
- **anchors are usable**: `branch_anchor_01` resolves to a sane world position
with height on +Y.
Expectations are read from `tools/blender/asset_report.json`, so this stays in
sync with the factory automatically.
## Why it uses the default (non-precise) Box3
`THREE.Box3.setFromObject(obj)` expands each mesh's **local** bounding box by the
world matrix, so a node carrying a rotation reports an inflated box. That is the
same trap Blender's `obj.bound_box` sets, and it is what three uses for frustum
culling. `join_group()` therefore bakes rotation into the vertices so the local
box is axis-aligned and tight. Passing `precise: true` here would hide exactly
the regression this page exists to catch — so don't.
Before that fix, three reported `tramp_01` as 3.29 × 1.27 m against a true
2.96 × 0.78 m.
## Running it
Needs three.js on `/world/vendor/` and the models on `/models/`. Once Lane A's
`server.py` lands, serve `web/` and this can move next to `selftest.html`
(Lane A: happy to fold it in — see THREADS).
Until then, the standalone recipe:
```sh
D=$(mktemp -d) && mkdir -p "$D/world"
ln -s ~/Documents/90sDJsim/web/world/vendor "$D/world/vendor"
ln -s "$PWD/web/world/models" "$D/models"
ln -s "$PWD/tools/blender/asset_report.json" "$D/asset_report.json"
cp tools/assetcheck/assets_in_three.html "$D/index.html"
python3 -m http.server 8805 --directory "$D"
# open http://127.0.0.1:8805 — look for "SUMMARY: ALL PASS IN THREE.JS"
```

View File

@ -0,0 +1,82 @@
<!doctype html>
<meta charset="utf-8">
<title>Lane E — GLB check in the real consumer (three.js r175)</title>
<style>
body { background:#14161a; color:#dfe3e8; font:13px/1.5 ui-monospace,Menlo,monospace; padding:16px; }
.pass { color:#7fd67f; } .fail { color:#ff6b6b; } h1 { font-size:15px; color:#9fb4c7; }
</style>
<h1>GLB verification — loaded by three.js GLTFLoader, not Blender</h1>
<pre id="out">loading…</pre>
<script type="importmap">
{ "imports": { "three": "/world/vendor/three.module.js",
"three/addons/": "/world/vendor/addons/" } }
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const out = document.getElementById('out');
const log = [];
const say = (s, cls) => {
log.push(s);
out.innerHTML += `<span class="${cls || ''}">${s}</span>\n`;
console.log(s);
};
const report = await (await fetch('/asset_report.json')).json();
const loader = new GLTFLoader();
let fails = 0;
say(`three.js r${THREE.REVISION} | ${report.assets.length} assets\n`);
for (const a of report.assets) {
let gltf = null;
for (const dir of ['/models/', '/models/debris/']) {
try { gltf = await loader.loadAsync(`${dir}${a.name}_v1.glb`); break; } catch (e) {}
}
if (!gltf) { say(`[FAIL] ${a.name.padEnd(16)} could not load`, 'fail'); fails++; continue; }
const size = new THREE.Vector3();
new THREE.Box3().setFromObject(gltf.scene).getSize(size);
// The whole point of this page. Blender is Z-up, glTF is Y-up, so a Blender
// asset measuring (dx, dy, dz) MUST arrive here as (dx, dz, dy). A Blender
// re-import can never catch a broken export_yup — it just flips it back.
const [bx, by, bz] = a.dims;
const exp = [bx, bz, by];
const got = [size.x, size.y, size.z];
const axisOk = got.every((v, i) => Math.abs(v - exp[i]) < 0.02);
const names = [];
gltf.scene.traverse(o => names.push(o.name));
const missing = a.nodes.filter(n => !names.includes(n));
const ok = axisOk && missing.length === 0;
if (!ok) fails++;
say(`[${ok ? 'PASS' : 'FAIL'}] ${a.name.padEnd(16)} ` +
`${got.map(v => v.toFixed(2)).join(' x ')} m (Y-up)`, ok ? 'pass' : 'fail');
if (!axisOk) say(` axis/scale: expected ${exp.map(v => v.toFixed(2)).join(' x ')}`, 'fail');
if (missing.length) say(` nodes lost in three.js: ${missing.join(', ')}`, 'fail');
}
// Empties are the risky part: glTF has no "empty", they arrive as bare Object3D
// nodes, and exporters have been known to prune childless ones. Anchors ARE the
// contract, so prove one survives with a usable world position.
const t = await loader.loadAsync('/models/tree_gum_01_v1.glb');
const anchor = t.scene.getObjectByName('branch_anchor_01');
if (anchor) {
t.scene.updateWorldMatrix(true, true);
const p = new THREE.Vector3().setFromMatrixPosition(anchor.matrixWorld);
const upright = p.y > 1.0 && p.y < 6.0;
if (!upright) fails++;
say(`\n[${upright ? 'PASS' : 'FAIL'}] branch_anchor_01 world pos ` +
`(${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)}) — ` +
`${upright ? 'height is on +Y, anchors are usable' : 'height is NOT on +Y!'}`,
upright ? 'pass' : 'fail');
} else { fails++; say('\n[FAIL] branch_anchor_01 missing entirely', 'fail'); }
say(`\nSUMMARY: ${fails === 0 ? 'ALL PASS IN THREE.JS' : fails + ' FAILURES'}`,
fails === 0 ? 'pass' : 'fail');
window.__done = true; window.__fails = fails;
</script>

View File

@ -0,0 +1,328 @@
{
"blender": "5.1.2",
"assets": [
{
"name": "ref_capsule",
"dims": [
0.4,
0.4,
1.7
],
"tris": 220,
"nodes": [
"head_height",
"ref_capsule",
"ref_capsule_mesh"
],
"status": "PASS",
"problems": []
},
{
"name": "tree_gum_01",
"dims": [
4.5522,
4.956,
7.9702
],
"tris": 396,
"nodes": [
"branch_anchor_01",
"branch_anchor_02",
"branch_anchor_03",
"canopy_01",
"canopy_02",
"canopy_03",
"tree_gum_01",
"trunk"
],
"status": "PASS",
"problems": []
},
{
"name": "tree_gum_02",
"dims": [
3.8871,
2.7787,
5.4972
],
"tris": 288,
"nodes": [
"branch_anchor_01",
"branch_anchor_02",
"canopy_01",
"canopy_02",
"tree_gum_02",
"trunk"
],
"status": "PASS",
"problems": []
},
{
"name": "fence_post",
"dims": [
0.13,
0.13,
2.03
],
"tris": 24,
"nodes": [
"fence_post",
"post"
],
"status": "PASS",
"problems": []
},
{
"name": "fence_panel",
"dims": [
2.4,
0.054,
1.8194
],
"tris": 324,
"nodes": [
"fence_panel",
"palings",
"rails"
],
"status": "PASS",
"problems": []
},
{
"name": "gate",
"dims": [
1.045,
0.0615,
1.75
],
"tris": 220,
"nodes": [
"gate",
"gate_frame",
"gate_palings",
"hinge_axis",
"hinges"
],
"status": "PASS",
"problems": []
},
{
"name": "house_yardside",
"dims": [
9.2,
1.0547,
2.9
],
"tris": 200,
"nodes": [
"door",
"fascia",
"fascia_anchor_01",
"fascia_anchor_02",
"fascia_anchor_03",
"gutter",
"house_yardside",
"roof",
"wall",
"window"
],
"status": "PASS",
"problems": []
},
{
"name": "shed_01",
"dims": [
2.58,
1.9708,
2.2224
],
"tris": 96,
"nodes": [
"door_anchor",
"doors",
"roof",
"shed_01",
"shell"
],
"status": "PASS",
"problems": []
},
{
"name": "shed_table",
"dims": [
1.6,
0.6,
0.9
],
"tris": 72,
"nodes": [
"pickup_anchor",
"shed_table",
"table_frame",
"table_top"
],
"status": "PASS",
"problems": []
},
{
"name": "garden_bed",
"dims": [
3.0,
1.2,
0.8609
],
"tris": 2580,
"nodes": [
"bed",
"garden_bed",
"plants_dead",
"plants_full",
"plants_tattered",
"soil"
],
"status": "PASS",
"problems": []
},
{
"name": "sail_post",
"dims": [
0.507,
0.52,
4.0327
],
"tris": 528,
"nodes": [
"footing",
"pad_eye",
"post",
"rake_pivot",
"sail_post",
"top_anchor"
],
"status": "PASS",
"problems": []
},
{
"name": "ladder_01",
"dims": [
0.455,
0.075,
3.0
],
"tris": 276,
"nodes": [
"ladder",
"ladder_01",
"ladder_base",
"ladder_top"
],
"status": "PASS",
"problems": []
},
{
"name": "shackle",
"dims": [
0.0569,
0.019,
0.0744
],
"tris": 560,
"nodes": [
"bow",
"pin",
"shackle"
],
"status": "PASS",
"problems": []
},
{
"name": "carabiner",
"dims": [
0.049,
0.009,
0.1027
],
"tris": 476,
"nodes": [
"body",
"carabiner",
"gate"
],
"status": "PASS",
"problems": []
},
{
"name": "turnbuckle",
"dims": [
0.0292,
0.0341,
0.1955
],
"tris": 728,
"nodes": [
"body",
"eye_a",
"eye_b",
"turnbuckle"
],
"status": "PASS",
"problems": []
},
{
"name": "tramp_01",
"dims": [
2.9555,
2.9555,
0.78
],
"tris": 976,
"nodes": [
"legs",
"mat",
"pad",
"rim",
"tramp_01"
],
"status": "PASS",
"problems": []
}
],
"debris": [
{
"file": "BlueCrate_v2.glb",
"dims": [
0.36,
0.36,
0.29
],
"sane": true
},
{
"file": "BlackTub_v2.glb",
"dims": [
0.36,
0.54,
0.2
],
"sane": true
},
{
"file": "WhiteTub_v2.glb",
"dims": [
0.36,
0.54,
0.2
],
"sane": true
},
{
"file": "WoodenBin_v2.glb",
"dims": [
0.35,
0.36,
0.31
],
"sane": true
}
]
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -1,28 +1,160 @@
/**
* Lane E selftests asset sanity.
* Lane E owns this file. Other lanes: yours is js/tests/<letter>.test.js.
*
* Lane E owns this file. Lane A pre-created it so adding your suite never means
* editing selftest.html.
* Why this exists when build_yard_assets.py already verifies: the Blender side
* re-imports every GLB and asserts dims, tri budget and node names, but it
* CANNOT catch an axis error. Blender exports Z-upY-up and imports Y-upZ-up,
* so a broken `export_yup` flips back on the way in and round-trips green. Only
* a native glTF reader can prove the file is right, and this is the only one in
* the repo. So this suite targets the failures that silently break other lanes:
* 1. the GLB loads at all through the vendored loader;
* 2. it's in metres with its height on +Y a model exported in centimetres
* looks fine alone and absurd next to a person;
* 3. the nodes other lanes query by name survived the export. glTF has no
* "empty" type, so anchors arrive as bare Object3D and are exactly what an
* exporter prunes.
*
* Most of Lane E's verification is the Blender-side contact sheet against the
* 1.7 m ref capsule (PLAN3D §5-E), which this harness can't see. What IS worth
* asserting here, once the GLBs land, is the stuff that silently breaks the
* other lanes:
* 1. every GLB loads without error through the vendored GLTFLoader.
* 2. scale sanity a loaded tree's bounding box is 49 m tall, the fence
* panel is ~1.6 m, the shackle is ~0.1 m. A model exported in centimetres
* looks fine alone and absurd next to a person.
* 3. the named nodes the other lanes query actually exist:
* tree_gum_01 `trunk`, `canopy_*`, `branch_anchor_*`
* house_yardside `fascia_anchor_*`
* Lane A sways the canopies by name; Lane B reads branch anchors.
*
* Loading is async `export default async function run(t)` is supported.
* Useful import:
* import { GLTFLoader } from '../../vendor/addons/loaders/GLTFLoader.js';
* Standalone version with a fuller report: tools/assetcheck/.
*/
/** @param {import('../testkit.js').Suite} t */
export default function run(t) {
t.skip('yard asset GLBs not landed yet — Lane E');
import * as THREE from '../../vendor/three.module.js';
import { assert } from '../testkit.js';
// GLTFLoader is imported DYNAMICALLY, below, and that is deliberate.
//
// Every three.js addon imports the bare specifier `three`, which only resolves
// via an <script type="importmap">. No page in this repo has one yet — index.html
// and selftest.html both import `../vendor/three.module.js` by relative path and
// so never needed it. A static import here would throw at module load, and
// selftest.html turns an un-importable lane module into a hard FAIL, which would
// redden Lane A's merge gate over a harness gap rather than a real defect.
//
// So: try it at runtime and skip with an actionable message if it's absent. The
// day the importmap lands this suite lights up on its own, no edit needed.
// Need + exact fix are logged in THREADS.md [E] — it blocks Lane D too, which
// can't load a ped without GLTFLoader/SkeletonUtils.
const LOADER_PATH = '../../vendor/addons/loaders/GLTFLoader.js';
/** Resolve off import.meta.url, not the document — survives selftest.html moving. */
const url = (a) => new URL(`../../models/${a.sub ?? ''}${a.name}_v1.glb`, import.meta.url).href;
/**
* Height ranges rather than exact dims: this guards against unit and axis
* regressions, not against Lane E retuning a silhouette. Exact measurements
* live in tools/blender/asset_report.json. `nodes` are the names other lanes
* query changing one is a contract break and should fail here.
*/
const ASSETS = [
{ name: 'ref_capsule', h: [1.68, 1.72], nodes: ['ref_capsule_mesh', 'head_height'] },
{ name: 'tree_gum_01', h: [4.0, 9.0],
nodes: ['trunk', 'canopy_01', 'canopy_02', 'canopy_03',
'branch_anchor_01', 'branch_anchor_02', 'branch_anchor_03'] },
{ name: 'tree_gum_02', h: [4.0, 9.0],
nodes: ['trunk', 'canopy_01', 'canopy_02', 'branch_anchor_01', 'branch_anchor_02'] },
{ name: 'fence_post', h: [1.8, 2.2], nodes: ['post'] },
{ name: 'fence_panel', h: [1.6, 2.0], nodes: ['palings', 'rails'] },
{ name: 'gate', h: [1.6, 2.0], nodes: ['gate_palings', 'gate_frame', 'hinges', 'hinge_axis'] },
{ name: 'house_yardside', h: [2.5, 3.5],
nodes: ['wall', 'door', 'window', 'roof', 'fascia', 'gutter',
'fascia_anchor_01', 'fascia_anchor_02', 'fascia_anchor_03'] },
{ name: 'shed_01', h: [1.9, 2.4], nodes: ['shell', 'roof', 'doors', 'door_anchor'] },
{ name: 'shed_table', h: [0.8, 1.0], nodes: ['table_top', 'table_frame', 'pickup_anchor'] },
{ name: 'garden_bed', h: [0.5, 1.1],
nodes: ['bed', 'soil', 'plants_full', 'plants_tattered', 'plants_dead'] },
{ name: 'sail_post', h: [3.8, 4.2],
nodes: ['footing', 'post', 'pad_eye', 'top_anchor', 'rake_pivot'] },
{ name: 'ladder_01', h: [2.8, 3.2], nodes: ['ladder', 'ladder_base', 'ladder_top'] },
{ name: 'shackle', h: [0.05, 0.15], nodes: ['bow', 'pin'] },
{ name: 'carabiner', h: [0.06, 0.15], nodes: ['body', 'gate'] },
{ name: 'turnbuckle', h: [0.12, 0.25], nodes: ['body', 'eye_a', 'eye_b'] },
{ name: 'tramp_01', h: [0.6, 1.0], nodes: ['mat', 'rim', 'pad', 'legs'], sub: 'debris/' },
];
function sizeOf(gltf) {
const s = new THREE.Vector3();
new THREE.Box3().setFromObject(gltf.scene).getSize(s);
return s;
}
/** @param {import('../testkit.js').Suite} t */
export default async function run(t) {
let GLTFLoader;
try {
({ GLTFLoader } = await import(LOADER_PATH));
} catch (err) {
t.skip('needs an importmap for the bare `three` specifier — see THREADS [E]. ' +
'Assets ARE verified meanwhile: tools/assetcheck/ (16/16 green in three.js r175)');
return;
}
const loader = new GLTFLoader();
const loaded = new Map();
const failed = new Map();
await Promise.all(ASSETS.map(async (a) => {
try { loaded.set(a.name, await loader.loadAsync(url(a))); }
catch (err) { failed.set(a.name, err?.message ?? String(err)); }
}));
t.test('every yard GLB loads through the vendored GLTFLoader', () => {
const lost = [...failed].map(([n, e]) => `${n} (${e})`).join('; ');
assert(failed.size === 0, `failed to load: ${lost}`);
});
// The anchor of the whole scale system. If this is wrong, every judgement
// made against the contact sheet was made against a lie.
t.test('ref_capsule is 1.70 m tall on +Y — the scale everything is judged against', () => {
const g = loaded.get('ref_capsule');
assert(g, 'ref_capsule did not load');
const s = sizeOf(g);
assert(Math.abs(s.y - 1.70) < 0.02, `capsule is ${s.y.toFixed(3)} m on Y, want 1.70`);
assert(s.x < 0.6 && s.z < 0.6,
`capsule is ${s.x.toFixed(2)} x ${s.z.toFixed(2)} in plan — height is not on +Y`);
});
for (const a of ASSETS) {
const gltf = loaded.get(a.name);
if (!gltf) continue; // already reported by the load test
const s = sizeOf(gltf);
t.test(`${a.name}: metre-scale, height on +Y`, () => {
assert(s.y >= a.h[0] && s.y <= a.h[1],
`${a.name} stands ${s.y.toFixed(3)} m, want ${a.h[0]}${a.h[1]} m ` +
`(box ${s.x.toFixed(2)} x ${s.y.toFixed(2)} x ${s.z.toFixed(2)})`);
});
t.test(`${a.name}: named nodes survive the export`, () => {
const names = new Set();
gltf.scene.traverse((o) => names.add(o.name));
const missing = a.nodes.filter((n) => !names.has(n));
assert(missing.length === 0,
`${a.name} lost ${missing.join(', ')} — other lanes query these by name`);
});
}
// Anchors are the actual product here: Lane B pins cloth corners to them and
// Lane A builds world.anchors from them. A surviving name isn't enough — the
// position has to be usable.
t.test('branch_anchor_01 resolves to a usable world position up the tree', () => {
const g = loaded.get('tree_gum_01');
assert(g, 'tree_gum_01 did not load');
const anchor = g.scene.getObjectByName('branch_anchor_01');
assert(anchor, 'branch_anchor_01 missing — glTF has no "empty", check it was not pruned');
g.scene.updateWorldMatrix(true, true);
const p = new THREE.Vector3().setFromMatrixPosition(anchor.matrixWorld);
assert(p.y > 1.0 && p.y < 6.0,
`anchor sits at y=${p.y.toFixed(2)} — want 16 m up the trunk`);
assert(Number.isFinite(p.x) && Number.isFinite(p.z), 'anchor world position is not finite');
});
// One GLB carries three wilt states as siblings; Lane A toggles .visible
// rather than reloading, so all three have to be present at once.
t.test('garden_bed carries all 3 damage states in one GLB', () => {
const g = loaded.get('garden_bed');
assert(g, 'garden_bed did not load');
for (const state of ['plants_full', 'plants_tattered', 'plants_dead']) {
assert(g.scene.getObjectByName(state), `${state} missing — Lane A toggles these by name`);
}
});
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.