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, }; }