// 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) ---------- 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); Rz90 around pivot (0,10,0): // rel (1,1,1) -> (-1,1,1) -> +pivot = (-1,11,1) const rotCenter = worldCenter(rotBuilt.cubes[0].matrix); assertVec(rotCenter, [-1, 11, 1], 'rotated cube center around pivot (expect (-1,11,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))'); // ---------- summary ---------- if (failures === 0) { console.log(`geo_builder tests OK (${checks} checks)`); } else { console.error(`${failures}/${checks} checks FAILED`); process.exit(1); }