UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

100 lines (88 loc) 4.88 kB
// @ts-check /** * Scans GLB/glTF files and returns structured binding data. * * Combines file discovery, JSON reading, component extraction, and manifest * type resolution into the `BindingEntry[]` array consumed by codegen. */ import { resolveEntrypointGlbs, collectSceneFiles, glbFriendlyName } from './glb.discovery.js'; import { readGlbJsonChunk, readGltfJsonFile, readRemoteGlbJsonChunk } from './glb.reader.js'; import { extractComponentBindings, inferTsType } from './glb.extractor.js'; import { componentsManifest } from './manifest.types.js'; import { needleLog } from '../vite/logging.js'; const PLUGIN = "needle:dts-generator"; /** * @typedef {Object} BindingEntry * @property {string} nodeName * @property {string} nodePath Full hierarchy path e.g. "Scene/Cube/Child_Of_Cube" * @property {string} componentName * @property {Record<string, string>} fieldTypes field name → TS type string * @property {boolean} isEngineComponent true if the component exists in components.needle.json * @property {string} nodeThreeType Three.js type of the parent node (e.g. `import("three").Mesh`) * @property {string} glbKey Friendly identifier derived from the GLB name (e.g. "myScene", "MaterialXNodes") * @property {string} glbSrc Project-relative path or URL of the GLB file * @property {string[]} [glbSourceFiles] Source files (relative to project root) that reference this GLB */ /** * Scan GLB/glTF files and return structured binding data. * Uses entrypoint GLBs (from index.html, gen.js, or source files) when available, * otherwise falls back to scanning all GLBs in assetsDir. * * @param {string} assetsDir Absolute path to the assets directory * @param {string} [projectRoot] Absolute path to the project root (enables entrypoint detection) * @param {string} [codegenDir] Absolute path to the codegen directory * @returns {Promise<BindingEntry[]>} */ export async function scanBindings(assetsDir, projectRoot, codegenDir) { /** @type {Array<{path: string, type: "glb"|"gltf", remote?: boolean, key?: string}>} */ const files = /** @type {any} */ ((projectRoot ? resolveEntrypointGlbs(projectRoot, assetsDir, codegenDir) : null) ?? collectSceneFiles(assetsDir)); needleLog(PLUGIN, `Discovered ${files.length} GLB(s):\n${files.map(f => ` ${f.path}`).join("\n")}`); /** @type {BindingEntry[]} */ const entries = []; for (const file of files) { let json = /** @type {Record<string, unknown> | null} */ (null); let contentDispositionFilename = /** @type {string | null} */ (null); if (file.remote) { needleLog(PLUGIN, `Fetching remote GLB: ${file.path}`); const result = await readRemoteGlbJsonChunk(file.path); if (!result) { needleLog(PLUGIN, `Skipped (fetch failed): ${file.path}`, "warn"); continue; } json = result.json; contentDispositionFilename = result.filename; needleLog(PLUGIN, `Remote GLB ok — Content-Disposition: ${contentDispositionFilename ?? "(none)"}`); } else { json = file.type === "glb" ? readGlbJsonChunk(file.path) : readGltfJsonFile(file.path); } if (!json) continue; // Derive a friendly identifier for this GLB (used as SceneData key). // For local files: basename without extension. // For remote: Content-Disposition filename > last non-generic URL segment. const localPathForName = file.remote ? file.path : ( projectRoot ? file.path.replace(projectRoot + "/", "").replace(projectRoot + "\\", "") : file.path ); const glbKey = glbFriendlyName(localPathForName, contentDispositionFilename); for (const { nodeName, nodePath, componentName, fields, nodeThreeType } of extractComponentBindings(json)) { /** @type {Record<string, string>} */ const fieldTypes = {}; if (componentName) { const manifestFields = componentsManifest.get(componentName); if (manifestFields) { for (const [k, tsType] of manifestFields) { fieldTypes[k] = tsType; } } else { for (const [k, v] of Object.entries(fields)) { fieldTypes[k] = inferTsType(v); } } } const isEngineComponent = componentName ? componentsManifest.has(componentName) : false; const glbSrc = file.remote ? file.path : localPathForName; const glbSourceFiles = /** @type {string[] | undefined} */ (/** @type {any} */ (file).sourceFiles); entries.push({ nodeName, nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType, glbKey, glbSrc, glbSourceFiles }); } } return entries; }