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

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 (218) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/SKILL.md +4 -1
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-DF01sSGQ.js → needle-engine.bundle-C-LG00ZZ.js} +10681 -10100
  5. package/dist/needle-engine.bundle-D7tzaiYE.min.js +1733 -0
  6. package/dist/{needle-engine.bundle-C-ixARur.umd.cjs → needle-engine.bundle-OPkPmdUM.umd.cjs} +161 -161
  7. package/dist/needle-engine.d.ts +1349 -317
  8. package/dist/needle-engine.js +556 -555
  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 +5 -0
  15. package/lib/engine/api.js +4 -0
  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_camera.fit.js +16 -4
  20. package/lib/engine/engine_camera.fit.js.map +1 -1
  21. package/lib/engine/engine_context.d.ts +20 -7
  22. package/lib/engine/engine_context.js +31 -15
  23. package/lib/engine/engine_context.js.map +1 -1
  24. package/lib/engine/engine_context_eventbus.d.ts +47 -0
  25. package/lib/engine/engine_context_eventbus.js +47 -0
  26. package/lib/engine/engine_context_eventbus.js.map +1 -0
  27. package/lib/engine/engine_disposable.d.ts +172 -0
  28. package/lib/engine/engine_disposable.js +136 -0
  29. package/lib/engine/engine_disposable.js.map +1 -0
  30. package/lib/engine/engine_gameobject.d.ts +1 -10
  31. package/lib/engine/engine_gameobject.js +20 -118
  32. package/lib/engine/engine_gameobject.js.map +1 -1
  33. package/lib/engine/engine_gltf_builtin_components.js +7 -69
  34. package/lib/engine/engine_gltf_builtin_components.js.map +1 -1
  35. package/lib/engine/engine_input.d.ts +23 -4
  36. package/lib/engine/engine_input.js +2 -1
  37. package/lib/engine/engine_input.js.map +1 -1
  38. package/lib/engine/engine_instantiate_resolve.d.ts +42 -0
  39. package/lib/engine/engine_instantiate_resolve.js +372 -0
  40. package/lib/engine/engine_instantiate_resolve.js.map +1 -0
  41. package/lib/engine/engine_mainloop_utils.js +2 -2
  42. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  43. package/lib/engine/engine_networking.d.ts +51 -37
  44. package/lib/engine/engine_networking.js +132 -82
  45. package/lib/engine/engine_networking.js.map +1 -1
  46. package/lib/engine/engine_networking.transport.websocket.d.ts +15 -0
  47. package/lib/engine/engine_networking.transport.websocket.js +38 -0
  48. package/lib/engine/engine_networking.transport.websocket.js.map +1 -0
  49. package/lib/engine/engine_networking_instantiate.js +2 -2
  50. package/lib/engine/engine_networking_instantiate.js.map +1 -1
  51. package/lib/engine/engine_networking_types.d.ts +39 -1
  52. package/lib/engine/engine_networking_types.js +7 -0
  53. package/lib/engine/engine_networking_types.js.map +1 -1
  54. package/lib/engine/engine_physics_rapier.d.ts +21 -3
  55. package/lib/engine/engine_physics_rapier.js +94 -25
  56. package/lib/engine/engine_physics_rapier.js.map +1 -1
  57. package/lib/engine/engine_serialization_builtin_serializer.js +1 -5
  58. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  59. package/lib/engine/engine_serialization_core.d.ts +1 -0
  60. package/lib/engine/engine_serialization_core.js +7 -0
  61. package/lib/engine/engine_serialization_core.js.map +1 -1
  62. package/lib/engine/engine_types.d.ts +29 -11
  63. package/lib/engine/engine_types.js +1 -1
  64. package/lib/engine/engine_types.js.map +1 -1
  65. package/lib/engine/engine_util_decorator.js +7 -2
  66. package/lib/engine/engine_util_decorator.js.map +1 -1
  67. package/lib/engine/engine_utils.d.ts +1 -1
  68. package/lib/engine/engine_utils.js +19 -5
  69. package/lib/engine/engine_utils.js.map +1 -1
  70. package/lib/engine-components/AnimationBuilder.d.ts +158 -0
  71. package/lib/engine-components/AnimationBuilder.js +305 -0
  72. package/lib/engine-components/AnimationBuilder.js.map +1 -0
  73. package/lib/engine-components/Animator.d.ts +6 -0
  74. package/lib/engine-components/Animator.js +23 -13
  75. package/lib/engine-components/Animator.js.map +1 -1
  76. package/lib/engine-components/AnimatorController.builder.d.ts +191 -0
  77. package/lib/engine-components/AnimatorController.builder.js +263 -0
  78. package/lib/engine-components/AnimatorController.builder.js.map +1 -0
  79. package/lib/engine-components/AnimatorController.d.ts +2 -119
  80. package/lib/engine-components/AnimatorController.js +33 -232
  81. package/lib/engine-components/AnimatorController.js.map +1 -1
  82. package/lib/engine-components/Collider.d.ts +18 -9
  83. package/lib/engine-components/Collider.js +61 -14
  84. package/lib/engine-components/Collider.js.map +1 -1
  85. package/lib/engine-components/Component.d.ts +72 -9
  86. package/lib/engine-components/Component.js +114 -10
  87. package/lib/engine-components/Component.js.map +1 -1
  88. package/lib/engine-components/ContactShadows.d.ts +1 -0
  89. package/lib/engine-components/ContactShadows.js +14 -1
  90. package/lib/engine-components/ContactShadows.js.map +1 -1
  91. package/lib/engine-components/DragControls.js +0 -7
  92. package/lib/engine-components/DragControls.js.map +1 -1
  93. package/lib/engine-components/DropListener.js +3 -0
  94. package/lib/engine-components/DropListener.js.map +1 -1
  95. package/lib/engine-components/EventList.d.ts +31 -9
  96. package/lib/engine-components/EventList.js +37 -76
  97. package/lib/engine-components/EventList.js.map +1 -1
  98. package/lib/engine-components/Joints.d.ts +4 -2
  99. package/lib/engine-components/Joints.js +19 -3
  100. package/lib/engine-components/Joints.js.map +1 -1
  101. package/lib/engine-components/Light.js +9 -1
  102. package/lib/engine-components/Light.js.map +1 -1
  103. package/lib/engine-components/OrbitControls.d.ts +0 -2
  104. package/lib/engine-components/OrbitControls.js +14 -1
  105. package/lib/engine-components/OrbitControls.js.map +1 -1
  106. package/lib/engine-components/RigidBody.d.ts +12 -4
  107. package/lib/engine-components/RigidBody.js +18 -4
  108. package/lib/engine-components/RigidBody.js.map +1 -1
  109. package/lib/engine-components/SceneSwitcher.js +3 -0
  110. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  111. package/lib/engine-components/api.d.ts +2 -1
  112. package/lib/engine-components/api.js +2 -1
  113. package/lib/engine-components/api.js.map +1 -1
  114. package/lib/engine-components/codegen/components.d.ts +7 -13
  115. package/lib/engine-components/codegen/components.js +7 -13
  116. package/lib/engine-components/codegen/components.js.map +1 -1
  117. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  118. package/lib/engine-components/timeline/PlayableDirector.d.ts +21 -11
  119. package/lib/engine-components/timeline/PlayableDirector.js +75 -67
  120. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  121. package/lib/engine-components/timeline/SignalAsset.d.ts +3 -1
  122. package/lib/engine-components/timeline/SignalAsset.js +1 -0
  123. package/lib/engine-components/timeline/SignalAsset.js.map +1 -1
  124. package/lib/engine-components/timeline/TimelineBuilder.d.ts +413 -0
  125. package/lib/engine-components/timeline/TimelineBuilder.js +506 -0
  126. package/lib/engine-components/timeline/TimelineBuilder.js.map +1 -0
  127. package/lib/engine-components/timeline/TimelineModels.d.ts +2 -1
  128. package/lib/engine-components/timeline/TimelineModels.js +3 -0
  129. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  130. package/lib/engine-components/timeline/TimelineTracks.d.ts +37 -6
  131. package/lib/engine-components/timeline/TimelineTracks.js +92 -26
  132. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  133. package/lib/engine-components/timeline/index.d.ts +2 -1
  134. package/lib/engine-components/timeline/index.js +2 -0
  135. package/lib/engine-components/timeline/index.js.map +1 -1
  136. package/lib/engine-components/web/CursorFollow.d.ts +0 -1
  137. package/lib/engine-components/web/CursorFollow.js +0 -1
  138. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  139. package/package.json +2 -83
  140. package/plugins/common/cloud.js +6 -1
  141. package/plugins/common/license.js +5 -2
  142. package/plugins/common/worker.js +9 -4
  143. package/plugins/vite/dependencies.js +1 -11
  144. package/plugins/vite/dependency-watcher.js +2 -2
  145. package/plugins/vite/editor-connection.js +3 -3
  146. package/plugins/vite/license.js +19 -1
  147. package/plugins/vite/reload.js +1 -1
  148. package/plugins/vite/server.js +2 -1
  149. package/src/engine/api.ts +7 -0
  150. package/src/engine/codegen/register_types.ts +10 -18
  151. package/src/engine/engine_camera.fit.ts +15 -4
  152. package/src/engine/engine_context.ts +32 -16
  153. package/src/engine/engine_context_eventbus.ts +73 -0
  154. package/src/engine/engine_disposable.ts +214 -0
  155. package/src/engine/engine_gameobject.ts +52 -157
  156. package/src/engine/engine_gltf_builtin_components.ts +7 -76
  157. package/src/engine/engine_input.ts +27 -6
  158. package/src/engine/engine_instantiate_resolve.ts +407 -0
  159. package/src/engine/engine_mainloop_utils.ts +2 -2
  160. package/src/engine/engine_networking.transport.websocket.ts +45 -0
  161. package/src/engine/engine_networking.ts +161 -137
  162. package/src/engine/engine_networking_instantiate.ts +2 -2
  163. package/src/engine/engine_networking_types.ts +41 -1
  164. package/src/engine/engine_physics_rapier.ts +102 -33
  165. package/src/engine/engine_serialization_builtin_serializer.ts +1 -6
  166. package/src/engine/engine_serialization_core.ts +9 -0
  167. package/src/engine/engine_types.ts +46 -27
  168. package/src/engine/engine_util_decorator.ts +7 -2
  169. package/src/engine/engine_utils.ts +16 -5
  170. package/src/engine-components/AnimationBuilder.ts +472 -0
  171. package/src/engine-components/Animator.ts +24 -12
  172. package/src/engine-components/AnimatorController.builder.ts +387 -0
  173. package/src/engine-components/AnimatorController.ts +20 -291
  174. package/src/engine-components/Collider.ts +66 -18
  175. package/src/engine-components/Component.ts +118 -20
  176. package/src/engine-components/ContactShadows.ts +15 -1
  177. package/src/engine-components/DragControls.ts +0 -9
  178. package/src/engine-components/DropListener.ts +3 -0
  179. package/src/engine-components/EventList.ts +45 -83
  180. package/src/engine-components/Joints.ts +20 -4
  181. package/src/engine-components/Light.ts +10 -2
  182. package/src/engine-components/OrbitControls.ts +16 -5
  183. package/src/engine-components/RigidBody.ts +18 -4
  184. package/src/engine-components/SceneSwitcher.ts +3 -0
  185. package/src/engine-components/api.ts +2 -1
  186. package/src/engine-components/codegen/components.ts +7 -13
  187. package/src/engine-components/timeline/PlayableDirector.ts +83 -81
  188. package/src/engine-components/timeline/SignalAsset.ts +4 -1
  189. package/src/engine-components/timeline/TimelineBuilder.ts +824 -0
  190. package/src/engine-components/timeline/TimelineModels.ts +5 -1
  191. package/src/engine-components/timeline/TimelineTracks.ts +96 -27
  192. package/src/engine-components/timeline/index.ts +2 -1
  193. package/src/engine-components/web/CursorFollow.ts +0 -1
  194. package/dist/needle-engine.bundle-CHmXdnE1.min.js +0 -1733
  195. package/lib/engine-components/AvatarLoader.d.ts +0 -80
  196. package/lib/engine-components/AvatarLoader.js +0 -232
  197. package/lib/engine-components/AvatarLoader.js.map +0 -1
  198. package/lib/engine-components/avatar/AvatarBlink_Simple.d.ts +0 -11
  199. package/lib/engine-components/avatar/AvatarBlink_Simple.js +0 -77
  200. package/lib/engine-components/avatar/AvatarBlink_Simple.js.map +0 -1
  201. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.d.ts +0 -14
  202. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.js +0 -69
  203. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.js.map +0 -1
  204. package/lib/engine-components/avatar/Avatar_Brain_LookAt.d.ts +0 -29
  205. package/lib/engine-components/avatar/Avatar_Brain_LookAt.js +0 -122
  206. package/lib/engine-components/avatar/Avatar_Brain_LookAt.js.map +0 -1
  207. package/lib/engine-components/avatar/Avatar_MouthShapes.d.ts +0 -15
  208. package/lib/engine-components/avatar/Avatar_MouthShapes.js +0 -80
  209. package/lib/engine-components/avatar/Avatar_MouthShapes.js.map +0 -1
  210. package/lib/engine-components/avatar/Avatar_MustacheShake.d.ts +0 -9
  211. package/lib/engine-components/avatar/Avatar_MustacheShake.js +0 -30
  212. package/lib/engine-components/avatar/Avatar_MustacheShake.js.map +0 -1
  213. package/src/engine-components/AvatarLoader.ts +0 -264
  214. package/src/engine-components/avatar/AvatarBlink_Simple.ts +0 -70
  215. package/src/engine-components/avatar/AvatarEyeLook_Rotation.ts +0 -64
  216. package/src/engine-components/avatar/Avatar_Brain_LookAt.ts +0 -140
  217. package/src/engine-components/avatar/Avatar_MouthShapes.ts +0 -84
  218. package/src/engine-components/avatar/Avatar_MustacheShake.ts +0 -32
