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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/.needle/generated/needle-bindings.gen.d.ts +5 -0
  2. package/CHANGELOG.md +52 -0
  3. package/components.needle.json +1 -1
  4. package/dist/{gltf-progressive-DJBMx-zB.umd.cjs → gltf-progressive-BmblPzFj.umd.cjs} +4 -4
  5. package/dist/{gltf-progressive-BryRjllq.min.js → gltf-progressive-CN_mbb66.min.js} +2 -2
  6. package/dist/{gltf-progressive-Cl167Vjx.js → gltf-progressive-DUlhxdv4.js} +5 -2
  7. package/dist/needle-engine.bundle-C-ixARur.umd.cjs +1733 -0
  8. package/dist/needle-engine.bundle-CHmXdnE1.min.js +1733 -0
  9. package/dist/{needle-engine.bundle-BGyKqxBH.js → needle-engine.bundle-DF01sSGQ.js} +10841 -10434
  10. package/dist/needle-engine.d.ts +244 -54
  11. package/dist/needle-engine.js +541 -536
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/dist/{postprocessing-B_9sKVU7.min.js → postprocessing-B571qGWR.min.js} +34 -34
  15. package/dist/{postprocessing-WDc9WwI3.js → postprocessing-CfrLAbLX.js} +0 -1
  16. package/dist/{postprocessing-B2wb6pzI.umd.cjs → postprocessing-CiGkAeM9.umd.cjs} +17 -17
  17. package/dist/{vendor-CAcsI0eU.js → vendor-BFrMaK9q.js} +8983 -9136
  18. package/dist/vendor-CJmyOrCq.min.js +1116 -0
  19. package/dist/vendor-DkMW3WY4.umd.cjs +1116 -0
  20. package/lib/engine/api.d.ts +14 -0
  21. package/lib/engine/api.js +4 -0
  22. package/lib/engine/api.js.map +1 -1
  23. package/lib/engine/debug/debug_environment.js +1 -1
  24. package/lib/engine/debug/debug_environment.js.map +1 -1
  25. package/lib/engine/debug/debug_spatial_console.d.ts +2 -0
  26. package/lib/engine/debug/debug_spatial_console.js +10 -7
  27. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  28. package/lib/engine/engine_addressables.d.ts +2 -0
  29. package/lib/engine/engine_addressables.js +6 -3
  30. package/lib/engine/engine_addressables.js.map +1 -1
  31. package/lib/engine/engine_application.js +8 -6
  32. package/lib/engine/engine_application.js.map +1 -1
  33. package/lib/engine/engine_audio.d.ts +68 -0
  34. package/lib/engine/engine_audio.js +172 -0
  35. package/lib/engine/engine_audio.js.map +1 -1
  36. package/lib/engine/engine_components.js +1 -1
  37. package/lib/engine/engine_components.js.map +1 -1
  38. package/lib/engine/engine_constants.js +6 -0
  39. package/lib/engine/engine_constants.js.map +1 -1
  40. package/lib/engine/engine_context.d.ts +33 -7
  41. package/lib/engine/engine_context.js +40 -2
  42. package/lib/engine/engine_context.js.map +1 -1
  43. package/lib/engine/engine_context_registry.js +1 -1
  44. package/lib/engine/engine_context_registry.js.map +1 -1
  45. package/lib/engine/engine_gameobject.js +2 -2
  46. package/lib/engine/engine_gameobject.js.map +1 -1
  47. package/lib/engine/engine_init.js +16 -1
  48. package/lib/engine/engine_init.js.map +1 -1
  49. package/lib/engine/engine_input.d.ts +3 -2
  50. package/lib/engine/engine_input.js +3 -2
  51. package/lib/engine/engine_input.js.map +1 -1
  52. package/lib/engine/engine_license.d.ts +2 -0
  53. package/lib/engine/engine_license.js +25 -15
  54. package/lib/engine/engine_license.js.map +1 -1
  55. package/lib/engine/engine_lifecycle_functions_internal.js +5 -0
  56. package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
  57. package/lib/engine/engine_mainloop_utils.js +5 -2
  58. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  59. package/lib/engine/engine_networking_blob.d.ts +1 -1
  60. package/lib/engine/engine_networking_blob.js +5 -11
  61. package/lib/engine/engine_networking_blob.js.map +1 -1
  62. package/lib/engine/engine_physics_rapier.js +0 -1
  63. package/lib/engine/engine_physics_rapier.js.map +1 -1
  64. package/lib/engine/engine_pmrem.js +2 -2
  65. package/lib/engine/engine_pmrem.js.map +1 -1
  66. package/lib/engine/engine_scenedata.d.ts +32 -0
  67. package/lib/engine/engine_scenedata.js +138 -0
  68. package/lib/engine/engine_scenedata.js.map +1 -0
  69. package/lib/engine/engine_serialization_builtin_serializer.d.ts +10 -16
  70. package/lib/engine/engine_serialization_builtin_serializer.js +55 -41
  71. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  72. package/lib/engine/engine_ssr.d.ts +18 -0
  73. package/lib/engine/engine_ssr.js +40 -0
  74. package/lib/engine/engine_ssr.js.map +1 -0
  75. package/lib/engine/engine_three_utils.d.ts +14 -7
  76. package/lib/engine/engine_three_utils.js +14 -7
  77. package/lib/engine/engine_three_utils.js.map +1 -1
  78. package/lib/engine/engine_types.d.ts +2 -0
  79. package/lib/engine/engine_types.js.map +1 -1
  80. package/lib/engine/engine_utils.js +2 -0
  81. package/lib/engine/engine_utils.js.map +1 -1
  82. package/lib/engine/engine_utils_hash.d.ts +9 -0
  83. package/lib/engine/engine_utils_hash.js +112 -0
  84. package/lib/engine/engine_utils_hash.js.map +1 -0
  85. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  86. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  87. package/lib/engine/webcomponents/jsx.d.ts +51 -0
  88. package/lib/engine/webcomponents/logo-element.d.ts +2 -1
  89. package/lib/engine/webcomponents/logo-element.js +2 -1
  90. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  91. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +4 -4
  92. package/lib/engine/webcomponents/needle menu/needle-menu.js +2 -1
  93. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  94. package/lib/engine/webcomponents/needle-button.d.ts +2 -1
  95. package/lib/engine/webcomponents/needle-button.js +2 -1
  96. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  97. package/lib/engine/webcomponents/needle-engine.d.ts +11 -4
  98. package/lib/engine/webcomponents/needle-engine.js +2 -1
  99. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  100. package/lib/engine/xr/NeedleXRSession.d.ts +3 -2
  101. package/lib/engine/xr/NeedleXRSession.js +51 -15
  102. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  103. package/lib/engine/xr/events.d.ts +1 -1
  104. package/lib/engine/xr/events.js.map +1 -1
  105. package/lib/engine-components/Animation.js +17 -16
  106. package/lib/engine-components/Animation.js.map +1 -1
  107. package/lib/engine-components/AnimatorController.d.ts +2 -0
  108. package/lib/engine-components/AnimatorController.js +4 -1
  109. package/lib/engine-components/AnimatorController.js.map +1 -1
  110. package/lib/engine-components/AudioSource.d.ts +19 -3
  111. package/lib/engine-components/AudioSource.js +121 -68
  112. package/lib/engine-components/AudioSource.js.map +1 -1
  113. package/lib/engine-components/DragControls.d.ts +7 -0
  114. package/lib/engine-components/DragControls.js +19 -0
  115. package/lib/engine-components/DragControls.js.map +1 -1
  116. package/lib/engine-components/Light.d.ts +6 -8
  117. package/lib/engine-components/Light.js +40 -27
  118. package/lib/engine-components/Light.js.map +1 -1
  119. package/lib/engine-components/Networking.d.ts +1 -1
  120. package/lib/engine-components/Networking.js +1 -1
  121. package/lib/engine-components/OrbitControls.js +16 -11
  122. package/lib/engine-components/OrbitControls.js.map +1 -1
  123. package/lib/engine-components/ReflectionProbe.js +2 -0
  124. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  125. package/lib/engine-components/RigidBody.js +3 -3
  126. package/lib/engine-components/RigidBody.js.map +1 -1
  127. package/lib/engine-components/SceneSwitcher.js +2 -0
  128. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  129. package/lib/engine-components/SeeThrough.js +2 -2
  130. package/lib/engine-components/SeeThrough.js.map +1 -1
  131. package/lib/engine-components/api.d.ts +1 -1
  132. package/lib/engine-components/api.js +1 -1
  133. package/lib/engine-components/api.js.map +1 -1
  134. package/lib/engine-components/postprocessing/Effects/BloomEffect.d.ts +1 -1
  135. package/lib/engine-components/postprocessing/Effects/Sharpening.js +1 -2
  136. package/lib/engine-components/postprocessing/Effects/Sharpening.js.map +1 -1
  137. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  138. package/lib/engine-components/postprocessing/PostProcessingHandler.js +5 -6
  139. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  140. package/lib/engine-components/postprocessing/VolumeParameter.d.ts +2 -0
  141. package/lib/engine-components/postprocessing/VolumeParameter.js +4 -1
  142. package/lib/engine-components/postprocessing/VolumeParameter.js.map +1 -1
  143. package/lib/engine-components/ui/Canvas.d.ts +1 -1
  144. package/lib/engine-components/ui/Canvas.js +2 -8
  145. package/lib/engine-components/ui/Canvas.js.map +1 -1
  146. package/lib/engine-components/ui/Text.d.ts +1 -0
  147. package/lib/engine-components/ui/Text.js +10 -7
  148. package/lib/engine-components/ui/Text.js.map +1 -1
  149. package/lib/engine-components/web/CursorFollow.js +21 -12
  150. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  151. package/lib/engine-components/web/ScrollFollow.d.ts +0 -1
  152. package/lib/engine-components/web/ScrollFollow.js +3 -2
  153. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  154. package/lib/engine-components/webxr/WebXRImageTracking.js +4 -0
  155. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  156. package/lib/needle-engine.d.ts +2 -0
  157. package/lib/needle-engine.js +2 -0
  158. package/lib/needle-engine.js.map +1 -1
  159. package/package.json +6 -4
  160. package/plugins/dts-generator/dts.codegen.js +334 -0
  161. package/plugins/dts-generator/dts.scan.js +99 -0
  162. package/plugins/dts-generator/dts.writer.js +59 -0
  163. package/plugins/dts-generator/glb.discovery.js +279 -0
  164. package/plugins/dts-generator/glb.extractor.js +215 -0
  165. package/plugins/dts-generator/glb.reader.js +167 -0
  166. package/plugins/dts-generator/index.js +36 -0
  167. package/plugins/dts-generator/manifest.types.js +174 -0
  168. package/plugins/types/index.d.ts +2 -1
  169. package/plugins/types/needle-bindings.d.ts +30 -0
  170. package/plugins/types/userconfig.d.ts +21 -2
  171. package/plugins/vite/asap.js +18 -9
  172. package/plugins/vite/dependencies.js +29 -0
  173. package/plugins/vite/dependency-watcher.d.ts +2 -2
  174. package/plugins/vite/dependency-watcher.js +3 -4
  175. package/plugins/vite/drop.d.ts +2 -2
  176. package/plugins/vite/drop.js +3 -4
  177. package/plugins/vite/dts-generator.d.ts +7 -0
  178. package/plugins/vite/dts-generator.js +191 -0
  179. package/plugins/vite/index.d.ts +10 -3
  180. package/plugins/vite/index.js +27 -10
  181. package/plugins/vite/local-files-core.js +3 -3
  182. package/plugins/vite/local-files-utils.d.ts +3 -1
  183. package/plugins/vite/local-files-utils.js +29 -5
  184. package/plugins/vite/logging.js +2 -2
  185. package/plugins/vite/meta.js +4 -2
  186. package/plugins/vite/poster.d.ts +2 -2
  187. package/plugins/vite/poster.js +3 -5
  188. package/plugins/vite/reload.d.ts +2 -2
  189. package/plugins/vite/reload.js +23 -22
  190. package/src/engine/api.ts +18 -1
  191. package/src/engine/debug/debug_environment.ts +1 -1
  192. package/src/engine/debug/debug_spatial_console.ts +10 -7
  193. package/src/engine/engine_addressables.ts +6 -3
  194. package/src/engine/engine_application.ts +8 -6
  195. package/src/engine/engine_audio.ts +184 -0
  196. package/src/engine/engine_components.ts +1 -1
  197. package/src/engine/engine_constants.ts +11 -6
  198. package/src/engine/engine_context.ts +50 -7
  199. package/src/engine/engine_context_registry.ts +1 -1
  200. package/src/engine/engine_gameobject.ts +2 -2
  201. package/src/engine/engine_init.ts +15 -1
  202. package/src/engine/engine_input.ts +3 -2
  203. package/src/engine/engine_license.ts +23 -19
  204. package/src/engine/engine_lifecycle_functions_internal.ts +7 -0
  205. package/src/engine/engine_mainloop_utils.ts +5 -2
  206. package/src/engine/engine_networking_blob.ts +5 -11
  207. package/src/engine/engine_physics_rapier.ts +0 -3
  208. package/src/engine/engine_pmrem.ts +3 -3
  209. package/src/engine/engine_scenedata.ts +136 -0
  210. package/src/engine/engine_serialization_builtin_serializer.ts +63 -46
  211. package/src/engine/engine_ssr.ts +48 -0
  212. package/src/engine/engine_three_utils.ts +15 -7
  213. package/src/engine/engine_types.ts +2 -0
  214. package/src/engine/engine_utils.ts +1 -0
  215. package/src/engine/engine_utils_hash.ts +65 -0
  216. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  217. package/src/engine/webcomponents/jsx.d.ts +51 -0
  218. package/src/engine/webcomponents/logo-element.ts +3 -1
  219. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -2
  220. package/src/engine/webcomponents/needle-button.ts +3 -1
  221. package/src/engine/webcomponents/needle-engine.ts +12 -4
  222. package/src/engine/xr/NeedleXRSession.ts +49 -14
  223. package/src/engine/xr/events.ts +1 -1
  224. package/src/engine-components/Animation.ts +19 -16
  225. package/src/engine-components/AnimatorController.ts +4 -1
  226. package/src/engine-components/AudioSource.ts +130 -79
  227. package/src/engine-components/DragControls.ts +18 -2
  228. package/src/engine-components/Light.ts +40 -26
  229. package/src/engine-components/Networking.ts +1 -1
  230. package/src/engine-components/OrbitControls.ts +18 -9
  231. package/src/engine-components/ReflectionProbe.ts +2 -0
  232. package/src/engine-components/RigidBody.ts +3 -3
  233. package/src/engine-components/SceneSwitcher.ts +1 -0
  234. package/src/engine-components/SeeThrough.ts +2 -2
  235. package/src/engine-components/api.ts +1 -1
  236. package/src/engine-components/postprocessing/Effects/BloomEffect.ts +1 -1
  237. package/src/engine-components/postprocessing/Effects/Sharpening.ts +1 -2
  238. package/src/engine-components/postprocessing/PostProcessingHandler.ts +4 -8
  239. package/src/engine-components/postprocessing/VolumeParameter.ts +4 -1
  240. package/src/engine-components/ui/Canvas.ts +2 -8
  241. package/src/engine-components/ui/Text.ts +12 -8
  242. package/src/engine-components/web/CursorFollow.ts +21 -13
  243. package/src/engine-components/web/ScrollFollow.ts +2 -2
  244. package/src/engine-components/webxr/WebXRImageTracking.ts +2 -0
  245. package/src/needle-engine.ts +3 -0
  246. package/src/vite-env.d.ts +16 -0
  247. package/dist/needle-engine.bundle-CiYtOO2O.min.js +0 -1732
  248. package/dist/needle-engine.bundle-DzVx9Z8D.umd.cjs +0 -1732
  249. package/dist/vendor-CEM38hLE.umd.cjs +0 -1116
  250. package/dist/vendor-HRlxIBga.min.js +0 -1116
