Merge remote-tracking branch 'origin/lane/e'
# Conflicts: # THREADS.md
This commit is contained in:
commit
8791eccc08
5
.gitignore
vendored
5
.gitignore
vendored
@ -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__/
|
||||
|
||||
77
THREADS.md
77
THREADS.md
@ -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.
|
||||
|
||||
50
tools/assetcheck/README.md
Normal file
50
tools/assetcheck/README.md
Normal 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"
|
||||
```
|
||||
82
tools/assetcheck/assets_in_three.html
Normal file
82
tools/assetcheck/assets_in_three.html
Normal 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>
|
||||
328
tools/blender/asset_report.json
Normal file
328
tools/blender/asset_report.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
1409
tools/blender/build_yard_assets.py
Normal file
1409
tools/blender/build_yard_assets.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
tools/blender/contact_sheet.png
Normal file
BIN
tools/blender/contact_sheet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@ -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-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
|
||||
* 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 4–9 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 1–6 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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
BIN
web/world/models/carabiner_v1.glb
Normal file
BIN
web/world/models/carabiner_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/debris/BlackTub_v2.glb
Normal file
BIN
web/world/models/debris/BlackTub_v2.glb
Normal file
Binary file not shown.
BIN
web/world/models/debris/BlueCrate_v2.glb
Normal file
BIN
web/world/models/debris/BlueCrate_v2.glb
Normal file
Binary file not shown.
BIN
web/world/models/debris/WhiteTub_v2.glb
Normal file
BIN
web/world/models/debris/WhiteTub_v2.glb
Normal file
Binary file not shown.
BIN
web/world/models/debris/WoodenBin_v2.glb
Normal file
BIN
web/world/models/debris/WoodenBin_v2.glb
Normal file
Binary file not shown.
BIN
web/world/models/debris/tramp_01_v1.glb
Normal file
BIN
web/world/models/debris/tramp_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/fence_panel_v1.glb
Normal file
BIN
web/world/models/fence_panel_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/fence_post_v1.glb
Normal file
BIN
web/world/models/fence_post_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/garden_bed_v1.glb
Normal file
BIN
web/world/models/garden_bed_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/gate_v1.glb
Normal file
BIN
web/world/models/gate_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/house_yardside_v1.glb
Normal file
BIN
web/world/models/house_yardside_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/ladder_01_v1.glb
Normal file
BIN
web/world/models/ladder_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/ref_capsule_v1.glb
Normal file
BIN
web/world/models/ref_capsule_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/sail_post_v1.glb
Normal file
BIN
web/world/models/sail_post_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/shackle_v1.glb
Normal file
BIN
web/world/models/shackle_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/shed_01_v1.glb
Normal file
BIN
web/world/models/shed_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/shed_table_v1.glb
Normal file
BIN
web/world/models/shed_table_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/textures/grass_atlas.png
Normal file
BIN
web/world/models/textures/grass_atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
web/world/models/tree_gum_01_v1.glb
Normal file
BIN
web/world/models/tree_gum_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/tree_gum_02_v1.glb
Normal file
BIN
web/world/models/tree_gum_02_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/turnbuckle_v1.glb
Normal file
BIN
web/world/models/turnbuckle_v1.glb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user