Come attraversare un grafo di scena 3D in TypeScript
Il grafo della scena in Aspose.3D FOSS per TypeScript è un albero di Node oggetti radicato in scene.rootNode. L’attraversamento è ricorsivo: ogni nodo espone un iterabile childNodes e una proprietà opzionale entity. Questa guida mostra come percorrere l’intero albero, identificare i tipi di entità e raccogliere le statistiche della mesh.
Prerequisiti
- Node.js 18 o versioni successive
- TypeScript 5.0 o versioni successive
@aspose/3dinstallato
Guida passo-passo
Passo 1: Installa e importa
Installa il pacchetto:
npm install @aspose/3dImporta le classi utilizzate in questa guida:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene e Mesh sono le classi principali. ObjLoadOptions è usato nell’esempio di caricamento; sostituire la classe di opzioni corrispondente per altri formati.
Passo 2: Carica una scena da un file
Crea un Scene e chiama scene.open() con un percorso file. Il rilevamento del formato è automatico dai numeri magici binari, quindi non è necessario specificare il formato per i file 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}`);Puoi anche caricare da un Buffer in memoria usando scene.openFromBuffer(buffer, options); utile nelle pipeline serverless dove l’I/O del disco non è disponibile.
Passo 3: Scrivi una funzione di attraversamento ricorsivo
La ricorsione su childNodes è il modello standard. La funzione visita ogni nodo in profondità prima:
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);Per una scena con una mesh denominata Cube, l’output avrà l’aspetto seguente:
[-] 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 è null per i nodi di gruppo, le ossa e i localizzatori. Il controllo constructor.name funziona per qualsiasi tipo di entità: Mesh, Camera, Light, ecc.
Passo 4: Accedi al Tipo di Entità su Ogni Nodo
Per eseguire un’azione in base al tipo di entità, utilizza un controllo instanceof dopo la guardia null:
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 è il modo più sicuro per confermare che l’entità sia una mesh poligonale prima di accedere a controlPoints, polygonCount o agli elementi dei vertici.
Passo 5: Filtra i nodi per tipo di entità
Per raccogliere solo i nodi contenenti mesh senza stampare l’albero completo, usa un accumulatore ricorsivo:
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 funzione accetta un array opzionale results così i chiamanti possono pre‑popolarlo per unire i risultati su più sottoalberi.
Passo 6: Raccogli tutte le mesh e stampa i conteggi dei vertici
Estendi il collector per stampare le statistiche per mesh:
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`);
}Esempio di output per una scena a due mesh:
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>
Consigli e migliori pratiche
- Controlla sempre il valore null di
node.entityprima di accedere alle proprietà specifiche dell’entità. Molti nodi sono nodi di gruppo puri che non contengono alcuna entità. - Usa
instanceofinvece diconstructor.nameper i controlli di tipo nei percorsi logici.instanceofè sicuro durante il refactoring; il confronto di stringhe suconstructor.namesi rompe con la minificazione. - Attraversa tramite
for...ofinvece dichildNodes: l’iterabile gestisce in modo sicuro tutte le dimensioni dell’array. Evita l’indicizzazione numerica per garantire la compatibilità futura. - Evita di modificare l’albero durante l’attraversamento: non aggiungere o rimuovere nodi all’interno della chiamata ricorsiva. Raccogli prima i risultati, poi modifica.
- Passa un array di risultati come parametro: questo evita di allocare un nuovo array ad ogni chiamata ricorsiva e facilita l’unione dei risultati dei sottoalberi.
Problemi comuni
| Sintomo | Causa | Risoluzione |
|---|---|---|
childNodes ha lunghezza zero su rootNode | Modello non caricato | Assicurati che scene.open() sia completato senza errori prima di attraversare |
node.entity instanceof Mesh non è mai vero | Percorso di importazione Mesh errato | Importa Mesh da @aspose/3d/entities, non dalla radice @aspose/3d |
| Il traversal salta le mesh nidificate | Non ricorsivo su tutti i figli | Assicurati che la chiamata ricorsiva copra ogni elemento in node.childNodes |
mesh.controlPoints.length è 0 | Mesh caricata ma non contiene geometria | Controlla la sorgente OBJ per gruppi vuoti; usa mesh.polygonCount come verifica secondaria |
| Stack overflow su gerarchie profonde | Albero della scena molto profondo (centinaia di livelli) | Sostituisci la ricorsione con uno stack esplicito usando Array.push / Array.pop |
Domande Frequenti
scene.rootNode porta da sé un’entità?
No. Il nodo radice è un contenitore creato automaticamente dalla libreria. Non ha alcuna entità. La tua geometria e gli altri oggetti della scena vivono su nodi figli uno o più livelli sotto rootNode.
Qual è la differenza tra node.entity e node.entities?node.entity contiene l’entità primaria singola (il caso più comune). Alcuni file FBX e COLLADA più vecchi possono generare nodi con più entità collegate; in tal caso node.entities (plurale) fornisce l’elenco completo.
Posso attraversare in ordine a larghezza anziché in ordine di profondità?
Sì. Usa una coda invece di una chiamata ricorsiva: push scene.rootNode in un array, poi shift e processa i nodi mentre push childNodes di ogni nodo nella coda.
È scene.open() sincrono?
Sì. scene.open() e scene.openFromBuffer() bloccano entrambi il thread chiamante fino a quando il file non è completamente analizzato. Avvolgili in un thread di lavoro se devi mantenere il ciclo di eventi reattivo.
Come ottengo le posizioni nello spazio mondiale da un nodo?
Leggi node.globalTransform; restituisce un GlobalTransform di sola lettura con la matrice nello spazio mondiale, composta da tutte le trasformazioni dei genitori. Per calcoli espliciti di matrici, chiama node.evaluateGlobalTransform(false).
Quali tipi di entità sono possibili oltre Mesh?Camera, Light e entità personalizzate di scheletro/osso. Controlla node.entity.constructor.name o usa instanceof con la classe specifica importata da @aspose/3d.