Como percorrer um grafo de cena 3D em TypeScript

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/3d instalado

Guia passo a passo

Passo 1: Instalar e Importar

Instale o pacote:

npm install @aspose/3d

Importe 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.entity antes de acessar propriedades específicas da entidade. Muitos nós são nós de grupo puros que não carregam nenhuma entidade.
  • Use instanceof em vez de constructor.name para verificações de tipo em caminhos de lógica. instanceof é seguro para refatoração; a comparação de strings em constructor.name quebra com a minificação.
  • Percorra com for...of em childNodes: 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

SintomaCausaSolução
childNodes tem comprimento zero em rootNodeModelo não carregadoVerifique se scene.open() foi concluído sem erros antes de percorrer
node.entity instanceof Mesh nunca é verdadeiroCaminho de importação de Mesh incorretoImporte Mesh de @aspose/3d/entities, não da raiz @aspose/3d
A travessia perde malhas aninhadasNão está recursando em todos os filhosVerifique se a chamada recursiva cobre cada elemento em node.childNodes
mesh.controlPoints.length é 0Malha carregada mas não contém geometriaVerifique 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.

Veja Também

 Português