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.

175 lines (162 loc) 6.66 kB
// @ts-check /** * Type tables and components.needle.json manifest loader. * * Knows about primitive TS types, known Three.js types, known Needle Engine * types, and how to resolve field types from the manifest. */ import { existsSync, readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** Primitive TS type strings that can safely appear in an ambient declaration. */ export const PRIMITIVE_TYPES = new Set(["number", "string", "boolean"]); /** * Known Three.js types → import("three").TypeName * @type {Record<string, string>} */ export const THREE_TYPES = { Color: `import("three").Color`, ColorRepresentation: `import("three").ColorRepresentation`, Euler: `import("three").Euler`, Texture: `import("three").Texture`, // Materials Material: `import("three").Material`, MeshStandardMaterial: `import("three").MeshStandardMaterial`, // Objects Object3D: `import("three").Object3D`, Mesh: `import("three").Mesh`, SkinnedMesh: `import("three").SkinnedMesh`, // Other Vector2: `import("three").Vector2`, Vector3: `import("three").Vector3`, Vector4: `import("three").Vector4`, Matrix3: `import("three").Matrix3`, Matrix4: `import("three").Matrix4`, Quaternion: `import("three").Quaternion`, // Animation AnimationClip: `import("three").AnimationClip`, AnimationMixer: `import("three").AnimationMixer`, }; /** * Known Needle Engine types → import("@needle-tools/engine").TypeName * @type {Record<string, string>} */ export const NEEDLE_TYPES = { AssetReference: `import("@needle-tools/engine").AssetReference`, EventList: `import("@needle-tools/engine").EventList`, GameObject: `import("@needle-tools/engine").GameObject`, LookAtConstraint: `import("@needle-tools/engine").LookAtConstraint`, RGBAColor: `import("@needle-tools/engine").RGBAColor`, RenderTexture: `import("@needle-tools/engine").RenderTexture`, Renderer: `import("@needle-tools/engine").Renderer`, Rigidbody: `import("@needle-tools/engine").Rigidbody`, Sprite: `import("@needle-tools/engine").Sprite`, Vec2: `import("@needle-tools/engine").Vec2`, }; /** * Map a single non-array, non-primitive type token to its TS representation. * Returns null if unknown. * @param {string} token * @returns {string | null} */ function mapKnownType(token) { if (token in THREE_TYPES) return THREE_TYPES[token]; if (token in NEEDLE_TYPES) return NEEDLE_TYPES[token]; return null; } /** * Convert a manifest type string to a safe ambient TS type. * Primitives and known Three.js/Needle types are resolved precisely. * For unknown types on a manifest component, falls back to * `import("@needle-tools/engine").ComponentName["fieldName"]`. * Truly unresolvable types → "unknown". * * @param {string} typeStr * @param {string} [componentName] The manifest component class name (enables indexed-access fallback) * @param {string} [fieldName] The field name on that component * @returns {string} */ export function manifestTypeToTs(typeStr, componentName, fieldName) { const parts = typeStr.split(" | ").map(p => p.trim()); const safeParts = parts.map(p => { if (p === "undefined" || p === "null") return p; const arrayMatch = p.match(/^(number|string|boolean)\[\]$/); if (arrayMatch) return p; if (PRIMITIVE_TYPES.has(p)) return p; const arrayTypeMatch = p.match(/^(\w+)\[\]$/); if (arrayTypeMatch) { const base = arrayTypeMatch[1]; const mapped = mapKnownType(base); if (mapped) return `${mapped}[]`; } const known = mapKnownType(p); if (known) return known; return null; }); if (safeParts.every(p => p !== null)) { return /** @type {string[]} */ (safeParts).join(" | "); } if (componentName && fieldName) { return `import("@needle-tools/engine").${componentName}["${fieldName}"]`; } return "unknown"; } /** * Load components.needle.json and build a lookup: * componentName → Map<fieldName, tsType> * Inherited fields are flattened (inheritedFrom chain is resolved). * * @returns {Map<string, Map<string, string>>} */ export function loadComponentsManifest() { /** @type {Map<string, Map<string, string>>} */ const manifest = new Map(); const manifestPath = join(__dirname, "../../components.needle.json"); if (!existsSync(manifestPath)) return manifest; try { /** @type {Array<{name: string, inheritedFrom?: string, children?: Array<{name: string, kind: string, type: string}>}>} */ const entries = JSON.parse(readFileSync(manifestPath, "utf8")); /** @type {Map<string, Map<string, string>>} */ const ownFields = new Map(); /** @type {Map<string, string>} */ const inheritedFrom = new Map(); for (const entry of entries) { if (!entry.name) continue; inheritedFrom.set(entry.name, entry.inheritedFrom || ""); /** @type {Map<string, string>} */ const fields = new Map(); if (Array.isArray(entry.children)) { for (const child of entry.children) { if (child.kind === "property" && child.name && child.type) { fields.set(child.name, manifestTypeToTs(child.type, entry.name, child.name)); } } } ownFields.set(entry.name, fields); } /** @param {string} name @returns {Map<string, string>} */ function resolveFields(name) { if (manifest.has(name)) return /** @type {Map<string, string>} */ (manifest.get(name)); const own = ownFields.get(name) ?? new Map(); const parent = inheritedFrom.get(name); if (parent && ownFields.has(parent)) { const parentFields = resolveFields(parent); const merged = new Map([...parentFields, ...own]); manifest.set(name, merged); return merged; } manifest.set(name, own); return own; } for (const name of ownFields.keys()) { resolveFields(name); } } catch (e) { console.warn("[needle:dts-generator] Failed to load components.needle.json:", (/** @type {any} */ (e))?.message ?? e); } return manifest; } /** @type {Map<string, Map<string, string>>} */ export const componentsManifest = loadComponentsManifest();