@@ -10,9 +10,10 @@ import * as main from "../engine/engine_mainloop_utils.js";
10
10
  import { syncDestroy, syncInstantiate, SyncInstantiateOptions } from "../engine/engine_networking_instantiate.js";
11
11
  import { Context, FrameEvent } from "../engine/engine_setup.js";
12
12
  import * as threeutils from "../engine/engine_three_utils.js";
13
- import { $componentName, type Collision, type ComponentInit, type Constructor, type ConstructorConcrete, type GuidsMap, type ICollider, type IComponent, type IGameObject, type SourceIdentifier } from "../engine/engine_types.js";
13
+ import { $componentName, type Collision, type ComponentInit, type Constructor, type ConstructorConcrete, type ICollider, type IComponent, type IGameObject, type SourceIdentifier } from "../engine/engine_types.js";
14
14
  import { TypeStore } from "../engine/engine_typestore.js";
15
15
  import type { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
16
+ import { DisposableStore, type IDisposable, type DisposeFn } from "../engine/engine_disposable.js";
16
17
  import { type IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
17
18
 
18
19
 
@@ -556,7 +557,7 @@ export abstract class GameObject extends Object3D implements Object3D, IGameObje
556
557
  * **Input event methods:**
557
558
  * {@link onPointerDown}, {@link onPointerUp}, {@link onPointerEnter}, {@link onPointerExit} and {@link onPointerMove}.
558
559
  *
559
- * @example
560
+ * @example Basic component
560
561
  * ```typescript
561
562
  * import { Behaviour } from "@needle-tools/engine";
562
563
  * export class MyComponent extends Behaviour {
@@ -568,13 +569,31 @@ export abstract class GameObject extends Object3D implements Object3D, IGameObje
568
569
  * }
569
570
  * }
570
571
  * ```
571
- *
572
+ *
573
+ * @example Automatic cleanup with autoCleanup
574
+ * ```typescript
575
+ * import { Behaviour, serializable, EventList } from "@needle-tools/engine";
576
+ * export class ScoreTracker extends Behaviour {
577
+ * @serializable(EventList)
578
+ * onScoreChanged?: EventList<number>;
579
+ *
580
+ * start() {
581
+ * // Registered during start → survives enable/disable, cleaned up on destroy
582
+ * this.autoCleanup(this.onScoreChanged?.on(score => console.log("Score:", score)));
583
+ * }
584
+ *
585
+ * onEnable() {
586
+ * // Registered during onEnable → cleaned up on disable
587
+ * this.autoCleanup(() => this.cleanupResources());
588
+ * }
589
+ * }
590
+ * ```
591
+ *
572
592
  * @group Components
573
593
  */
574
594
  export abstract class Component implements IComponent, EventTarget,
575
595
  Partial<INeedleXRSessionEventReceiver>,
576
- Partial<IPointerEventHandler>
577
- {
596
+ Partial<IPointerEventHandler> {
578
597
  /**
579
598
  * Indicates whether this object is a component
580
599
  * @internal
@@ -686,6 +705,74 @@ export abstract class Component implements IComponent, EventTarget,
686
705
  return true;
687
706
  }
688
707
 
708
+ private __disableCleanups?: DisposableStore;
709
+ private __destroyCleanups?: DisposableStore;
710
+
711
+ /**
712
+ * @experimental
713
+ * Register a resource for automatic cleanup tied to this component's lifecycle.
714
+ * Accepts {@link IDisposable} objects, cleanup functions, or event unsubscribe functions.
715
+ * `null` and `undefined` are safe no-ops (convenient for conditional subscriptions).
716
+ *
717
+ * **Lifecycle-aware:** The cleanup store is chosen automatically based on when `autoCleanup` is called:
718
+ * - Called during {@link onEnable} → cleaned up on {@link onDisable} (and re-registered on re-enable)
719
+ * - Called during {@link awake} or {@link start} → cleaned up on {@link onDestroy} (survives enable/disable cycles)
720
+ * - Called at any other time (e.g. from update) → cleaned up on {@link onDisable}
721
+ *
722
+ * @param disposable An {@link IDisposable}, a cleanup/unsubscribe function, or `null`/`undefined`
723
+ *
724
+ * @example EventList subscriptions
725
+ * ```ts
726
+ * import { Behaviour, serializable, EventList } from "@needle-tools/engine";
727
+ *
728
+ * export class MyComponent extends Behaviour {
729
+ * @serializable(EventList)
730
+ * onScoreChanged?: EventList<number>;
731
+ *
732
+ * onEnable() {
733
+ * this.autoCleanup(this.onScoreChanged?.on(score => {
734
+ * console.log("Score:", score);
735
+ * }));
736
+ * }
737
+ * }
738
+ * ```
739
+ *
740
+ * @example Lifetime subscriptions in awake
741
+ * ```ts
742
+ * import { Behaviour } from "@needle-tools/engine";
743
+ *
744
+ * export class Persistent extends Behaviour {
745
+ * awake() {
746
+ * // Registered during awake → survives enable/disable, cleaned up on destroy
747
+ * this.autoCleanup(() => this.save());
748
+ * }
749
+ * }
750
+ * ```
751
+ */
752
+ protected autoCleanup(disposable: IDisposable | DisposeFn | Function | null | undefined): void {
753
+ if (!disposable) {
754
+ if (isDevEnvironment()) {
755
+ console.warn(`[Component] autoCleanup called with null or undefined, no cleanup registered. This is safe, but you may want to check if this was intentional (${this[$componentName] || this.constructor.name})`);
756
+ }
757
+ return;
758
+ }
759
+ // During onEnable → cleaned up on disable
760
+ if (this.__inEnableOrDisableCallback) {
761
+ if (!this.__disableCleanups) this.__disableCleanups = new DisposableStore();
762
+ this.__disableCleanups.add(disposable);
763
+ }
764
+ // During awake or start → cleaned up on destroy
765
+ else if (!this.__didCompleteStart) {
766
+ if (!this.__destroyCleanups) this.__destroyCleanups = new DisposableStore();
767
+ this.__destroyCleanups.add(disposable);
768
+ }
769
+ // After start (update, etc.) → cleaned up on disable
770
+ else {
771
+ if (!this.__disableCleanups) this.__disableCleanups = new DisposableStore();
772
+ this.__disableCleanups.add(disposable);
773
+ }
774
+ }
775
+
689
776
  private get __isActive(): boolean {
690
777
  return this.gameObject.visible;
691
778
  }
@@ -720,12 +807,6 @@ export abstract class Component implements IComponent, EventTarget,
720
807
  */
721
808
  sourceId?: SourceIdentifier;
722
809
 
723
- /**
724
- * Called when this component needs to remap guids after an instantiate operation.
725
- * @param guidsMap Mapping from old guids to newly generated guids
726
- */
727
- resolveGuids?(guidsMap: GuidsMap): void;
728
-
729
810
  /**
730
811
  * Called once when the component becomes active for the first time.
731
812
  * This is the first lifecycle callback to be invoked
@@ -988,6 +1069,12 @@ export abstract class Component implements IComponent, EventTarget,
988
1069
  /** @internal */
989
1070
  private __didStart: boolean = false;
990
1071
 
1072
+ /** True while start() has finished executing (used by autoCleanup to distinguish start from update) */
1073
+ private __didCompleteStart: boolean = false;
1074
+
1075
+ /** True while onEnable() is executing (used by autoCleanup to route to disable store) */
1076
+ private __inEnableOrDisableCallback: boolean = false;
1077
+
991
1078
  /** @internal */
992
1079
  protected __didEnable: boolean = false;
993
1080
 
@@ -1004,6 +1091,8 @@ export abstract class Component implements IComponent, EventTarget,
1004
1091
  constructor(init?: ComponentInit<Component>) {
1005
1092
  this.__didAwake = false;
1006
1093
  this.__didStart = false;
1094
+ this.__didCompleteStart = false;
1095
+ this.__inEnableOrDisableCallback = false;
1007
1096
  this.__didEnable = false;
1008
1097
  this.__isEnabled = undefined;
1009
1098
  this.__destroyed = false;
@@ -1015,6 +1104,8 @@ export abstract class Component implements IComponent, EventTarget,
1015
1104
  __internalNewInstanceCreated(init?: ComponentInit<this>): this {
1016
1105
  this.__didAwake = false;
1017
1106
  this.__didStart = false;
1107
+ this.__didCompleteStart = false;
1108
+ this.__inEnableOrDisableCallback = false;
1018
1109
  this.__didEnable = false;
1019
1110
  this.__isEnabled = undefined;
1020
1111
  this.__destroyed = false;
@@ -1051,11 +1142,12 @@ export abstract class Component implements IComponent, EventTarget,
1051
1142
  if (this.__didStart) return;
1052
1143
  this.__didStart = true;
1053
1144
  if (this.start) this.start();
1145
+ this.__didCompleteStart = true;
1054
1146
  }
1055
1147
 
1056
1148
 
1057
1149
  /** @internal */
1058
- __internalEnable(isAddingToScene?: boolean): boolean {
1150
+ __internalEnable(isHierarchyChange?: boolean): boolean {
1059
1151
  if (this.__destroyed) {
1060
1152
  if (isDevEnvironment()) {
1061
1153
  console.warn("[Needle Engine Dev] Trying to enable destroyed component");
@@ -1066,33 +1158,38 @@ export abstract class Component implements IComponent, EventTarget,
1066
1158
  // But a user can change enable during awake
1067
1159
  if (!this.__didAwake) return false;
1068
1160
  if (this.__didEnable) {
1069
- // We dont want to change the enable state if we are adding to scene
1070
- // But we want to change the state when e.g. a user changes the enable state during awake
1071
- if (isAddingToScene !== true)
1161
+ // Don't change __isEnabled for hierarchy-driven changes (e.g. adding to scene, visibility)
1162
+ if (isHierarchyChange !== true)
1072
1163
  this.__isEnabled = true;
1073
1164
  return false;
1074
1165
  }
1075
- // console.trace("INTERNAL ENABLE");
1076
1166
  this.__didEnable = true;
1077
1167
  this.__isEnabled = true;
1168
+ this.__inEnableOrDisableCallback = true;
1078
1169
  this.onEnable();
1170
+ this.__inEnableOrDisableCallback = false;
1079
1171
  return true;
1080
1172
  }
1081
1173
 
1082
1174
  /** @internal */
1083
- __internalDisable(isRemovingFromScene?: boolean) {
1175
+ __internalDisable(isHierarchyChange?: boolean) {
1084
1176
  // Don't change enable before awake
1085
1177
  // But a user can change enable during awake
1086
1178
  if (!this.__didAwake) return;
1087
1179
  if (!this.__didEnable) {
1088
- // We dont want to change the enable state if we are removing from a scene
1089
- if (isRemovingFromScene !== true)
1180
+ // Don't change __isEnabled for hierarchy-driven changes (e.g. removing from scene, visibility)
1181
+ if (isHierarchyChange !== true)
1090
1182
  this.__isEnabled = false;
1091
1183
  return;
1092
1184
  }
1093
1185
  this.__didEnable = false;
1094
- this.__isEnabled = false;
1186
+ // Don't change __isEnabled for hierarchy-driven changes — the component itself is still "enabled"
1187
+ if (isHierarchyChange !== true)
1188
+ this.__isEnabled = false;
1189
+ this.__inEnableOrDisableCallback = true;
1095
1190
  this.onDisable();
1191
+ this.__disableCleanups?.dispose();
1192
+ this.__inEnableOrDisableCallback = false;
1096
1193
  }
1097
1194
 
1098
1195
  /** @internal */
@@ -1101,6 +1198,7 @@ export abstract class Component implements IComponent, EventTarget,
1101
1198
  this.__destroyed = true;
1102
1199
  if (this.__didAwake) {
1103
1200
  this.onDestroy?.call(this);
1201
+ this.__destroyCleanups?.dispose();
1104
1202
  this.dispatchEvent(new CustomEvent("destroyed", { detail: this }));
1105
1203
  }
1106
1204
  destroyComponentInstance(this as any);
@@ -100,7 +100,7 @@ export class ContactShadows extends Behaviour {
100
100
  const obj = new Object3D();
101
101
  obj.name = "ContactShadows";
102
102
  instance = addComponent(obj, ContactShadows, {
103
- autoFit: false,
103
+ autoFit: true,
104
104
  occludeBelowGround: false
105
105
  });
106
106
  this._instances.set(context, instance);
@@ -169,6 +169,10 @@ export class ContactShadows extends Behaviour {
169
169
  return this._needsUpdate;
170
170
  }
171
171
  private _needsUpdate: boolean = false;
172
+ private _needsFit: boolean = false;
173
+
174
+ // TODO: support auto-refit for moveable/animated objects (e.g. via mesh-bvh / scene BVH).
175
+ // Currently there's no reliable way to detect object position/scale changes.
172
176
 
173
177
  /** All shadow objects are parented to this object.
174
178
  * The gameObject itself should not be transformed because we want the ContactShadows object e.g. also have a GroundProjectedEnv component
@@ -366,6 +370,11 @@ export class ContactShadows extends Behaviour {
366
370
 
367
371
  onEnable(): void {
368
372
  this._needsUpdate = true;
373
+ this.autoCleanup(this.context.events.on("scene-content-changed", () => {
374
+ if (!this.autoFit) return;
375
+ this._needsFit = true;
376
+ this._needsUpdate = true;
377
+ }));
369
378
  }
370
379
 
371
380
  /** @internal */
@@ -393,6 +402,11 @@ export class ContactShadows extends Behaviour {
393
402
  /** @internal */
394
403
  onBeforeRender(_frame: XRFrame | null): void {
395
404
 
405
+ if (this._needsFit && this.autoFit) {
406
+ this._needsFit = false;
407
+ this.fitShadows();
408
+ }
409
+
396
410
  if (this.manualUpdate) {
397
411
  if (!this._needsUpdate) return;
398
412
  }
@@ -10,7 +10,6 @@ import { getBoundingBox, getTempVector, getWorldPosition, setWorldPosition } fro
10
10
  import { type IGameObject } from "../engine/engine_types.js";
11
11
  import { getParam } from "../engine/engine_utils.js";
12
12
  import { NeedleXRSession } from "../engine/engine_xr.js";
13
- import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
14
13
  import { Behaviour, GameObject } from "./Component.js";
15
14
  import { EventList } from "./EventList.js";
16
15
  import { UsageMarker } from "./Interactable.js";
@@ -1500,11 +1499,6 @@ class LegacyDragVisualsHelper {
1500
1499
  }
1501
1500
  }
1502
1501
 
1503
- if (this._selected) {
1504
- // TODO move somewhere else
1505
- Avatar_POI.Remove(context, this._selected);
1506
- }
1507
-
1508
1502
  this._selected = newSelected;
1509
1503
  this._context = context;
1510
1504
  this._rbs.length = 0;
@@ -1524,9 +1518,6 @@ class LegacyDragVisualsHelper {
1524
1518
  return;
1525
1519
  }
1526
1520
 
1527
- // TODO move somewhere else
1528
- Avatar_POI.Add(context, this._selected, null);
1529
-
1530
1521
  this._groundOffsetFactor = 0;
1531
1522
  this._hasGroundPlane = true;
1532
1523
  this._groundOffset.set(0, 0, 0);
@@ -597,6 +597,9 @@ export class DropListener extends Behaviour {
597
597
  });
598
598
  this.dispatchEvent(evt);
599
599
  this.onDropped?.invoke(evt.detail);
600
+ if (obj) {
601
+ this.context.events.emit("scene-content-changed", { source: this, object: obj });
602
+ }
600
603
 
601
604
  // send network event
602
605
  if (!isRemote && ctx.url?.startsWith("http") && this.context.connection.isConnected && obj) {
@@ -1,13 +1,17 @@
1
1
  import { isDevEnvironment } from "../engine/debug/index.js";
2
- import { InstantiateContext } from "../engine/engine_gameobject.js";
3
2
  import type { IComponent, IEventList } from "../engine/engine_types.js";
4
3
 
5
4
  const argumentsBuffer = new Array<any>();
6
5
 
7
6
  /**
8
- * CallInfo represents a single callback method that can be invoked by the {@link EventList}.
7
+ * CallInfo represents a single callback method that can be invoked by the {@link EventList}.
9
8
  */
10
9
  export class CallInfo {
10
+
11
+ /** @internal Used by the instantiate resolver to recursively resolve references */
12
+ static { CallInfo.prototype.$serializedTypes = { target: Object, arguments: Array }; }
13
+ declare $serializedTypes: Record<string, any>;
14
+
11
15
  /**
12
16
  * When the CallInfo is enabled it will be invoked when the EventList is invoked
13
17
  */
@@ -95,8 +99,7 @@ export class CallInfo {
95
99
  }
96
100
  }
97
101
 
98
- const isUpperCase = (string) => /^[A-Z]*$/.test(string);
99
-
102
+ /** @deprecated No longer automatically dispatched. Use `eventList.on()` directly instead. */
100
103
  export class EventListEvent<TArgs extends any> extends Event { //implements ArrayLike<T> {
101
104
  args?: TArgs;
102
105
  }
@@ -150,73 +153,13 @@ export class EventListEvent<TArgs extends any> extends Event { //implements Arra
150
153
  */
151
154
  export class EventList<TArgs extends any = any> implements IEventList {
152
155
 
156
+ /** @internal Used by the instantiate resolver to recursively resolve references */
157
+ static { EventList.prototype.$serializedTypes = { methods: Array }; }
158
+ declare $serializedTypes: Record<string, any>;
159
+
153
160
  /** checked during instantiate to create a new instance */
154
161
  readonly isEventList = true;
155
162
 
156
- /**
157
- * @internal Used by the Needle Engine instantiate call to remap the event listeners to the new instance
158
- */
159
- __internalOnInstantiate(ctx: InstantiateContext) {
160
- const newMethods = new Array<CallInfo>();
161
- for (let i = 0; i < this.methods.length; i++) {
162
- const method = this.methods[i];
163
- if (method.target instanceof Function) {
164
- // can not clone a function
165
- }
166
- else {
167
- const target = method.target as { uuid?: string };
168
- let key = target?.uuid;
169
- if ((target as IComponent)) {
170
- key = (target as IComponent).guid;
171
- }
172
- if (key) {
173
- const newTarget = ctx[key];
174
- if (newTarget) {
175
- // remap the arguments to the new instance (e.g. if an object is passed as an argument to the event list and this object has been cloned we want to remap it to the clone)
176
- const newArguments = method.arguments?.map(arg => {
177
- if (arg instanceof Object && arg.uuid) {
178
- return ctx[arg.uuid].clone;
179
- }
180
- else if ((arg as IComponent)?.isComponent) {
181
- return ctx[(arg as IComponent).guid].clone;
182
- }
183
- return arg;
184
- });
185
- newMethods.push(new CallInfo(newTarget.clone, method.methodName, newArguments, method.enabled));
186
- }
187
- else if (isDevEnvironment()) {
188
- console.warn("Could not find target for event listener");
189
- }
190
- }
191
- }
192
- }
193
- const newInstance = new EventList(newMethods);
194
- return newInstance;
195
- }
196
-
197
- private target?: object;
198
- private key?: string;
199
-
200
- // TODO: serialization should not take care of the args but instead give them to the eventlist directly
201
- // so we can handle passing them on here instead of in the serializer
202
- // this would also allow us to pass them on to the component EventTarget
203
-
204
- /** set an event target to try invoke the EventTarget dispatchEvent when this EventList is invoked */
205
- setEventTarget(key: string, target: object) {
206
- this.key = key;
207
- this.target = target;
208
- if (this.key !== undefined) {
209
- let temp = "";
210
- let foundFirstLetter = false;
211
- for (const c of this.key) {
212
- if (foundFirstLetter && isUpperCase(c))
213
- temp += "-";
214
- foundFirstLetter = true;
215
- temp += c.toLowerCase();
216
- }
217
- this.key = temp;
218
- }
219
- }
220
163
 
221
164
  /** How many callback methods are subscribed to this event */
222
165
  get listenerCount() { return this.methods?.length ?? 0; }
@@ -285,24 +228,9 @@ export class EventList<TArgs extends any = any> implements IEventList {
285
228
  this._methodsCopy.length = 0;
286
229
  this._methodsCopy.push(...this.methods);
287
230
 
288
- // first invoke all the methods that were subscribed to this eventlist
289
231
  for (const m of this._methodsCopy) {
290
232
  m.invoke(...args);
291
233
  }
292
-
293
-
294
- // then try to dispatch the event on the object that is owning this eventlist
295
- // with this we get automatic event listener support for unity events on all componnets
296
- // so example for a component with a click UnityEvent you can also subscribe to the component like this:
297
- // myComponent.addEventListener("click", args => {...")
298
- if (typeof this.target === "object" && typeof this.key === "string") {
299
- const fn = this.target["dispatchEvent"];
300
- if (typeof fn === "function") {
301
- const evt = new EventListEvent(this.key);
302
- evt.args = args;
303
- fn.call(this.target, evt);
304
- }
305
- }
306
234
  }
307
235
  finally {
308
236
  this._isInvoking = false;
@@ -313,12 +241,32 @@ export class EventList<TArgs extends any = any> implements IEventList {
313
241
 
314
242
  /** Add a new event listener to this event
315
243
  * @returns a function to remove the event listener
244
+ * @see {@link removeEventListener} for more details and return value information
245
+ * @see {@link off} for an alias with better readability when unsubscribing from events
246
+ * @example
247
+ * ```ts
248
+ * const off = myEvent.addEventListener(args => console.log("Clicked!", args));
249
+ * // later
250
+ * off();
251
+ * ```
316
252
  */
317
253
  addEventListener(callback: (args: TArgs) => void): Function {
318
254
  this.methods.push(new CallInfo(callback));
319
255
  return () => this.removeEventListener(callback);
320
256
  }
321
257
 
258
+ /**
259
+ * Alias for addEventListener for better readability when subscribing to events. You can use it like this:
260
+ * ```ts
261
+ * myEvent.on(args => console.log("Clicked!", args));
262
+ * ```
263
+ * @returns a function to remove the event listener
264
+ * @see {@link addEventListener} for more details and return value information
265
+ */
266
+ on(callback: (args: TArgs) => void): Function {
267
+ return this.addEventListener(callback);
268
+ }
269
+
322
270
  /**
323
271
  * Remove an event listener from this event.
324
272
  * @returns true if the event listener was found and removed, false otherwise
@@ -336,6 +284,20 @@ export class EventList<TArgs extends any = any> implements IEventList {
336
284
  return found;
337
285
  }
338
286
 
287
+ /**
288
+ * Alias for removeEventListener for better readability when unsubscribing from events. You can use it like this:
289
+ * ```ts
290
+ * const off = myEvent.on(args => console.log("Clicked!", args));
291
+ * // later
292
+ * off();
293
+ * ```
294
+ *
295
+ * @see {@link removeEventListener} for more details and return value information
296
+ */
297
+ off(callback: Function | null | undefined) {
298
+ return this.removeEventListener(callback);
299
+ }
300
+
339
301
  /**
340
302
  * Remove all event listeners from this event. Use with caution! This will remove all listeners!
341
303
  */
@@ -28,6 +28,7 @@ export abstract class Joint extends Behaviour {
28
28
  return this._rigidBody;
29
29
  }
30
30
  private _rigidBody: Rigidbody | null = null;
31
+ private _jointHandle: any = null;
31
32
 
32
33
 
33
34
  onEnable() {
@@ -36,14 +37,28 @@ export abstract class Joint extends Behaviour {
36
37
  this.startCoroutine(this.create());
37
38
  }
38
39
 
40
+ onDisable() {
41
+ if (this._jointHandle) {
42
+ this.context.physics.engine?.removeJoint(this._jointHandle);
43
+ this._jointHandle = null;
44
+ }
45
+ }
46
+
39
47
  private *create() {
40
48
  yield;
49
+ if (this._jointHandle) return;
41
50
  if (this.rigidBody && this.connectedBody && this.activeAndEnabled) {
42
- this.createJoint(this.rigidBody, this.connectedBody)
51
+ const result = this.createJoint(this.rigidBody, this.connectedBody);
52
+ if (result instanceof Promise) {
53
+ yield result.then(handle => this._jointHandle = handle);
54
+ }
55
+ else {
56
+ this._jointHandle = result;
57
+ }
43
58
  }
44
59
  }
45
60
 
46
- protected abstract createJoint(self: Rigidbody, other: Rigidbody);
61
+ protected abstract createJoint(self: Rigidbody, other: Rigidbody): any;
47
62
  }
48
63
 
49
64
  /**
@@ -70,7 +85,7 @@ export abstract class Joint extends Behaviour {
70
85
  export class FixedJoint extends Joint {
71
86
 
72
87
  protected createJoint(self: Rigidbody, other: Rigidbody) {
73
- this.context.physics.engine?.addFixedJoint(self, other);
88
+ return this.context.physics.engine?.addFixedJoint(self, other) ?? null;
74
89
  }
75
90
  }
76
91
 
@@ -110,7 +125,8 @@ export class HingeJoint extends Joint {
110
125
 
111
126
  protected createJoint(self: Rigidbody, other: Rigidbody) {
112
127
  if (this.axis && this.anchor)
113
- this.context.physics.engine?.addHingeJoint(self, other, this.anchor, this.axis);
128
+ return this.context.physics.engine?.addHingeJoint(self, other, this.anchor, this.axis) ?? null;
129
+ return null;
114
130
  }
115
131
 
116
132
  }
@@ -1,12 +1,12 @@
1
1
  import { CameraHelper, Color, DirectionalLight, DirectionalLightHelper, Light as ThreeLight, OrthographicCamera, PointLight, SpotLight, Vector3 } from "three";
2
2
 
3
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
- import { FrameEvent } from "../engine/engine_setup.js";
5
4
  import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
6
5
  import type { ILight } from "../engine/engine_types.js";
7
6
  import { getParam } from "../engine/engine_utils.js";
8
7
  import type { NeedleXREventArgs } from "../engine/xr/api.js";
9
8
  import { Behaviour, GameObject } from "./Component.js";
9
+ import { isDevEnvironment } from "../engine/debug/index.js";
10
10
 
11
11
  // https://threejs.org/examples/webgl_shadowmap_csm.html
12
12
 
@@ -135,7 +135,10 @@ export class Light extends Behaviour implements ILight {
135
135
  this._type = value;
136
136
  break;
137
137
  default:
138
- throw new Error("Invalid light type: " + value);
138
+ if (debug || isDevEnvironment())
139
+ console.warn(`[Light] Unsupported light type: ${LightType[value]} (${value}) on '${this.name}'`);
140
+ this._type = "unsupported" as any;
141
+ break;
139
142
  }
140
143
  }
141
144
  private _type: ILight["type"] = "point";
@@ -474,6 +477,11 @@ export class Light extends Behaviour implements ILight {
474
477
  // const pointHelper = new PointLightHelper(pointLight, this.range, this.color);
475
478
  // scene.add(pointHelper);
476
479
  break;
480
+
481
+ default:
482
+ if (debug) console.warn(`[Light] Unsupported light type: ${LightType[this.type as any]} (${this.type}) on '${this.name}'`);
483
+ break;
484
+
477
485
  }
478
486
  }
479
487
 
@@ -663,7 +663,15 @@ export class OrbitControls extends Behaviour implements ICameraController {
663
663
  }
664
664
  this._controls.enabled = true;
665
665
 
666
- if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
666
+ // Interrupt programmatic transitions on meaningful new user interaction:
667
+ // - Middle/right button down (always intentional camera control)
668
+ // - Mouse wheel (zoom intent)
669
+ // - Left button drag start: getPointerDown(0) with a position delta — a bare click
670
+ // without movement shouldn't cancel an animation, but starting to drag should.
671
+ // Using getPointerDown (not getPointerPressed) ensures we only interrupt once at
672
+ // drag onset, not continuously every frame during a drag.
673
+ const leftDragStart = this.context.input.getPointerDown(0) && (this.context.input.getPointerPositionDelta(0)?.length() || 0) > .1;
674
+ if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || leftDragStart) {
667
675
  this._inputs += 1;
668
676
  }
669
677
  if (this._inputs > 0 && this.allowInterrupt) {
@@ -1095,13 +1103,16 @@ export class OrbitControls extends Behaviour implements ICameraController {
1095
1103
  // Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
1096
1104
  // Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
1097
1105
 
1098
- /**
1099
- * Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
1106
+ /**
1107
+ * Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
1100
1108
  * @param options The options for fitting the camera. Use to provide objects to fit to, fit direction and size and other settings.
1101
1109
  */
1102
1110
  fitCamera(options?: OrbitFitCameraOptions);
1103
- /** @deprecated Use fitCamera(options) */
1104
- fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1111
+ // Deprecated overload commented out: it accepted Object3D as first arg, which caused
1112
+ // TypeScript autocomplete to show Object3D properties (position, worldPosition, etc.)
1113
+ // instead of OrbitFitCameraOptions. The implementation still handles Object3D at runtime
1114
+ // for backwards-compat — use fitCamera({ objects: [...] }) instead.
1115
+ // fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1105
1116
  fitCamera(objectsOrOptions?: Object3D | Array<Object3D> | OrbitFitCameraOptions, options?: OrbitFitCameraOptions): void {
1106
1117
 
1107
1118