@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
@@ -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}/`) && !req.url.startsWith('/@fs/')) {
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
 
@@ -158,16 +187,6 @@ function handleOptimizeDeps(config) {
158
187
  config.optimizeDeps.exclude.push('@needle-tools/engine');
159
188
  needleLog("needle-dependencies", 'Detected local @needle-tools/engine package → will exclude it from optimization');
160
189
  }
161
- // When engine is excluded from optimizeDeps, three-mesh-bvh must also be excluded.
162
- // The BVH worker (generateMeshBVH.worker.js) uses bare imports like `import 'three'`
163
- // which only resolve correctly when served through Vite's dev server module system.
164
- // If three-mesh-bvh is pre-bundled, the worker URL points into the cache and bare
165
- // imports fail at runtime → "Unknown error. Please check the server console."
166
- if (!config.optimizeDeps.include?.includes('three-mesh-bvh') &&
167
- !config.optimizeDeps.exclude.includes('three-mesh-bvh')) {
168
- config.optimizeDeps.exclude.push('three-mesh-bvh');
169
- needleLog("needle-dependencies", 'Detected local @needle-tools/engine package → will also exclude three-mesh-bvh from optimization');
170
- }
171
190
  }
172
191
  }
173
192
 
@@ -98,7 +98,7 @@ function watchPackageJson(server, projectDir, packageJsonPath, cachePath) {
98
98
 
99
99
  setTimeout(() => {
100
100
  requireInstall = testIfInstallIsRequired(projectDir, packageJson);
101
- }, 1000);
101
+ }, 1000).unref();
102
102
 
103
103
  setInterval(() => {
104
104
  if (!packageJson || lastEditTime === undefined) return;
@@ -149,7 +149,7 @@ function watchPackageJson(server, projectDir, packageJsonPath, cachePath) {
149
149
  restart(server, projectDir, cachePath);
150
150
  }
151
151
  }
152
- }, 2000);
152
+ }, 2000).unref();
153
153
  }
154
154
 
155
155
  /** @param {string} projectDir @param {PackageJson | undefined} packageJson @returns {boolean} */
@@ -27,7 +27,7 @@ export async function editorConnection(command, config, userSettings, pluginsArr
27
27
  if (typeof config.generator === "string" && !config.generator.includes("Unity")) return;
28
28
 
29
29
  if (!config) {
30
- setTimeout(() => needleLog("needle-editor-sync", "Needle Editor Sync can not be installed automatically to vite: missing config", "warn"), 1000);
30
+ setTimeout(() => needleLog("needle-editor-sync", "Needle Editor Sync can not be installed automatically to vite: missing config", "warn"), 1000).unref();
31
31
  return createPlugin(false);
32
32
  }
33
33
 
@@ -42,14 +42,14 @@ export async function editorConnection(command, config, userSettings, pluginsArr
42
42
  // }
43
43
  // }
44
44
  if (needleEditorSettings && needleEditorSettings.enabled === false) {
45
- setTimeout(() => needleLog("needle-editor-sync", "Needle Editor Sync is not enabled. Add a 'Needle Editor Sync' component to your scene to enable", "warn"), 1000);
45
+ setTimeout(() => needleLog("needle-editor-sync", "Needle Editor Sync is not enabled. Add a 'Needle Editor Sync' component to your scene to enable", "warn"), 1000).unref();
46
46
  return createPlugin(false);
47
47
  }
48
48
 
49
49
  // Check if the editor package is installed
50
50
  let path = root + `/node_modules/${editorSyncPackageName}/plugins/index.js`;
51
51
  if (existsSync(path) === false) {
52
- setTimeout(() => needleLog("needle-editor-sync", `${editorSyncPackageName} is not installed: Add the "Needle Editor Sync" component to your scene if you want to send changes directly from the Unity Editor to web app`, "warn"), 1000);
52
+ setTimeout(() => needleLog("needle-editor-sync", `${editorSyncPackageName} is not installed: Add the "Needle Editor Sync" component to your scene if you want to send changes directly from the Unity Editor to web app`, "warn"), 1000).unref();
53
53
  return createPlugin(false);
54
54
  }
55
55
 
@@ -8,7 +8,9 @@ import { loadConfig } from './config.js';
8
8
  * @returns {import('vite').Plugin}
9
9
  */
10
10
  export function needleLicense(command, config, userSettings) {
11
- let license = undefined;
11
+ /** @type {import('../common/license.js').LicenseResult | null | undefined} */
12
+ let licenseResult = undefined;
13
+ let appliedLicense = false;
12
14
 
13
15
  return {
14
16
  name: "needle:license",
@@ -23,7 +25,7 @@ export function needleLicense(command, config, userSettings) {
23
25
  }
24
26
  }
25
27
 
26
- license = await resolveLicense({
28
+ licenseResult = await resolveLicense({
27
29
  team: team,
28
30
  accessToken: userSettings?.license?.accessToken,
29
31
  loglevel: userSettings?.debugLicense === true ? "verbose" : undefined
@@ -31,25 +33,62 @@ export function needleLicense(command, config, userSettings) {
31
33
 
32
34
  },
33
35
  async transform(src, id) {
34
- const isNeedleEngineFile = id.includes("engine/engine_license") || id.includes("needle-tools_engine");
36
+ if (appliedLicense === true) {
37
+ return;
38
+ }
39
+
40
+ // Vite 4 and 8 handling:
41
+ const isNeedleEngineFile = id.includes("engine/engine_license")
42
+ || id.includes("needle-tools_engine")
43
+ || id.includes("@needle-tools")
44
+ || id.includes("needle-engine");
35
45
  // sometimes the actual license parameter is in a unnamed chunk file
36
46
  const isViteChunkFile = id.includes("chunk") && id.includes(".vite");
37
47
  if (isNeedleEngineFile || isViteChunkFile) {
38
48
 
39
- if (!license) {
49
+ if (!licenseResult) {
40
50
  return;
41
51
  }
42
52
 
43
- const index = src.indexOf("NEEDLE_ENGINE_LICENSE_TYPE");
53
+ let modified = false;
54
+
55
+ // Replace license type
56
+ const index = src.indexOf("_PQYikbj");
44
57
  if (index >= 0) {
45
58
  const end = src.indexOf(";", index);
46
59
  if (end >= 0) {
47
60
  const line = src.substring(index, end);
48
- const replaced = "NEEDLE_ENGINE_LICENSE_TYPE = \"" + license + "\"";
61
+ const replaced = "_PQYikbj = \"" + licenseResult.type + "\"";
49
62
  src = src.replace(line, replaced);
50
- return { code: src, map: null }
63
+ modified = true;
64
+ }
65
+ }
66
+
67
+ // Replace license JWT (same pattern)
68
+ if (licenseResult.jwt) {
69
+ const jwtIndex = src.indexOf("_$QHon");
70
+ if (jwtIndex >= 0) {
71
+ const jwtEnd = src.indexOf(";", jwtIndex);
72
+ if (jwtEnd >= 0) {
73
+ const jwtLine = src.substring(jwtIndex, jwtEnd);
74
+ const jwtReplaced = "_$QHon = \"" + licenseResult.jwt + "\"";
75
+ src = src.replace(jwtLine, jwtReplaced);
76
+ }
51
77
  }
52
78
  }
79
+
80
+ if (modified) {
81
+ appliedLicense = true;
82
+ return { code: src, map: null }
83
+ }
84
+ // @TODO: detect local needle engine dev setup and log error if not found
85
+ }
86
+ },
87
+ buildEnd() {
88
+ if (!appliedLicense) {
89
+ if (process.env.NEEDLE_TEST_ENV) {
90
+ console.error("ERR: License was not applied!");
91
+ }
53
92
  }
54
93
  }
55
94
  }
@@ -291,7 +291,7 @@ export function needleMakeFilesLocal(command, _config, userSettings) {
291
291
  failedDownloads,
292
292
  localizationStats,
293
293
  }, activeHandlers);
294
- src = fixRelativeNewURL(src);
294
+ src = fixRelativeNewURL(src, viteConfig?.base);
295
295
  }
296
296
  catch (err) {
297
297
  needleLog("needle:local-files", "Error in transform: " + getErrMessage(err), "error");
@@ -303,7 +303,7 @@ export function needleMakeFilesLocal(command, _config, userSettings) {
303
303
  },
304
304
  renderChunk(code, chunk) {
305
305
  if (!chunk.fileName?.endsWith(".js")) return null;
306
- const fixed = fixRelativeNewURL(code);
306
+ const fixed = fixRelativeNewURL(code, viteConfig?.base);
307
307
  if (fixed === code) return null;
308
308
  return {
309
309
  code: fixed,
@@ -314,7 +314,7 @@ export function needleMakeFilesLocal(command, _config, userSettings) {
314
314
  for (const output of Object.values(bundle)) {
315
315
  if (output.type !== "chunk") continue;
316
316
  if (!output.fileName?.endsWith(".js")) continue;
317
- const fixed = fixRelativeNewURL(output.code);
317
+ const fixed = fixRelativeNewURL(output.code, viteConfig?.base);
318
318
  if (fixed !== output.code) output.code = fixed;
319
319
  }
320
320
  },
@@ -24,9 +24,11 @@ export function normalizeWebPath(path: string): string;
24
24
  export function ensureTrailingSlash(path: string): string;
25
25
  /**
26
26
  * @param {string} src
27
+ * @param {string} [base] - Vite base path (e.g. "/" or "/app/"). When provided,
28
+ * ext/ paths are made absolute so they resolve correctly under SPA routing.
27
29
  * @returns {string}
28
30
  */
29
- export function fixRelativeNewURL(src: string): string;
31
+ export function fixRelativeNewURL(src: string, base?: string): string;
30
32
  /**
31
33
  * @param {string} src
32
34
  * @returns {string}
@@ -113,25 +113,49 @@ export function ensureTrailingSlash(path) {
113
113
 
114
114
  /**
115
115
  * @param {string} src
116
+ * @param {string} [base] - Vite base path (e.g. "/" or "/app/"). When provided,
117
+ * ext/ paths are made absolute so they resolve correctly under SPA routing.
116
118
  * @returns {string}
117
119
  */
118
- export function fixRelativeNewURL(src) {
120
+ export function fixRelativeNewURL(src, base) {
121
+ /** @param {string} path */
122
+ function makeAbsolute(path) {
123
+ // Strip any leading ./ ../ or / prefix to get the clean ext/... path
124
+ const clean = path.replace(/^(?:\.\.?\/)+/, '').replace(/^\//, '');
125
+ if (base) return base.replace(/\/+$/, '') + '/' + clean;
126
+ return clean;
127
+ }
128
+
119
129
  src = src.replace(
120
130
  /(?<==\s*)(["'])((?:(?:\.{1,2}\/)|\/)?ext\/[^"']*\/)\1/g,
121
131
  (/** @type {string} */ _match, /** @type {string} */ quote, /** @type {string} */ path) => {
122
- const runtimePath = path.startsWith("/ext/") ? path.substring(1) : path;
123
- return `new URL(${quote}${runtimePath}${quote}, self.location?.href || ${quote}${quote}).href`;
132
+ const resolved = makeAbsolute(path);
133
+ return `new URL(${quote}${resolved}${quote}, self.location?.href || ${quote}${quote}).href`;
124
134
  }
125
135
  );
126
136
 
127
137
  src = src.replace(
128
138
  /new\s+URL\s*\(\s*(["'`])((?:(?:\.{1,2}\/)|\/)?ext\/[^"'`]+)\1\s*\)/g,
129
139
  (/** @type {string} */ _match, /** @type {string} */ quote, /** @type {string} */ path) => {
130
- const runtimePath = path.startsWith("/ext/") ? path.substring(1) : path;
131
- return `new URL(${quote}${runtimePath}${quote}, self.location?.href)`;
140
+ const resolved = makeAbsolute(path);
141
+ return `new URL(${quote}${resolved}${quote}, self.location?.href)`;
132
142
  }
133
143
  );
134
144
 
145
+ // Make remaining ext/ string literals absolute (e.g. font paths passed as
146
+ // function arguments like addVariant("normal","normal","ext/fonts/...")).
147
+ // The regexes above already consumed paths inside new URL() calls and
148
+ // assignment-based patterns, so this only catches leftover bare strings.
149
+ if (base) {
150
+ src = src.replace(
151
+ /(["'`])((?:\.{1,2}\/)?ext\/[^"'`]+)\1/g,
152
+ (/** @type {string} */ _match, /** @type {string} */ quote, /** @type {string} */ path) => {
153
+ const resolved = makeAbsolute(path);
154
+ return `${quote}${resolved}${quote}`;
155
+ }
156
+ );
157
+ }
158
+
135
159
  return src;
136
160
  }
137
161
 
@@ -225,7 +225,7 @@ async function scheduleReload(server, level = 0) {
225
225
  if (existsSync(lockFile)) {
226
226
  if (level === 0)
227
227
  needleLog(pluginName, "Lock file exists, waiting for export to finish...");
228
- setTimeout(() => scheduleReload(server, level += 1), 300);
228
+ setTimeout(() => scheduleReload(server, level += 1), 300).unref();
229
229
  return;
230
230
  }
231
231
 
@@ -60,7 +60,8 @@ export function needleServer(command, config, userSettings) {
60
60
  })
61
61
  .catch((err) => console.error("ERR: [needle:server] 'open' package not found - please make sure to install 'open' in your package.json\n", err));
62
62
  }
63
- }, 100)
63
+ }, 100);
64
+ i.unref();
64
65
  }
