Як обходити 3D граф сцени у TypeScript
Граф сцени у Aspose.3D FOSS для TypeScript — це дерево, що складається з Node об’єктів, коренем яких є scene.rootNode. Обхід є рекурсивним: кожен вузол надає childNodes iterable та необов’язковий entity властивість. Цей посібник показує, як пройти все дерево, визначити типи сутностей і зібрати статистику сітки.
Передумови
- 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 для групових вузлів, кісток і локаторів. Це constructor.name перевірка працює для будь‑якого типу сутності: Mesh, Camera, Light, тощо.
Крок 4: Отримання типу сутності на кожному вузлі
Щоб виконати дію на основі типу сутності, використайте instanceof перевірте після захисту від 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 є найнадійнішим способом підтвердити, що сутність є polygon mesh перед доступом controlPoints, polygonCount, або елементи vertex.
Крок 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: iterable безпечно обробляє всі розміри масивів. Уникайте числового індексування для забезпечення сумісності в майбутньому. - Уникайте мутування дерева під час обходу: не додавайте та не видаляйте вузли всередині рекурсивного виклику. Спочатку зберіть результати, потім модифікуйте.
- Передайте масив результатів як параметр: це запобігає створенню нового масиву при кожному рекурсивному виклику і спрощує об’єднання результатів піддерев.
Типові проблеми
| Симптом | Причина | Виправлення |
|---|---|---|
childNodes має нульову довжину на rootNode | Модель не завантажена | Переконайтеся scene.open() завершено без помилок перед обходом |
node.entity instanceof Mesh ніколи не істинно | Неправильно Mesh import path | Імпорт Mesh from @aspose/3d/entities, не from @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 (множина) надає повний список.
Чи можу я обходити в порядку breadth‑first замість depth‑first? Так. Використовуйте чергу замість рекурсивного виклику: push scene.rootNode у масив, потім зсуваємо та обробляємо вузли, додаючи кожен вузол childNodes у кінець черги.
Чи scene.open() синхронний? Так. scene.open() і scene.openFromBuffer() обидва блокують викликаючий потік, доки файл повністю не буде проаналізовано. Обгорніть їх у worker thread, якщо потрібно, щоб event loop залишався чутливим.
Як отримати позиції у світовому просторі з вузла? Читати node.globalTransform; він повертає лише для читання GlobalTransform з world-space matrix, складеною з усіх трансформацій предків. Для явних matrix math викличте node.evaluateGlobalTransform(false).
Які типи сутностей можливі, крім Mesh? Camera, Light, і користувацькі скелет/кісткові сутності. Перевірте node.entity.constructor.name або використати instanceof з конкретним класом, імпортованим з @aspose/3d.