// Unit tests for geo_builder.js — run with: node static/js/tests/geo_builder.test.js // No framework: asserts, prints a summary, exits non-zero on failure. 'use strict'; const path = require('path'); const GeoBuilder = require(path.join(__dirname, '..', 'geo_builder.js')); const zombie = require(path.join(__dirname, 'fixtures', 'zombie.geo.json')); let failures = 0; let checks = 0; function approx(a, b, tol = 1e-6) { return Math.abs(a - b) <= tol; } function assertVec(actual, expected, label, tol = 1e-6) { checks++; const ok = approx(actual[0], expected[0], tol) && approx(actual[1], expected[1], tol) && approx(actual[2], expected[2], tol); if (!ok) { failures++; console.error(`FAIL ${label}: got [${actual.map(v => v.toFixed(4)).join(', ')}] expected [${expected.join(', ')}]`); } } function assert(cond, label) { checks++; if (!cond) { failures++; console.error(`FAIL ${label}`); } } function worldCenter(matrix) { return GeoBuilder.transformPoint(matrix, [0, 0, 0]); } // ---------- 1. Real zombie model (nested hierarchy, identity rotations) ---------- const built = GeoBuilder.build(zombie); const tw = built.texture_width, th = built.texture_height; assert(built.cubes.length === 8, `zombie cube count (got ${built.cubes.length})`); const centers = built.cubes.map(c => ({ c, center: worldCenter(c.matrix) })); function cubeNear(center) { return centers.filter(({ center: c }) => approx(c[0], center[0], 1e-6) && approx(c[1], center[1], 1e-6) && approx(c[2], center[2], 1e-6) ).map(x => x.c); } assert(cubeNear([0, 28, 0]).length >= 2, 'head + headwear at (0,28,0)'); // head & headwear assert(cubeNear([0, 18, 0]).length === 1, 'body at (0,18,0)'); assert(cubeNear([6, 18, 0]).length === 1, 'leftarm at (6,18,0)'); assert(cubeNear([-6, 18, 0]).length === 1, 'rightarm at (-6,18,0)'); assert(cubeNear([2, 6, 0]).length === 1, 'leftleg at (2,6,0)'); assert(cubeNear([-2, 6, 0]).length === 1, 'rightleg at (-2,6,0)'); assert(cubeNear([0, 11.75, -3.5]).length === 1, 'd at (0,11.75,-3.5)'); // Head = the (0,28,0) cube with uv [0,0]; headwear has uv [32,0]. const headCube = cubeNear([0, 28, 0]).find(c => c.uv[0] === 0 && c.uv[1] === 0); assert(!!headCube, 'head cube identified'); if (headCube) { // Head north face (last face) must sample the vanilla (8..16, 8..16) region. const geo = GeoBuilder.cubeGeometry(headCube.size, headCube.uv, tw, th); const northStart = 5 * 8; // face order: east,west,top,bottom,south,north let minU = 2, maxU = -1, minV = 2, maxV = -1; for (let i = 0; i < 8; i += 2) { minU = Math.min(minU, geo.uvs[northStart + i]); maxU = Math.max(maxU, geo.uvs[northStart + i]); minV = Math.min(minV, geo.uvs[northStart + i + 1]); maxV = Math.max(maxV, geo.uvs[northStart + i + 1]); } assert(approx(minU, 8 / 64) && approx(maxU, 16 / 64), `head north U in (8..16)/64 (got ${minU.toFixed(3)}..${maxU.toFixed(3)})`); assert(approx(minV, 1 - 16 / 64) && approx(maxV, 1 - 8 / 64), `head north V in [0.75,0.875] (got ${minV.toFixed(3)}..${maxV.toFixed(3)})`); } // All UVs within [0,1]. let allInRange = true; for (const { c } of centers) { const g = GeoBuilder.cubeGeometry(c.size, c.uv, tw, th); for (const u of g.uvs) if (u < 0 || u > 1) { allInRange = false; break; } } assert(allInRange, 'all UVs within [0,1]'); // Unmapped bones default to the pack's main texture. assert(headCube.texture === 'needsofnature:textures/entity/zombie/zombie.png', 'head uses default texture'); // ---------- 2. Synthetic rotated bone: Rz(-90°) around pivot (0,10,0) ---------- // Rotations are negated to match the Bedrock/GeckoLib direction. const rotated = { 'minecraft:geometry': [{ description: { identifier: 'geometry.t', texture_width: 64, texture_height: 64 }, bones: [{ name: 'waist', pivot: [0, 10, 0], rotation: [0, 0, 90], cubes: [{ origin: [0, 10, 0], size: [2, 2, 2], uv: [0, 0] }], }], }], }; const rotBuilt = GeoBuilder.build(rotated); assert(rotBuilt.cubes.length === 1, 'rotated model has one cube'); // cube center = origin + size/2 = (1,11,1); Rz(-90) around pivot (0,10,0): // rel (1,1,1) -> (1,-1,1) -> +pivot = (1,9,1) const rotCenter = worldCenter(rotBuilt.cubes[0].matrix); assertVec(rotCenter, [1, 9, 1], 'rotated cube center around pivot (expect (1,9,1))'); // ---------- 3. Nested pivot with rotation ---------- const nested = { 'minecraft:geometry': [{ description: { identifier: 'geometry.n', texture_width: 64, texture_height: 64 }, bones: [ { name: 'root', pivot: [0, 0, 0] }, { name: 'waist', pivot: [0, 12, 0], parent: 'root', rotation: [0, 0, 0] }, { name: 'head', pivot: [0, 24, 0], parent: 'waist', cubes: [{ origin: [-4, 24, -4], size: [8, 8, 8], uv: [0, 0] }] }, ], }], }; const nestedBuilt = GeoBuilder.build(nested); assertVec(worldCenter(nestedBuilt.cubes[0].matrix), [0, 28, 0], 'nested pivot head center (expect (0,28,0))'); // ---------- 4. Real wolf model: rotated bones must produce a quadruped layout ---------- const wolf = require(path.join(__dirname, 'fixtures', 'wolf.geo.json')); const wolfBuilt = GeoBuilder.build(wolf); const wolfCenters = wolfBuilt.cubes.map(c => ({ c, center: worldCenter(c.matrix) })); const wBody = wolfCenters.find(({ c }) => c.size[0] === 8 && c.size[1] === 6 && c.size[2] === 7); assert(!!wBody, 'wolf body cube found'); if (wBody) { // Body high (y~10.5) and in the front half (z<2). Under the old rotation // sign the body landed at the back (z=7) — this catches the regression. assertVec(wBody.center, [-1, 10.5, -3], 'wolf body center (expect (-1,10.5,-3))'); } const wHead = wolfCenters.find(({ c }) => c.size[0] === 6 && c.size[1] === 6 && c.size[2] === 4); assert(!!wHead, 'wolf head cube found'); if (wHead) { assertVec(wHead.center, [-1, 10.5, -7], 'wolf head center (expect (-1,10.5,-7))'); } // 4 legs (size 2x8x2 cubes whose y is near the ground). const wLegs = wolfCenters.filter(({ c, center }) => c.size[0] === 2 && c.size[1] === 8 && c.size[2] === 2 && center[1] < 6); assert(wLegs.length === 4, `wolf has 4 legs near the ground (got ${wLegs.length})`); wLegs.forEach(({ center }) => assert(approx(center[1], 4.0, 0.2), `leg at ground level y=${center[1].toFixed(1)}`)); // Tail: the cube with the largest |z|, behind the body. let wTail = wolfCenters[0]; for (const w of wolfCenters) { if (Math.abs(w.center[2]) > Math.abs(wTail.center[2])) wTail = w; } assert(wTail.center[2] > 8, `wolf tail behind body (z=${wTail.center[2].toFixed(1)})`); assert(wBody && wTail.center[2] > wBody.center[2], 'tail z > body z'); // ---------- summary ---------- if (failures === 0) { console.log(`geo_builder tests OK (${checks} checks)`); } else { console.error(`${failures}/${checks} checks FAILED`); process.exit(1); }