65
66
  },
66
67
  }
package/src/engine/api.ts CHANGED
@@ -114,6 +114,9 @@ export * from "./engine_addressables.js";
114
114
  /** Animation playback and control utilities */
115
115
  export { AnimationUtils } from "./engine_animation.js";
116
116
 
117
+ /** Standalone audio clip for playback control */
118
+ export { AudioClip } from "./engine_audio.js";
119
+
117
120
  /** Application-level state and utilities */
118
121
  export { Application } from "./engine_application.js";
119
122
 
@@ -164,6 +167,9 @@ export * from "./engine_constants.js";
164
167
  */
165
168
  export * from "./engine_context.js";
166
169
 
170
+ /** Typed event bus for decoupled component communication via {@link Context.events} */
171
+ export * from "./engine_context_eventbus.js";
172
+
167
173
  /** Registry for managing multiple engine contexts */
168
174
  export * from "./engine_context_registry.js";
169
175
 
@@ -173,6 +179,10 @@ export * from "./engine_context_registry.js";
173
179
  */
174
180
  export * from "./engine_coroutine.js"
175
181
 
182
+ /** DisposableStore for managing lifecycle-bound cleanup in components */
183
+ export { DisposableStore, on, isDisposable } from "./engine_disposable.js";
184
+ export type { DisposeFn, IDisposable } from "./engine_disposable.js";
185
+
176
186
  /** Factory functions for creating primitives and objects */
