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