@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
JavaScript
// @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;
}