177
187
  export * from "./engine_create_objects.js";
178
188
 
@@ -220,7 +230,7 @@ export * from "./engine_input.js";
220
230
  export { InstancingUtil } from "./engine_instancing.js";
221
231
 
222
232
  /** License checking utilities */
223
- export { hasCommercialLicense, hasIndieLicense, hasProLicense } from "./engine_license.js";
233
+ export { _BGpnKD, __tFkl, UFadk } from "./engine_license.js";
224
234
 
225
235
 
226
236
  // ============================================================================
@@ -7,11 +7,6 @@ import { Animation } from "../../engine-components/Animation.js";
7
7
  import { Animator } from "../../engine-components/Animator.js";
8
8
  import { AudioListener } from "../../engine-components/AudioListener.js";
9
9
  import { AudioSource } from "../../engine-components/AudioSource.js";
10
- import { Avatar_Brain_LookAt } from "../../engine-components/avatar/Avatar_Brain_LookAt.js";
11
- import { Avatar_MouthShapes } from "../../engine-components/avatar/Avatar_MouthShapes.js";
12
- import { Avatar_MustacheShake } from "../../engine-components/avatar/Avatar_MustacheShake.js";
13
- import { AvatarBlink_Simple } from "../../engine-components/avatar/AvatarBlink_Simple.js";
14
- import { AvatarEyeLook_Rotation } from "../../engine-components/avatar/AvatarEyeLook_Rotation.js";
15
10
  import { AxesHelper } from "../../engine-components/AxesHelper.js";
