Как обходить 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 равно 0 | Mesh загружена, но не содержит геометрию | Проверьте источник 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.