// Drill into the head/mouth region. Front = +Z. Looks at body vertices in the // front quarter and reports the vertical (Y) profile from nose back, plus the // lowest points near the mouth — to test "lower lip sits below the body". import { NodeIO } from '@gltf-transform/core'; import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); const doc = await io.read(process.argv[2]); const root = doc.getRoot(); const joints = root.listSkins()[0].listJoints(); const isBody = joints.map((n) => n.getName().startsWith('Spine')); const mesh = root.listMeshes()[0]; const prim = mesh.listPrimitives().reduce((a, b) => (b.getIndices().getCount() > a.getIndices().getCount() ? b : a)); const pos = prim.getAttribute('POSITION'); const jA = prim.getAttribute('JOINTS_0'), wA = prim.getAttribute('WEIGHTS_0'); const n = pos.getCount(); const p = [0, 0, 0], j = [0, 0, 0, 0], w = [0, 0, 0, 0]; // bucket body vertices by Z slab from nose (+Z) backward; track Y min/max per slab const slabs = {}; let bodyMinY = 1e9, bodyMaxY = -1e9; const front = []; // {x,y,z} of front-most body verts for (let i = 0; i < n; i++) { jA.getElement(i, j); wA.getElement(i, w); let best = 0, bw = -1; for (let k = 0; k < 4; k++) if (w[k] > bw) { bw = w[k]; best = j[k]; } if (!isBody[best]) continue; pos.getElement(i, p); bodyMinY = Math.min(bodyMinY, p[1]); bodyMaxY = Math.max(bodyMaxY, p[1]); const slab = Math.round(p[2] * 10) / 10; const e = slabs[slab] || (slabs[slab] = { c: 0, ymin: 1e9, ymax: -1e9 }); e.c++; e.ymin = Math.min(e.ymin, p[1]); e.ymax = Math.max(e.ymax, p[1]); if (p[2] > 0.30) front.push([p[0], p[1], p[2]]); } console.log(`body Y range overall: ${bodyMinY.toFixed(3)} .. ${bodyMaxY.toFixed(3)}\n`); console.log('Z slab (nose=+Z → tail) bodyY min..max (height)'); for (const z of Object.keys(slabs).map(Number).sort((a, b) => b - a)) { const e = slabs[z]; console.log(` z=${z.toFixed(1).padStart(5)} n=${String(e.c).padStart(4)} Y ${e.ymin.toFixed(3)} .. ${e.ymax.toFixed(3)} (${(e.ymax - e.ymin).toFixed(3)})`); } // the lowest front verts (potential lower lip / jaw) front.sort((a, b) => a[1] - b[1]); console.log('\nlowest 6 front (z>0.30) body verts [x, y, z]:'); for (const v of front.slice(0, 6)) console.log(' ', v.map((x) => x.toFixed(3).padStart(7)).join(', ')); console.log('frontmost 6 (max z) body verts [x, y, z]:'); front.sort((a, b) => b[2] - a[2]); for (const v of front.slice(0, 6)) console.log(' ', v.map((x) => x.toFixed(3).padStart(7)).join(', '));