Cómo recorrer un grafo de escena 3D en TypeScript
El grafo de escena en Aspose.3D FOSS for TypeScript es un árbol de Node objetos con raíz en scene.rootNode. El recorrido es recursivo: cada nodo expone un iterable childNodes y una propiedad opcional entity. Esta guía muestra cómo recorrer todo el árbol, identificar los tipos de entidad y recopilar estadísticas de mallas.
Requisitos
- Node.js 18 o posterior
- TypeScript 5.0 o posterior
@aspose/3dinstalado
Guía paso a paso
Paso 1: Instalar e Importar
Instala el paquete:
npm install @aspose/3dImporte las clases utilizadas en esta guía:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene y Mesh son las clases principales. ObjLoadOptions se usa en el ejemplo de carga; sustituya la clase de opciones correspondiente para otros formatos.
Paso 2: Cargar una escena desde un archivo
Cree un Scene y llame a scene.open() con una ruta de archivo. La detección de formato es automática a partir de los números mágicos binarios, por lo que no necesita especificar el formato para archivos GLB, STL o 3MF:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
const scene = new Scene();
scene.open('model.obj', new ObjLoadOptions());
console.log(`Root node: "${scene.rootNode.name}"`);
console.log(`Top-level children: ${scene.rootNode.childNodes.length}`);También puedes cargar desde un Buffer en memoria usando scene.openFromBuffer(buffer, options); útil en canalizaciones sin servidor donde el disk I/O no está disponible.
Paso 3: Escribe una función de recorrido recursivo
La recursión sobre childNodes es el patrón estándar. La función visita cada nodo en profundidad:
function traverse(node: any, depth = 0): void {
const indent = ' '.repeat(depth);
const entityType = node.entity ? node.entity.constructor.name : '-';
console.log(`${indent}[${entityType}] ${node.name}`);
for (const child of node.childNodes) {
traverse(child, depth + 1);
}
}
traverse(scene.rootNode);Para una escena con una malla llamada Cube, la salida se verá así:
[-] RootNode
[Mesh] Cube<button class=“hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50” title=“Copy code”
<div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div>
<div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div>
node.entity es null para nodos de grupo, huesos y localizadores. La comprobación constructor.name funciona para cualquier tipo de entidad: Mesh, Camera, Light, etc.
Paso 4: Acceder al Tipo de Entidad en Cada Nodo
Para tomar acción según el tipo de entidad, use una verificación instanceof después de la protección contra nulos:
import { Mesh } from '@aspose/3d/entities';
function visitWithTypeCheck(node: any, depth = 0): void {
const indent = ' '.repeat(depth);
if (node.entity instanceof Mesh) {
const mesh = node.entity as Mesh;
console.log(`${indent}MESH "${node.name}": ${mesh.controlPoints.length} vertices`);
} else if (node.entity) {
console.log(`${indent}${node.entity.constructor.name} "${node.name}"`);
} else {
console.log(`${indent}GROUP "${node.name}"`);
}
for (const child of node.childNodes) {
visitWithTypeCheck(child, depth + 1);
}
}
visitWithTypeCheck(scene.rootNode);instanceof Mesh es la forma más segura de confirmar que la entidad es una malla de polígonos antes de acceder a controlPoints, polygonCount o a los elementos de vértice.
Paso 5: Filtrar nodos por tipo de entidad
Para recopilar solo los nodos que contienen malla sin imprimir el árbol completo, use un acumulador recursivo:
import { Mesh } from '@aspose/3d/entities';
function collectMeshes(
node: any,
results: Array<{ name: string; mesh: Mesh }> = []
): Array<{ name: string; mesh: Mesh }> {
if (node.entity instanceof Mesh) {
results.push({ name: node.name, mesh: node.entity as Mesh });
}
for (const child of node.childNodes) {
collectMeshes(child, results);
}
return results;
}
const meshNodes = collectMeshes(scene.rootNode);
console.log(`Found ${meshNodes.length} mesh node(s)`);La función acepta una matriz opcional results para que los llamadores puedan pre‑poblarla al fusionar resultados a través de varios subárboles.
Paso 6: Recopilar todas las mallas y imprimir el recuento de vértices
Extienda el colector para imprimir estadísticas por malla:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';
function collectMeshes(node: any, results: Array<{name: string, mesh: Mesh}> = []) {
if (node.entity instanceof Mesh) {
results.push({ name: node.name, mesh: node.entity as Mesh });
}
for (const child of node.childNodes) {
collectMeshes(child, results);
}
return results;
}
const scene = new Scene();
scene.open('model.obj', new ObjLoadOptions());
const meshes = collectMeshes(scene.rootNode);
for (const { name, mesh } of meshes) {
console.log(`${name}: ${mesh.controlPoints.length} vertices, ${mesh.polygonCount} polygons`);
}Ejemplo de salida para una escena de dos mallas:
Cube: 8 vertices, 6 polygons
Sphere: 482 vertices, 480 polygons<button class=“hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50” title=“Copy code”
<div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div>
<div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div>
Consejos y Mejores Prácticas
- Siempre verifique nulos
node.entityantes de acceder a propiedades específicas de la entidad. Muchos nodos son nodos de grupo puros que no llevan entidad. - Use
instanceofen lugar deconstructor.namepara verificaciones de tipo en rutas lógicas.instanceofes seguro para refactorizaciones; la comparación de cadenas enconstructor.namese rompe con la minificación. - Recorra mediante
for...ofen lugar dechildNodes: el iterable maneja de forma segura todos los tamaños de arreglos. Evite el indexado numérico para compatibilidad futura. - Evite mutar el árbol durante el recorrido: no añada ni elimine nodos dentro de la llamada recursiva. Primero recopile los resultados y luego modifique.
- Pase un arreglo de resultados como parámetro: esto evita asignar un nuevo arreglo en cada llamada recursiva y facilita la fusión de resultados de subárboles.
Problemas comunes
| Síntoma | Causa | Solución |
|---|---|---|
childNodes tiene longitud cero en rootNode | Modelo no cargado | Asegúrese de que scene.open() se haya completado sin errores antes de recorrer |
node.entity instanceof Mesh nunca es verdadero | Ruta de importación de Mesh incorrecta | Importe Mesh desde @aspose/3d/entities, no desde la raíz @aspose/3d |
| El recorrido omite mallas anidadas | No se recursiona en todos los hijos | Asegúrese de que la llamada recursiva cubra cada elemento en node.childNodes |
mesh.controlPoints.length es 0 | Malla cargada pero no contiene geometría | Verifique la fuente OBJ en busca de grupos vacíos; use mesh.polygonCount como verificación secundaria |
| Desbordamiento de pila en jerarquías profundas | Árbol de escena muy profundo (cientos de niveles) | Reemplace la recursión con una pila explícita usando Array.push / Array.pop |
Preguntas frecuentes
¿scene.rootNode lleva una entidad?
No. El nodo raíz es un contenedor creado automáticamente por la biblioteca. No tiene entidad. Tu geometría y otros objetos de escena viven en nodos hijos uno o más niveles por debajo de rootNode.
¿Cuál es la diferencia entre node.entity y node.entities?node.entity contiene la única entidad primaria (el caso común). Algunos archivos FBX y COLLADA más antiguos pueden generar nodos con múltiples entidades adjuntas; en ese caso node.entities (plural) proporciona la lista completa.
¿Puedo recorrer en orden de amplitud en lugar de profundidad?
Sí. Use una cola en lugar de una llamada recursiva: push scene.rootNode en un arreglo, luego shift y procese los nodos mientras push childNodes de cada nodo al final de la cola.
¿Es scene.open() sincrónico?
Sí. scene.open() y scene.openFromBuffer() bloquean el hilo de llamada hasta que el archivo se haya analizado completamente. Envuélvalos en un hilo de trabajo si necesita mantener el bucle de eventos receptivo.
¿Cómo obtener posiciones en espacio mundial de un nodo?
Lea node.globalTransform; devuelve un GlobalTransform de solo lectura con la matriz en espacio mundial, compuesta a partir de todas las transformaciones de los ancestros. Para matemáticas de matrices explícitas, llame a node.evaluateGlobalTransform(false).
¿Qué tipos de entidad son posibles además de Mesh?
Camera, Light y entidades personalizadas de esqueleto/hueso. Consulte node.entity.constructor.name o use instanceof con la clase específica importada de @aspose/3d.