assetcheck/look.html: the verify that answers "is this the right shape"

Dims, tri counts and node names all pass on a bike leaning the wrong way and
a gutter that never fell. The Blender contact sheet is close but renders
through Blender's view transform — it showed this sprint's jacaranda as
near-white lavender when the material is #8C7FC0, so a colour call made there
is a call about Blender, not about the game.

Smallest thing that answers the question: the game's own vendored three.js,
daylight, a ground plane, and the 1.7 m ref capsule beside whatever you name.

    /tools/assetcheck/look.html?a=swing_set_01,tree_jacaranda_01&cam=10,4.5,22

It caught two things this sprint the numbers could not: the lilac reads lilac
in the real renderer (the thumb was lying), and the swing set's wreck lands on
+Z with a 2.9 m reach against a 0.95 m standing footprint — which is now baked
as a placement extra and asserted.

Sized at draw time, every time: a tab that boots hidden reports 0x0 and you
get a correct scene rendered into nothing. That cost me the first screenshot
of this very page, so it is in the MANUAL gotcha list where it already was.
This commit is contained in:
type-two 2026-07-18 16:01:39 +10:00
parent b9b64604b3
commit ba062cf679
2 changed files with 126 additions and 3 deletions

View File

@ -17,6 +17,7 @@ file.*
| does rigging save the garden here? | `tools/garden_bench/` + `tools/site_audit/gardenfly.js` | garden outcomes, separation targets, margin rule |
| per-anchor wind exposure over a storm | `tools/storm_envelope/` | the storm-side second harness |
| regenerate all canon GLBs | `blender -b -P tools/blender/build_yard_assets.py` | deterministic; `--only <name>` for one asset |
| does this asset LOOK right? | `tools/assetcheck/look.html?a=name1,name2` | the game's renderer + the 1.7 m capsule; the only check that answers "wrong shape" |
| job-sheet/invoice design preview | `tools/jobsheet/index.html?v=...` | E's design-ahead handover pattern |
| deploy to partly.party/hardyards | `sh tools/deploy_hardyards.sh` | ships `web/` ONLY; self-verifies |
@ -61,8 +62,17 @@ the site JSON, wired explicitly, so a site says what it contains. The bike
- `rating_hint` (float) on anchor nodes — effective failure = `hw.rating ×
ratingHint`. Unset ⇒ 1 at adoption; the sim reads the anchor, live.
- `collateral` (string) on anchor nodes — what breaks when this anchor's
corner blows. `collateral_key` on a structure whose priced thing has a
different name than the structure (house → "gutter").
corner blows. `collateral_key` on ANY asset that carries a price, naming
which collateral string that price answers to. Not just the odd ones out
(house → "gutter"): the carport went five sprints resolving only because
site_02 happens to id its structure "carport", and a second carport placed
in the editor would have been a free failure.
- `tie_off: false` on an `*_anchor` node that is NOT a tie-off (a carry point,
a light hint, a stand position). **Silence is not neutral here**
`adoptAnchor` does `rating_hint ?? 1`, so an unrated anchor node is the best
steel in the game the moment a site names it. Three e.test.js rules enforce
this class of honesty across every GLB: no silent anchors, no collateral
string nobody prices, no `anchor_type` outside `ANCHOR_TYPE`.
- Named nodes matter: `pickup_anchor` (where carried items sit), the
`fascia_anchor_*` family, etc. Check `world.js` adoptAnchor before renaming
anything.
@ -72,7 +82,13 @@ the site JSON, wired explicitly, so a site says what it contains. The bike
toward three.js **Z**. Never write a docstring about orientation without
measuring the exported GLB in three.js coords — E's bike comment lied about its
own geometry and only a browser-coords assert caught the flip. Dims/tri-count
verifies CANNOT catch a wrong shape or a flipped lean; look at the render.
verifies CANNOT catch a wrong shape or a flipped lean; look at the render —
`tools/assetcheck/look.html` exists for exactly that, and the Blender contact
sheet is NOT a substitute (its view transform rendered a #8C7FC0 canopy as
near-white; a colour judgement made there is a judgement about Blender).
Better still, bake the orientation claim as an extra and assert the claim
against the geometry, so the note and the mesh can only lie together
(`swing_set_01.wreck_falls_toward`).
## Authoring a site (a "level")

107
tools/assetcheck/look.html Normal file
View File