@@ -4,7 +4,6 @@ import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudi
4
4
  import { isDevEnvironment } from "../engine/debug/index.js";
5
5
  import { Application, ApplicationEvents } from "../engine/engine_application.js";
6
6
  import { findObjectOfType } from "../engine/engine_components.js";
7
- import { Mathf } from "../engine/engine_math.js";
8
7
  import { serializable } from "../engine/engine_serialization_decorator.js";
9
8
  import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
10
9
  import { AudioListener } from "./AudioListener.js";
@@ -140,12 +139,23 @@ export class AudioSource extends Behaviour {
140
139
  get isPlaying(): boolean { return this.sound?.isPlaying ?? false; }
141
140
 
142
141
  /**
143
- * The total duration of the current audio clip in seconds.
142
+ * The total duration of the currently loaded audio clip in seconds.
144
143
  *
145
144
  * @returns Duration in seconds or undefined if no clip is loaded
145
+ * @remarks For MediaStream clips, duration is not directly available and will return undefined. If the audio clip has not started loading or is still loading, duration may also be undefined until the audio buffer is ready.
146
146
  */
147
147
  get duration() {
148
- return this.sound?.buffer?.duration;
148
+ if (this.sound?.buffer?.duration) {
149
+ return this.sound.buffer.duration;
150
+ }
151
+ else if (this._audioElement?.duration) {
152
+ return this._audioElement.duration;
153
+ }
154
+ else if (this._loadedClip instanceof MediaStream) {
155
+ // MediaStream duration is not directly available; return undefined or estimate if possible
156
+ return undefined;
157
+ }
158
+ return undefined;
149
159
  }
150
160
 
151
161
  /**
@@ -200,7 +210,8 @@ export class AudioSource extends Behaviour {
200
210
  /**
201
211
  * Controls how the audio is positioned in space.
202
212
  * Values range from 0 (2D, non-positional) to 1 (fully 3D positioned).
203
- * Note: 2D playback is not fully supported in the current implementation.
213
+ * Internally uses a dual-path audio graph to crossfade between a spatialized (PannerNode)
214
+ * and a non-spatialized (direct) signal path.
204
215
  */
205
216
  @serializable()
206
217
  get spatialBlend(): number {
@@ -292,12 +303,18 @@ export class AudioSource extends Behaviour {
292
303
  // set this from audio context time, used to set clip offset when setting "time" property
293
304
  // there is maybe a better way to set a audio clip current time?!
294
305
  private _lastClipStartedLoading: string | MediaStream | null = null;
306
+ private _loadedClip: string | MediaStream | null = null;
295
307
  private _audioElement: HTMLAudioElement | null = null;
296
308
 
309
+ // Spatial blend dual-path audio nodes
310
+ private _entryNode: GainNode | null = null;
311
+ private _spatialGain: GainNode | null = null;
312
+ private _bypassGain: GainNode | null = null;
313
+
297
314
  /**
298
315
  * Returns the underlying {@link PositionalAudio} object, creating it if necessary.
299
316
  * The audio source needs a user interaction to be initialized due to browser autoplay policies.
300
- *
317
+ *
301
318
  * @returns The three.js PositionalAudio object or null if unavailable
302
319
  */
303
320
  public get Sound(): PositionalAudio | null {
@@ -312,45 +329,12 @@ export class AudioSource extends Behaviour {
312
329
  if (listener?.listener) {
313
330
  this.sound = new PositionalAudio(listener.listener);
314
331
  this.gameObject?.add(this.sound);
315
-
316
- // this._listener = listener;
317
- // this._originalSoundMatrixWorldFunction = this.sound.updateMatrixWorld;
318
- // this.sound.updateMatrixWorld = this._onSoundMatrixWorld;
319
332
  }
320
333
  else if (debug) console.warn("No audio listener found in scene - can not play audio");
321
334
  }
322
335
  return this.sound;
323
336
  }
324
337
 
325
- // This is a hacky workaround to get the PositionalAudio behave like a 2D audio source
326
- // private _listener: AudioListener | null = null;
327
- // private _originalSoundMatrixWorldFunction: Function | null = null;
328
- // private _onSoundMatrixWorld = (force: boolean) => {
329
- // if (this._spatialBlend > .05) {
330
- // if (this._originalSoundMatrixWorldFunction) {
331
- // this._originalSoundMatrixWorldFunction.call(this.sound, force);
332
- // }
333
- // }
334
- // else {
335
- // // we use another object's matrix world function (but bound to the positional audio)
336
- // // this is just a little trick to prevent calling the PositionalAudio's updateMatrixWorld function
337
- // this.gameObject.updateMatrixWorld?.call(this.sound, force);
338
- // if (this.sound && this._listener) {
339
- // this.sound.gain.connect(this._listener.listener.getInput());
340
- // // const pos = getTempVector().setFromMatrixPosition(this._listener.gameObject.matrixWorld);
341
- // // const ctx = this.sound.context;
342
- // // const delay = this._listener.listener.timeDelta;
343
- // // const time = ctx.currentTime ;
344
- // // this.sound.panner.positionX.setValueAtTime(pos.x, time);
345
- // // this.sound.panner.positionY.setValueAtTime(pos.y, time);
346
- // // this.sound.panner.positionZ.setValueAtTime(pos.z, time);
347
- // // this.sound.panner.orientationX.setValueAtTime(0, time);
348
- // // this.sound.panner.orientationY.setValueAtTime(0, time);
349
- // // this.sound.panner.orientationZ.setValueAtTime(-1, time);
350
- // }
351
- // }
352
- // }
353
-
354
338
  /**
355
339
  * Indicates whether the audio source is queued to play when possible.
356
340
  * This may be true before user interaction has been registered.
@@ -376,8 +360,10 @@ export class AudioSource extends Behaviour {
376
360
  if (this.playOnAwake) this.shouldPlay = true;
377
361
 
378
362
  if (this.preload) {
379
- if (typeof this.clip === "string") {
380
- this.audioLoader.load(this.clip, this.createAudio, () => { }, console.error);
363
+ if (typeof this.clip === "string" && this.clip.length > 0) {
364
+ this.audioLoader.load(this.clip, this.createAudio, () => { }, e => {
365
+ console.error(`[AudioSource] Error preloading audio clip "${this.clip}":`, e);
366
+ });
381
367
  }
382
368
  }
383
369
  }
@@ -433,9 +419,9 @@ export class AudioSource extends Behaviour {
433
419
  this.sound?.setVolume(this.volume);
434
420
  }
435
421
 
436
- private createAudio = (buffer?: AudioBuffer) => {
437
- if(this.destroyed) {
438
- if(debug) console.warn("AudioSource destroyed, not creating audio", this.name);
422
+ private createAudio = (buffer?: AudioBuffer) => {
423
+ if (this.destroyed) {
424
+ if (debug) console.warn("AudioSource destroyed, not creating audio", this.name);
439
425
  return;
440
426
  }
441
427
 
@@ -451,26 +437,17 @@ export class AudioSource extends Behaviour {
451
437
  sound.stop();
452
438
 
453
439
  if (buffer) sound.setBuffer(buffer);
440
+ this._loadedClip = this.clip;
454
441
  sound.loop = this._loop;
455
442
  if (this.context.application.muted) sound.setVolume(0);
456
443
  else sound.setVolume(this.volume);
457
444
  sound.autoplay = this.shouldPlay && AudioSource.userInteractionRegistered;
445
+ this.setupSpatialBlendNodes();
458
446
  this.applySpatialDistanceSettings();
459
447
 
460
448
  if (sound.isPlaying)
461
449
  sound.stop();
462
450
 
463
- // const panner = sound.panner;
464
- // panner.coneOuterGain = 1;
465
- // sound.setDirectionalCone(360, 360, 1);
466
- // const src = sound.context.createBufferSource();
467
- // src.buffer = sound.buffer;
468
- // src.connect(sound.panner);
469
- // src.start(this.audioContext?.currentTime);
470
- // const gain = sound.context.createGain();
471
- // gain.gain.value = 1 - this.spatialBlend;
472
- // src.connect(gain);
473
-
474
451
  // make sure we only play the sound if the user has interacted with the page
475
452
  AudioSource.registerWaitForAllowAudio(this.__onAllowAudioCallback);
476
453
  }
@@ -479,30 +456,97 @@ export class AudioSource extends Behaviour {
479
456
  this.play();
480
457
  }
481
458
 
459
+ /**
460
+ * Sets up the dual-path audio graph for spatial blend crossfading.
461
+ * Creates two parallel signal paths from source to output:
462
+ * - 3D path: entry → panner → spatialGain → gain (spatialized)
463
+ * - 2D path: entry → bypassGain → gain (non-spatialized)
464
+ */
465
+ private setupSpatialBlendNodes() {
466
+ const sound = this.sound;
467
+ if (!sound || this._entryNode) return;
468
+
469
+ const ctx = sound.context;
470
+ this._entryNode = ctx.createGain();
471
+ this._spatialGain = ctx.createGain();
472
+ this._bypassGain = ctx.createGain();
473
+
474
+ // Disconnect the default panner → gain connection set up by three.js
475
+ try { sound.panner.disconnect(sound.gain); } catch { /* not connected */ }
476
+
477
+ // 3D path: entry → panner → spatialGain → gain
478
+ this._entryNode.connect(sound.panner);
479
+ sound.panner.connect(this._spatialGain);
480
+ this._spatialGain.connect(sound.gain);
481
+
482
+ // 2D path: entry → bypassGain → gain
483
+ this._entryNode.connect(this._bypassGain);
484
+ this._bypassGain.connect(sound.gain);
485
+
486
+ // Override getOutput so three.js connects source → entryNode instead of panner
487
+ sound.getOutput = () => this._entryNode! as unknown as PannerNode;
488
+
489
+ // Wrap connect() to undo PositionalAudio's redundant panner → gain reconnection
490
+ const origConnect = sound.connect;
491
+ sound.connect = function () {
492
+ origConnect.call(sound);
493
+ try { sound.panner.disconnect(sound.gain); } catch { /* expected */ }
494
+ return sound;
495
+ };
496
+
497
+ // Wrap disconnect() to prevent error when panner → gain doesn't exist
498
+ const origDisconnect = sound.disconnect;
499
+ sound.disconnect = function () {
500
+ try { origDisconnect.call(sound); } catch { /* panner → gain already removed */ }
501
+ return sound;
502
+ };
503
+
504
+ this.updateSpatialBlendGains();
505
+ }
506
+
507
+ /** Updates the spatial/bypass gain values based on the current spatialBlend. */
508
+ private updateSpatialBlendGains() {
509
+ if (!this._spatialGain || !this._bypassGain || !this.sound) return;
510
+ const t = this.sound.context.currentTime;
511
+ // Use setTargetAtTime for smooth transitions (avoids clicks/pops)
512
+ this._spatialGain.gain.setTargetAtTime(this._spatialBlend, t, 0.01);
513
+ this._bypassGain.gain.setTargetAtTime(1 - this._spatialBlend, t, 0.01);
514
+ }
515
+
482
516
  private applySpatialDistanceSettings() {
483
517
  const sound = this.sound;
484
518
  if (!sound) return;
485
519
  this._needUpdateSpatialDistanceSettings = false;
486
- const dist = Mathf.lerp(10 * this._maxDistance / Math.max(0.0001, this.spatialBlend), this._minDistance, this.spatialBlend);
487
- if (debug) console.log(this.name, this._minDistance, this._maxDistance, this.spatialBlend, "Ref distance=" + dist);
488
- sound.setRefDistance(dist);
489
- sound.setMaxDistance(Math.max(0.01, this._maxDistance));
490
- // sound.setRolloffFactor(this.spatialBlend);
491
- // sound.panner.positionZ.automationRate
520
+
521
+ const ref = Math.max(0.0001, this._minDistance);
522
+ const max = Math.max(0.01, this._maxDistance);
523
+ if (debug) console.log(this.name, ref, max, this.spatialBlend);
524
+ sound.setRefDistance(ref);
525
+ sound.setMaxDistance(max);
492
526
 
493
527
  // https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel
494
528
  switch (this.rollOffMode) {
495
529
  case AudioRolloffMode.Logarithmic:
496
- sound.setDistanceModel('exponential');
530
+ // Unity's logarithmic rolloff matches Web Audio's 'inverse' model:
531
+ // gain = refDistance / (refDistance + rolloffFactor * (distance - refDistance))
532
+ // We compute rolloffFactor so gain reaches ~0.01 (-40dB) at maxDistance,
533
+ // matching Unity's behavior of effective silence at maxDistance.
534
+ sound.setDistanceModel('inverse');
535
+ sound.setRolloffFactor(99 * ref / Math.max(0.001, max - ref));
497
536
  break;
498
537
  case AudioRolloffMode.Linear:
538
+ // gain = 1 - rolloffFactor * (distance - refDistance) / (maxDistance - refDistance)
539
+ // With rolloffFactor=1 this reaches exactly 0 at maxDistance.
499
540
  sound.setDistanceModel('linear');
541
+ sound.setRolloffFactor(1);
500
542
  break;
501
543
  case AudioRolloffMode.Custom:
502
544
  console.warn("Custom rolloff for AudioSource is not supported: " + this.name);
503
545
  break;
504
546
  }
505
547
 
548
+ this.updateSpatialBlendGains();
549
+
506
550
  if (this.spatialBlend > 0) {
507
551
  if (debug && !this.helper) {
508
552
  this.helper = new PositionalAudioHelper(sound, sound.getRefDistance());
@@ -519,7 +563,7 @@ export class AudioSource extends Behaviour {
519
563
  if (typeof clip === "string") {
520
564
  if (debug)
521
565
  console.log(clip);
522
- if (clip.endsWith(".mp3") || clip.endsWith(".wav")) {
566
+ if (clip.endsWith(".mp3") || clip.endsWith(".wav") || clip.endsWith(".ogg") || clip.endsWith(".flac") || clip.endsWith(".aac") || clip.endsWith(".webm")) {
523
567
  if (!this.audioLoader)
524
568
  this.audioLoader = new AudioLoader();
525
569
  this.shouldPlay = true;
@@ -530,9 +574,12 @@ export class AudioSource extends Behaviour {
530
574
  this._lastClipStartedLoading = clip;
531
575
  if (debug)
532
576
  console.log("load audio", clip);
533
- const buffer = await this.audioLoader.loadAsync(clip).catch(console.error);
534
- if(this.destroyed) return;
535
- if(this._lastClipStartedLoading === clip) this._lastClipStartedLoading = null;
577
+ const buffer = await this.audioLoader.loadAsync(clip).catch(e => {
578
+ console.error(`[AudioSource] Error loading audio clip "${clip}":`, e);
579
+ return null;
580
+ });
581
+ if (this.destroyed) return;
582
+ if (this._lastClipStartedLoading === clip) this._lastClipStartedLoading = null;
536
583
  if (buffer) this.createAudio(buffer);
537
584
  }
538
585
  else console.warn("Unsupported audio clip type", clip)
@@ -548,8 +595,9 @@ export class AudioSource extends Behaviour {
548
595
  * If no argument is provided, plays the currently assigned clip.
549
596
  *
550
597
  * @param clip - Optional audio clip or {@link MediaStream} to play
598
+ * @returns A promise that resolves when playback starts successfully, or rejects on error
551
599
  */
552
- play(clip: string | MediaStream | undefined = undefined) {
600
+ async play(clip: string | MediaStream | undefined = undefined): Promise<boolean> {
553
601
  // use audio source's clip when no clip is passed in
554
602
  if (!clip && this.clip)
555
603
  clip = this.clip;
@@ -557,27 +605,27 @@ export class AudioSource extends Behaviour {
557
605
  // We only support strings and media stream
558
606
  // TODO: maybe we should return here if an invalid value is passed in
559
607
  if (clip !== undefined && typeof clip !== "string" && !(clip instanceof MediaStream)) {
560
- if (isDevEnvironment())
561
- console.warn("Called play on AudioSource with unknown argument type:", clip + "\nUsing the assigned clip instead:", this.clip)
562
608
  // e.g. when a AudioSource.Play is called from SpatialTrigger onEnter this event is called with the TriggerReceiver... to still make this work we *re-use* our already assigned clip. Because otherwise calling `play` would not play the clip...
563
609
  clip = this.clip;
610
+ if (isDevEnvironment()) console.warn("[AudioSource] Called play on AudioSource with unknown argument type:", clip + "\nUsing the assigned clip instead:", this.clip)
564
611
  }
565
612
 
566
- // Check if we need to call load first
567
- let needsLoading = !this.sound || (clip && clip !== this.clip);
613
+ // Load if the sound hasn't been created yet or if the clip changed since last load
614
+ let needsLoading = !this.sound || (clip && clip !== this._loadedClip);
568
615
  if (typeof clip === "string" && !this.audioLoader) needsLoading = true;
569
616
  if (clip instanceof MediaStream || typeof clip === "string")
570
617
  this.clip = clip;
571
618
  if (needsLoading) {
572
619
  this.shouldPlay = true;
573
- this.onNewClip(clip);
574
- return;
620
+ return this.onNewClip(clip).then(() => true).catch(() => false);
575
621
  }
576
622
 
577
623
  this.shouldPlay = true;
578
624
  this._hasEnded = false;
579
- if (debug)
580
- console.log("play", this.sound?.getVolume(), this.sound);
625
+
626
+ if (debug) console.log("[AudioSource] play", this.sound?.getVolume(), this.sound);
627
+ // If a different clip was passed in, needsLoading above would be true and onNewClip handles it.
628
+ // This guard prevents double-playing the same already-playing sound (which would throw in three.js).
581
629
  if (this.sound && !this.sound.isPlaying) {
582
630
  const muted = this.context.application.muted;
583
631
  if (muted) this.sound.setVolume(0);
@@ -597,13 +645,17 @@ export class AudioSource extends Behaviour {
597
645
  this.context.domElement.shadowRoot?.append(this._audioElement);
598
646
  this._audioElement.srcObject = this.clip;
599
647
  this._audioElement.autoplay = false;
648
+ return true;
600
649
 
601
650
  }
602
651
  else {
603
652
  if (this._audioElement) this._audioElement.remove();
604
653
  this.sound.play(muted ? .1 : 0);
654
+ return true;
605
655
  }
606
656
  }
657
+
658
+ return false;
607
659
  }
608
660
 
609
661
  /**
@@ -611,7 +663,7 @@ export class AudioSource extends Behaviour {
611
663
  * Use play() to resume from the paused position.
612
664
  */
613
665
  pause() {
614
- if (debug) console.log("Pause", this);
666
+ if (debug) console.log("[AudioSource] pause", this);
615
667
  this._hasEnded = true;
616
668
  this.shouldPlay = false;
617
669
  if (this.sound && this.sound.isPlaying && this.sound.source) {
@@ -626,13 +678,13 @@ export class AudioSource extends Behaviour {
626
678
  * Unlike pause(), calling play() after stop() will start from the beginning.
627
679
  */
628
680
  stop() {
629
- if (debug) console.log("Pause", this);
681
+ if (debug) console.log("[AudioSource] stop", this);
630
682
  this._hasEnded = true;
631
683
  this.shouldPlay = false;
632
684
  if (this.sound && this.sound.source) {
633
685
  this._lastContextTime = this.sound?.context.currentTime;
634
686
  if (debug)
635
- console.log(this._lastContextTime)
687
+ console.log("[AudioSource] lastContextTime", this._lastContextTime);
636
688
  this.sound.stop();
637
689
  }
638
690
  this._audioElement?.remove();
@@ -657,8 +709,7 @@ export class AudioSource extends Behaviour {
657
709
 
658
710
  if (this.sound && !this.sound.isPlaying && this.shouldPlay && !this._hasEnded) {
659
711
  this._hasEnded = true;
660
- if (debug)
661
- console.log("Audio clip ended", this.clip);
712
+ if (debug) console.log("[AudioSource] Audio clip ended", this.clip);
662
713
  this.dispatchEvent(new CustomEvent("ended", { detail: this }));
663
714
  }
664
715
 
@@ -12,6 +12,7 @@ import { getParam } from "../engine/engine_utils.js";
12
12
  import { NeedleXRSession } from "../engine/engine_xr.js";
13
13
  import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
14
14
  import { Behaviour, GameObject } from "./Component.js";
15
+ import { EventList } from "./EventList.js";
15
16
  import { UsageMarker } from "./Interactable.js";
16
17
  import { Rigidbody } from "./RigidBody.js";
17
18
  import { SyncedTransform } from "./SyncedTransform.js";
@@ -155,7 +156,19 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
155
156
  @serializable()
156
157
  public showGizmo: boolean = false;
157
158
 
158
- /**
159
+ /** Invoked once when a drag begins (after the minimum drag distance threshold is met). */
160
+ @serializable(EventList)
161
+ dragStarted: EventList = new EventList();
162
+
163
+ /** Invoked every frame while the object is being dragged. */
164
+ @serializable(EventList)
165
+ dragUpdated: EventList = new EventList();
166
+
167
+ /** Invoked once when the last pointer is released and the drag ends. */
168
+ @serializable(EventList)
169
+ dragEnded: EventList = new EventList();
170
+
171
+ /**
159
172
  * Returns the object currently being dragged by this DragControls component, if any.
160
173
  * @returns The object being dragged or null if no object is currently dragged
161
174
  */
@@ -457,6 +470,7 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
457
470
  if (!object) return;
458
471
 
459
472
  this._isDragging = true;
473
+ this.dragStarted?.invoke();
460
474
 
461
475
  const sync = GameObject.getComponentInChildren(object, SyncedTransform);
462
476
  if (debug) console.log("DRAG START", sync, object);
@@ -497,9 +511,10 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
497
511
  const object = this._targetObject || this.gameObject;
498
512
 
499
513
  InstancingUtil.markDirty(object);
514
+ this.dragUpdated?.invoke();
500
515
  }
501
516
 
502
- /**
517
+ /**
503
518
  * Called when the last pointer has been removed from this object.
504
519
  * Cleans up drag state and applies final velocities to rigidbodies.
505
520
  * @param evt Pointer event data for the last pointer that was lifted
@@ -507,6 +522,7 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
507
522
  private onLastDragEnd(evt: PointerEventData | null) {
508
523
  if (!this || !this._isDragging) return;
509
524
  this._isDragging = false;
525
+ this.dragEnded?.invoke();
510
526
  for (const rb of this._draggingRigidbodies) {
511
527
  rb.setVelocity(rb.smoothedVelocity.multiplyScalar(this.context.time.deltaTime));
512
528
  }
@@ -106,11 +106,40 @@ enum LightShadows {
106
106
  export class Light extends Behaviour implements ILight {
107
107
 
108
108
  /**
109
- * The type of light (spot, directional, point, etc.)
110
- * Can not be changed at runtime.
109
+ * The type of light as a lowercase string: `"directional"`, `"point"`, `"spot"`.
110
+ * Implements {@link ILight.type}. Can not be changed at runtime.
111
111
  */
112
+ get type(): ILight["type"] {
113
+ return this._type;
114
+ }
115
+
116
+ /** Numeric LightType serialized from Unity/Blender — converts to string on write */
112
117
  @serializable()
113
- private type: LightType = 0;
118
+ set type(value: LightType | ILight["type"]) {
119
+ if (this.light && this.__didAwake) {
120
+ throw new Error("Changing the light type at runtime is not supported");
121
+ }
122
+ switch (value) {
123
+ case LightType.Directional:
124
+ this._type = "directional";
125
+ break;
126
+ case LightType.Point:
127
+ this._type = "point";
128
+ break;
129
+ case LightType.Spot:
130
+ this._type = "spot";
131
+ break;
132
+ case "directional":
133
+ case "point":
134
+ case "spot":
135
+ this._type = value;
136
+ break;
137
+ default:
138
+ throw new Error("Invalid light type: " + value);
139
+ }
140
+ }
141
+ private _type: ILight["type"] = "point";
142
+
114
143
 
115
144
  /**
116
145
  * The maximum distance the light affects.
@@ -345,7 +374,7 @@ export class Light extends Behaviour implements ILight {
345
374
  */
346
375
  public getWorldPosition(vec: Vector3): Vector3 {
347
376
  if (this.light) {
348
- if (this.type === LightType.Directional) {
377
+ if (this.type === "directional") {
349
378
  return this.light.getWorldPosition(vec).multiplyScalar(1);
350
379
  }
351
380
  return this.light.getWorldPosition(vec);
@@ -372,12 +401,13 @@ export class Light extends Behaviour implements ILight {
372
401
  else if (this.light.parent !== this.gameObject)
373
402
  this.gameObject.add(this.light);
374
403
  }
375
- if (this.type === LightType.Directional)
376
- this.startCoroutine(this.updateMainLightRoutine(), FrameEvent.LateUpdate);
404
+ this.context.lights.push(this);
377
405
  }
378
406
 
379
407
  onDisable() {
380
408
  if (debug) console.log("DISABLE LIGHT", this.name);
409
+ const index = this.context.lights.indexOf(this);
410
+ if (index !== -1) this.context.lights.splice(index, 1);
381
411
  if (this.light) {
382
412
  if (this.selfIsLight)
383
413
  this.light.intensity = 0;
@@ -399,14 +429,14 @@ export class Light extends Behaviour implements ILight {
399
429
  this._intensity = this.light.intensity;
400
430
 
401
431
  switch (this.type) {
402
- case LightType.Directional:
432
+ case "directional":
403
433
  this.setDirectionalLight(this.light as DirectionalLight);
404
434
  break;
405
435
  }
406
436
  }
407
437
  else if (!this.light) {
408
438
  switch (this.type) {
409
- case LightType.Directional:
439
+ case "directional":
410
440
  // console.log(this);
411
441
  const dirLight = new DirectionalLight(this.color, this.intensity * Math.PI);
412
442
  // directional light target is at 0 0 0 by default
@@ -425,7 +455,7 @@ export class Light extends Behaviour implements ILight {
425
455
  }
426
456
  break;
427
457
 
428
- case LightType.Spot:
458
+ case "spot":
429
459
  const spotLight = new SpotLight(this.color, this.intensity * Math.PI, this.range, toRadians(this.spotAngle / 2), 1 - toRadians(this.innerSpotAngle / 2) / toRadians(this.spotAngle / 2), 2);
430
460
  spotLight.position.set(0, 0, 0);
431
461
  spotLight.rotation.set(0, 0, 0);
@@ -437,7 +467,7 @@ export class Light extends Behaviour implements ILight {
437
467
  spotLightTarget.rotation.set(0, 0, 0);
438
468
  break;
439
469
 
440
- case LightType.Point:
470
+ case "point":
441
471
  const pointLight = new PointLight(this.color, this.intensity * Math.PI, this.range);
442
472
  this.light = pointLight;
443
473
 
@@ -526,22 +556,6 @@ export class Light extends Behaviour implements ILight {
526
556
 
527
557
  }
528
558
 
529
- /**
530
- * Coroutine that updates the main light reference in the context
531
- * if this directional light should be the main light
532
- */
533
- *updateMainLightRoutine() {
534
- while (true) {
535
- if (this.type === LightType.Directional) {
536
- if (!this.context.mainLight || this.intensity > this.context.mainLight.intensity) {
537
- this.context.mainLight = this;
538
- }
539
- yield;
540
- }
541
- break;
542
- }
543
- }
544
-
545
559
  /**
546
560
  * Controls whether the renderer's shadow map type can be changed when soft shadows are used
547
561
  */
@@ -37,7 +37,7 @@ const debug = getParam("debugnet");
37
37
  * @see {@link RoomEvents} for room lifecycle events
38
38
  * @see {@link isLocalNetwork} for local network detection
39
39
  * @link https://engine.needle.tools/docs/how-to-guides/networking/
40
- * @summary Networking configuration
40
+ * @summary Configures the websocket server URL for multiplayer networking
41
41
  * @category Networking
42
42
  * @group Components
43
43
  */
@@ -758,19 +758,26 @@ export class OrbitControls extends Behaviour implements ICameraController {
758
758
 
759
759
  if (this.targetBounds) {
760
760
  // #region target bounds
761
- const targetVector = this._controls.target;
762
761
  const boundsCenter = this.targetBounds.worldPosition;
763
762
  const boundsHalfSize = getTempVector(this.targetBounds.worldScale).multiplyScalar(0.5);
764
763
  const min = getTempVector(boundsCenter).sub(boundsHalfSize);
765
764
  const max = getTempVector(boundsCenter).add(boundsHalfSize);
766
- const newTarget = getTempVector(this._controls.target).clamp(min, max);
767
- const duration = .1;
768
- if (duration <= 0) targetVector.copy(newTarget);
769
- else targetVector.lerp(newTarget, this.context.time.deltaTime / duration);
765
+
770
766
  if (this._lookTargetLerpActive) {
771
- if (duration <= 0) this._lookTargetEndPosition.copy(newTarget);
772
- else this._lookTargetEndPosition.lerp(newTarget, this.context.time.deltaTime / (duration * 5));
767
+ // During a programmatic transition (fitCamera / setLookTargetPosition with immediate: false),
768
+ // only clamp the destination. The look-target lerp (above) handles moving _controls.target
769
+ // towards the endpoint — we must not fight it by also lerping _controls.target here.
770
+ this._lookTargetEndPosition.clamp(min, max);
771
+ }
772
+ else {
773
+ // Interactive use (pan/orbit): smoothly push the target back into bounds
774
+ const targetVector = this._controls.target;
775
+ const newTarget = getTempVector(targetVector).clamp(min, max);
776
+ const duration = .1;
777
+ if (duration <= 0) targetVector.copy(newTarget);
778
+ else targetVector.lerp(newTarget, Math.min(1, this.context.time.deltaTime / duration));
773
779
  }
780
+
774
781
  if (debug) {
775
782
  Gizmos.DrawWireBox(boundsCenter, boundsHalfSize.multiplyScalar(2), 0xffaa00);
776
783
  }
@@ -847,7 +854,8 @@ export class OrbitControls extends Behaviour implements ICameraController {
847
854
  this._controls.update(this.context.time.deltaTime);
848
855
 
849
856
  if (debug) {
850
- Gizmos.DrawWireSphere(this._controls.target, 0.1, 0x00ff00);
857
+ const distance = this._controls.getDistance();
858
+ Gizmos.DrawWireSphere(this._controls.target, 0.01 * distance, 0x00ff00);
851
859
  }
852
860
  }
853
861
  }
@@ -1022,7 +1030,8 @@ export class OrbitControls extends Behaviour implements ICameraController {
1022
1030
 
1023
1031
  if (debug) {
1024
1032
  console.warn("OrbitControls: setLookTargetPosition", position, immediateOrDuration);
1025
- Gizmos.DrawWireSphere(this._lookTargetEndPosition, .2, 0xff0000, 2);
1033
+ const distance = this._controls.getDistance();
1034
+ Gizmos.DrawWireSphere(this._lookTargetEndPosition, 0.01 * distance, 0xff5500, 2);
1026
1035
  }
1027
1036
 
1028
1037
  if (immediateOrDuration === true) {
@@ -290,6 +290,8 @@ export class ReflectionProbe extends Behaviour {
290
290
  const current = block.getOverride("envMap")?.value;
291
291
  if (current === this.texture) {
292
292
  block.removeOveride("envMap");
293
+ block.removeOveride("envMapRotation");
294
+ block.removeOveride("envMapIntensity");
293
295
  }
294
296
  }
295
297
  }
@@ -382,12 +382,12 @@ export class Rigidbody extends Behaviour implements IRigidbody {
382
382
  this._watch.start(true, true);
383
383
  this.startCoroutine(this.beforePhysics(), FrameEvent.LateUpdate);
384
384
  if (isDevEnvironment()) {
385
- if (!globalThis["NEEDLE_USE_RAPIER"])
386
- console.warn(`Rigidbody could not be created: Rapier physics are explicitly disabled.`);
385
+ if (globalThis["NEEDLE_USE_RAPIER"] === false)
386
+ console.warn(`RAPIER physics are disabled in your build. Enable them by setting NEEDLE_USE_RAPIER to true in your build config: Rigidbody could not be created.`);
387
387
  else {
388
388
  MODULES.RAPIER_PHYSICS.ready().then(async () => {
389
389
  await delayForFrames(3);
390
- if (!this.context.physics.engine?.getBody(this))
390
+ if (this.activeAndEnabled && !this.context.physics.engine?.getBody(this))
391
391
  console.warn(`Rigidbody could not be created. Ensure \"${this.name}\" has a Collider component.`);
392
392
  })
393
393
  }