@needle-tools/engine 5.1.0-alpha.1 → 5.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/.needle/generated/needle-bindings.gen.d.ts +5 -0
  2. package/CHANGELOG.md +52 -0
  3. package/components.needle.json +1 -1
  4. package/dist/{gltf-progressive-DJBMx-zB.umd.cjs → gltf-progressive-BmblPzFj.umd.cjs} +4 -4
  5. package/dist/{gltf-progressive-BryRjllq.min.js → gltf-progressive-CN_mbb66.min.js} +2 -2
  6. package/dist/{gltf-progressive-Cl167Vjx.js → gltf-progressive-DUlhxdv4.js} +5 -2
  7. package/dist/needle-engine.bundle-C-ixARur.umd.cjs +1733 -0
  8. package/dist/needle-engine.bundle-CHmXdnE1.min.js +1733 -0
  9. package/dist/{needle-engine.bundle-BGyKqxBH.js → needle-engine.bundle-DF01sSGQ.js} +10841 -10434
  10. package/dist/needle-engine.d.ts +244 -54
  11. package/dist/needle-engine.js +541 -536
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/dist/{postprocessing-B_9sKVU7.min.js → postprocessing-B571qGWR.min.js} +34 -34
  15. package/dist/{postprocessing-WDc9WwI3.js → postprocessing-CfrLAbLX.js} +0 -1
  16. package/dist/{postprocessing-B2wb6pzI.umd.cjs → postprocessing-CiGkAeM9.umd.cjs} +17 -17
  17. package/dist/{vendor-CAcsI0eU.js → vendor-BFrMaK9q.js} +8983 -9136
  18. package/dist/vendor-CJmyOrCq.min.js +1116 -0
  19. package/dist/vendor-DkMW3WY4.umd.cjs +1116 -0
  20. package/lib/engine/api.d.ts +14 -0
  21. package/lib/engine/api.js +4 -0
  22. package/lib/engine/api.js.map +1 -1
  23. package/lib/engine/debug/debug_environment.js +1 -1
  24. package/lib/engine/debug/debug_environment.js.map +1 -1
  25. package/lib/engine/debug/debug_spatial_console.d.ts +2 -0
  26. package/lib/engine/debug/debug_spatial_console.js +10 -7
  27. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  28. package/lib/engine/engine_addressables.d.ts +2 -0
  29. package/lib/engine/engine_addressables.js +6 -3
  30. package/lib/engine/engine_addressables.js.map +1 -1
  31. package/lib/engine/engine_application.js +8 -6
  32. package/lib/engine/engine_application.js.map +1 -1
  33. package/lib/engine/engine_audio.d.ts +68 -0
  34. package/lib/engine/engine_audio.js +172 -0
  35. package/lib/engine/engine_audio.js.map +1 -1
  36. package/lib/engine/engine_components.js +1 -1
  37. package/lib/engine/engine_components.js.map +1 -1
  38. package/lib/engine/engine_constants.js +6 -0
  39. package/lib/engine/engine_constants.js.map +1 -1
  40. package/lib/engine/engine_context.d.ts +33 -7
  41. package/lib/engine/engine_context.js +40 -2
  42. package/lib/engine/engine_context.js.map +1 -1
  43. package/lib/engine/engine_context_registry.js +1 -1
  44. package/lib/engine/engine_context_registry.js.map +1 -1
  45. package/lib/engine/engine_gameobject.js +2 -2
  46. package/lib/engine/engine_gameobject.js.map +1 -1
  47. package/lib/engine/engine_init.js +16 -1
  48. package/lib/engine/engine_init.js.map +1 -1
  49. package/lib/engine/engine_input.d.ts +3 -2
  50. package/lib/engine/engine_input.js +3 -2
  51. package/lib/engine/engine_input.js.map +1 -1
  52. package/lib/engine/engine_license.d.ts +2 -0
  53. package/lib/engine/engine_license.js +25 -15
  54. package/lib/engine/engine_license.js.map +1 -1
  55. package/lib/engine/engine_lifecycle_functions_internal.js +5 -0
  56. package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
  57. package/lib/engine/engine_mainloop_utils.js +5 -2
  58. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  59. package/lib/engine/engine_networking_blob.d.ts +1 -1
  60. package/lib/engine/engine_networking_blob.js +5 -11
  61. package/lib/engine/engine_networking_blob.js.map +1 -1
  62. package/lib/engine/engine_physics_rapier.js +0 -1
  63. package/lib/engine/engine_physics_rapier.js.map +1 -1
  64. package/lib/engine/engine_pmrem.js +2 -2
  65. package/lib/engine/engine_pmrem.js.map +1 -1
  66. package/lib/engine/engine_scenedata.d.ts +32 -0
  67. package/lib/engine/engine_scenedata.js +138 -0
  68. package/lib/engine/engine_scenedata.js.map +1 -0
  69. package/lib/engine/engine_serialization_builtin_serializer.d.ts +10 -16
  70. package/lib/engine/engine_serialization_builtin_serializer.js +55 -41
  71. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  72. package/lib/engine/engine_ssr.d.ts +18 -0
  73. package/lib/engine/engine_ssr.js +40 -0
  74. package/lib/engine/engine_ssr.js.map +1 -0
  75. package/lib/engine/engine_three_utils.d.ts +14 -7
  76. package/lib/engine/engine_three_utils.js +14 -7
  77. package/lib/engine/engine_three_utils.js.map +1 -1
  78. package/lib/engine/engine_types.d.ts +2 -0
  79. package/lib/engine/engine_types.js.map +1 -1
  80. package/lib/engine/engine_utils.js +2 -0
  81. package/lib/engine/engine_utils.js.map +1 -1
  82. package/lib/engine/engine_utils_hash.d.ts +9 -0
  83. package/lib/engine/engine_utils_hash.js +112 -0
  84. package/lib/engine/engine_utils_hash.js.map +1 -0
  85. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  86. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  87. package/lib/engine/webcomponents/jsx.d.ts +51 -0
  88. package/lib/engine/webcomponents/logo-element.d.ts +2 -1
  89. package/lib/engine/webcomponents/logo-element.js +2 -1
  90. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  91. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +4 -4
  92. package/lib/engine/webcomponents/needle menu/needle-menu.js +2 -1
  93. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  94. package/lib/engine/webcomponents/needle-button.d.ts +2 -1
  95. package/lib/engine/webcomponents/needle-button.js +2 -1
  96. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  97. package/lib/engine/webcomponents/needle-engine.d.ts +11 -4
  98. package/lib/engine/webcomponents/needle-engine.js +2 -1
  99. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  100. package/lib/engine/xr/NeedleXRSession.d.ts +3 -2
  101. package/lib/engine/xr/NeedleXRSession.js +51 -15
  102. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  103. package/lib/engine/xr/events.d.ts +1 -1
  104. package/lib/engine/xr/events.js.map +1 -1
  105. package/lib/engine-components/Animation.js +17 -16
  106. package/lib/engine-components/Animation.js.map +1 -1
  107. package/lib/engine-components/AnimatorController.d.ts +2 -0
  108. package/lib/engine-components/AnimatorController.js +4 -1
  109. package/lib/engine-components/AnimatorController.js.map +1 -1
  110. package/lib/engine-components/AudioSource.d.ts +19 -3
  111. package/lib/engine-components/AudioSource.js +121 -68
  112. package/lib/engine-components/AudioSource.js.map +1 -1
  113. package/lib/engine-components/DragControls.d.ts +7 -0
  114. package/lib/engine-components/DragControls.js +19 -0
  115. package/lib/engine-components/DragControls.js.map +1 -1
  116. package/lib/engine-components/Light.d.ts +6 -8
  117. package/lib/engine-components/Light.js +40 -27
  118. package/lib/engine-components/Light.js.map +1 -1
  119. package/lib/engine-components/Networking.d.ts +1 -1
  120. package/lib/engine-components/Networking.js +1 -1
  121. package/lib/engine-components/OrbitControls.js +16 -11
  122. package/lib/engine-components/OrbitControls.js.map +1 -1
  123. package/lib/engine-components/ReflectionProbe.js +2 -0
  124. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  125. package/lib/engine-components/RigidBody.js +3 -3
  126. package/lib/engine-components/RigidBody.js.map +1 -1
  127. package/lib/engine-components/SceneSwitcher.js +2 -0
  128. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  129. package/lib/engine-components/SeeThrough.js +2 -2
  130. package/lib/engine-components/SeeThrough.js.map +1 -1
  131. package/lib/engine-components/api.d.ts +1 -1
  132. package/lib/engine-components/api.js +1 -1
  133. package/lib/engine-components/api.js.map +1 -1
  134. package/lib/engine-components/postprocessing/Effects/BloomEffect.d.ts +1 -1
  135. package/lib/engine-components/postprocessing/Effects/Sharpening.js +1 -2
  136. package/lib/engine-components/postprocessing/Effects/Sharpening.js.map +1 -1
  137. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  138. package/lib/engine-components/postprocessing/PostProcessingHandler.js +5 -6
  139. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  140. package/lib/engine-components/postprocessing/VolumeParameter.d.ts +2 -0
  141. package/lib/engine-components/postprocessing/VolumeParameter.js +4 -1
  142. package/lib/engine-components/postprocessing/VolumeParameter.js.map +1 -1
  143. package/lib/engine-components/ui/Canvas.d.ts +1 -1
  144. package/lib/engine-components/ui/Canvas.js +2 -8
  145. package/lib/engine-components/ui/Canvas.js.map +1 -1
  146. package/lib/engine-components/ui/Text.d.ts +1 -0
  147. package/lib/engine-components/ui/Text.js +10 -7
  148. package/lib/engine-components/ui/Text.js.map +1 -1
  149. package/lib/engine-components/web/CursorFollow.js +21 -12
  150. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  151. package/lib/engine-components/web/ScrollFollow.d.ts +0 -1
  152. package/lib/engine-components/web/ScrollFollow.js +3 -2
  153. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  154. package/lib/engine-components/webxr/WebXRImageTracking.js +4 -0
  155. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  156. package/lib/needle-engine.d.ts +2 -0
  157. package/lib/needle-engine.js +2 -0
  158. package/lib/needle-engine.js.map +1 -1
  159. package/package.json +6 -4
  160. package/plugins/dts-generator/dts.codegen.js +334 -0
  161. package/plugins/dts-generator/dts.scan.js +99 -0
  162. package/plugins/dts-generator/dts.writer.js +59 -0
  163. package/plugins/dts-generator/glb.discovery.js +279 -0
  164. package/plugins/dts-generator/glb.extractor.js +215 -0
  165. package/plugins/dts-generator/glb.reader.js +167 -0
  166. package/plugins/dts-generator/index.js +36 -0
  167. package/plugins/dts-generator/manifest.types.js +174 -0
  168. package/plugins/types/index.d.ts +2 -1
  169. package/plugins/types/needle-bindings.d.ts +30 -0
  170. package/plugins/types/userconfig.d.ts +21 -2
  171. package/plugins/vite/asap.js +18 -9
  172. package/plugins/vite/dependencies.js +29 -0
  173. package/plugins/vite/dependency-watcher.d.ts +2 -2
  174. package/plugins/vite/dependency-watcher.js +3 -4
  175. package/plugins/vite/drop.d.ts +2 -2
  176. package/plugins/vite/drop.js +3 -4
  177. package/plugins/vite/dts-generator.d.ts +7 -0
  178. package/plugins/vite/dts-generator.js +191 -0
  179. package/plugins/vite/index.d.ts +10 -3
  180. package/plugins/vite/index.js +27 -10
  181. package/plugins/vite/local-files-core.js +3 -3
  182. package/plugins/vite/local-files-utils.d.ts +3 -1
  183. package/plugins/vite/local-files-utils.js +29 -5
  184. package/plugins/vite/logging.js +2 -2
  185. package/plugins/vite/meta.js +4 -2
  186. package/plugins/vite/poster.d.ts +2 -2
  187. package/plugins/vite/poster.js +3 -5
  188. package/plugins/vite/reload.d.ts +2 -2
  189. package/plugins/vite/reload.js +23 -22
  190. package/src/engine/api.ts +18 -1
  191. package/src/engine/debug/debug_environment.ts +1 -1
  192. package/src/engine/debug/debug_spatial_console.ts +10 -7
  193. package/src/engine/engine_addressables.ts +6 -3
  194. package/src/engine/engine_application.ts +8 -6
  195. package/src/engine/engine_audio.ts +184 -0
  196. package/src/engine/engine_components.ts +1 -1
  197. package/src/engine/engine_constants.ts +11 -6
  198. package/src/engine/engine_context.ts +50 -7
  199. package/src/engine/engine_context_registry.ts +1 -1
  200. package/src/engine/engine_gameobject.ts +2 -2
  201. package/src/engine/engine_init.ts +15 -1
  202. package/src/engine/engine_input.ts +3 -2
  203. package/src/engine/engine_license.ts +23 -19
  204. package/src/engine/engine_lifecycle_functions_internal.ts +7 -0
  205. package/src/engine/engine_mainloop_utils.ts +5 -2
  206. package/src/engine/engine_networking_blob.ts +5 -11
  207. package/src/engine/engine_physics_rapier.ts +0 -3
  208. package/src/engine/engine_pmrem.ts +3 -3
  209. package/src/engine/engine_scenedata.ts +136 -0
  210. package/src/engine/engine_serialization_builtin_serializer.ts +63 -46
  211. package/src/engine/engine_ssr.ts +48 -0
  212. package/src/engine/engine_three_utils.ts +15 -7
  213. package/src/engine/engine_types.ts +2 -0
  214. package/src/engine/engine_utils.ts +1 -0
  215. package/src/engine/engine_utils_hash.ts +65 -0
  216. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  217. package/src/engine/webcomponents/jsx.d.ts +51 -0
  218. package/src/engine/webcomponents/logo-element.ts +3 -1
  219. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -2
  220. package/src/engine/webcomponents/needle-button.ts +3 -1
  221. package/src/engine/webcomponents/needle-engine.ts +12 -4
  222. package/src/engine/xr/NeedleXRSession.ts +49 -14
  223. package/src/engine/xr/events.ts +1 -1
  224. package/src/engine-components/Animation.ts +19 -16
  225. package/src/engine-components/AnimatorController.ts +4 -1
  226. package/src/engine-components/AudioSource.ts +130 -79
  227. package/src/engine-components/DragControls.ts +18 -2
  228. package/src/engine-components/Light.ts +40 -26
  229. package/src/engine-components/Networking.ts +1 -1
  230. package/src/engine-components/OrbitControls.ts +18 -9
  231. package/src/engine-components/ReflectionProbe.ts +2 -0
  232. package/src/engine-components/RigidBody.ts +3 -3
  233. package/src/engine-components/SceneSwitcher.ts +1 -0
  234. package/src/engine-components/SeeThrough.ts +2 -2
  235. package/src/engine-components/api.ts +1 -1
  236. package/src/engine-components/postprocessing/Effects/BloomEffect.ts +1 -1
  237. package/src/engine-components/postprocessing/Effects/Sharpening.ts +1 -2
  238. package/src/engine-components/postprocessing/PostProcessingHandler.ts +4 -8
  239. package/src/engine-components/postprocessing/VolumeParameter.ts +4 -1
  240. package/src/engine-components/ui/Canvas.ts +2 -8
  241. package/src/engine-components/ui/Text.ts +12 -8
  242. package/src/engine-components/web/CursorFollow.ts +21 -13
  243. package/src/engine-components/web/ScrollFollow.ts +2 -2
  244. package/src/engine-components/webxr/WebXRImageTracking.ts +2 -0
  245. package/src/needle-engine.ts +3 -0
  246. package/src/vite-env.d.ts +16 -0
  247. package/dist/needle-engine.bundle-CiYtOO2O.min.js +0 -1732
  248. package/dist/needle-engine.bundle-DzVx9Z8D.umd.cjs +0 -1732
  249. package/dist/vendor-CEM38hLE.umd.cjs +0 -1116
  250. package/dist/vendor-HRlxIBga.min.js +0 -1116