16
11
  import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint.js";
17
12
  import { BoxHelperComponent } from "../../engine-components/BoxHelperComponent.js";
@@ -106,11 +101,12 @@ import { TestRunner } from "../../engine-components/TestRunner.js";
106
101
  import { TestSimulateUserData } from "../../engine-components/TestRunner.js";
107
102
  import { PlayableDirector } from "../../engine-components/timeline/PlayableDirector.js";
108
103
  import { SignalReceiver } from "../../engine-components/timeline/SignalAsset.js";
109
- import { AnimationTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
110
- import { AudioTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
111
- import { MarkerTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
104
+ import { TimelineAnimationTrack } from "../../engine-components/timeline/TimelineTracks.js";
105
+ import { TimelineAudioTrack } from "../../engine-components/timeline/TimelineTracks.js";
106
+ import { TimelineMarkerTrack } from "../../engine-components/timeline/TimelineTracks.js";
112
107
  import { SignalTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
113
- import { ControlTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
108
+ import { TimelineActivationTrack } from "../../engine-components/timeline/TimelineTracks.js";
109
+ import { TimelineControlTrack } from "../../engine-components/timeline/TimelineTracks.js";
114
110
  import { TransformGizmo } from "../../engine-components/TransformGizmo.js";
115
111
  import { BaseUIComponent } from "../../engine-components/ui/BaseUIComponent.js";
116
112
  import { UIRootComponent } from "../../engine-components/ui/BaseUIComponent.js";
@@ -167,11 +163,6 @@ export function initBuiltinTypes() {
167
163
  TypeStore.add("Animator", Animator);
168
164
  TypeStore.add("AudioListener", AudioListener);
169
165
  TypeStore.add("AudioSource", AudioSource);
170
- TypeStore.add("Avatar_Brain_LookAt", Avatar_Brain_LookAt);
171
- TypeStore.add("Avatar_MouthShapes", Avatar_MouthShapes);
172
- TypeStore.add("Avatar_MustacheShake", Avatar_MustacheShake);
173
- TypeStore.add("AvatarBlink_Simple", AvatarBlink_Simple);
174
- TypeStore.add("AvatarEyeLook_Rotation", AvatarEyeLook_Rotation);
175
166
  TypeStore.add("AxesHelper", AxesHelper);
176
167
  TypeStore.add("BasicIKConstraint", BasicIKConstraint);
177
168
  TypeStore.add("BoxHelperComponent", BoxHelperComponent);
@@ -266,11 +257,12 @@ export function initBuiltinTypes() {
266
257
  TypeStore.add("TestSimulateUserData", TestSimulateUserData);
267
258
  TypeStore.add("PlayableDirector", PlayableDirector);
268
259
  TypeStore.add("SignalReceiver", SignalReceiver);
269
- TypeStore.add("AnimationTrackHandler", AnimationTrackHandler);
270
- TypeStore.add("AudioTrackHandler", AudioTrackHandler);
271
- TypeStore.add("MarkerTrackHandler", MarkerTrackHandler);
260
+ TypeStore.add("TimelineAnimationTrack", TimelineAnimationTrack);
261
+ TypeStore.add("TimelineAudioTrack", TimelineAudioTrack);
262
+ TypeStore.add("TimelineMarkerTrack", TimelineMarkerTrack);
272
263
  TypeStore.add("SignalTrackHandler", SignalTrackHandler);
273
- TypeStore.add("ControlTrackHandler", ControlTrackHandler);
264
+ TypeStore.add("TimelineActivationTrack", TimelineActivationTrack);
265
+ TypeStore.add("TimelineControlTrack", TimelineControlTrack);
274
266
  TypeStore.add("TransformGizmo", TransformGizmo);
275
267
  TypeStore.add("BaseUIComponent", BaseUIComponent);
276
268
  TypeStore.add("UIRootComponent", UIRootComponent);
@@ -21,4 +21,188 @@ export function ensureAudioContextIsResumed() {
21
21
  }, 500);
22
22
  });
23
23
  });
24
+ }
25
+
26
+
27
+ /**
28
+ * Represents an audio clip that can be loaded and played independently.
29
+ * The AudioClip class encapsulates the URL of the audio resource and provides
30
+ * methods for playback control (play, pause, stop) and querying duration.
31
+ */
32
+ export class AudioClip {
33
+
34
+ /**
35
+ * Creates a new AudioClip instance with the specified URL.
36
+ * @param url The URL of the audio resource to load. This can be a path to an audio file or a MediaStream URL.
37
+ */
38
+ constructor(public readonly url: string) {
39
+ }
40
+
41
+ /** Whether the clip is currently playing.
42
+ * @returns `true` if the clip is actively playing audio.
43
+ */
44
+ get isPlaying(): boolean {
45
+ return this._audioElement !== undefined
46
+ && !this._audioElement.paused
47
+ && !this._audioElement.ended;
48
+ }
49
+
50
+ /**
51
+ * The total duration of the audio clip in seconds.
52
+ * Loads the audio metadata if not already available.
53
+ * @returns A promise that resolves with the duration in seconds.
54
+ */
55
+ getDuration(): Promise<number> {
56
+ if (this._duration !== undefined) {
57
+ return Promise.resolve(this._duration);
58
+ }
59
+ return this.ensureAudioElement().then(audio => {
60
+ this._duration = audio.duration;
61
+ return audio.duration;
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Plays the audio clip from the current position.
67
+ * @returns A promise that resolves when playback finishes, or rejects on error.
68
+ * If the clip is looping, the promise will never resolve on its own – call {@link stop} or {@link pause} to end playback.
69
+ */
70
+ // #region Play
71
+ play(): Promise<void> {
72
+ return this.ensureAudioElement().then(audio => {
73
+ return new Promise<void>((resolve, reject) => {
74
+ const onEnded = () => {
75
+ cleanup();
76
+ resolve();
77
+ };
78
+ const onError = () => {
79
+ cleanup();
80
+ reject(new Error(`Playback error for ${this.url}`));
81
+ };
82
+ const onPause = () => {
83
+ // pause/stop also resolve the promise
84
+ cleanup();
85
+ resolve();
86
+ };
87
+ const cleanup = () => {
88
+ audio.removeEventListener("ended", onEnded);
89
+ audio.removeEventListener("error", onError);
90
+ audio.removeEventListener("pause", onPause);
91
+ };
92
+ audio.addEventListener("ended", onEnded);
93
+ audio.addEventListener("error", onError);
94
+ audio.addEventListener("pause", onPause);
95
+ audio.play().catch(err => {
96
+ cleanup();
97
+ reject(err);
98
+ });
99
+ });
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Pauses playback at the current position.
105
+ * Call {@link play} to resume.
106
+ */
107
+ // #region Pause/Stop
108
+ pause(): void {
109
+ this._audioElement?.pause();
110
+ }
111
+
112
+ /**
113
+ * Stops playback and resets the position to the beginning.
114
+ */
115
+ stop(): void {
116
+ if (this._audioElement) {
117
+ this._audioElement.pause();
118
+ this._audioElement.currentTime = 0;
119
+ }
120
+ }
121
+
122
+ /** Whether the clip should loop when reaching the end. */
123
+ get loop(): boolean { return this._loop; }
124
+ set loop(value: boolean) {
125
+ this._loop = value;
126
+ if (this._audioElement) this._audioElement.loop = value;
127
+ }
128
+
129
+ /** Playback volume from 0 (silent) to 1 (full). */
130
+ get volume(): number { return this._volume; }
131
+ set volume(value: number) {
132
+ this._volume = value;
133
+ if (this._audioElement) this._audioElement.volume = value;
134
+ }
135
+
136
+ /** Current playback position in seconds. */
137
+ get currentTime(): number { return this._audioElement?.currentTime ?? 0; }
138
+ set currentTime(value: number) {
139
+ if (this._audioElement) this._audioElement.currentTime = value;
140
+ }
141
+
142
+ /** Normalized playback progress from 0 to 1.
143
+ * @returns The current playback position as a value between 0 and 1, or 0 if the duration is unknown.
144
+ */
145
+ get progress(): number {
146
+ if (!this._audioElement || !this._duration) return 0;
147
+ return this._audioElement.currentTime / this._duration;
148
+ }
149
+
150
+ /**
151
+ * Seeks to a normalized position (0–1) in the clip.
152
+ * @param position A value between 0 (start) and 1 (end).
153
+ */
154
+ // #region Seek
155
+ seek(position: number): void {
156
+ if (this._audioElement && this._duration) {
157
+ this._audioElement.currentTime = Math.max(0, Math.min(1, position)) * this._duration;
158
+ }
159
+ }
160
+
161
+ /** The underlying HTMLAudioElement, or `undefined` if not yet created.
162
+ * Use this to connect the element to the Web Audio API via `createMediaElementSource()`.
163
+ * @returns The HTMLAudioElement if the clip has been loaded or played, otherwise `undefined`.
164
+ */
165
+ get audioElement(): HTMLAudioElement | undefined { return this._audioElement; }
166
+
167
+ private _audioElement?: HTMLAudioElement;
168
+ private _duration?: number;
169
+ private _loadPromise?: Promise<HTMLAudioElement>;
170
+ private _loop: boolean = false;
171
+ private _volume: number = 1;
172
+
173
+ /** Lazily creates and loads the shared HTMLAudioElement. */
174
+ private ensureAudioElement(): Promise<HTMLAudioElement> {
175
+ if (this._audioElement && this._loadPromise) {
176
+ return this._loadPromise;
177
+ }
178
+ const audio = this._audioElement ?? new Audio(this.url);
179
+ this._audioElement = audio;
180
+ audio.loop = this._loop;
181
+ audio.volume = this._volume;
182
+
183
+ if (audio.readyState >= HTMLMediaElement.HAVE_METADATA) {
184
+ this._duration = audio.duration;
185
+ this._loadPromise = Promise.resolve(audio);
186
+ return this._loadPromise;
187
+ }
188
+
189
+ this._loadPromise = new Promise<HTMLAudioElement>((resolve, reject) => {
190
+ const onLoaded = () => {
191
+ cleanup();
192
+ this._duration = audio.duration;
193
+ resolve(audio);
194
+ };
195
+ const onError = (e: Event) => {
196
+ cleanup();
197
+ reject(new Error(`Failed to load audio clip from ${this.url}: ${e}`));
198
+ };
199
+ const cleanup = () => {
200
+ audio.removeEventListener("loadedmetadata", onLoaded);
201
+ audio.removeEventListener("error", onError);
202
+ };
203
+ audio.addEventListener("loadedmetadata", onLoaded);
204
+ audio.addEventListener("error", onError);
205
+ });
206
+ return this._loadPromise;
207
+ }
24
208
  }
@@ -257,12 +257,23 @@ export function fitCamera(options?: FitCameraOptions): null | FitCameraReturnTyp
257
257
  else {
258
258
  direction.sub(camera.worldPosition);
259
259
  }
260
- if (centerCamera === "y")
261
- direction.y = 0;
260
+ if (centerCamera === "y") {
261
+ // Preserve the camera's current elevation angle when it's already above the center,
262
+ // but clamp to a minimum elevation to prevent the camera from ending up at or below
263
+ // the scene center (which causes a "looking up from below" effect).
264
+ // direction points FROM camera TO center, so negative Y = camera is above center.
265
+ const horizontalLen = Math.sqrt(direction.x * direction.x + direction.z * direction.z);
266
+ if (horizontalLen > 0.0001) {
267
+ const minY = -horizontalLen * verticalOffset * 4;
268
+ if (direction.y > minY) direction.y = minY;
269
+ }
270
+ else {
271
+ // Camera is directly above/below center — pick a default slight angle from +Z
272
+ direction.set(0, -verticalOffset * 4, 1);
273
+ }
274
+ }
262
275
  direction.normalize();
263
276
  direction.multiplyScalar(distance);
264
- if (centerCamera === "y")
265
- direction.y += -verticalOffset * 4 * distance;
266
277
 
267
278
  let cameraLocalPosition = center.clone().sub(direction);
268
279
  if (options.cameraOffset) {
@@ -8,9 +8,9 @@ import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
8
8
  import { Context, registerComponent } from "./engine_setup.js";
9
9
  import type { ComponentInit, Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
10
10
  import { $componentName } from "./engine_types.js";
11
+ import { TypeStore } from "./engine_typestore.js";
11
12
  import { getParam } from "./engine_utils.js";
12
13
  import { apply } from "./js-extensions/index.js";
13
- import { TypeStore } from "./engine_typestore.js";
14
14
 
15
15
  const COMPONENT_GUID_NAMESPACE = 'eff8ba80-635d-11ec-90d6-0242ac120003';
16
16