چگونه یک گراف صحنه 3D را در TypeScript پیمایش کنیم

چگونه یک گراف صحنه 3D را در TypeScript پیمایش کنیم

گراف صحنه در Aspose.3D FOSS برای TypeScript یک درخت از اشیاء Node است که ریشه آن scene.rootNode می‌باشد. پیمایش به صورت بازگشتی است: هر گره یک قابل تکرار childNodes و یک ویژگی اختیاری entity را ارائه می‌دهد. این راهنما نشان می‌دهد چگونه کل درخت را پیمایش کنیم، انواع موجودیت‌ها را شناسایی کنیم و آمار مش‌ها را جمع‌آوری کنیم.

پیش‌نیازها

  • Node.js 18 یا بالاتر
  • TypeScript 5.0 یا بالاتر
  • @aspose/3d نصب شده

راهنمای گام به گام

مرحله ۱: نصب و وارد کردن

بسته را نصب کنید:

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 در مثال بارگذاری استفاده می‌شود؛ برای سایر فرمت‌ها کلاس گزینه‌های متناظر را جایگزین کنید.


مرحله ۲: بارگذاری یک صحنه از یک فایل

یک 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، و غیره.


مرحله ۴: دسترسی به نوع موجودیت در هر گره

برای انجام عمل بر اساس نوع موجودیت، پس از محافظ null از بررسی instanceof استفاده کنید:

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 ایمن‌ترین راه برای تأیید این است که موجودیت یک مش چندضلعی باشد قبل از دسترسی به 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 اختیاری را می‌پذیرد تا فراخوانندگان بتوانند آن را برای ادغام نتایج در چندین زیردرخت پیش‌پر کنند.


مرحله ۶: جمع‌آوری تمام مش‌ها و چاپ تعداد رئوس

جمع‌کننده را گسترش دهید تا آمار هر مش را چاپ کند:

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>

نکات و بهترین شیوه‌ها

  • همیشه بررسی نال node.entity را قبل از دسترسی به ویژگی‌های خاص موجودیت انجام دهید. بسیاری از گره‌ها گره‌های گروهی خالص هستند که هیچ موجودیتی ندارند.
  • از instanceof به‌جای constructor.name برای بررسی نوع در مسیرهای منطقی استفاده کنید. instanceof ایمن برای بازسازی است؛ مقایسه رشته‌ای روی constructor.name با فشرده‌سازی خراب می‌شود.
  • از طریق for...of به‌جای childNodes پیمایش کنید: قابل تکرار همه اندازه‌های آرایه را به‌صورت ایمن مدیریت می‌کند. برای سازگاری آینده از ایندکس‌گذاری عددی پرهیز کنید.
  • از تغییر درخت در حین پیمایش خودداری کنید: داخل فراخوانی بازگشتی گره‌ها را اضافه یا حذف نکنید. ابتدا نتایج را جمع‌آوری کنید، سپس تغییر دهید.
  • یک آرایه نتایج را به‌عنوان پارامتر پاس دهید: این کار از تخصیص آرایه جدید در هر فراخوانی بازگشتی جلوگیری می‌کند و ادغام نتایج زیردرخت‌ها را آسان می‌سازد.

مشکلات رایج

علائمدلیلراه‌حل
childNodes طول صفر دارد در rootNodeمدل بارگذاری نشدهاطمینان حاصل کنید scene.open() بدون خطا تکمیل شده باشد قبل از پیمایش
node.entity instanceof Mesh هرگز درست نیستمسیر وارد کردن Mesh اشتباه استMesh را از @aspose/3d/entities وارد کنید، نه از ریشه @aspose/3d
پیمایش مش‌های تو در تو را از دست می‌دهدعدم بازگشت به تمام فرزنداناطمینان حاصل کنید فراخوانی بازگشتی تمام عناصر در node.childNodes را پوشش می‌دهد
mesh.controlPoints.length برابر ۰ استمش بارگذاری شد اما شامل هندسه‌ای نیستمنبع OBJ را برای گروه‌های خالی بررسی کنید؛ از mesh.polygonCount به عنوان بررسی ثانویه استفاده کنید
سرریز پشته در سلسله‌مراتب‌های عمیقدرخت صحنه بسیار عمیق (صدها سطح)بازگشت را با یک پشته صریح با استفاده از Array.push / Array.pop جایگزین کنید

سوالات متداول

آیا scene.rootNode خود یک موجودیت دارد؟
خیر. گره ریشه یک محفظه است که به‌صورت خودکار توسط کتابخانه ایجاد می‌شود. این گره موجودیتی ندارد. هندسه و سایر اشیای صحنه شما در گره‌های فرزند، یک یا چند سطح زیر rootNode، قرار می‌گیرند.

node.entity و node.entities چه تفاوتی دارند؟ node.entity یک موجودیت اصلی تک را نگه می‌دارد (مورد معمول). برخی فایل‌های قدیمی FBX و COLLADA ممکن است گره‌هایی با چند موجودیت پیوست‌شده تولید کنند؛ در این صورت node.entities (جمع) فهرست کامل را فراهم می‌کند.

آیا می‌توانم به‌جای عمق‑اول، به‌صورت پهنای‑اول پیمایش کنم؟
بله. به‌جای فراخوانی بازگشتی از یک صف استفاده کنید: scene.rootNode را به یک آرایه اضافه کنید، سپس shift کنید و گره‌ها را پردازش کنید در حالی که childNodes هر گره را به انتهای صف می‌افزایید.

آیا scene.open() همزمان است؟ بله. scene.open() و scene.openFromBuffer() هر دو نخ فراخوانی را تا زمانی که فایل به‌طور کامل تجزیه شود مسدود می‌کنند. اگر نیاز دارید حلقهٔ رویداد پاسخگو بماند، آن‌ها را در یک نخ کارگر بپیچید.

چگونه موقعیت‌های فضای جهانی را از یک گره دریافت کنم؟
node.globalTransform را بخوانید؛ این یک GlobalTransform فقط‑خواندنی با ماتریس فضای جهانی برمی‌گرداند که از ترکیب تمام تبدیلات نیاکان ساخته شده است. برای محاسبهٔ ماتریس به‌صورت صریح، node.evaluateGlobalTransform(false) را فراخوانی کنید.

چه انواع موجودیتی به جز Mesh ممکن است؟
Camera، Light، و موجودیت‌های سفارشی اسکلت/استخوان. به node.entity.constructor.name مراجعه کنید یا از instanceof با کلاس خاص وارد شده از @aspose/3d استفاده کنید.

همچنین ببینید

 فارسی