#!/usr/bin/env node import { NodeIO } from "@gltf-transform/core"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { MeshoptDecoder } from "meshoptimizer"; const file = process.argv[2]; if (!file) throw new Error("Usage: node inspect-navigation-components.mjs "); await MeshoptDecoder.ready; const document = await new NodeIO() .registerExtensions(ALL_EXTENSIONS) .registerDependencies({ "meshopt.decoder": MeshoptDecoder }) .read(file); const meshNode = document.getRoot().listNodes().find((node) => node.getMesh()); const primitive = meshNode?.getMesh()?.listPrimitives()[0]; const positionAccessor = primitive?.getAttribute("POSITION"); const sourcePositions = positionAccessor?.getArray(); const sourceIndices = primitive?.getIndices()?.getArray(); if (!sourcePositions || !meshNode || !positionAccessor) throw new Error(`${file}: no POSITION accessor.`); const matrix = meshNode.getWorldMatrix(); const normalizer = positionAccessor.getNormalized() ? sourcePositions instanceof Int16Array ? (value) => Math.max(value / 32_767, -1) : sourcePositions instanceof Uint16Array ? (value) => value / 65_535 : sourcePositions instanceof Int8Array ? (value) => Math.max(value / 127, -1) : sourcePositions instanceof Uint8Array ? (value) => value / 255 : (value) => value : (value) => value; const positions = new Float64Array(sourcePositions.length); for (let offset = 0; offset < sourcePositions.length; offset += 3) { const x = normalizer(sourcePositions[offset]); const y = normalizer(sourcePositions[offset + 1]); const z = normalizer(sourcePositions[offset + 2]); positions[offset] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]; positions[offset + 1] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]; positions[offset + 2] = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]; } const indices = sourceIndices ? Array.from(sourceIndices) : Array.from({ length: positions.length / 3 }, (_, index) => index); const triangleCount = Math.floor(indices.length / 3); const parent = Array.from({ length: triangleCount }, (_, index) => index); const vertexOwner = new Map(); const vertexIds = []; const welded = new Map(); for (let offset = 0; offset < positions.length; offset += 3) { const key = [ Math.round(positions[offset] * 1_000), Math.round(positions[offset + 1] * 1_000), Math.round(positions[offset + 2] * 1_000), ].join(":"); if (!welded.has(key)) welded.set(key, welded.size); vertexIds.push(welded.get(key)); } function root(start) { let value = start; while (parent[value] !== value) { parent[value] = parent[parent[value]]; value = parent[value]; } return value; } function union(left, right) { const leftRoot = root(left); const rightRoot = root(right); if (leftRoot !== rightRoot) parent[rightRoot] = leftRoot; } for (let triangle = 0; triangle < triangleCount; triangle += 1) { for (let corner = 0; corner < 3; corner += 1) { const vertex = vertexIds[indices[triangle * 3 + corner]]; if (vertexOwner.has(vertex)) union(triangle, vertexOwner.get(vertex)); else vertexOwner.set(vertex, triangle); } } const components = new Map(); for (let triangle = 0; triangle < triangleCount; triangle += 1) { const component = root(triangle); const entry = components.get(component) ?? { triangles: 0, minimum: [Infinity, Infinity, Infinity], maximum: [-Infinity, -Infinity, -Infinity], }; entry.triangles += 1; for (let corner = 0; corner < 3; corner += 1) { const pointIndex = indices[triangle * 3 + corner] * 3; for (let axis = 0; axis < 3; axis += 1) { entry.minimum[axis] = Math.min(entry.minimum[axis], positions[pointIndex + axis]); entry.maximum[axis] = Math.max(entry.maximum[axis], positions[pointIndex + axis]); } } components.set(component, entry); } const result = [...components.values()] .sort((left, right) => right.triangles - left.triangles) .map((component, index) => ({ rank: index + 1, ...component, span: component.maximum.map((value, axis) => value - component.minimum[axis]), })); console.log(JSON.stringify({ file, triangles: triangleCount, components: result.length, top: result.slice(0, 20), }, null, 2));