@ -0,0 +1,107 @@
<!doctype html>
<meta charset="utf-8">
<title>assetcheck — LOOK at it, in the real renderer</title>
<!--
The verify that dims and node names cannot do.
build_yard_assets.py re-imports each GLB and asserts its box, its tri count
and its node names; assets_in_three.html proves the axis mapping natively.
Neither can answer "is this the right SHAPE" — a bike leaning the wrong way
and a gutter that never fell both pass every number in the repo. The Blender
contact sheet is close, but it renders through Blender's view transform: it
showed this sprint's jacaranda as near-white lavender when the material is
#8C7FC0, so a colour judgement made there is a judgement about Blender.
This page is deliberately the smallest thing that answers the question: the
game's own vendored three.js, daylight, a ground plane, and the 1.7 m ref
capsule standing beside whatever you name.
/tools/assetcheck/look.html?a=swing_set_01,tree_jacaranda_01
...&cam=12,4.5,20&look=0,2,0 camera position / aim
...&a=tramp_01:debris an asset under models/debris/
Assets are laid out along X in the order given, spaced by their own width,
with the capsule first. Served by server.py from the repo root.
-->
<style>body{margin:0;background:#a8bccc;overflow:hidden}
#hud{position:fixed;left:8px;top:8px;font:12px/1.5 ui-monospace,Menlo,monospace;
color:#22303a;background:#ffffffaa;padding:6px 9px;border-radius:4px}</style>
<div id="hud">loading…</div>
<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 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const q = new URLSearchParams(location.search);
const names = (q.get('a') ?? 'swing_set_01,tree_jacaranda_01,tree_gum_01').split(',');
const vec = (s, d) => (s ?? d).split(',').map(Number);
const r = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild(r.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#a8bccc');
// Roughly the game's daylight: a bright sky dome over grass bounce, plus a sun
// high and to one side. Not the storm grade — this is for reading a shape, and
// a shape you can only read in perfect light is a shape that does not work.
scene.add(new THREE.HemisphereLight('#cfe2f0', '#6d7a52', 1.1));
const sun = new THREE.DirectionalLight('#fff3df', 2.0);
sun.position.set(6, 10, 4);
scene.add(sun);
const ground = new THREE.Mesh(new THREE.PlaneGeometry(120, 120),
new THREE.MeshStandardMaterial({ color: '#7e8f5c' }));
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
const loader = new GLTFLoader();
const load = async (spec) => {
const [name, sub] = spec.split(':');
const dir = sub ? `${sub}/` : '';
const g = await loader.loadAsync(`/web/world/models/${dir}${name}_v1.glb`);
// The optional-node flag has to be honoured here too, or the garden bed
// shows all three wilt states at once and you "see" a bug that isn't one.
g.scene.traverse((o) => { if (o.userData?.hidden_by_default) o.visible = false; });
return { name, obj: g.scene };
};
const items = [{ name: 'ref_capsule', obj: (await load('ref_capsule')).obj },
...(await Promise.all(names.map(load)))];
let x = 0;
const lines = [];
for (const it of items) {
const size = new THREE.Box3().setFromObject(it.obj).getSize(new THREE.Vector3());
x += size.x / 2 + 0.9;
it.obj.position.x = x;
x += size.x / 2;
scene.add(it.obj);
lines.push(`${it.name.padEnd(22)} ${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)} m`);
}
const cam = new THREE.PerspectiveCamera(38, 1, 0.1, 300);
cam.position.set(...vec(q.get('cam'), `${x * 0.55},4.0,${x * 0.95 + 6}`));
cam.lookAt(...vec(q.get('look'), `${x / 2},2,0`));
// Size at DRAW time, every time, and never once at module scope: a tab that
// boots hidden reports 0×0 and you get a correct scene rendered into nothing.
// (MANUAL, "hidden-canvas boot needs one resize before projections are sane" —
// it cost me the first screenshot of this very page.)
const draw = () => {
r.setSize(innerWidth, innerHeight, false);
r.domElement.style.width = '100%';
r.domElement.style.height = '100%';
cam.aspect = innerWidth / Math.max(innerHeight, 1);
cam.updateProjectionMatrix();
r.render(scene, cam);
};
addEventListener('resize', draw);
draw();
document.getElementById('hud').textContent =
`three r${THREE.REVISION} — capsule is 1.70 m\n${lines.join('\n')}`;
document.getElementById('hud').style.whiteSpace = 'pre';
window.__ready = true;
</script>