Files
healer-man/scripts/dungeon-pipeline/recast-mesh-adapter.test.mjs
T
2026-10-07 13:05:04 -04:00

106 lines
4.1 KiB
JavaScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { getPositionsAndIndices } from "@recast-navigation/three";
import {
BufferAttribute,
BufferGeometry,
InterleavedBuffer,
InterleavedBufferAttribute,
Mesh,
Object3D,
Vector3,
} from "three";
import { unpackMeshesForRecast } from "./recast-mesh-adapter.mjs";
test("Recast adapter decodes interleaved positions, indices, normalized positions, and parent transforms", () => {
const parent = new Object3D();
parent.position.set(10, 2, -4);
parent.rotation.y = Math.PI / 2;
parent.scale.set(2, 3, 4);
const interleavedGeometry = new BufferGeometry();
const interleavedData = new InterleavedBuffer(new Float32Array([
0, 0, 0, 91, 1,
1, 0, 0, 92, 2,
0, 0, 1, 93, 3,
]), 5);
interleavedGeometry.setAttribute("position", new InterleavedBufferAttribute(interleavedData, 3, 0));
interleavedGeometry.setIndex(new BufferAttribute(new Uint16Array([0, 1, 2]), 1));
const interleavedMesh = new Mesh(interleavedGeometry);
interleavedMesh.name = "interleaved";
interleavedMesh.position.set(1, 0, 2);
parent.add(interleavedMesh);
const normalizedGeometry = new BufferGeometry();
normalizedGeometry.setAttribute("position", new BufferAttribute(new Uint16Array([
0, 0, 0,
65535, 0, 0,
0, 65535, 0,
]), 3, true));
const normalizedMesh = new Mesh(normalizedGeometry);
normalizedMesh.name = "normalized";
normalizedMesh.position.set(-3, 5, 7);
parent.add(normalizedMesh);
parent.updateMatrixWorld(true);
const originalInterleaved = Array.from(interleavedData.array);
const originalNormalized = Array.from(normalizedGeometry.getAttribute("position").array);
const result = unpackMeshesForRecast([interleavedMesh, normalizedMesh]);
assert.deepEqual(result.report, {
adapter: "decoded-three-buffer-attributes-v1",
sourceMeshCount: 2,
unpackedPositionAccessorCount: 2,
interleavedPositionAttributeCount: 1,
normalizedPositionAttributeCount: 1,
nonFloatPositionAttributeCount: 1,
decodedIndexAccessorCount: 1,
});
for (const [index, sourceMesh] of [interleavedMesh, normalizedMesh].entries()) {
assert.deepEqual(
result.meshes[index].matrixWorld.toArray(),
sourceMesh.matrixWorld.toArray(),
"inspection and generator transforms must agree before generator mutation",
);
}
const [positions, indices] = getPositionsAndIndices(result.meshes);
const expectedWorld = [];
const expectedIndices = [];
for (const [sourceMeshIndex, sourceMesh] of [interleavedMesh, normalizedMesh].entries()) {
const attr = sourceMesh.geometry.getAttribute("position");
for (let i = 0; i < attr.count; i += 1) {
const point = new Vector3(attr.getX(i), attr.getY(i), attr.getZ(i))
.applyMatrix4(sourceMesh.matrixWorld);
expectedWorld.push(point.x, point.y, point.z);
if (sourceMeshIndex === 1) expectedIndices.push(3 + i);
}
if (sourceMeshIndex === 0) expectedIndices.push(0, 1, 2);
}
assert.equal(positions.length, expectedWorld.length);
for (let i = 0; i < expectedWorld.length; i += 1) {
assert.ok(Math.abs(positions[i] - expectedWorld[i]) < 1e-5, `world position component ${i}`);
}
assert.deepEqual(Array.from(indices), expectedIndices);
assert.deepEqual(Array.from(interleavedData.array), originalInterleaved);
assert.deepEqual(Array.from(normalizedGeometry.getAttribute("position").array), originalNormalized);
assert.equal(result.meshes[0].geometry.getAttribute("position").array.length, 9);
assert.equal(result.meshes[1].geometry.getAttribute("position").array.length, 9);
});
test("Recast adapter rejects malformed indices before Uint32 conversion can truncate them", () => {
const geometry = new BufferGeometry();
geometry.setAttribute("position", new BufferAttribute(new Float32Array([
0, 0, 0,
1, 0, 0,
0, 0, 1,
]), 3));
geometry.setIndex(new BufferAttribute(new Float32Array([0, 1.5, 2]), 1));
const mesh = new Mesh(geometry);
mesh.name = "malformed-index-fixture";
assert.throws(
() => unpackMeshesForRecast([mesh]),
/malformed-index-fixture: index 1 references invalid position 1\.5/,
);
});