@@ -0,0 +1,99 @@
1
+ // @ts-check
2
+ /**
3
+ * Scans GLB/glTF files and returns structured binding data.
4
+ *
5
+ * Combines file discovery, JSON reading, component extraction, and manifest
6
+ * type resolution into the `BindingEntry[]` array consumed by codegen.
7
+ */
8
+
9
+ import { resolveEntrypointGlbs, collectSceneFiles, glbFriendlyName } from './glb.discovery.js';
10
+ import { readGlbJsonChunk, readGltfJsonFile, readRemoteGlbJsonChunk } from './glb.reader.js';
11
+ import { extractComponentBindings, inferTsType } from './glb.extractor.js';
12
+ import { componentsManifest } from './manifest.types.js';
13
+ import { needleLog } from '../vite/logging.js';
14
+
15
+ const PLUGIN = "needle:dts-generator";
16
+
17
+ /**
18
+ * @typedef {Object} BindingEntry
19
+ * @property {string} nodeName
20
+ * @property {string} nodePath Full hierarchy path e.g. "Scene/Cube/Child_Of_Cube"
21
+ * @property {string} componentName
22
+ * @property {Record<string, string>} fieldTypes field name → TS type string
23
+ * @property {boolean} isEngineComponent true if the component exists in components.needle.json
24
+ * @property {string} nodeThreeType Three.js type of the parent node (e.g. `import("three").Mesh`)
25
+ * @property {string} glbKey Friendly identifier derived from the GLB name (e.g. "myScene", "MaterialXNodes")
26
+ * @property {string} glbSrc Project-relative path or URL of the GLB file
27
+ * @property {string[]} [glbSourceFiles] Source files (relative to project root) that reference this GLB
28
+ */
29
+
30
+ /**
31
+ * Scan GLB/glTF files and return structured binding data.
32
+ * Uses entrypoint GLBs (from index.html, gen.js, or source files) when available,
33
+ * otherwise falls back to scanning all GLBs in assetsDir.
34
+ *
35
+ * @param {string} assetsDir Absolute path to the assets directory
36
+ * @param {string} [projectRoot] Absolute path to the project root (enables entrypoint detection)
37
+ * @param {string} [codegenDir] Absolute path to the codegen directory
38
+ * @returns {Promise<BindingEntry[]>}
39
+ */
40
+ export async function scanBindings(assetsDir, projectRoot, codegenDir) {
41
+ /** @type {Array<{path: string, type: "glb"|"gltf", remote?: boolean, key?: string}>} */
42
+ const files = /** @type {any} */ ((projectRoot ? resolveEntrypointGlbs(projectRoot, assetsDir, codegenDir) : null)
43
+ ?? collectSceneFiles(assetsDir));
44
+
45
+ needleLog(PLUGIN, `Discovered ${files.length} GLB(s):\n${files.map(f => ` ${f.path}`).join("\n")}`);
46
+
47
+ /** @type {BindingEntry[]} */
48
+ const entries = [];
49
+
50
+ for (const file of files) {
51
+ let json = /** @type {Record<string, unknown> | null} */ (null);
52
+ let contentDispositionFilename = /** @type {string | null} */ (null);
53
+
54
+ if (file.remote) {
55
+ needleLog(PLUGIN, `Fetching remote GLB: ${file.path}`);
56
+ const result = await readRemoteGlbJsonChunk(file.path);
57
+ if (!result) { needleLog(PLUGIN, `Skipped (fetch failed): ${file.path}`, "warn"); continue; }
58
+ json = result.json;
59
+ contentDispositionFilename = result.filename;
60
+ needleLog(PLUGIN, `Remote GLB ok — Content-Disposition: ${contentDispositionFilename ?? "(none)"}`);
61
+ } else {
62
+ json = file.type === "glb" ? readGlbJsonChunk(file.path) : readGltfJsonFile(file.path);
63
+ }
64
+ if (!json) continue;
65
+
66
+ // Derive a friendly identifier for this GLB (used as SceneData key).
67
+ // For local files: basename without extension.
68
+ // For remote: Content-Disposition filename > last non-generic URL segment.
69
+ const localPathForName = file.remote ? file.path : (
70
+ projectRoot
71
+ ? file.path.replace(projectRoot + "/", "").replace(projectRoot + "\\", "")
72
+ : file.path
73
+ );
74
+ const glbKey = glbFriendlyName(localPathForName, contentDispositionFilename);
75
+
76
+ for (const { nodeName, nodePath, componentName, fields, nodeThreeType } of extractComponentBindings(json)) {
77
+ /** @type {Record<string, string>} */
78
+ const fieldTypes = {};
79
+ if (componentName) {
80
+ const manifestFields = componentsManifest.get(componentName);
81
+ if (manifestFields) {
82
+ for (const [k, tsType] of manifestFields) {
83
+ fieldTypes[k] = tsType;
84
+ }
85
+ } else {
86
+ for (const [k, v] of Object.entries(fields)) {
87
+ fieldTypes[k] = inferTsType(v);
88
+ }
89
+ }
90
+ }
91
+ const isEngineComponent = componentName ? componentsManifest.has(componentName) : false;
92
+ const glbSrc = file.remote ? file.path : localPathForName;
93
+ const glbSourceFiles = /** @type {string[] | undefined} */ (/** @type {any} */ (file).sourceFiles);
94
+ entries.push({ nodeName, nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType, glbKey, glbSrc, glbSourceFiles });
95
+ }
96
+ }
97
+
98
+ return entries;
99
+ }
@@ -0,0 +1,59 @@
1
+ // @ts-check
2
+ /**
3
+ * File writer — the only module that performs I/O writes.
4
+ *
5
+ * Orchestrates scanning, codegen, and writes the output files.
6
+ * Returns false if content was already up-to-date (no write performed).
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { scanBindings } from './dts.scan.js';
12
+ import { generateDts, generateHtmlCustomData } from './dts.codegen.js';
13
+
14
+ /**
15
+ * Scan assets and write `needle-bindings.d.ts` and `needle-html-data.json`
16
+ * to the output directory.
17
+ * Returns the number of bindings written, or `false` if content was already up-to-date.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string} opts.assetsDir Absolute path to assets directory
21
+ * @param {string} opts.outputPath Absolute path to write needle-bindings.d.ts
22
+ * @param {string} [opts.projectRoot] Project root — enables entrypoint GLB detection
23
+ * @param {string} [opts.codegenDir] Codegen directory — used to find gen.js and write needle-html-data.json (no binding copy written here)
24
+ * @returns {Promise<number | false>}
25
+ */
26
+ export async function generateBindingsDts({ assetsDir, outputPath, projectRoot, codegenDir }) {
27
+ const entries = await scanBindings(assetsDir, projectRoot, codegenDir);
28
+ const content = generateDts(entries);
29
+ const htmlContent = generateHtmlCustomData(entries);
30
+
31
+ mkdirSync(dirname(outputPath), { recursive: true });
32
+
33
+ let dtsChanged = false;
34
+ try {
35
+ const existing = existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "";
36
+ if (existing !== content) {
37
+ writeFileSync(outputPath, content, "utf8");
38
+ dtsChanged = true;
39
+ }
40
+ } catch (_e) {
41
+ writeFileSync(outputPath, content, "utf8");
42
+ dtsChanged = true;
43
+ }
44
+
45
+ const htmlDataPath = join(dirname(outputPath), "needle-html-data.json");
46
+ try {
47
+ const existingHtml = existsSync(htmlDataPath) ? readFileSync(htmlDataPath, "utf8") : "";
48
+ if (existingHtml !== htmlContent) {
49
+ writeFileSync(htmlDataPath, htmlContent, "utf8");
50
+ }
51
+ } catch (_e) {
52
+ writeFileSync(htmlDataPath, htmlContent, "utf8");
53
+ }
54
+
55
+ if (!dtsChanged) return false;
56
+ const nodeCount = new Set(entries.map(e => e.nodeName)).size;
57
+ const bindingCount = new Set(entries.filter(e => e.componentName).map(e => `${e.nodeName}/${e.componentName}`)).size;
58
+ return bindingCount > 0 ? bindingCount : nodeCount;
59
+ }
@@ -0,0 +1,279 @@
1
+ // @ts-check
2
+ /**
3
+ * GLB / glTF file discovery.
4
+ *
5
+ * Resolves which scene files to process — either from the project's entrypoints
6
+ * (index.html `<needle-engine src>` or gen.js push calls) or by recursively
7
+ * walking the assets directory.
8
+ */
9
+
10
+ import { existsSync, readFileSync, readdirSync } from 'fs';
11
+ import { join, basename, extname } from 'path';
12
+
13
+ /**
14
+ * Generic URL path segments that carry no useful identity — fall back to
15
+ * the previous segment when the last segment matches one of these.
16
+ */
17
+ const GENERIC_SEGMENTS = new Set(["file", "index", "scene", "assets", "glb", "gltf", "model", "download"]);
18
+
19
+ /**
20
+ * Derive a short, human-friendly identifier from a GLB URL or local path.
21
+ *
22
+ * Rules (in priority order):
23
+ * 1. If `contentDispositionFilename` is provided → strip extension, use it.
24
+ * 2. Remote URL → walk path segments right-to-left, skip generic ones,
25
+ * strip extension from first useful segment.
26
+ * 3. Local path → basename without extension.
27
+ *
28
+ * Result is identifier-safe: non-alphanumeric chars replaced with `_`,
29
+ * leading digits prefixed with `_`.
30
+ *
31
+ * @param {string} pathOrUrl
32
+ * @param {string | null} [contentDispositionFilename]
33
+ * @returns {string}
34
+ */
35
+ export function glbFriendlyName(pathOrUrl, contentDispositionFilename) {
36
+ let raw = "";
37
+
38
+ if (contentDispositionFilename) {
39
+ raw = basename(contentDispositionFilename, extname(contentDispositionFilename));
40
+ } else if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
41
+ try {
42
+ const segments = new URL(pathOrUrl).pathname.split("/").filter(Boolean);
43
+ // Walk right-to-left, skip generic segments
44
+ for (let i = segments.length - 1; i >= 0; i--) {
45
+ const seg = segments[i];
46
+ const withoutExt = seg.replace(/\.(glb|gltf)$/i, "");
47
+ if (!GENERIC_SEGMENTS.has(withoutExt.toLowerCase())) {
48
+ raw = withoutExt;
49
+ break;
50
+ }
51
+ }
52
+ if (!raw) raw = segments[segments.length - 1] ?? "scene";
53
+ } catch (_e) {
54
+ raw = "scene";
55
+ }
56
+ } else {
57
+ raw = basename(pathOrUrl, extname(pathOrUrl));
58
+ }
59
+
60
+ // Make identifier-safe
61
+ let id = raw.replace(/[^a-zA-Z0-9]/g, "_");
62
+ if (/^\d/.test(id)) id = "_" + id;
63
+ return id || "scene";
64
+ }
65
+
66
+ /** Source file extensions that may contain `<needle-engine src="...">` markup. */
67
+ const SOURCE_EXTENSIONS = /\.(html|svelte|tsx|jsx|vue)$/i;
68
+
69
+ /**
70
+ * Parse `<needle-engine src="...">` from HTML.
71
+ * Handles both single strings and JSON arrays.
72
+ * @param {string} html
73
+ * @returns {string[]}
74
+ */
75
+ function parseSrcAttribute(html) {
76
+ const re = /<needle-engine[\s\S]*?\ssrc=["']([^"']+)["']/gi;
77
+ /** @type {string[]} */
78
+ const out = [];
79
+ let m;
80
+ while ((m = re.exec(html)) !== null) {
81
+ const val = m[1].trim();
82
+ if (val.startsWith("[")) {
83
+ try {
84
+ const arr = JSON.parse(val);
85
+ if (Array.isArray(arr)) out.push(...arr.filter(v => typeof v === "string"));
86
+ } catch (_e) { /* malformed JSON, skip */ }
87
+ } else {
88
+ out.push(val);
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ /**
95
+ * Parse `needle_exported_files.push("path/to/file.glb")` lines from gen.js.
96
+ * @param {string} src
97
+ * @returns {string[]}
98
+ */
99
+ function parseGenJs(src) {
100
+ const re = /needle_exported_files\.push\(["']([^"']+\.(?:glb|gltf))["']\)/gi;
101
+ /** @type {string[]} */
102
+ const out = [];
103
+ let m;
104
+ while ((m = re.exec(src)) !== null) {
105
+ out.push(m[1]);
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /**
111
+ * Resolve a list of (possibly relative) GLB path strings to absolute paths.
112
+ * Remote URLs (http/https) are passed through as-is.
113
+ * The `key` field preserves the original src value (relative path or URL)
114
+ * for use as the SceneMap key in the generated .d.ts.
115
+ *
116
+ * @param {Array<{glbPath: string, sourceFile: string | null}>} pathEntries
117
+ * @param {string} projectRoot
118
+ * @param {string} assetsDir
119
+ * @returns {Array<{path: string, type: "glb"|"gltf", remote?: boolean, key: string, sourceFiles: string[]}>}
120
+ */
121
+ function resolveGlbPaths(pathEntries, projectRoot, assetsDir) {
122
+ /** @type {Map<string, {path: string, type: "glb"|"gltf", remote?: boolean, key: string, sourceFiles: string[]}>} */
123
+ const byKey = new Map();
124
+ for (const { glbPath: p, sourceFile } of pathEntries) {
125
+ if (p.startsWith("http://") || p.startsWith("https://")) {
126
+ const type = p.toLowerCase().endsWith(".gltf") ? "gltf" : "glb";
127
+ const existing = byKey.get(p);
128
+ if (existing) {
129
+ if (sourceFile) existing.sourceFiles.push(sourceFile);
130
+ } else {
131
+ byKey.set(p, { path: p, type, remote: true, key: p, sourceFiles: sourceFile ? [sourceFile] : [] });
132
+ }
133
+ continue;
134
+ }
135
+ const clean = p.replace(/^\.\//, "").replace(/^\//, "");
136
+ const candidates = [
137
+ join(projectRoot, clean),
138
+ join(assetsDir, clean.replace(/^assets\//, "")),
139
+ ];
140
+ for (const candidate of candidates) {
141
+ if (existsSync(candidate)) {
142
+ const type = candidate.toLowerCase().endsWith(".gltf") ? "gltf" : "glb";
143
+ const existing = byKey.get(clean);
144
+ if (existing) {
145
+ if (sourceFile) existing.sourceFiles.push(sourceFile);
146
+ } else {
147
+ byKey.set(clean, { path: candidate, type, key: clean, sourceFiles: sourceFile ? [sourceFile] : [] });
148
+ }
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ return Array.from(byKey.values());
154
+ }
155
+
156
+ /**
157
+ * Walk `src/` (or the project root) for source files that may contain
158
+ * `<needle-engine src="...">` and return GLB path + source file pairs.
159
+ *
160
+ * @param {string} projectRoot
161
+ * @returns {Array<{glbPath: string, sourceFile: string}>}
162
+ */
163
+ function scanSourceFilesForGlbs(projectRoot) {
164
+ /** @type {Array<{glbPath: string, sourceFile: string}>} */
165
+ const out = [];
166
+ const srcDir = join(projectRoot, "src");
167
+ const searchRoot = existsSync(srcDir) ? srcDir : projectRoot;
168
+
169
+ /** @param {string} dir */
170
+ function walk(dir) {
171
+ let entries;
172
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
173
+ for (const entry of entries) {
174
+ const fullPath = join(dir, entry.name);
175
+ if (entry.isDirectory()) {
176
+ // Skip node_modules and hidden dirs
177
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
178
+ walk(fullPath);
179
+ } else if (SOURCE_EXTENSIONS.test(entry.name)) {
180
+ try {
181
+ const content = readFileSync(fullPath, "utf8");
182
+ const relPath = fullPath.replace(projectRoot + "/", "").replace(projectRoot + "\\", "");
183
+ for (const glbPath of parseSrcAttribute(content)) {
184
+ out.push({ glbPath, sourceFile: relPath });
185
+ }
186
+ } catch (_e) { /* ignore unreadable files */ }
187
+ }
188
+ }
189
+ }
190
+
191
+ walk(searchRoot);
192
+ return out;
193
+ }
194
+
195
+ /**
196
+ * Resolve the entrypoint GLB file paths for a project.
197
+ *
198
+ * Sources (all merged, deduplicated by key):
199
+ * 1. `<needle-engine src="...">` in `index.html`
200
+ * 2. `needle_exported_files.push("...")` lines in `{codegenDir}/gen.js`
201
+ * 3. `<needle-engine src="...">` in any `.svelte`, `.tsx`, `.jsx`, `.vue`, `.html` under `src/`
202
+ *
203
+ * Returns `null` if nothing found — caller should fall back to `collectSceneFiles`.
204
+ *
205
+ * @param {string} projectRoot Absolute path to the project root
206
+ * @param {string} assetsDir Absolute path to the assets directory
207
+ * @param {string} [codegenDir] Absolute path to the codegen directory
208
+ * @returns {Array<{path: string, type: "glb"|"gltf", remote?: boolean, key: string, sourceFiles: string[]}> | null}
209
+ */
210
+ export function resolveEntrypointGlbs(projectRoot, assetsDir, codegenDir) {
211
+ /** @type {Array<{glbPath: string, sourceFile: string | null}>} */
212
+ const allEntries = [];
213
+
214
+ // 1. index.html
215
+ const htmlPath = join(projectRoot, "index.html");
216
+ if (existsSync(htmlPath)) {
217
+ try {
218
+ for (const glbPath of parseSrcAttribute(readFileSync(htmlPath, "utf8"))) {
219
+ allEntries.push({ glbPath, sourceFile: "index.html" });
220
+ }
221
+ } catch (_e) { /* ignore */ }
222
+ }
223
+
224
+ // 2. gen.js
225
+ const genDir = codegenDir ?? join(projectRoot, "src", "generated");
226
+ const genPath = join(genDir, "gen.js");
227
+ if (existsSync(genPath)) {
228
+ try {
229
+ const relGenPath = genPath.replace(projectRoot + "/", "").replace(projectRoot + "\\", "");
230
+ for (const glbPath of parseGenJs(readFileSync(genPath, "utf8"))) {
231
+ allEntries.push({ glbPath, sourceFile: relGenPath });
232
+ }
233
+ } catch (_e) { /* ignore */ }
234
+ }
235
+
236
+ // 3. Source files (SvelteKit, React, Vue, etc.)
237
+ allEntries.push(...scanSourceFilesForGlbs(projectRoot));
238
+
239
+ if (allEntries.length === 0) return null;
240
+
241
+ const resolved = resolveGlbPaths(allEntries, projectRoot, assetsDir);
242
+ return resolved.length > 0 ? resolved : null;
243
+ }
244
+
245
+ /**
246
+ * Recursively collect all scene GLB/glTF files in a directory.
247
+ * Skips LOD and image sub-glbs that aren't scene roots.
248
+ *
249
+ * @param {string} assetsDir
250
+ * @returns {Array<{path: string, type: "glb"|"gltf"}>}
251
+ */
252
+ export function collectSceneFiles(assetsDir) {
253
+ if (!existsSync(assetsDir)) return [];
254
+
255
+ /** @type {Array<{path: string, type: "glb"|"gltf"}>} */
256
+ const out = [];
257
+
258
+ /** @param {string} dir */
259
+ function walk(dir) {
260
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
261
+ const fullPath = join(dir, entry.name);
262
+ if (entry.isDirectory()) {
263
+ walk(fullPath);
264
+ continue;
265
+ }
266
+ if (/^image_\d+_.*\.glb$/i.test(entry.name)) continue;
267
+ if (/^mesh_lod_\d+_.*\.glb$/i.test(entry.name)) continue;
268
+
269
+ if (/\.glb$/i.test(entry.name)) {
270
+ out.push({ path: fullPath, type: "glb" });
271
+ } else if (/\.gltf$/i.test(entry.name)) {
272
+ out.push({ path: fullPath, type: "gltf" });
273
+ }
274
+ }
275
+ }
276
+
277
+ walk(assetsDir);
278
+ return out;
279
+ }
@@ -0,0 +1,215 @@
1
+ // @ts-check
2
+ /**
3
+ * glTF node tree walker and NEEDLE_components extractor.
4
+ *
5
+ * Walks the parsed glTF JSON, sanitizes node names to match Three.js runtime
6
+ * behaviour, and extracts component data from the NEEDLE_components extension.
7
+ */
8
+
9
+ const NEEDLE_COMPONENTS_EXTENSION = "NEEDLE_components";
10
+
11
+ /**
12
+ * Replicate Three.js GLTFLoader's sanitizeNodeName + createUniqueName logic.
13
+ * - Whitespace → `_`
14
+ * - Reserved chars `[ ] . : /` → removed
15
+ * - Duplicate names get `_1`, `_2`, … suffix
16
+ *
17
+ * @param {string} rawName
18
+ * @param {Record<string, number>} namesUsed Mutated in-place — pass the same object for all nodes in a GLB
19
+ * @returns {string}
20
+ */
21
+ export function sanitizeNodeName(rawName, namesUsed) {
22
+ const sanitized = rawName.replace(/\s/g, "_").replace(/[\[\].:\/]/g, "");
23
+ if (sanitized in namesUsed) {
24
+ return sanitized + "_" + (++namesUsed[sanitized]);
25
+ }
26
+ namesUsed[sanitized] = 0;
27
+ return sanitized;
28
+ }
29
+
30
+ /**
31
+ * Infer the Three.js runtime type of a glTF node from its JSON properties.
32
+ * - `mesh` present with skinning → `import("three").SkinnedMesh`
33
+ * - `mesh` present → `import("three").Mesh`
34
+ * - `camera` present, perspective → `import("three").PerspectiveCamera`
35
+ * - `camera` present, orthographic→ `import("three").OrthographicCamera`
36
+ * - KHR_lights_punctual → `import("three").Light`
37
+ * - otherwise → `import("three").Object3D`
38
+ *
39
+ * @param {Record<string, unknown>} node
40
+ * @param {Array<Record<string, unknown>>} cameras Raw glTF cameras array
41
+ * @param {Array<Record<string, unknown>>} meshes Raw glTF meshes array
42
+ * @returns {string}
43
+ */
44
+ export function inferNodeThreeType(node, cameras, meshes) {
45
+ if ("mesh" in node) {
46
+ const hasSkin = "skin" in node;
47
+ if (!hasSkin) {
48
+ const meshIdx = /** @type {number} */ (node.mesh);
49
+ const mesh = meshes[meshIdx];
50
+ const primitives = /** @type {Array<Record<string, unknown>>} */ (Array.isArray(mesh?.primitives) ? mesh.primitives : []);
51
+ const isSkinned = primitives.some(p => {
52
+ const attrs = /** @type {Record<string, unknown>} */ (p?.attributes ?? {});
53
+ return "WEIGHTS_0" in attrs;
54
+ });
55
+ if (isSkinned) return `import("three").SkinnedMesh`;
56
+ } else {
57
+ return `import("three").SkinnedMesh`;
58
+ }
59
+ return `import("three").Mesh`;
60
+ }
61
+ if ("camera" in node) {
62
+ const camIdx = /** @type {number} */ (node.camera);
63
+ const cam = cameras[camIdx];
64
+ if (cam && typeof cam === "object") {
65
+ if (cam.type === "perspective") return `import("three").PerspectiveCamera`;
66
+ if (cam.type === "orthographic") return `import("three").OrthographicCamera`;
67
+ }
68
+ return `import("three").Camera`;
69
+ }
70
+ if (node.extensions && /** @type {any} */ (node.extensions)["KHR_lights_punctual"] != null) return `import("three").Light`;
71
+ return `import("three").Object3D`;
72
+ }
73
+
74
+ /**
75
+ * Infer a TypeScript type string from a raw JSON value.
76
+ * Only primitives are typed precisely; everything else → `unknown`.
77
+ *
78
+ * @param {unknown} value
79
+ * @returns {string}
80
+ */
81
+ export function inferTsType(value) {
82
+ if (value === null || value === undefined) return "unknown";
83
+ switch (typeof value) {
84
+ case "number": return "number";
85
+ case "string": return "string";
86
+ case "boolean": return "boolean";
87
+ default: return "unknown";
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Build a map of nodeIndex → sanitized name and nodeIndex → parent index
93
+ * from the glTF node tree, then compute the full path for each node.
94
+ *
95
+ * @param {Array<Record<string, unknown>>} nodes Raw glTF nodes array
96
+ * @param {Record<string, number>} namesUsed Already-used name registry (shared with extraction pass)
97
+ * @returns {{ nameMap: Map<number, string>, pathMap: Map<number, string> }}
98
+ */
99
+ function buildNodePaths(nodes, namesUsed) {
100
+ /** @type {Map<number, string>} nodeIndex → sanitized name */
101
+ const nameMap = new Map();
102
+ /** @type {Map<number, number>} nodeIndex → parent nodeIndex */
103
+ const parentMap = new Map();
104
+
105
+ // First pass: sanitize all names
106
+ for (let i = 0; i < nodes.length; i++) {
107
+ const node = nodes[i];
108
+ if (!node || typeof node !== "object") continue;
109
+ const rawName = typeof node.name === "string" ? node.name : "";
110
+ if (!rawName) continue;
111
+ nameMap.set(i, sanitizeNodeName(rawName, namesUsed));
112
+ }
113
+
114
+ // Second pass: build parent map from children arrays
115
+ for (let i = 0; i < nodes.length; i++) {
116
+ const node = nodes[i];
117
+ if (!node || typeof node !== "object") continue;
118
+ const children = Array.isArray(node.children) ? node.children : [];
119
+ for (const childIdx of children) {
120
+ parentMap.set(childIdx, i);
121
+ }
122
+ }
123
+
124
+ // Third pass: compute full path per node by walking up the parent chain
125
+ /** @type {Map<number, string>} nodeIndex → full path string */
126
+ const pathMap = new Map();
127
+ for (const [idx] of nameMap) {
128
+ const parts = [];
129
+ /** @type {number | undefined} */
130
+ let cur = idx;
131
+ while (cur !== undefined && nameMap.has(cur)) {
132
+ parts.unshift(/** @type {string} */ (nameMap.get(cur)));
133
+ cur = parentMap.get(cur);
134
+ }
135
+ pathMap.set(idx, parts.join("/"));
136
+ }
137
+
138
+ return { nameMap, pathMap };
139
+ }
140
+
141
+ /**
142
+ * Walk the glTF JSON and collect every node name + any NEEDLE_components extension blocks.
143
+ * Node names are sanitized and de-duplicated to match Three.js GLTFLoader behaviour.
144
+ *
145
+ * Nodes without NEEDLE_components are returned with an empty componentName so that
146
+ * callers can decide whether to emit them.
147
+ *
148
+ * @param {Record<string, unknown>} json Parsed glTF JSON
149
+ * @returns {Array<{nodeName: string, nodePath: string, componentName: string, fields: Record<string, unknown>, nodeThreeType: string}>}
150
+ */
151
+ export function extractComponentBindings(json) {
152
+ const nodes = /** @type {Array<Record<string, unknown>>} */ (Array.isArray(json.nodes) ? json.nodes : []);
153
+ const cameras = /** @type {Array<Record<string, unknown>>} */ (Array.isArray(json.cameras) ? json.cameras : []);
154
+ const meshes = /** @type {Array<Record<string, unknown>>} */ (Array.isArray(json.meshes) ? json.meshes : []);
155
+
156
+ // Collect scene root node indices — these map to THREE.Scene at runtime
157
+ const scenes = /** @type {Array<Record<string, unknown>>} */ (Array.isArray(json.scenes) ? json.scenes : []);
158
+ /** @type {Set<number>} */
159
+ const sceneRootNodeIndices = new Set();
160
+ for (const scene of scenes) {
161
+ const sceneNodes = Array.isArray(scene.nodes) ? /** @type {number[]} */ (scene.nodes) : [];
162
+ for (const idx of sceneNodes) sceneRootNodeIndices.add(idx);
163
+ }
164
+ /** @type {Array<{nodeName: string, nodePath: string, componentName: string, fields: Record<string, unknown>, nodeThreeType: string}>} */
165
+ const results = [];
166
+ /** @type {Record<string, number>} */
167
+ const namesUsed = {};
168
+
169
+ const { nameMap, pathMap } = buildNodePaths(nodes, namesUsed);
170
+
171
+ for (let i = 0; i < nodes.length; i++) {
172
+ const node = nodes[i];
173
+ if (!node || typeof node !== "object") continue;
174
+
175
+ const nodeName = nameMap.get(i);
176
+ if (!nodeName) continue;
177
+
178
+ const nodePath = pathMap.get(i) ?? nodeName;
179
+ const nodeThreeType = sceneRootNodeIndices.has(i)
180
+ ? `import("three").Scene`
181
+ : inferNodeThreeType(/** @type {Record<string, unknown>} */ (node), cameras, meshes);
182
+ const ext = node.extensions?.[NEEDLE_COMPONENTS_EXTENSION];
183
+
184
+ if (!ext || typeof ext !== "object") {
185
+ results.push({ nodeName, nodePath, componentName: "", fields: {}, nodeThreeType });
186
+ continue;
187
+ }
188
+
189
+ const lists = [
190
+ ...(Array.isArray(ext.components) ? ext.components : []),
191
+ ...(Array.isArray(ext.builtin_components) ? ext.builtin_components : []),
192
+ ];
193
+
194
+ const internalKeys = new Set([
195
+ "name", "type", "guid", "gameObject", "enabled",
196
+ "didAwake", "didStart", "transformHandle", "destroyCancellationToken",
197
+ ]);
198
+
199
+ for (const comp of lists) {
200
+ if (!comp || typeof comp !== "object") continue;
201
+ const componentName = typeof comp.name === "string" ? comp.name.trim() : "";
202
+ if (!componentName) continue;
203
+
204
+ /** @type {Record<string, unknown>} */
205
+ const fields = {};
206
+ for (const [k, v] of Object.entries(comp)) {
207
+ if (!internalKeys.has(k)) fields[k] = v;
208
+ }
209
+
210
+ results.push({ nodeName, nodePath, componentName, fields, nodeThreeType });
211
+ }
212
+ }
213
+
214
+ return results;
215
+ }