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/, ); });