@needle-tools/engine 5.1.0-canary.fbdfce3 → 5.1.0-experimental.03e8105

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 (311) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/SKILL.md +4 -1
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-BFSj2Fz8.js → needle-engine.bundle-BNqUjnSQ.js} +19180 -18386
  5. package/dist/needle-engine.bundle-Bt8ULD7E.umd.cjs +1733 -0
  6. package/dist/needle-engine.bundle-DF6ovbwD.min.js +1733 -0
  7. package/dist/needle-engine.d.ts +1487 -356
  8. package/dist/needle-engine.js +544 -542
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/dist/three.js +1 -0
  12. package/dist/three.min.js +21 -21
  13. package/dist/three.umd.cjs +16 -16
  14. package/lib/engine/api.d.ts +8 -1
  15. package/lib/engine/api.js +7 -1
  16. package/lib/engine/api.js.map +1 -1
  17. package/lib/engine/codegen/register_types.js +10 -18
  18. package/lib/engine/codegen/register_types.js.map +1 -1
  19. package/lib/engine/engine_audio.d.ts +68 -0
  20. package/lib/engine/engine_audio.js +172 -0
  21. package/lib/engine/engine_audio.js.map +1 -1
  22. package/lib/engine/engine_camera.fit.js +16 -4
  23. package/lib/engine/engine_camera.fit.js.map +1 -1
  24. package/lib/engine/engine_components.js +1 -1
  25. package/lib/engine/engine_components.js.map +1 -1
  26. package/lib/engine/engine_context.d.ts +21 -8
  27. package/lib/engine/engine_context.js +32 -16
  28. package/lib/engine/engine_context.js.map +1 -1
  29. package/lib/engine/engine_context_eventbus.d.ts +47 -0
  30. package/lib/engine/engine_context_eventbus.js +47 -0
  31. package/lib/engine/engine_context_eventbus.js.map +1 -0
  32. package/lib/engine/engine_disposable.d.ts +172 -0
  33. package/lib/engine/engine_disposable.js +136 -0
  34. package/lib/engine/engine_disposable.js.map +1 -0
  35. package/lib/engine/engine_gameobject.d.ts +1 -10
  36. package/lib/engine/engine_gameobject.js +22 -120
  37. package/lib/engine/engine_gameobject.js.map +1 -1
  38. package/lib/engine/engine_gltf_builtin_components.js +7 -69
  39. package/lib/engine/engine_gltf_builtin_components.js.map +1 -1
  40. package/lib/engine/engine_init.js +7 -7
  41. package/lib/engine/engine_init.js.map +1 -1
  42. package/lib/engine/engine_input.d.ts +24 -5
  43. package/lib/engine/engine_input.js +3 -2
  44. package/lib/engine/engine_input.js.map +1 -1
  45. package/lib/engine/engine_instantiate_resolve.d.ts +42 -0
  46. package/lib/engine/engine_instantiate_resolve.js +372 -0
  47. package/lib/engine/engine_instantiate_resolve.js.map +1 -0
  48. package/lib/engine/engine_license.d.ts +7 -7
  49. package/lib/engine/engine_license.js +183 -57
  50. package/lib/engine/engine_license.js.map +1 -1
  51. package/lib/engine/engine_mainloop_utils.js +7 -4
  52. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  53. package/lib/engine/engine_networking.d.ts +51 -37
  54. package/lib/engine/engine_networking.js +132 -82
  55. package/lib/engine/engine_networking.js.map +1 -1
  56. package/lib/engine/engine_networking.transport.websocket.d.ts +15 -0
  57. package/lib/engine/engine_networking.transport.websocket.js +38 -0
  58. package/lib/engine/engine_networking.transport.websocket.js.map +1 -0
  59. package/lib/engine/engine_networking_blob.js +4 -4
  60. package/lib/engine/engine_networking_blob.js.map +1 -1
  61. package/lib/engine/engine_networking_instantiate.js +2 -2
  62. package/lib/engine/engine_networking_instantiate.js.map +1 -1
  63. package/lib/engine/engine_networking_types.d.ts +39 -1
  64. package/lib/engine/engine_networking_types.js +7 -0
  65. package/lib/engine/engine_networking_types.js.map +1 -1
  66. package/lib/engine/engine_physics_rapier.d.ts +21 -3
  67. package/lib/engine/engine_physics_rapier.js +94 -25
  68. package/lib/engine/engine_physics_rapier.js.map +1 -1
  69. package/lib/engine/engine_scenedata.js +2 -2
  70. package/lib/engine/engine_scenedata.js.map +1 -1
  71. package/lib/engine/engine_serialization_builtin_serializer.js +28 -5
  72. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  73. package/lib/engine/engine_serialization_core.d.ts +1 -0
  74. package/lib/engine/engine_serialization_core.js +7 -0
  75. package/lib/engine/engine_serialization_core.js.map +1 -1
  76. package/lib/engine/engine_types.d.ts +29 -11
  77. package/lib/engine/engine_types.js +1 -1
  78. package/lib/engine/engine_types.js.map +1 -1
  79. package/lib/engine/engine_util_decorator.js +7 -2
  80. package/lib/engine/engine_util_decorator.js.map +1 -1
  81. package/lib/engine/engine_utils.d.ts +1 -1
  82. package/lib/engine/engine_utils.js +19 -5
  83. package/lib/engine/engine_utils.js.map +1 -1
  84. package/lib/engine/engine_utils_qrcode.js +2 -2
  85. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  86. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  87. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  88. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  89. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  90. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +1 -1
  91. package/lib/engine/webcomponents/needle menu/needle-menu.js +6 -6
  92. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  93. package/lib/engine/webcomponents/needle-engine.d.ts +10 -4
  94. package/lib/engine/webcomponents/needle-engine.js +3 -3
  95. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  96. package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
  97. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  98. package/lib/engine/xr/NeedleXRSession.d.ts +3 -2
  99. package/lib/engine/xr/NeedleXRSession.js +50 -14
  100. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  101. package/lib/engine/xr/TempXRContext.js +2 -2
  102. package/lib/engine/xr/TempXRContext.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/AnimationBuilder.d.ts +158 -0
  108. package/lib/engine-components/AnimationBuilder.js +305 -0
  109. package/lib/engine-components/AnimationBuilder.js.map +1 -0
  110. package/lib/engine-components/Animator.d.ts +6 -0
  111. package/lib/engine-components/Animator.js +23 -13
  112. package/lib/engine-components/Animator.js.map +1 -1
  113. package/lib/engine-components/AnimatorController.builder.d.ts +191 -0
  114. package/lib/engine-components/AnimatorController.builder.js +263 -0
  115. package/lib/engine-components/AnimatorController.builder.js.map +1 -0
  116. package/lib/engine-components/AnimatorController.d.ts +2 -119
  117. package/lib/engine-components/AnimatorController.js +33 -232
  118. package/lib/engine-components/AnimatorController.js.map +1 -1
  119. package/lib/engine-components/AudioSource.d.ts +19 -3
  120. package/lib/engine-components/AudioSource.js +121 -68
  121. package/lib/engine-components/AudioSource.js.map +1 -1
  122. package/lib/engine-components/Collider.d.ts +18 -9
  123. package/lib/engine-components/Collider.js +61 -14
  124. package/lib/engine-components/Collider.js.map +1 -1
  125. package/lib/engine-components/Component.d.ts +72 -9
  126. package/lib/engine-components/Component.js +114 -10
  127. package/lib/engine-components/Component.js.map +1 -1
  128. package/lib/engine-components/ContactShadows.d.ts +1 -0
  129. package/lib/engine-components/ContactShadows.js +14 -1
  130. package/lib/engine-components/ContactShadows.js.map +1 -1
  131. package/lib/engine-components/DragControls.d.ts +7 -0
  132. package/lib/engine-components/DragControls.js +19 -7
  133. package/lib/engine-components/DragControls.js.map +1 -1
  134. package/lib/engine-components/DropListener.js +3 -0
  135. package/lib/engine-components/DropListener.js.map +1 -1
  136. package/lib/engine-components/EventList.d.ts +31 -9
  137. package/lib/engine-components/EventList.js +37 -76
  138. package/lib/engine-components/EventList.js.map +1 -1
  139. package/lib/engine-components/Joints.d.ts +4 -2
  140. package/lib/engine-components/Joints.js +19 -3
  141. package/lib/engine-components/Joints.js.map +1 -1
  142. package/lib/engine-components/Light.js +9 -1
  143. package/lib/engine-components/Light.js.map +1 -1
  144. package/lib/engine-components/Networking.d.ts +1 -1
  145. package/lib/engine-components/Networking.js +1 -1
  146. package/lib/engine-components/OrbitControls.d.ts +0 -2
  147. package/lib/engine-components/OrbitControls.js +30 -12
  148. package/lib/engine-components/OrbitControls.js.map +1 -1
  149. package/lib/engine-components/RigidBody.d.ts +12 -4
  150. package/lib/engine-components/RigidBody.js +18 -4
  151. package/lib/engine-components/RigidBody.js.map +1 -1
  152. package/lib/engine-components/SceneSwitcher.js +3 -0
  153. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  154. package/lib/engine-components/SeeThrough.js +2 -2
  155. package/lib/engine-components/SeeThrough.js.map +1 -1
  156. package/lib/engine-components/api.d.ts +2 -1
  157. package/lib/engine-components/api.js +2 -1
  158. package/lib/engine-components/api.js.map +1 -1
  159. package/lib/engine-components/codegen/components.d.ts +7 -13
  160. package/lib/engine-components/codegen/components.js +7 -13
  161. package/lib/engine-components/codegen/components.js.map +1 -1
  162. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  163. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  164. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  165. package/lib/engine-components/timeline/PlayableDirector.d.ts +21 -11
  166. package/lib/engine-components/timeline/PlayableDirector.js +75 -67
  167. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  168. package/lib/engine-components/timeline/SignalAsset.d.ts +3 -1
  169. package/lib/engine-components/timeline/SignalAsset.js +1 -0
  170. package/lib/engine-components/timeline/SignalAsset.js.map +1 -1
  171. package/lib/engine-components/timeline/TimelineBuilder.d.ts +413 -0
  172. package/lib/engine-components/timeline/TimelineBuilder.js +506 -0
  173. package/lib/engine-components/timeline/TimelineBuilder.js.map +1 -0
  174. package/lib/engine-components/timeline/TimelineModels.d.ts +2 -1
  175. package/lib/engine-components/timeline/TimelineModels.js +3 -0
  176. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  177. package/lib/engine-components/timeline/TimelineTracks.d.ts +37 -6
  178. package/lib/engine-components/timeline/TimelineTracks.js +92 -26
  179. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  180. package/lib/engine-components/timeline/index.d.ts +2 -1
  181. package/lib/engine-components/timeline/index.js +2 -0
  182. package/lib/engine-components/timeline/index.js.map +1 -1
  183. package/lib/engine-components/ui/Canvas.d.ts +1 -1
  184. package/lib/engine-components/ui/Canvas.js +2 -8
  185. package/lib/engine-components/ui/Canvas.js.map +1 -1
  186. package/lib/engine-components/ui/Text.d.ts +1 -0
  187. package/lib/engine-components/ui/Text.js +10 -7
  188. package/lib/engine-components/ui/Text.js.map +1 -1
  189. package/lib/engine-components/web/CursorFollow.d.ts +0 -1
  190. package/lib/engine-components/web/CursorFollow.js +21 -13
  191. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  192. package/lib/engine-components/webxr/WebXRImageTracking.js +4 -0
  193. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  194. package/package.json +2 -83
  195. package/plugins/common/cloud.js +6 -1
  196. package/plugins/common/license.js +31 -10
  197. package/plugins/common/worker.js +9 -4
  198. package/plugins/vite/asap.js +17 -8
  199. package/plugins/vite/dependencies.js +29 -10
  200. package/plugins/vite/dependency-watcher.js +2 -2
  201. package/plugins/vite/editor-connection.js +3 -3
  202. package/plugins/vite/license.js +46 -7
  203. package/plugins/vite/local-files-core.js +3 -3
  204. package/plugins/vite/local-files-utils.d.ts +3 -1
  205. package/plugins/vite/local-files-utils.js +29 -5
  206. package/plugins/vite/reload.js +1 -1
  207. package/plugins/vite/server.js +2 -1
  208. package/src/engine/api.ts +11 -1
  209. package/src/engine/codegen/register_types.ts +10 -18
  210. package/src/engine/engine_audio.ts +184 -0
  211. package/src/engine/engine_camera.fit.ts +15 -4
  212. package/src/engine/engine_components.ts +1 -1
  213. package/src/engine/engine_context.ts +34 -18
  214. package/src/engine/engine_context_eventbus.ts +73 -0
  215. package/src/engine/engine_disposable.ts +214 -0
  216. package/src/engine/engine_gameobject.ts +54 -159
  217. package/src/engine/engine_gltf_builtin_components.ts +7 -76
  218. package/src/engine/engine_init.ts +7 -7
  219. package/src/engine/engine_input.ts +28 -7
  220. package/src/engine/engine_instantiate_resolve.ts +407 -0
  221. package/src/engine/engine_license.ts +197 -55
  222. package/src/engine/engine_mainloop_utils.ts +7 -4
  223. package/src/engine/engine_networking.transport.websocket.ts +45 -0
  224. package/src/engine/engine_networking.ts +161 -137
  225. package/src/engine/engine_networking_blob.ts +4 -4
  226. package/src/engine/engine_networking_instantiate.ts +2 -2
  227. package/src/engine/engine_networking_types.ts +41 -1
  228. package/src/engine/engine_physics_rapier.ts +102 -33
  229. package/src/engine/engine_scenedata.ts +3 -3
  230. package/src/engine/engine_serialization_builtin_serializer.ts +32 -9
  231. package/src/engine/engine_serialization_core.ts +9 -0
  232. package/src/engine/engine_types.ts +46 -27
  233. package/src/engine/engine_util_decorator.ts +7 -2
  234. package/src/engine/engine_utils.ts +16 -5
  235. package/src/engine/engine_utils_qrcode.ts +2 -2
  236. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  237. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  238. package/src/engine/webcomponents/needle menu/needle-menu.ts +6 -6
  239. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  240. package/src/engine/webcomponents/needle-engine.ts +12 -6
  241. package/src/engine/xr/NeedleXRSession.ts +48 -13
  242. package/src/engine/xr/TempXRContext.ts +2 -2
  243. package/src/engine/xr/events.ts +1 -1
  244. package/src/engine-components/Animation.ts +19 -16
  245. package/src/engine-components/AnimationBuilder.ts +472 -0
  246. package/src/engine-components/Animator.ts +24 -12
  247. package/src/engine-components/AnimatorController.builder.ts +387 -0
  248. package/src/engine-components/AnimatorController.ts +20 -291
  249. package/src/engine-components/AudioSource.ts +130 -79
  250. package/src/engine-components/Collider.ts +66 -18
  251. package/src/engine-components/Component.ts +118 -20
  252. package/src/engine-components/ContactShadows.ts +15 -1
  253. package/src/engine-components/DragControls.ts +18 -11
  254. package/src/engine-components/DropListener.ts +3 -0
  255. package/src/engine-components/EventList.ts +45 -83
  256. package/src/engine-components/Joints.ts +20 -4
  257. package/src/engine-components/Light.ts +10 -2
  258. package/src/engine-components/Networking.ts +1 -1
  259. package/src/engine-components/OrbitControls.ts +34 -14
  260. package/src/engine-components/RigidBody.ts +18 -4
  261. package/src/engine-components/SceneSwitcher.ts +3 -0
  262. package/src/engine-components/SeeThrough.ts +2 -2
  263. package/src/engine-components/api.ts +2 -1
  264. package/src/engine-components/codegen/components.ts +7 -13
  265. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  266. package/src/engine-components/timeline/PlayableDirector.ts +83 -81
  267. package/src/engine-components/timeline/SignalAsset.ts +4 -1
  268. package/src/engine-components/timeline/TimelineBuilder.ts +824 -0
  269. package/src/engine-components/timeline/TimelineModels.ts +5 -1
  270. package/src/engine-components/timeline/TimelineTracks.ts +96 -27
  271. package/src/engine-components/timeline/index.ts +2 -1
  272. package/src/engine-components/ui/Canvas.ts +2 -8
  273. package/src/engine-components/ui/Text.ts +12 -8
  274. package/src/engine-components/web/CursorFollow.ts +21 -14
  275. package/src/engine-components/webxr/WebXRImageTracking.ts +2 -0
  276. package/dist/needle-engine.bundle-CmxIO5uH.min.js +0 -1732
  277. package/dist/needle-engine.bundle-tJIZukCz.umd.cjs +0 -1732
  278. package/lib/engine-components/AvatarLoader.d.ts +0 -80
  279. package/lib/engine-components/AvatarLoader.js +0 -232
  280. package/lib/engine-components/AvatarLoader.js.map +0 -1
  281. package/lib/engine-components/avatar/AvatarBlink_Simple.d.ts +0 -11
  282. package/lib/engine-components/avatar/AvatarBlink_Simple.js +0 -77
  283. package/lib/engine-components/avatar/AvatarBlink_Simple.js.map +0 -1
  284. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.d.ts +0 -14
  285. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.js +0 -69
  286. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.js.map +0 -1
  287. package/lib/engine-components/avatar/Avatar_Brain_LookAt.d.ts +0 -29
  288. package/lib/engine-components/avatar/Avatar_Brain_LookAt.js +0 -122
  289. package/lib/engine-components/avatar/Avatar_Brain_LookAt.js.map +0 -1
  290. package/lib/engine-components/avatar/Avatar_MouthShapes.d.ts +0 -15
  291. package/lib/engine-components/avatar/Avatar_MouthShapes.js +0 -80
  292. package/lib/engine-components/avatar/Avatar_MouthShapes.js.map +0 -1
  293. package/lib/engine-components/avatar/Avatar_MustacheShake.d.ts +0 -9
  294. package/lib/engine-components/avatar/Avatar_MustacheShake.js +0 -30
  295. package/lib/engine-components/avatar/Avatar_MustacheShake.js.map +0 -1
  296. package/plugins/dts-generator/dts.codegen.js +0 -334
  297. package/plugins/dts-generator/dts.scan.js +0 -99
  298. package/plugins/dts-generator/dts.writer.js +0 -59
  299. package/plugins/dts-generator/glb.discovery.js +0 -279
  300. package/plugins/dts-generator/glb.extractor.js +0 -215
  301. package/plugins/dts-generator/glb.reader.js +0 -167
  302. package/plugins/dts-generator/index.js +0 -36
  303. package/plugins/dts-generator/manifest.types.js +0 -174
  304. package/plugins/gltf-packer.mjs +0 -1
  305. package/src/engine-components/AvatarLoader.ts +0 -264
  306. package/src/engine-components/avatar/AvatarBlink_Simple.ts +0 -70
  307. package/src/engine-components/avatar/AvatarEyeLook_Rotation.ts +0 -64
  308. package/src/engine-components/avatar/Avatar_Brain_LookAt.ts +0 -140
  309. package/src/engine-components/avatar/Avatar_MouthShapes.ts +0 -84
  310. package/src/engine-components/avatar/Avatar_MustacheShake.ts +0 -32
  311. package/src/vite-env.d.ts +0 -16
@@ -1,279 +0,0 @@
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
- }
@@ -1,215 +0,0 @@
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
- }
@@ -1,167 +0,0 @@
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
- }