Як обходити 3D граф сцени у TypeScript

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

Див. також

 Українська