import assert from "node:assert/strict"; import test from "node:test"; import { countSurfaceBucketReferences, selectDetourAnchorComponents, } from "./detour-component-selection.mjs"; function topologyFixture() { const polygons = [ [0, 0, 0, 0, 0, 0, 0, 2, 4], [1, 0, 0, 0, 0, 1, 2, 1, 3], [2, 1, 1, 0, 0, 0, 3, 3, 4], [3, 2, 2, 0, 0, 0, 6, 1, 3], ]; const link = (from, to, edge) => [from, to, edge, 0, 0, 255, 0, 1, from, 0, 0, to, 0, 0]; return { schemaVersion: 1, polygonCount: 4, tileCount: 3, detailTriangleCount: 7, geometryTriangleCount: 7, polygonFanTriangleCount: 5, directedLinkCount: 2, unrepresentedLinkCount: 0, offMeshPolygonCount: 0, weakComponents: { count: 3, largestPolygonCount: 2, polygonCounts: [2, 1, 1] }, polygons, links: [link(0, 2, 0), link(2, 0, 1)], }; } test("source anchors retain complete weak Detour components and remap only exact source links", () => { const selected = selectDetourAnchorComponents(topologyFixture(), [ { id: "entry", kind: "entry", position: [0, 0, 0], polygonId: 0, distance: 0.2, maximumDistance: 0.5, required: true }, { id: "boss", kind: "boss", position: [100, 0, 0], polygonId: 3, distance: 0.1, maximumDistance: 0.25, required: true }, { id: "far-combat", kind: "combat", position: [50, 0, 0], polygonId: 1, distance: 7, maximumDistance: 4 }, ]); assert.deepEqual([...selected.selectedSourcePolygonIds], [0, 2, 3]); assert.deepEqual([...selected.sourceToSelectedPolygonId], [[0, 0], [2, 1], [3, 2]]); assert.equal(selected.topology.polygonCount, 3); assert.equal(selected.topology.directedLinkCount, 2); assert.equal(selected.topology.detailTriangleCount, 6); assert.deepEqual(selected.topology.polygons.map((row) => row.slice(0, 8)), [ [0, 0, 0, 0, 0, 0, 0, 2], [1, 1, 1, 0, 0, 0, 2, 3], [2, 2, 2, 0, 0, 0, 5, 1], ]); assert.deepEqual(selected.topology.links.map((row) => row.slice(0, 2)), [[0, 1], [1, 0]]); assert.equal(selected.summary.retainedAnchorIds.includes("entry"), true); assert.equal(selected.summary.retainedAnchorIds.includes("boss"), true); assert.equal(selected.summary.unmatchedAnchors[0].id, "far-combat"); assert.equal(selected.summary.excluded.polygonCount, 1); assert.equal(selected.summary.excluded.disposition, "unanchored-components-excluded; not individually proven decorative"); }); test("selection fails closed when an anchored component exceeds any runtime budget", () => { const source = topologyFixture(); assert.throws(() => selectDetourAnchorComponents(source, [ { id: "entry", polygonId: 0, distance: 0, maximumDistance: 1 }, ], { limits: { polygons: 1 } }), /polygons budget/); assert.throws(() => selectDetourAnchorComponents(source, [ { id: "entry", polygonId: 0, distance: 0, maximumDistance: 1 }, ], { limits: { directedLinks: 1 } }), /directedLinks budget/); assert.throws(() => selectDetourAnchorComponents(source, [ { id: "entry", polygonId: 0, distance: 0, maximumDistance: 1 }, ], { limits: { detailTriangles: 1 } }), /detailTriangles budget/); assert.throws(() => selectDetourAnchorComponents(source, [ { id: "entry", polygonId: 0, distance: 0, maximumDistance: 1 }, ], { limits: { surfaceBucketReferences: 3 }, surfaceBucketReferenceCount: 4 }), /surfaceBucketReferences budget/); }); test("source anchors outside their explicit projection limit do not select components", () => { assert.throws(() => selectDetourAnchorComponents(topologyFixture(), [ { id: "unprojected", polygonId: 2, distance: 4.001, maximumDistance: 4 }, ]), /No source anchors map/); }); test("surface bucket references match runtime's XZ cell coverage rule", () => { const positions = new Float32Array([ 0, 0, 0, 8, 0, 0, 0, 0, 8, -0.1, 0, -0.1, ]); const indices = new Uint32Array([0, 1, 2, 0, 2, 3]); assert.equal(countSurfaceBucketReferences(positions, indices, 8), 10); });