Как обходить 3D граф сцены в TypeScript

Как обходить 3D граф сцены в TypeScript

Граф сцены в Aspose.3D FOSS для TypeScript представляет собой дерево из Node объектов, корневой точкой которых является scene.rootNode. Обход рекурсивный: каждый узел предоставляет childNodes итерируемый объект и необязательный entity property. Это руководство показывает, как пройти всё дерево, определить типы сущностей и собрать статистику сетки.

Требования

  • Node.js 18 или новее
  • TypeScript 5.0 или новее
  • @aspose/3d установлен

Пошаговое руководство

Шаг 1: Установка и импорт

Установите пакет:

npm install @aspose/3d

Импортируйте классы, используемые в этом руководстве:

import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';

Scene и Mesh являются основными классами. ObjLoadOptions используется в примере загрузки; замените соответствующий класс параметров для других форматов.


Шаг 2: Загрузка сцены из файла

Создайте Scene и вызовите scene.open() с путем к файлу. Определение формата происходит автоматически по бинарным магическим числам, поэтому вам не нужно указывать формат для файлов GLB, STL или 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}`);

Вы также можете загрузить из Buffer в памяти с помощью scene.openFromBuffer(buffer, options); полезно в безсерверных конвейерах, где дисковый ввод-вывод недоступен.


Шаг 3: Написание рекурсивной функции обхода

Рекурсия по childNodes это стандартный шаблон. Функция проходит каждый узел в глубину:

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);

Для сцены с одной сеткой, названной Cube, вывод будет выглядеть так:

[-] 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=“Скопировать код”

<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 для групповых узлов, костей и локаторов. The constructor.name проверка работает для любого типа сущности: Mesh, Camera, Light, и т.д.


Шаг 4: Получить тип сущности на каждом узле

Чтобы выполнить действие в зависимости от типа сущности, используйте an instanceof проверьте после null guard:

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 является самым безопасным способом подтвердить, что объект является polygon mesh перед доступом controlPoints, polygonCount, или элементы вершин.


Шаг 5: Фильтрация узлов по типу сущности

Чтобы собрать только узлы, содержащие сетку, без вывода полного дерева, используйте рекурсивный аккумулятор:

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)`);

Функция принимает необязательный results массив, чтобы вызывающие могли предварительно заполнить его для объединения результатов из нескольких поддеревьев.


Шаг 6: Сбор всех сеток и вывод количества вершин

Расширьте сборщик, чтобы выводить статистику по каждой сетке:

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`);
}

Пример вывода для сцены с двумя сетками:

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=“Скопировать код”

<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>

Советы и лучшие практики

  • Всегда проверяйте на null node.entity перед доступом к свойствам, специфичным для сущности. Многие узлы являются чисто групповыми и не содержат сущности.
  • Используйте instanceof завершено constructor.name для проверки типов в логических путях. instanceof является безопасным при рефакторинге; сравнение строк на constructor.name ломается при минификации.
  • Обход через for...of по childNodes: итерируемый объект безопасно обрабатывает массивы любого размера. Избегайте числовой индексации для обеспечения совместимости в будущем.
  • Избегайте изменения дерева во время обхода: не добавляйте и не удаляйте узлы внутри рекурсивного вызова. Сначала соберите результаты, затем изменяйте.
  • Передайте массив результатов в качестве параметра: это избегает выделения нового массива при каждом рекурсивном вызове и упрощает объединение результатов поддеревьев.

Распространённые проблемы

СимптомПричинаИсправление
childNodes имеет нулевую длину на rootNodeМодель не загруженаУбедитесь scene.open() завершено без ошибок перед обходом
node.entity instanceof Mesh никогда не истинноНеправильно Mesh import pathИмпорт Mesh из @aspose/3d/entities, не из @aspose/3d корень
Обход пропускает вложенные мешиНе рекурсивно обходим все дочерние элементыУбедитесь, что рекурсивный вызов охватывает каждый элемент в node.childNodes
mesh.controlPoints.length равно 0Mesh загружена, но не содержит геометриюПроверьте источник OBJ на пустые группы; используйте mesh.polygonCount в качестве вторичной проверки
Переполнение стека при глубокой иерархииОчень глубокое дерево сцены (сотни уровней)Замените рекурсию на явный стек, используя Array.push / Array.pop

Часто задаваемые вопросы

Делает scene.rootNode само несёт сущность? Нет. Корневой узел — это контейнер, создаваемый библиотекой автоматически. У него нет сущности. Ваша геометрия и другие объекты сцены находятся в дочерних узлах на один или несколько уровней ниже. rootNode.

В чём разница между node.entity и node.entities? node.entity содержит единственную основную сущность (обычный случай). Некоторые более старые файлы FBX и COLLADA могут создавать узлы с несколькими присоединёнными сущностями; в этом случае node.entities (множественное число) предоставляет полный список.

Могу ли я выполнять обход в ширину вместо обхода в глубину? Да. Используйте очередь вместо рекурсивного вызова: push scene.rootNode в массив, затем сдвигать и обрабатывать узлы, одновременно помещая каждый узел childNodes в конец очереди.

Является scene.open() синхронным? Да. scene.open() и scene.openFromBuffer() оба блокируют вызывающий поток, пока файл полностью не будет разобран. Оберните их в рабочий поток, если нужно, чтобы цикл событий оставался отзывчивым.

Как получить позиции в мировом пространстве из узла? Читать node.globalTransform; он возвращает только для чтения GlobalTransform с матрицей мирового пространства, составленной из всех трансформаций предков. Для явных матричных вычислений вызовите node.evaluateGlobalTransform(false).

Какие типы сущностей возможны помимо Mesh? Camera, Light, и пользовательские скелетные/костные сущности. Проверьте node.entity.constructor.name или используйте instanceof с конкретным классом, импортированным из @aspose/3d.

См. также

 Русский