Files
healer-man/scripts/arena-pipeline/extract-navigation-component.mjs
2026-09-05 20:42:38 -04:00

102 lines
4.0 KiB
JavaScript

import { readFile } from "node:fs/promises";
import { MeshoptDecoder } from "meshoptimizer";
import { Vector3 } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { writeWmoGeometry } from "./build-wmo-arenas.mjs";
globalThis.self = globalThis;
globalThis.ProgressEvent ??= class ProgressEvent {
constructor(type, values = {}) { this.type = type; Object.assign(this, values); }
};
class UnionFind {
constructor(size) { this.parent = Array.from({ length: size }, (_, index) => index); }
find(value) {
if (this.parent[value] !== value) this.parent[value] = this.find(this.parent[value]);
return this.parent[value];
}
union(left, right) {
const a = this.find(left);
const b = this.find(right);
if (a !== b) this.parent[b] = a;
}
}
function key(point) {
return point.map((value) => Math.round(value * 1000)).join(":");
}
async function loadTriangles(file) {
await MeshoptDecoder.ready;
const bytes = await readFile(file);
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const scene = await new Promise((resolve, reject) => new GLTFLoader()
.setMeshoptDecoder(MeshoptDecoder)
.parse(buffer, "", (gltf) => resolve(gltf.scene), reject));
scene.updateMatrixWorld(true);
const triangles = [];
const points = [new Vector3(), new Vector3(), new Vector3()];
scene.traverse((object) => {
if (!object.isMesh || !object.geometry?.getAttribute("position")) return;
const positions = object.geometry.getAttribute("position");
const indices = object.geometry.getIndex();
const count = indices?.count ?? positions.count;
const vertexIndex = (element) => indices ? indices.getX(element) : element;
for (let element = 0; element < count; element += 3) {
triangles.push(points.map((point, corner) => [
...point.fromBufferAttribute(positions, vertexIndex(element + corner)).applyMatrix4(object.matrixWorld).toArray(),
]));
}
});
return triangles;
}
function distanceSquared(point, target) {
return (point[0] - target[0]) ** 2 + (point[1] - target[1]) ** 2 + (point[2] - target[2]) ** 2;
}
export async function extractNavigationComponent({ source, output, teamSpawns }) {
const triangles = await loadTriangles(source);
const union = new UnionFind(triangles.length);
const ownerByVertex = new Map();
for (const [triangleIndex, triangle] of triangles.entries()) {
for (const point of triangle) {
const positionKey = key(point);
const owner = ownerByVertex.get(positionKey);
if (owner == null) ownerByVertex.set(positionKey, triangleIndex);
else union.union(owner, triangleIndex);
}
}
const components = new Map();
for (let index = 0; index < triangles.length; index += 1) {
const root = union.find(index);
const component = components.get(root) ?? [];
component.push(triangles[index]);
components.set(root, component);
}
const scored = [...components.values()].map((component) => {
const vertices = component.flat();
const spawnDistances = teamSpawns.map((spawn) => Math.sqrt(Math.min(...vertices.map((point) => distanceSquared(point, spawn)))));
return { component, spawnDistances, score: Math.max(...spawnDistances) };
}).sort((left, right) => left.score - right.score || right.component.length - left.component.length);
const selected = scored[0];
if (!selected?.component.length) throw new Error(`${source}: no navigation component could be selected.`);
const values = selected.component.flat(2);
const positions = new Float32Array(values);
const indices = Uint32Array.from({ length: positions.length / 3 }, (_, index) => index);
await writeWmoGeometry(output, [{
name: "rated-connected-navigation",
positions,
normals: null,
indices,
collisionIndices: indices,
flagCounts: {},
}], false);
return {
sourceComponents: components.size,
triangles: selected.component.length,
vertices: positions.length / 3,
spawnDistances: selected.spawnDistances,
};
}