@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,167 @@
1
+ // @ts-check
2
+ /**
3
+ * GLB / glTF JSON chunk readers — local files and remote URLs.
4
+ *
5
+ * All functions return the parsed glTF JSON object or null on failure.
6
+ * Remote fetches use HTTP Range requests and cache by Last-Modified header.
7
+ */
8
+
9
+ import { openSync, readSync, closeSync, readFileSync } from 'fs';
10
+ import { needleLog } from '../vite/logging.js';
11
+
12
+ const PLUGIN = "needle:dts-generator";
13
+
14
+ /**
15
+ * Read the JSON chunk from a binary GLB file without loading the binary blob.
16
+ * @param {string} filePath
17
+ * @returns {Record<string, unknown> | null}
18
+ */
19
+ export function readGlbJsonChunk(filePath) {
20
+ const fd = openSync(filePath, "r");
21
+ try {
22
+ const header = Buffer.allocUnsafe(20);
23
+ const bytesRead = readSync(fd, header, 0, header.length, 0);
24
+ if (bytesRead < 20) return null;
25
+
26
+ const magic = header.readUInt32LE(0);
27
+ const version = header.readUInt32LE(4);
28
+ const chunkLength = header.readUInt32LE(12);
29
+ const chunkType = header.readUInt32LE(16);
30
+
31
+ if (magic !== 0x46546c67 || version !== 2) return null;
32
+ if (chunkType !== 0x4E4F534A) return null; // not JSON chunk
33
+
34
+ const jsonBuffer = Buffer.allocUnsafe(chunkLength);
35
+ const jsonBytesRead = readSync(fd, jsonBuffer, 0, chunkLength, 20);
36
+ if (jsonBytesRead < chunkLength) return null;
37
+
38
+ return JSON.parse(jsonBuffer.toString("utf8").replace(/\u0000+$/g, ""));
39
+ } catch (_e) {
40
+ return null;
41
+ } finally {
42
+ closeSync(fd);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {string} filePath
48
+ * @returns {Record<string, unknown> | null}
49
+ */
50
+ export function readGltfJsonFile(filePath) {
51
+ try {
52
+ return JSON.parse(readFileSync(filePath, "utf8"));
53
+ } catch (_e) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Parse the filename from a `Content-Disposition` header value.
60
+ * Handles `filename="foo.glb"` and `filename*=UTF-8''foo.glb` forms.
61
+ * Returns null if not present or not parseable.
62
+ * @param {string | null} header
63
+ * @returns {string | null}
64
+ */
65
+ export function parseContentDispositionFilename(header) {
66
+ if (!header) return null;
67
+ // RFC 5987 extended form: filename*=UTF-8''foo.glb
68
+ const extMatch = header.match(/filename\*\s*=\s*(?:[^']*'')?([^;]+)/i);
69
+ if (extMatch) {
70
+ try { return decodeURIComponent(extMatch[1].trim()); } catch (_e) { /* fall through */ }
71
+ }
72
+ // Plain form: filename="foo.glb" or filename=foo.glb
73
+ const plainMatch = header.match(/filename\s*=\s*"?([^";]+)"?/i);
74
+ if (plainMatch) return plainMatch[1].trim();
75
+ return null;
76
+ }
77
+
78
+ /** @type {Map<string, { lastModified: string, json: Record<string, unknown>, filename: string | null }>} */
79
+ const _remoteGlbCache = new Map();
80
+
81
+ /**
82
+ * Fetch the JSON chunk of a remote GLB.
83
+ * Tries an initial Range request for the header bytes; if the server already returned
84
+ * enough data (200 + full body, or 206 with sufficient bytes) the JSON chunk is
85
+ * extracted directly. Otherwise a second Range request fetches the JSON chunk.
86
+ * Uses `Last-Modified` header for caching — avoids re-parsing if unchanged.
87
+ * Also captures `Content-Disposition` filename for friendly key generation.
88
+ * Returns `null` on any network or parse error.
89
+ *
90
+ * @param {string} url
91
+ * @returns {Promise<{ json: Record<string, unknown>, filename: string | null } | null>}
92
+ */
93
+ export async function readRemoteGlbJsonChunk(url) {
94
+ try {
95
+ const cached = _remoteGlbCache.get(url);
96
+
97
+ const headerRes = await fetch(url, { headers: { Range: "bytes=0-19" } });
98
+ if (!headerRes.ok) {
99
+ needleLog(PLUGIN, `Remote GLB fetch failed (${headerRes.status}): ${url}`, "warn");
100
+ return null;
101
+ }
102
+
103
+ const lastModified = headerRes.headers.get("Last-Modified") ?? "";
104
+ const filename = parseContentDispositionFilename(headerRes.headers.get("Content-Disposition"));
105
+
106
+ if (cached && lastModified && cached.lastModified === lastModified) {
107
+ // Prefer the filename we resolved during the full fetch (may have come from the JSON chunk
108
+ // response). Only fall back to the fresh header value if the cache has nothing.
109
+ return { json: cached.json, filename: cached.filename ?? filename };
110
+ }
111
+
112
+ const firstBytes = Buffer.from(await headerRes.arrayBuffer());
113
+ if (firstBytes.length < 20) return null;
114
+
115
+ const magic = firstBytes.readUInt32LE(0);
116
+ const version = firstBytes.readUInt32LE(4);
117
+ const chunkLength = firstBytes.readUInt32LE(12);
118
+ const chunkType = firstBytes.readUInt32LE(16);
119
+
120
+ if (magic !== 0x46546c67 || version !== 2) return null; // not a GLB
121
+ if (chunkType !== 0x4E4F534A) return null; // chunk0 not JSON
122
+
123
+ let jsonBytes;
124
+ let resolvedFilename = filename;
125
+
126
+ if (firstBytes.length >= 20 + chunkLength) {
127
+ // The first response already contained the full JSON chunk (server ignored Range or returned 200).
128
+ jsonBytes = firstBytes.slice(20, 20 + chunkLength);
129
+ } else {
130
+ // Try a second Range request for just the JSON chunk bytes.
131
+ const jsonEnd = 20 + chunkLength - 1;
132
+ const jsonRes = await fetch(url, { headers: { Range: `bytes=20-${jsonEnd}` } });
133
+ if (!jsonRes.ok) {
134
+ needleLog(PLUGIN, `Remote GLB JSON chunk fetch failed (${jsonRes.status}): ${url}`, "warn");
135
+ return null;
136
+ }
137
+ const jsonResBytes = Buffer.from(await jsonRes.arrayBuffer());
138
+ // Server may return 200 + full body even for a Range request — slice out our window.
139
+ if (jsonResBytes.length >= 20 + chunkLength) {
140
+ jsonBytes = jsonResBytes.slice(20, 20 + chunkLength);
141
+ } else if (jsonResBytes.length >= chunkLength) {
142
+ jsonBytes = jsonResBytes.slice(0, chunkLength);
143
+ } else {
144
+ needleLog(PLUGIN, `Remote GLB unexpected response (${jsonRes.status}, ${jsonResBytes.length} bytes, expected ${chunkLength}): ${url}`, "warn");
145
+ return null;
146
+ }
147
+
148
+ const filenameFromJsonRes = parseContentDispositionFilename(jsonRes.headers.get("Content-Disposition"));
149
+ resolvedFilename = filename ?? filenameFromJsonRes;
150
+ }
151
+
152
+ if (resolvedFilename === null) {
153
+ try {
154
+ const headRes = await fetch(url, { method: "HEAD" });
155
+ resolvedFilename = parseContentDispositionFilename(headRes.headers.get("Content-Disposition"));
156
+ } catch (_e) { /* ignore — fall back to URL-based name */ }
157
+ }
158
+ const json = /** @type {Record<string, unknown>} */ (
159
+ JSON.parse(jsonBytes.toString("utf8").replace(/\u0000+$/g, ""))
160
+ );
161
+ _remoteGlbCache.set(url, { lastModified, json, filename: resolvedFilename });
162
+ return { json, filename: resolvedFilename };
163
+ } catch (e) {
164
+ needleLog(PLUGIN, `Failed to fetch remote GLB: ${url} — ${/** @type {any} */ (e)?.message ?? e}`, "warn");
165
+ return null;
166
+ }
167
+ }
@@ -0,0 +1,36 @@
1
+ // @ts-check
2
+ /**
3
+ * Needle Engine — HTML binding DTS generator
4
+ *
5
+ * Scans all GLB/glTF files in the project's assets directory, extracts
6
+ * NEEDLE_components data, and emits a `needle-bindings.d.ts` virtual-module
7
+ * declaration so that TypeScript can type-check HTML ↔ 3D component bindings.
8
+ *
9
+ * Typical generated output:
10
+ *
11
+ * declare module "needle-bindings" {
12
+ * interface SceneData {
13
+ * Sphere: {
14
+ * MyBall: { speed: number; label: string; };
15
+ * };
16
+ * }
17
+ * }
18
+ *
19
+ * How component field types are resolved:
20
+ * - For built-in Needle Engine components, types are read from
21
+ * `components.needle.json` which lists only @serializable fields with
22
+ * their proper TypeScript types.
23
+ * - For user-defined components (not in the manifest), types are inferred
24
+ * from the JSON value in the GLB (number/string/boolean → typed, else → unknown).
25
+ * - Known Three.js types (Color, Vector3, Object3D, …) are emitted as
26
+ * `import("three").TypeName` and known Needle types (RGBAColor, AssetReference, …)
27
+ * as `import("@needle-tools/engine").TypeName`.
28
+ * - Truly unknown types fall back to `unknown`.
29
+ */
30
+
31
+ export { resolveEntrypointGlbs, collectSceneFiles } from './glb.discovery.js';
32
+ export { readGlbJsonChunk, readGltfJsonFile, readRemoteGlbJsonChunk } from './glb.reader.js';
33
+ export { extractComponentBindings, sanitizeNodeName, inferNodeThreeType, inferTsType } from './glb.extractor.js';
34
+ export { scanBindings } from './dts.scan.js';
35
+ export { generateDts, generateHtmlCustomData } from './dts.codegen.js';
36
+ export { generateBindingsDts } from './dts.writer.js';
@@ -0,0 +1,174 @@
1
+ // @ts-check
2
+ /**
3
+ * Type tables and components.needle.json manifest loader.
4
+ *
5
+ * Knows about primitive TS types, known Three.js types, known Needle Engine
6
+ * types, and how to resolve field types from the manifest.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ /** Primitive TS type strings that can safely appear in an ambient declaration. */
17
+ export const PRIMITIVE_TYPES = new Set(["number", "string", "boolean"]);
18
+
19
+ /**
20
+ * Known Three.js types → import("three").TypeName
21
+ * @type {Record<string, string>}
22
+ */
23
+ export const THREE_TYPES = {
24
+ Color: `import("three").Color`,
25
+ ColorRepresentation: `import("three").ColorRepresentation`,
26
+ Euler: `import("three").Euler`,
27
+ Texture: `import("three").Texture`,
28
+ // Materials
29
+ Material: `import("three").Material`,
30
+ MeshStandardMaterial: `import("three").MeshStandardMaterial`,
31
+ // Objects
32
+ Object3D: `import("three").Object3D`,
33
+ Mesh: `import("three").Mesh`,
34
+ SkinnedMesh: `import("three").SkinnedMesh`,
35
+ // Other
36
+ Vector2: `import("three").Vector2`,
37
+ Vector3: `import("three").Vector3`,
38
+ Vector4: `import("three").Vector4`,
39
+ Matrix3: `import("three").Matrix3`,
40
+ Matrix4: `import("three").Matrix4`,
41
+ Quaternion: `import("three").Quaternion`,
42
+ // Animation
43
+ AnimationClip: `import("three").AnimationClip`,
44
+ AnimationMixer: `import("three").AnimationMixer`,
45
+ };
46
+
47
+ /**
48
+ * Known Needle Engine types → import("@needle-tools/engine").TypeName
49
+ * @type {Record<string, string>}
50
+ */
51
+ export const NEEDLE_TYPES = {
52
+ AssetReference: `import("@needle-tools/engine").AssetReference`,
53
+ EventList: `import("@needle-tools/engine").EventList`,
54
+ GameObject: `import("@needle-tools/engine").GameObject`,
55
+ LookAtConstraint: `import("@needle-tools/engine").LookAtConstraint`,
56
+ RGBAColor: `import("@needle-tools/engine").RGBAColor`,
57
+ RenderTexture: `import("@needle-tools/engine").RenderTexture`,
58
+ Renderer: `import("@needle-tools/engine").Renderer`,
59
+ Rigidbody: `import("@needle-tools/engine").Rigidbody`,
60
+ Sprite: `import("@needle-tools/engine").Sprite`,
61
+ Vec2: `import("@needle-tools/engine").Vec2`,
62
+ };
63
+
64
+ /**
65
+ * Map a single non-array, non-primitive type token to its TS representation.
66
+ * Returns null if unknown.
67
+ * @param {string} token
68
+ * @returns {string | null}
69
+ */
70
+ function mapKnownType(token) {
71
+ if (token in THREE_TYPES) return THREE_TYPES[token];
72
+ if (token in NEEDLE_TYPES) return NEEDLE_TYPES[token];
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Convert a manifest type string to a safe ambient TS type.
78
+ * Primitives and known Three.js/Needle types are resolved precisely.
79
+ * For unknown types on a manifest component, falls back to
80
+ * `import("@needle-tools/engine").ComponentName["fieldName"]`.
81
+ * Truly unresolvable types → "unknown".
82
+ *
83
+ * @param {string} typeStr
84
+ * @param {string} [componentName] The manifest component class name (enables indexed-access fallback)
85
+ * @param {string} [fieldName] The field name on that component
86
+ * @returns {string}
87
+ */
88
+ export function manifestTypeToTs(typeStr, componentName, fieldName) {
89
+ const parts = typeStr.split(" | ").map(p => p.trim());
90
+ const safeParts = parts.map(p => {
91
+ if (p === "undefined" || p === "null") return p;
92
+ const arrayMatch = p.match(/^(number|string|boolean)\[\]$/);
93
+ if (arrayMatch) return p;
94
+ if (PRIMITIVE_TYPES.has(p)) return p;
95
+ const arrayTypeMatch = p.match(/^(\w+)\[\]$/);
96
+ if (arrayTypeMatch) {
97
+ const base = arrayTypeMatch[1];
98
+ const mapped = mapKnownType(base);
99
+ if (mapped) return `${mapped}[]`;
100
+ }
101
+ const known = mapKnownType(p);
102
+ if (known) return known;
103
+ return null;
104
+ });
105
+ if (safeParts.every(p => p !== null)) {
106
+ return /** @type {string[]} */ (safeParts).join(" | ");
107
+ }
108
+ if (componentName && fieldName) {
109
+ return `import("@needle-tools/engine").${componentName}["${fieldName}"]`;
110
+ }
111
+ return "unknown";
112
+ }
113
+
114
+ /**
115
+ * Load components.needle.json and build a lookup:
116
+ * componentName → Map<fieldName, tsType>
117
+ * Inherited fields are flattened (inheritedFrom chain is resolved).
118
+ *
119
+ * @returns {Map<string, Map<string, string>>}
120
+ */
121
+ export function loadComponentsManifest() {
122
+ /** @type {Map<string, Map<string, string>>} */
123
+ const manifest = new Map();
124
+ const manifestPath = join(__dirname, "../../components.needle.json");
125
+ if (!existsSync(manifestPath)) return manifest;
126
+ try {
127
+ /** @type {Array<{name: string, inheritedFrom?: string, children?: Array<{name: string, kind: string, type: string}>}>} */
128
+ const entries = JSON.parse(readFileSync(manifestPath, "utf8"));
129
+
130
+ /** @type {Map<string, Map<string, string>>} */
131
+ const ownFields = new Map();
132
+ /** @type {Map<string, string>} */
133
+ const inheritedFrom = new Map();
134
+ for (const entry of entries) {
135
+ if (!entry.name) continue;
136
+ inheritedFrom.set(entry.name, entry.inheritedFrom || "");
137
+ /** @type {Map<string, string>} */
138
+ const fields = new Map();
139
+ if (Array.isArray(entry.children)) {
140
+ for (const child of entry.children) {
141
+ if (child.kind === "property" && child.name && child.type) {
142
+ fields.set(child.name, manifestTypeToTs(child.type, entry.name, child.name));
143
+ }
144
+ }
145
+ }
146
+ ownFields.set(entry.name, fields);
147
+ }
148
+
149
+ /** @param {string} name @returns {Map<string, string>} */
150
+ function resolveFields(name) {
151
+ if (manifest.has(name)) return /** @type {Map<string, string>} */ (manifest.get(name));
152
+ const own = ownFields.get(name) ?? new Map();
153
+ const parent = inheritedFrom.get(name);
154
+ if (parent && ownFields.has(parent)) {
155
+ const parentFields = resolveFields(parent);
156
+ const merged = new Map([...parentFields, ...own]);
157
+ manifest.set(name, merged);
158
+ return merged;
159
+ }
160
+ manifest.set(name, own);
161
+ return own;
162
+ }
163
+
164
+ for (const name of ownFields.keys()) {
165
+ resolveFields(name);
166
+ }
167
+ } catch (e) {
168
+ console.warn("[needle:dts-generator] Failed to load components.needle.json:", (/** @type {any} */ (e))?.message ?? e);
169
+ }
170
+ return manifest;
171
+ }
172
+
173
+ /** @type {Map<string, Map<string, string>>} */
174
+ export const componentsManifest = loadComponentsManifest();
@@ -1,3 +1,4 @@
1
1
  export * from "./needleConfig.js";
2
2
  export * from './userconfig.js';
3
- export * from "./webmanifest.js";
3
+ export * from "./webmanifest.js";
4
+ export * from "./needle-bindings.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ambient module declaration for `needle-bindings`.
3
+ *
4
+ * `SceneData` is keyed by GLB friendly name, then by the node hierarchy:
5
+ * ctx.sceneData.Minimal.Camera.$object // → THREE.Camera
6
+ * ctx.sceneData.Minimal.Camera.$components.OrbitControls.autoRotate = true;
7
+ * ctx.sceneData.Minimal.UI.Button.$components.Button
8
+ *
9
+ * Each node entry has:
10
+ * $object — the Three.js Object3D (typed precisely, e.g. Mesh, Camera, Light)
11
+ * $components — Needle components attached to this node
12
+ * [childName] — child nodes, recursively typed
13
+ *
14
+ * When the `needle:dts-generator` Vite plugin is active it writes
15
+ * `.needle/generated/needle-bindings.gen.d.ts` next to the installed package,
16
+ * which augments this interface with the actual bindings extracted from
17
+ * the project's GLB files at build time.
18
+ */
19
+
20
+ // Pull in the project-local generated augmentation (written by needle:dts-generator).
21
+ /// <reference path="../../.needle/generated/needle-bindings.gen.d.ts" />
22
+
23
+ declare module "needle-bindings" {
24
+ /**
25
+ * Scene data keyed by GLB friendly name, then by node hierarchy.
26
+ * Fallback index signature allows unknown names without crashing.
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
29
+ interface SceneData {}
30
+ }
@@ -26,10 +26,17 @@ export type userSettings = {
26
26
  noCopy?: boolean;
27
27
  /** When enabled the needle-engine include directory will be copied */
28
28
  copyIncludesFromEngine?: boolean;
29
- /** set to false to tree-shake rapier physics engine to the reduce bundle size */
29
+
30
+
31
+ /** Set to `false` to prevent the Rapier physics engine from loading.
32
+ * NOTE: by default Needle Engine uses dynamic loading of the Rapier physics engine. This means that the Rapier code is only loaded when a physics component is used in the scene.
33
+ */
30
34
  useRapier?: boolean;
31
- /** experimental */
35
+ /** Set to `false` to prevent postprocessing effects from loading.
36
+ * NOTE: by default Needle Engine uses dynamic loading of postprocessing modules. This means that the postprocessing code is only loaded when a postprocessing effect is used in the scene.
37
+ */
32
38
  usePostprocessing?: boolean;
39
+
33
40
  noDependencyWatcher?: boolean;
34
41
  /** set to false to suppress editor-sync package installation and connection */
35
42
  dontInstallEditor?: boolean;
@@ -169,4 +176,16 @@ export type userSettings = {
169
176
 
170
177
  /** Set to true to disable the plugin that ensures VSCode workspace settings for custom-elements.json data */
171
178
  noCustomElementData?: boolean;
179
+
180
+ /**
181
+ * Generate Typescript declaration files for references 3D assets in your project.
182
+ * These will be available via `context.sceneData` in your code.
183
+ * @default enabled
184
+ */
185
+ dts?: {
186
+ /** When set to false, disables the generation of TypeScript declaration files.
187
+ * @default true
188
+ */
189
+ enabled?: boolean;
190
+ }
172
191
  }
@@ -22,7 +22,7 @@ export async function needleAsap(command, config, userSettings) {
22
22
 
23
23
  fixMainTs();
24
24
 
25
- if (command != "build") {
25
+ if (command === "serve") {
26
26
  return null;
27
27
  }
28
28
 
@@ -50,7 +50,7 @@ export async function needleAsap(command, config, userSettings) {
50
50
  const tags = [];
51
51
 
52
52
  try {
53
- generateGltfPreloadLinks(config, html, tags);
53
+ generateGltfPreloadLinks(config, html, tags, viteConfig?.base);
54
54
  }
55
55
  catch (err) {
56
56
  console.error("Error generating gltf preload links", err);
@@ -173,18 +173,21 @@ function fixMainTs() {
173
173
  * @param {import('vite').ResolvedConfig} _config
174
174
  * @param {import('vite').HtmlTagDescriptor[]} tags
175
175
  */
176
- function generateScriptPreloadLinks(_config, tags) {
176
+ function generateScriptPreloadLinks(config, tags) {
177
177
  try {
178
+ const base = config.base || '/';
178
179
  const chunks = preloadScriptPaths;
179
180
  // console.log("ASAP", chunks)
180
181
  if (chunks.length > 0) {
181
182
  for (const chunk of chunks) {
183
+ // Apply base path so preload hrefs resolve correctly under SPA routing
184
+ const href = chunk.startsWith('./') ? base + chunk.slice(2) : chunk;
182
185
  tags.push({
183
186
  tag: 'link',
184
187
  attrs: {
185
188
  rel: "modulepreload",
186
189
  as: "script",
187
- href: chunk,
190
+ href: href,
188
191
  }
189
192
  });
190
193
  }
@@ -203,15 +206,16 @@ const codegenRegex = /\"(?<gltf>.+(.glb|.gltf)(\?.*)?)\"/gm;
203
206
  /**
204
207
  * @param {import('../types').needleConfig} config
205
208
  * @param {string} html
206
- * @param {import('vite').HtmlTagDescriptor[]} tags
209
+ * @param {import('vite').HtmlTagDescriptor[]} tags
210
+ * @param {string} [base]
207
211
  **/
208
- function generateGltfPreloadLinks(config, html, tags) {
212
+ function generateGltfPreloadLinks(config, html, tags, base) {
209
213
 
210
214
  // TODO: try to get the <needle-engine src> element src attribute and preload that
211
215
  const needleEngineMatches = tryParseNeedleEngineSrcAttributeFromHtml(html);
212
216
  if (needleEngineMatches?.length) {
213
217
  for (const item of needleEngineMatches) {
214
- insertPreloadLink(tags, item, "model/gltf+json");
218
+ insertPreloadLink(tags, item, "model/gltf+json", base);
215
219
  }
216
220
  }
217
221
 
@@ -246,7 +250,7 @@ function generateGltfPreloadLinks(config, html, tags) {
246
250
  }
247
251
  }
248
252
  needleLog("needle:asap", `Insert glTF preload link: ${value}`);
249
- insertPreloadLink(tags, value, "model/gltf+json");
253
+ insertPreloadLink(tags, value, "model/gltf+json", base);
250
254
  }
251
255
  }
252
256
  }
@@ -258,9 +262,14 @@ function generateGltfPreloadLinks(config, html, tags) {
258
262
  * @param {import('vite').HtmlTagDescriptor[]} tags
259
263
  * @param {string} href
260
264
  * @param {string} type
265
+ * @param {string} [base]
261
266
  */
262
- function insertPreloadLink(tags, href, type) {
267
+ function insertPreloadLink(tags, href, type, base) {
263
268
  if (!href) return;
269
+ // Apply base path so preload hrefs resolve correctly under SPA routing
270
+ if (base && !href.startsWith('http') && !href.startsWith('/')) {
271
+ href = base + (href.startsWith('./') ? href.slice(2) : href);
272
+ }
264
273
  tags.push({
265
274
  tag: 'link',
266
275
  attrs: {
@@ -61,6 +61,35 @@ export function needleDependencies(command, config, userSettings) {
61
61
  }
62
62
  },
63
63
  },
64
+ // Vite 8's optimizer rebases `new URL(specifier, import.meta.url)` paths
65
+ // inside pre-bundled dependencies (PR #21434), but only handles truly relative
66
+ // paths (./ ../). Bare-specifier paths like `three-mesh-bvh/src/workers/...`
67
+ // are treated as relative to the *source file* that contained the `new URL()`
68
+ // call, producing a wrong URL such as:
69
+ // /node_modules/@needle-tools/engine/.../three-mesh-bvh/src/workers/generateMeshBVH.worker.js
70
+ // This middleware rewrites those broken URLs so the dev server finds the file at
71
+ // its real location in node_modules.
72
+ {
73
+ name: 'needle:worker-url-rewrite',
74
+ configureServer(server) {
75
+ const rewritePackages = ['three-mesh-bvh'];
76
+ server.middlewares.use((req, _res, next) => {
77
+ if (req.url) {
78
+ for (const pkg of rewritePackages) {
79
+ const marker = `/${pkg}/`;
80
+ if (req.url.includes(marker) && !req.url.startsWith(`/node_modules/${pkg}/`)) {
81
+ const idx = req.url.indexOf(marker);
82
+ const rewritten = '/node_modules' + req.url.slice(idx);
83
+ needleLog('needle-dependencies', `Rewriting worker URL → ${rewritten}`);
84
+ req.url = rewritten;
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ next();
90
+ });
91
+ },
92
+ },
64
93
  ]
65
94
  }
66
95
 
@@ -1,10 +1,10 @@
1
1
  /**
2
- * @param {"build" | "serve"} command
2
+ * @param {"build" | "serve" | undefined} _command
3
3
  * @param {unknown} _config
4
4
  * @param {import('../types').userSettings} userSettings
5
5
  * @returns {import('vite').Plugin | null}
6
6
  */
7
- export function needleDependencyWatcher(command: "build" | "serve", _config: unknown, userSettings: import("../types").userSettings): import("vite").Plugin | null;
7
+ export function needleDependencyWatcher(_command: "build" | "serve" | undefined, _config: unknown, userSettings: import("../types").userSettings): import("vite").Plugin | null;
8
8
  export type PackageJson = {
9
9
  dependencies?: Record<string, string>;
10
10
  devDependencies?: Record<string, string>;
@@ -12,14 +12,12 @@ function log(...msg) {
12
12
  }
13
13
 
14
14
  /**
15
- * @param {"build" | "serve"} command
15
+ * @param {"build" | "serve" | undefined} _command
16
16
  * @param {unknown} _config
17
17
  * @param {import('../types').userSettings} userSettings
18
18
  * @returns {import('vite').Plugin | null}
19
19
  */
20
- export function needleDependencyWatcher(command, _config, userSettings) {
21
- if (command === "build") return null;
22
-
20
+ export function needleDependencyWatcher(_command, _config, userSettings) {
23
21
  if (userSettings?.noDependencyWatcher === true) return null;
24
22
 
25
23
  const dir = process.cwd();
@@ -28,6 +26,7 @@ export function needleDependencyWatcher(command, _config, userSettings) {
28
26
 
29
27
  return /** @type {import('vite').Plugin} */ ({
30
28
  name: 'needle-dependency-watcher',
29
+ apply: 'serve',
31
30
  /** @param {import('vite').ViteDevServer} server */
32
31
  configureServer(server) {
33
32
  manageClients(server);
@@ -1,7 +1,7 @@
1
1
  /** Experimental, allow dropping files from Unity into the running scene.
2
- * @param {"build" | "serve"} command
2
+ * @param {"build" | "serve" | undefined} _command
3
3
  * @param {import('../types/needleConfig').needleMeta | null | undefined} config
4
4
  * @param {import('../types/userconfig.js').userSettings} userSettings
5
5
  * @returns {import('vite').Plugin | null | undefined}
6
6
  */
7
- export function needleDrop(command: "build" | "serve", config: import("../types/needleConfig").needleMeta | null | undefined, userSettings: import("../types/userconfig.js").userSettings): import("vite").Plugin | null | undefined;
7
+ export function needleDrop(_command: "build" | "serve" | undefined, config: import("../types/needleConfig").needleMeta | null | undefined, userSettings: import("../types/userconfig.js").userSettings): import("vite").Plugin | null | undefined;
@@ -7,18 +7,17 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
9
  /** Experimental, allow dropping files from Unity into the running scene.
10
- * @param {"build" | "serve"} command
10
+ * @param {"build" | "serve" | undefined} _command
11
11
  * @param {import('../types/needleConfig').needleMeta | null | undefined} config
12
12
  * @param {import('../types/userconfig.js').userSettings} userSettings
13
13
  * @returns {import('vite').Plugin | null | undefined}
14
14
  */
15
- export function needleDrop(command, config, userSettings) {
16
- if (command === "build") return;
17
-
15
+ export function needleDrop(_command, config, userSettings) {
18
16
  if(userSettings.useDrop !== true) return null;
19
17
 
20
18
  return {
21
19
  name: "needle:drop",
20
+ apply: 'serve',
22
21
  config(/** @type {{ server?: { hmr?: { port?: number } } }} */ viteConfig) {
23
22
  if(userSettings)
24
23
  if (!viteConfig.server) viteConfig.server = {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @param {"build" | "serve"} _command Vite command (unused — runs in both modes)
3
+ * @param {import('../types/needleConfig').needleMeta | null | undefined} _config
4
+ * @param {import('../types').userSettings} [_userSettings]
5
+ * @returns {import('vite').Plugin | null}
6
+ */
7
+ export function needleDtsGenerator(_command: "build" | "serve", _config: import("../types/needleConfig").needleMeta | null | undefined, _userSettings?: import("../types").userSettings): import("vite").Plugin | null;