Como percorrer um grafo de cena 3D em TypeScript
O grafo de cena no Aspose.3D FOSS para TypeScript é uma árvore de objetos Node com raiz em scene.rootNode. A travessia é recursiva: cada nó expõe um iterável childNodes e uma propriedade opcional entity. Este guia mostra como percorrer toda a árvore, identificar tipos de entidade e coletar estatísticas de malhas.
Pré-requisitos
- Node.js 18 ou posterior
- TypeScript 5.0 ou posterior
@aspose/3dinstalado
Guia passo a passo
Passo 1: Instalar e Importar
Instale o pacote:
npm install @aspose/3dImporte as classes usadas neste guia:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene e Mesh são as classes principais. ObjLoadOptions é usado no exemplo de carregamento; substitua pela classe de opções correspondente para outros formatos.
Passo 2: Carregar uma cena a partir de um arquivo
Crie um Scene e chame scene.open() com um caminho de arquivo. A detecção de formato é automática a partir dos números mágicos binários, portanto você não precisa especificar o formato para arquivos GLB, STL ou 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}`);Você também pode carregar de um Buffer na memória usando scene.openFromBuffer(buffer, options); útil em pipelines sem servidor onde o disco I/O não está disponível.
Passo 3: Escrever uma função de travessia recursiva
A recursão sobre childNodes é o padrão padrão. A função visita cada nó em profundidade primeiro:
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 uma cena com uma malha chamada Cube, a saída ficará assim:
[-] 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 para nós de grupo, ossos e localizadores. A verificação constructor.name funciona para qualquer tipo de entidade: Mesh, Camera, Light, etc.
Passo 4: Acessar o tipo de entidade em cada nó
Para executar uma ação com base no tipo de entidade, use uma verificação instanceof após a proteção contra nulo:
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 é a forma mais segura de confirmar que a entidade é uma malha de polígonos antes de acessar controlPoints, polygonCount ou elementos de vértice.
Passo 5: Filtrar nós por tipo de entidade
Para coletar apenas nós que contêm malhas sem imprimir a árvore completa, use um 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)`);A função aceita um array opcional results para que os chamadores possam pré-populá-lo ao mesclar resultados de várias subárvores.
Passo 6: Coletar todas as malhas e imprimir contagens de vértices
Estenda o coletor para imprimir estatísticas por malha:
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`);
}Exemplo de saída para uma cena com duas malhas:
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>
Dicas e Melhores Práticas
- Sempre verifique null em
node.entityantes de acessar propriedades específicas da entidade. Muitos nós são nós de grupo puros que não carregam nenhuma entidade. - Use
instanceofem vez deconstructor.namepara verificações de tipo em caminhos de lógica.instanceofé seguro para refatoração; a comparação de strings emconstructor.namequebra com a minificação. - Percorra com
for...ofemchildNodes: o iterável lida com todos os tamanhos de array com segurança. Evite indexação numérica para compatibilidade futura. - Evite mutar a árvore durante a travessia: não adicione ou remova nós dentro da chamada recursiva. Colete os resultados primeiro e depois modifique.
- Passe um array de resultados como parâmetro: isso evita alocar um novo array em cada chamada recursiva e facilita a mesclagem de resultados de subárvores.
Problemas Comuns
| Sintoma | Causa | Solução |
|---|---|---|
childNodes tem comprimento zero em rootNode | Modelo não carregado | Verifique se scene.open() foi concluído sem erros antes de percorrer |
node.entity instanceof Mesh nunca é verdadeiro | Caminho de importação de Mesh incorreto | Importe Mesh de @aspose/3d/entities, não da raiz @aspose/3d |
| A travessia perde malhas aninhadas | Não está recursando em todos os filhos | Verifique se a chamada recursiva cobre cada elemento em node.childNodes |
mesh.controlPoints.length é 0 | Malha carregada mas não contém geometria | Verifique o fonte OBJ por grupos vazios; use mesh.polygonCount como verificação secundária |
| Estouro de pilha em hierarquias profundas | Árvore de cena muito profunda (centenas de níveis) | Substitua a recursão por uma pilha explícita usando Array.push / Array.pop |
Perguntas Frequentes
scene.rootNode em si carrega uma entidade?
Não. O nó raiz é um contêiner criado automaticamente pela biblioteca. Ele não tem entidade. Sua geometria e outros objetos de cena vivem em nós filhos um ou mais níveis abaixo de rootNode.
Qual é a diferença entre node.entity e node.entities?
node.entity contém a única entidade primária (o caso comum). Alguns arquivos FBX e COLLADA mais antigos podem produzir nós com múltiplas entidades anexadas; nesse caso node.entities (plural) fornece a lista completa.
Posso percorrer em ordem de largura em vez de profundidade?
Sim. Use uma fila em vez de uma chamada recursiva: empurre scene.rootNode para um array, depois extraia e processe os nós enquanto empurra os childNodes de cada nó para o final da fila.
scene.open() é síncrono?
Sim. scene.open() e scene.openFromBuffer() ambos bloqueiam a thread chamante até que o arquivo seja totalmente analisado. Envolva-os em uma thread de trabalho se você precisar manter o loop de eventos responsivo.
Como obter posições no espaço mundial de um nó?
Leia node.globalTransform; ele retorna um GlobalTransform somente leitura com a matriz no espaço mundial, composta a partir de todas as transformações ancestrais. Para operações matemáticas de matriz explícitas, chame node.evaluateGlobalTransform(false).
Quais tipos de entidade são possíveis além de Mesh?
Camera, Light e entidades de esqueleto/osso personalizadas. Verifique node.entity.constructor.name ou use instanceof com a classe específica importada de @aspose/3d.