158 lines
6.8 KiB
JavaScript
158 lines
6.8 KiB
JavaScript
// 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);
|
|
}
|