import { readFile } from "node:fs/promises"; import path from "node:path"; import { NodeIO } from "@gltf-transform/core"; const projectRoot = path.resolve(import.meta.dirname, "../.."); const catalog = JSON.parse(await readFile(path.join(projectRoot, "shared/arena-stages.json"), "utf8")); const io = new NodeIO(); const requested = new Set(process.argv.slice(2).filter((value) => !value.startsWith("--"))); const detail = process.argv.includes("--all"); 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 round(values) { return values.map((value) => Math.round(value * 100) / 100); } for (const stage of catalog.stages) { if (requested.size && !requested.has(stage.id)) continue; const base = path.join(projectRoot, "public", ...stage.assetBaseUrl.split("/").filter(Boolean)); let document; try { document = await io.read(path.join(base, "navigation.glb")); } catch { continue; } const vertices = []; const triangles = []; const byPosition = new Map(); for (const mesh of document.getRoot().listMeshes()) { for (const primitive of mesh.listPrimitives()) { const positions = primitive.getAttribute("POSITION")?.getArray(); if (!positions) continue; const indices = primitive.getIndices()?.getArray() ?? Uint32Array.from({ length: positions.length / 3 }, (_, index) => index); for (let offset = 0; offset < indices.length; offset += 3) { const triangle = []; for (let corner = 0; corner < 3; corner += 1) { const index = indices[offset + corner]; const vertex = [positions[index * 3], positions[index * 3 + 1], positions[index * 3 + 2]]; const key = vertex.map((value) => Math.round(value * 1000)).join(":"); if (!byPosition.has(key)) { byPosition.set(key, vertices.length); vertices.push(vertex); } triangle.push(byPosition.get(key)); } triangles.push(triangle); } } } const union = new UnionFind(vertices.length); for (const triangle of triangles) { union.union(triangle[0], triangle[1]); union.union(triangle[0], triangle[2]); } const grouped = new Map(); for (let index = 0; index < vertices.length; index += 1) { const root = union.find(index); const component = grouped.get(root) ?? []; component.push(vertices[index]); grouped.set(root, component); } const components = [...grouped.values()]; components.sort((left, right) => right.length - left.length); const main = components[0] ?? []; const min = [0, 1, 2].map((axis) => Math.min(...main.map((vertex) => vertex[axis]))); const max = [0, 1, 2].map((axis) => Math.max(...main.map((vertex) => vertex[axis]))); const center = min.map((value, axis) => (value + max[axis]) / 2); const axis = max[0] - min[0] >= max[2] - min[2] ? 0 : 2; const sorted = [...main].sort((left, right) => left[axis] - right[axis]); const inset = Math.max(1, Math.floor(sorted.length * 0.08)); const teamSpawns = [sorted[inset] ?? sorted[0], sorted[sorted.length - inset - 1] ?? sorted.at(-1)]; console.log(JSON.stringify({ id: stage.id, componentCount: components.length, mainVertices: main.length, nextVertices: components[1]?.length ?? 0, mainBounds: { min: round(min), max: round(max) }, center: round(center), suggestedTeamSpawns: teamSpawns.map(round), topComponents: components.slice(0, detail ? components.length : 8).map((component) => ({ vertices: component.length, min: round([0, 1, 2].map((componentAxis) => Math.min(...component.map((vertex) => vertex[componentAxis])))), max: round([0, 1, 2].map((componentAxis) => Math.max(...component.map((vertex) => vertex[componentAxis])))), })), })); }