142 lines
3.8 KiB
JavaScript
142 lines
3.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
buildDetourGeometryRanges,
|
|
extractDetourTopology,
|
|
stableSha256,
|
|
} from "./detour-topology.mjs";
|
|
|
|
const NULL_LINK = 0xffffffff;
|
|
|
|
function fakePoly({
|
|
type = 0,
|
|
firstLink = NULL_LINK,
|
|
vertexCount = 4,
|
|
vertexIndices = [0, 1, 2, 3],
|
|
} = {}) {
|
|
return {
|
|
getType: () => type,
|
|
firstLink: () => firstLink,
|
|
vertCount: () => vertexCount,
|
|
verts: (index) => vertexIndices[index],
|
|
};
|
|
}
|
|
|
|
function fakeTile({ x, y, layer = 0, polys, details, links, vertices = [] }) {
|
|
return {
|
|
header: () => ({
|
|
polyCount: () => polys.length,
|
|
maxLinkCount: () => links.length,
|
|
x: () => x,
|
|
y: () => y,
|
|
layer: () => layer,
|
|
}),
|
|
polys: (index) => polys[index],
|
|
detailMeshes: (index) => details[index],
|
|
links: (index) => links[index],
|
|
verts: (index) => vertices[index],
|
|
};
|
|
}
|
|
|
|
test("extracts stable surface polygon IDs and only actual directed Detour links", () => {
|
|
const tiles = [
|
|
fakeTile({
|
|
x: 4,
|
|
y: 8,
|
|
polys: [
|
|
fakePoly({ firstLink: 0 }),
|
|
fakePoly({ type: 1 }), // Detour off-mesh connection polygons have no surface triangles.
|
|
],
|
|
details: [{ triCount: () => 2 }, { triCount: () => 0 }],
|
|
vertices: [0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1],
|
|
links: [{
|
|
ref: () => 2000,
|
|
next: () => NULL_LINK,
|
|
edge: () => 2,
|
|
side: () => 255,
|
|
bmin: () => 0,
|
|
bmax: () => 0,
|
|
}],
|
|
}),
|
|
fakeTile({
|
|
x: 5,
|
|
y: 8,
|
|
polys: [fakePoly({ firstLink: 0 }), fakePoly()],
|
|
details: [{ triCount: () => 1 }, { triCount: () => 4 }],
|
|
links: [{
|
|
ref: () => 1001, // Points to the omitted off-mesh polygon, not a floor polygon.
|
|
next: () => NULL_LINK,
|
|
edge: () => 3,
|
|
side: () => 1,
|
|
bmin: () => 32,
|
|
bmax: () => 192,
|
|
}],
|
|
}),
|
|
];
|
|
const topology = extractDetourTopology({
|
|
getMaxTiles: () => tiles.length,
|
|
getTile: (index) => tiles[index],
|
|
getPolyRefBase: (tile) => tile === tiles[0] ? 1000 : 2000,
|
|
});
|
|
|
|
assert.deepEqual(topology.polygons, [
|
|
[0, 0, 4, 8, 0, 0, 0, 2, 4],
|
|
[1, 1, 5, 8, 0, 0, 2, 1, 4],
|
|
[2, 1, 5, 8, 0, 1, 3, 4, 4],
|
|
]);
|
|
assert.deepEqual(topology.links, [[
|
|
0, 1, 2, 255, 0, 0,
|
|
2, 3,
|
|
1, 0, 1,
|
|
0, 0, 1,
|
|
]]);
|
|
assert.equal(topology.detailTriangleCount, 7);
|
|
assert.equal(topology.polygonFanTriangleCount, 6);
|
|
assert.equal(topology.offMeshPolygonCount, 1);
|
|
assert.equal(topology.unrepresentedLinkCount, 1);
|
|
assert.deepEqual(topology.weakComponents, {
|
|
count: 2,
|
|
largestPolygonCount: 2,
|
|
polygonCounts: [2, 1],
|
|
});
|
|
});
|
|
|
|
test("rejects invalid Detour link chains instead of emitting unverifiable topology", () => {
|
|
const tile = fakeTile({
|
|
x: 0,
|
|
y: 0,
|
|
polys: [fakePoly({ firstLink: 2 })],
|
|
details: [{ triCount: () => 1 }],
|
|
links: [{
|
|
ref: () => 1000,
|
|
next: () => NULL_LINK,
|
|
edge: () => 0,
|
|
side: () => 255,
|
|
bmin: () => 0,
|
|
bmax: () => 0,
|
|
}],
|
|
});
|
|
|
|
assert.throws(() => extractDetourTopology({
|
|
getMaxTiles: () => 1,
|
|
getTile: () => tile,
|
|
getPolyRefBase: () => 1000,
|
|
}), /invalid link index 2/);
|
|
});
|
|
|
|
test("fingerprints ignore object key order but include source values", () => {
|
|
assert.equal(stableSha256({ b: 2, a: 1 }), stableSha256({ a: 1, b: 2 }));
|
|
assert.notEqual(stableSha256({ a: 1 }), stableSha256({ a: 2 }));
|
|
});
|
|
|
|
test("maps filtered output triangles to contiguous per-polygon ranges", () => {
|
|
assert.deepEqual(buildDetourGeometryRanges(4, [0, 0, 2, 2, 3, 3]), [
|
|
[0, 2],
|
|
[-1, 0],
|
|
[2, 2],
|
|
[4, 2],
|
|
]);
|
|
assert.throws(() => buildDetourGeometryRanges(2, [1, 0]), /stable polygon order/);
|
|
assert.throws(() => buildDetourGeometryRanges(2, [2]), /invalid Detour polygon/);
|
|
});
|