Files
healer-man/scripts/dungeon-pipeline/detour-topology.mjs
T
2026-10-06 22:25:34 -04:00

220 lines
7.4 KiB
JavaScript

import { createHash } from "node:crypto";
const NULL_LINK_VALUES = new Set([-1, 0xffffffff]);
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
);
}
export function stableSha256(value) {
const bytes = typeof value === "string" ? value : JSON.stringify(canonicalize(value));
return createHash("sha256").update(bytes).digest("hex");
}
export function buildDetourGeometryRanges(polygonCount, trianglePolygonIds) {
const starts = new Int32Array(polygonCount).fill(-1);
const counts = new Uint32Array(polygonCount);
let previousPolygonId = -1;
for (let triangleIndex = 0; triangleIndex < trianglePolygonIds.length; triangleIndex += 1) {
const polygonId = trianglePolygonIds[triangleIndex];
if (!Number.isInteger(polygonId) || polygonId < 0 || polygonId >= polygonCount) {
throw new Error(`Exported triangle ${triangleIndex} references invalid Detour polygon ${polygonId}.`);
}
if (polygonId < previousPolygonId) {
throw new Error("Exported Detour triangles are not in stable polygon order.");
}
previousPolygonId = polygonId;
if (counts[polygonId] === 0) starts[polygonId] = triangleIndex;
counts[polygonId] += 1;
}
return Array.from({ length: polygonCount }, (_, polygonId) => [
starts[polygonId],
counts[polygonId],
]);
}
function findComponents(nodeCount, links) {
const parents = new Int32Array(nodeCount);
const sizes = new Int32Array(nodeCount);
for (let id = 0; id < nodeCount; id += 1) {
parents[id] = id;
sizes[id] = 1;
}
const rootOf = (node) => {
let root = node;
while (parents[root] !== root) root = parents[root];
while (parents[node] !== node) {
const next = parents[node];
parents[node] = root;
node = next;
}
return root;
};
for (const [from, to] of links) {
const left = rootOf(from);
const right = rootOf(to);
if (left === right) continue;
if (sizes[left] < sizes[right]) {
parents[left] = right;
sizes[right] += sizes[left];
} else {
parents[right] = left;
sizes[left] += sizes[right];
}
}
const components = new Map();
for (let id = 0; id < nodeCount; id += 1) {
const root = rootOf(id);
components.set(root, (components.get(root) ?? 0) + 1);
}
const sizesByDescending = [...components.values()].sort((left, right) => right - left);
return {
count: sizesByDescending.length,
largestPolygonCount: sizesByDescending[0] ?? 0,
polygonCounts: sizesByDescending,
};
}
/**
* Copies the source Detour surface-polygon order and directed link chain into
* compact, stable-ID records. Triangle ranges follow NavMeshHelper's emitted
* tile/polygon/detail-triangle iteration order; links are only actual Detour
* links and never inferred from near-coincident geometry.
*/
export function extractDetourTopology(navMesh, {
sourceCollisionSha256,
settingsSha256,
} = {}) {
if (!navMesh || typeof navMesh.getMaxTiles !== "function") {
throw new Error("Detour topology requires a populated NavMesh.");
}
const polygons = [];
const polygonIdByReference = new Map();
let triangleCount = 0;
let polygonFanTriangleCount = 0;
let tileCount = 0;
let offMeshPolygonCount = 0;
for (let tileIndex = 0; tileIndex < navMesh.getMaxTiles(); tileIndex += 1) {
const tile = navMesh.getTile(tileIndex);
const header = tile.header();
if (!header || header.polyCount() === 0) continue;
tileCount += 1;
const polyRefBase = navMesh.getPolyRefBase(tile);
for (let polygonIndex = 0; polygonIndex < header.polyCount(); polygonIndex += 1) {
const polygon = tile.polys(polygonIndex);
if (polygon.getType() !== 0) {
offMeshPolygonCount += 1;
continue;
}
const detail = tile.detailMeshes(polygonIndex);
const triangleCountForPolygon = detail.triCount();
const polygonVertexCount = polygon.vertCount();
const id = polygons.length;
polygons.push([
id,
tileIndex,
header.x(),
header.y(),
header.layer(),
polygonIndex,
triangleCount,
triangleCountForPolygon,
polygonVertexCount,
]);
triangleCount += triangleCountForPolygon;
polygonFanTriangleCount += Math.max(0, polygonVertexCount - 2);
polygonIdByReference.set(polyRefBase + polygonIndex, id);
}
}
const links = [];
let unrepresentedLinkCount = 0;
for (const polygonRecord of polygons) {
const [fromId, tileIndex, , , , polygonIndex] = polygonRecord;
const tile = navMesh.getTile(tileIndex);
const polygon = tile.polys(polygonIndex);
const header = tile.header();
const maxLinkCount = header.maxLinkCount();
let linkIndex = polygon.firstLink();
let traversed = 0;
while (!NULL_LINK_VALUES.has(linkIndex)) {
if (!Number.isInteger(linkIndex) || linkIndex < 0 || linkIndex >= maxLinkCount) {
throw new Error(
`Detour polygon ${tileIndex}:${polygonIndex} has invalid link index ${linkIndex}.`,
);
}
if (traversed >= maxLinkCount) {
throw new Error(`Detour polygon ${tileIndex}:${polygonIndex} contains a link cycle.`);
}
traversed += 1;
const link = tile.links(linkIndex);
const toId = polygonIdByReference.get(link.ref());
if (toId === undefined) {
unrepresentedLinkCount += 1;
} else {
const edgeIndex = link.edge();
const polygonVertexCount = polygon.vertCount();
if (edgeIndex < 0 || edgeIndex >= polygonVertexCount) {
throw new Error(
`Detour polygon ${tileIndex}:${polygonIndex} has invalid linked edge ${edgeIndex}.`,
);
}
const edgeStartVertex = polygon.verts(edgeIndex);
const edgeEndVertex = polygon.verts((edgeIndex + 1) % polygonVertexCount);
const edgeStartOffset = edgeStartVertex * 3;
const edgeEndOffset = edgeEndVertex * 3;
links.push([
fromId,
toId,
edgeIndex,
link.side(),
link.bmin(),
link.bmax(),
edgeStartVertex,
edgeEndVertex,
tile.verts(edgeStartOffset),
tile.verts(edgeStartOffset + 1),
tile.verts(edgeStartOffset + 2),
tile.verts(edgeEndOffset),
tile.verts(edgeEndOffset + 1),
tile.verts(edgeEndOffset + 2),
]);
}
linkIndex = link.next();
}
}
const weakComponents = findComponents(polygons.length, links.map(([from, to]) => [from, to]));
return {
schemaVersion: 1,
kind: "recast-detour-surface-links",
coordinateSpace: "GLB navigation POSITION coordinates (meters, Three.js Y-up)",
encoding: {
polygons: "[id,tileIndex,tileX,tileY,tileLayer,tilePolygonIndex,firstGeometryTriangle,geometryTriangleCount,vertexCount]",
links: "[fromPolygonId,toPolygonId,fromEdge,side,bmin,bmax,fromEdgeStartVertex,fromEdgeEndVertex,startX,startY,startZ,endX,endY,endZ] (directed Detour link records)",
},
sourceCollisionSha256: sourceCollisionSha256 ?? null,
settingsSha256: settingsSha256 ?? null,
tileCount,
polygonCount: polygons.length,
detailTriangleCount: triangleCount,
polygonFanTriangleCount,
geometryTriangleCount: null,
directedLinkCount: links.length,
unrepresentedLinkCount,
offMeshPolygonCount,
weakComponents,
polygons,
links,
};
}