@needle-tools/engine 5.1.0-canary.deec6e4 → 5.1.0-canary.e7c2511

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 (243) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-CvtELXh0.js → needle-engine.bundle-D-eWNCu1.js} +15969 -15550
  4. package/dist/needle-engine.bundle-D3ZUII8o.min.js +1733 -0
  5. package/dist/needle-engine.bundle-_rOpvUGL.umd.cjs +1733 -0
  6. package/dist/needle-engine.d.ts +746 -156
  7. package/dist/needle-engine.js +529 -529
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine/api.d.ts +5 -0
  11. package/lib/engine/api.js +4 -0
  12. package/lib/engine/api.js.map +1 -1
  13. package/lib/engine/codegen/register_types.js +2 -10
  14. package/lib/engine/codegen/register_types.js.map +1 -1
  15. package/lib/engine/engine_audio.d.ts +68 -0
  16. package/lib/engine/engine_audio.js +172 -0
  17. package/lib/engine/engine_audio.js.map +1 -1
  18. package/lib/engine/engine_components.js +1 -1
  19. package/lib/engine/engine_components.js.map +1 -1
  20. package/lib/engine/engine_context.d.ts +1 -1
  21. package/lib/engine/engine_context.js +2 -2
  22. package/lib/engine/engine_context.js.map +1 -1
  23. package/lib/engine/engine_disposable.d.ts +171 -0
  24. package/lib/engine/engine_disposable.js +136 -0
  25. package/lib/engine/engine_disposable.js.map +1 -0
  26. package/lib/engine/engine_gameobject.d.ts +1 -10
  27. package/lib/engine/engine_gameobject.js +22 -120
  28. package/lib/engine/engine_gameobject.js.map +1 -1
  29. package/lib/engine/engine_gltf_builtin_components.js +7 -69
  30. package/lib/engine/engine_gltf_builtin_components.js.map +1 -1
  31. package/lib/engine/engine_init.js +6 -6
  32. package/lib/engine/engine_init.js.map +1 -1
  33. package/lib/engine/engine_input.d.ts +1 -1
  34. package/lib/engine/engine_input.js +1 -1
  35. package/lib/engine/engine_input.js.map +1 -1
  36. package/lib/engine/engine_instantiate_resolve.d.ts +42 -0
  37. package/lib/engine/engine_instantiate_resolve.js +372 -0
  38. package/lib/engine/engine_instantiate_resolve.js.map +1 -0
  39. package/lib/engine/engine_license.js +1 -1
  40. package/lib/engine/engine_license.js.map +1 -1
  41. package/lib/engine/engine_mainloop_utils.js +5 -2
  42. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  43. package/lib/engine/engine_networking.js +3 -1
  44. package/lib/engine/engine_networking.js.map +1 -1
  45. package/lib/engine/engine_networking_blob.js +1 -1
  46. package/lib/engine/engine_networking_blob.js.map +1 -1
  47. package/lib/engine/engine_physics_rapier.d.ts +11 -3
  48. package/lib/engine/engine_physics_rapier.js +88 -25
  49. package/lib/engine/engine_physics_rapier.js.map +1 -1
  50. package/lib/engine/engine_scenedata.js +2 -2
  51. package/lib/engine/engine_scenedata.js.map +1 -1
  52. package/lib/engine/engine_serialization_builtin_serializer.js +28 -5
  53. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  54. package/lib/engine/engine_serialization_core.d.ts +1 -0
  55. package/lib/engine/engine_serialization_core.js +7 -0
  56. package/lib/engine/engine_serialization_core.js.map +1 -1
  57. package/lib/engine/engine_types.d.ts +17 -9
  58. package/lib/engine/engine_types.js +1 -1
  59. package/lib/engine/engine_types.js.map +1 -1
  60. package/lib/engine/engine_util_decorator.js +7 -2
  61. package/lib/engine/engine_util_decorator.js.map +1 -1
  62. package/lib/engine/engine_utils.d.ts +1 -1
  63. package/lib/engine/engine_utils.js +19 -5
  64. package/lib/engine/engine_utils.js.map +1 -1
  65. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  66. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  67. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +1 -1
  68. package/lib/engine/webcomponents/needle menu/needle-menu.js +1 -1
  69. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  70. package/lib/engine/webcomponents/needle-engine.d.ts +10 -4
  71. package/lib/engine/webcomponents/needle-engine.js +1 -1
  72. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  73. package/lib/engine/xr/NeedleXRSession.d.ts +3 -2
  74. package/lib/engine/xr/NeedleXRSession.js +50 -14
  75. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  76. package/lib/engine/xr/events.d.ts +1 -1
  77. package/lib/engine/xr/events.js.map +1 -1
  78. package/lib/engine-components/Animation.js +17 -16
  79. package/lib/engine-components/Animation.js.map +1 -1
  80. package/lib/engine-components/Animator.d.ts +6 -0
  81. package/lib/engine-components/Animator.js +17 -12
  82. package/lib/engine-components/Animator.js.map +1 -1
  83. package/lib/engine-components/AnimatorController.builder.d.ts +113 -0
  84. package/lib/engine-components/AnimatorController.builder.js +195 -0
  85. package/lib/engine-components/AnimatorController.builder.js.map +1 -0
  86. package/lib/engine-components/AnimatorController.d.ts +2 -119
  87. package/lib/engine-components/AnimatorController.js +31 -232
  88. package/lib/engine-components/AnimatorController.js.map +1 -1
  89. package/lib/engine-components/AudioSource.d.ts +19 -3
  90. package/lib/engine-components/AudioSource.js +121 -68
  91. package/lib/engine-components/AudioSource.js.map +1 -1
  92. package/lib/engine-components/Collider.d.ts +18 -9
  93. package/lib/engine-components/Collider.js +61 -14
  94. package/lib/engine-components/Collider.js.map +1 -1
  95. package/lib/engine-components/Component.d.ts +58 -6
  96. package/lib/engine-components/Component.js +77 -0
  97. package/lib/engine-components/Component.js.map +1 -1
  98. package/lib/engine-components/DragControls.d.ts +7 -0
  99. package/lib/engine-components/DragControls.js +19 -7
  100. package/lib/engine-components/DragControls.js.map +1 -1
  101. package/lib/engine-components/EventList.d.ts +31 -9
  102. package/lib/engine-components/EventList.js +37 -76
  103. package/lib/engine-components/EventList.js.map +1 -1
  104. package/lib/engine-components/Joints.d.ts +4 -2
  105. package/lib/engine-components/Joints.js +19 -3
  106. package/lib/engine-components/Joints.js.map +1 -1
  107. package/lib/engine-components/Light.js +9 -1
  108. package/lib/engine-components/Light.js.map +1 -1
  109. package/lib/engine-components/Networking.d.ts +1 -1
  110. package/lib/engine-components/Networking.js +1 -1
  111. package/lib/engine-components/OrbitControls.js +16 -11
  112. package/lib/engine-components/OrbitControls.js.map +1 -1
  113. package/lib/engine-components/RigidBody.d.ts +12 -4
  114. package/lib/engine-components/RigidBody.js +18 -4
  115. package/lib/engine-components/RigidBody.js.map +1 -1
  116. package/lib/engine-components/SeeThrough.js +2 -2
  117. package/lib/engine-components/SeeThrough.js.map +1 -1
  118. package/lib/engine-components/api.d.ts +1 -1
  119. package/lib/engine-components/api.js +1 -1
  120. package/lib/engine-components/api.js.map +1 -1
  121. package/lib/engine-components/codegen/components.d.ts +3 -9
  122. package/lib/engine-components/codegen/components.js +3 -9
  123. package/lib/engine-components/codegen/components.js.map +1 -1
  124. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  125. package/lib/engine-components/timeline/PlayableDirector.d.ts +16 -6
  126. package/lib/engine-components/timeline/PlayableDirector.js +63 -61
  127. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  128. package/lib/engine-components/timeline/SignalAsset.d.ts +3 -1
  129. package/lib/engine-components/timeline/SignalAsset.js +1 -0
  130. package/lib/engine-components/timeline/SignalAsset.js.map +1 -1
  131. package/lib/engine-components/timeline/TimelineBuilder.d.ts +247 -0
  132. package/lib/engine-components/timeline/TimelineBuilder.js +400 -0
  133. package/lib/engine-components/timeline/TimelineBuilder.js.map +1 -0
  134. package/lib/engine-components/timeline/TimelineModels.d.ts +2 -1
  135. package/lib/engine-components/timeline/TimelineModels.js +3 -0
  136. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  137. package/lib/engine-components/timeline/TimelineTracks.d.ts +23 -0
  138. package/lib/engine-components/timeline/TimelineTracks.js +71 -13
  139. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  140. package/lib/engine-components/timeline/index.d.ts +2 -1
  141. package/lib/engine-components/timeline/index.js +2 -0
  142. package/lib/engine-components/timeline/index.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/webxr/WebXRImageTracking.js +4 -0
  152. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  153. package/package.json +2 -83
  154. package/plugins/common/worker.js +9 -4
  155. package/plugins/vite/asap.js +17 -8
  156. package/plugins/vite/dependencies.js +29 -0
  157. package/plugins/vite/dependency-watcher.js +2 -2
  158. package/plugins/vite/editor-connection.js +3 -3
  159. package/plugins/vite/local-files-core.js +3 -3
  160. package/plugins/vite/local-files-utils.d.ts +3 -1
  161. package/plugins/vite/local-files-utils.js +29 -5
  162. package/plugins/vite/reload.js +1 -1
  163. package/plugins/vite/server.js +2 -1
  164. package/src/engine/api.ts +7 -0
  165. package/src/engine/codegen/register_types.ts +2 -10
  166. package/src/engine/engine_audio.ts +184 -0
  167. package/src/engine/engine_components.ts +1 -1
  168. package/src/engine/engine_context.ts +3 -3
  169. package/src/engine/engine_disposable.ts +213 -0
  170. package/src/engine/engine_gameobject.ts +54 -159
  171. package/src/engine/engine_gltf_builtin_components.ts +7 -76
  172. package/src/engine/engine_init.ts +6 -6
  173. package/src/engine/engine_input.ts +1 -1
  174. package/src/engine/engine_instantiate_resolve.ts +407 -0
  175. package/src/engine/engine_license.ts +1 -1
  176. package/src/engine/engine_mainloop_utils.ts +5 -2
  177. package/src/engine/engine_networking.ts +3 -1
  178. package/src/engine/engine_networking_blob.ts +1 -1
  179. package/src/engine/engine_physics_rapier.ts +82 -27
  180. package/src/engine/engine_scenedata.ts +3 -3
  181. package/src/engine/engine_serialization_builtin_serializer.ts +32 -9
  182. package/src/engine/engine_serialization_core.ts +9 -0
  183. package/src/engine/engine_types.ts +22 -13
  184. package/src/engine/engine_util_decorator.ts +7 -2
  185. package/src/engine/engine_utils.ts +16 -5
  186. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +1 -1
  187. package/src/engine/webcomponents/needle menu/needle-menu.ts +1 -1
  188. package/src/engine/webcomponents/needle-engine.ts +10 -4
  189. package/src/engine/xr/NeedleXRSession.ts +48 -13
  190. package/src/engine/xr/events.ts +1 -1
  191. package/src/engine-components/Animation.ts +19 -16
  192. package/src/engine-components/Animator.ts +18 -11
  193. package/src/engine-components/AnimatorController.builder.ts +261 -0
  194. package/src/engine-components/AnimatorController.ts +19 -291
  195. package/src/engine-components/AudioSource.ts +130 -79
  196. package/src/engine-components/Collider.ts +66 -18
  197. package/src/engine-components/Component.ts +79 -9
  198. package/src/engine-components/DragControls.ts +18 -11
  199. package/src/engine-components/EventList.ts +45 -83
  200. package/src/engine-components/Joints.ts +20 -4
  201. package/src/engine-components/Light.ts +10 -2
  202. package/src/engine-components/Networking.ts +1 -1
  203. package/src/engine-components/OrbitControls.ts +18 -9
  204. package/src/engine-components/RigidBody.ts +18 -4
  205. package/src/engine-components/SeeThrough.ts +2 -2
  206. package/src/engine-components/api.ts +1 -1
  207. package/src/engine-components/codegen/components.ts +3 -9
  208. package/src/engine-components/timeline/PlayableDirector.ts +61 -64
  209. package/src/engine-components/timeline/SignalAsset.ts +4 -1
  210. package/src/engine-components/timeline/TimelineBuilder.ts +565 -0
  211. package/src/engine-components/timeline/TimelineModels.ts +5 -1
  212. package/src/engine-components/timeline/TimelineTracks.ts +74 -13
  213. package/src/engine-components/timeline/index.ts +2 -1
  214. package/src/engine-components/ui/Canvas.ts +2 -8
  215. package/src/engine-components/ui/Text.ts +12 -8
  216. package/src/engine-components/web/CursorFollow.ts +21 -13
  217. package/src/engine-components/webxr/WebXRImageTracking.ts +2 -0
  218. package/dist/needle-engine.bundle-1s2gOoKZ.min.js +0 -1732
  219. package/dist/needle-engine.bundle-j4nGJXCs.umd.cjs +0 -1732
  220. package/lib/engine-components/AvatarLoader.d.ts +0 -80
  221. package/lib/engine-components/AvatarLoader.js +0 -232
  222. package/lib/engine-components/AvatarLoader.js.map +0 -1
  223. package/lib/engine-components/avatar/AvatarBlink_Simple.d.ts +0 -11
  224. package/lib/engine-components/avatar/AvatarBlink_Simple.js +0 -77
  225. package/lib/engine-components/avatar/AvatarBlink_Simple.js.map +0 -1
  226. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.d.ts +0 -14
  227. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.js +0 -69
  228. package/lib/engine-components/avatar/AvatarEyeLook_Rotation.js.map +0 -1
  229. package/lib/engine-components/avatar/Avatar_Brain_LookAt.d.ts +0 -29
  230. package/lib/engine-components/avatar/Avatar_Brain_LookAt.js +0 -122
  231. package/lib/engine-components/avatar/Avatar_Brain_LookAt.js.map +0 -1
  232. package/lib/engine-components/avatar/Avatar_MouthShapes.d.ts +0 -15
  233. package/lib/engine-components/avatar/Avatar_MouthShapes.js +0 -80
  234. package/lib/engine-components/avatar/Avatar_MouthShapes.js.map +0 -1
  235. package/lib/engine-components/avatar/Avatar_MustacheShake.d.ts +0 -9
  236. package/lib/engine-components/avatar/Avatar_MustacheShake.js +0 -30
  237. package/lib/engine-components/avatar/Avatar_MustacheShake.js.map +0 -1
  238. package/src/engine-components/AvatarLoader.ts +0 -264
  239. package/src/engine-components/avatar/AvatarBlink_Simple.ts +0 -70
  240. package/src/engine-components/avatar/AvatarEyeLook_Rotation.ts +0 -64
  241. package/src/engine-components/avatar/Avatar_Brain_LookAt.ts +0 -140
  242. package/src/engine-components/avatar/Avatar_MouthShapes.ts +0 -84
  243. package/src/engine-components/avatar/Avatar_MustacheShake.ts +0 -32
@@ -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
 
@@ -173,6 +176,10 @@ export * from "./engine_context_registry.js";
173
176
  */
174
177
  export * from "./engine_coroutine.js"
175
178
 
179
+ /** DisposableStore for managing lifecycle-bound cleanup in components */
180
+ export { DisposableStore, on, isDisposable } from "./engine_disposable.js";
181
+ export type { DisposeFn, IDisposable } from "./engine_disposable.js";
182
+
176
183
  /** Factory functions for creating primitives and objects */
177
184
  export * from "./engine_create_objects.js";
178
185
 
@@ -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";
@@ -110,6 +105,7 @@ import { AnimationTrackHandler } from "../../engine-components/timeline/Timeline
110
105
  import { AudioTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
111
106
  import { MarkerTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
112
107
  import { SignalTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
108
+ import { ActivationTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
113
109
  import { ControlTrackHandler } 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";
@@ -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);
@@ -270,6 +261,7 @@ export function initBuiltinTypes() {
270
261
  TypeStore.add("AudioTrackHandler", AudioTrackHandler);
271
262
  TypeStore.add("MarkerTrackHandler", MarkerTrackHandler);
272
263
  TypeStore.add("SignalTrackHandler", SignalTrackHandler);
264
+ TypeStore.add("ActivationTrackHandler", ActivationTrackHandler);
273
265
  TypeStore.add("ControlTrackHandler", ControlTrackHandler);
274
266
  TypeStore.add("TransformGizmo", TransformGizmo);
275
267
  TypeStore.add("BaseUIComponent", BaseUIComponent);
@@ -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
  }
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  import 'three/examples/jsm/renderers/webgl-legacy/nodes/WebGLNodes.js';
2
2
 
3
+ import type { SceneData } from 'needle-bindings';
3
4
  import type { EffectComposer } from "postprocessing";
4
5
  import {
5
6
  BufferGeometry, Camera, Color, DepthTexture, Group,
@@ -36,9 +37,8 @@ import * as looputils from './engine_mainloop_utils.js';
36
37
  import { NetworkConnection } from './engine_networking.js';
37
38
  import { Physics } from './engine_physics.js';
38
39
  import { PlayerViewManager } from './engine_playerview.js';
39
- import { RendererData as SceneLighting } from './engine_scenelighting.js';
40
40
  import { getSceneData } from './engine_scenedata.js';
41
- import type { SceneData } from 'needle-bindings';
41
+ import { RendererData as SceneLighting } from './engine_scenelighting.js';
42
42
  import { getTempColor, logHierarchy } from './engine_three_utils.js';
43
43
  import { Time } from './engine_time.js';
44
44
  import { patchTonemapping } from './engine_tonemapping.js';
@@ -863,7 +863,7 @@ export class Context implements IContext {
863
863
  this.scene = new Scene();
864
864
  this.addressables?.dispose();
865
865
  this.lightmaps?.clear();
866
- this.physics?.engine?.clearCaches();
866
+ this.physics?.engine?.dispose();
867
867
  this.lodsManager.disable();
868
868
  this.accessibility?.clear();
869
869
 
@@ -0,0 +1,213 @@
1
+ /** A function that performs cleanup when called */
2
+ export type DisposeFn = () => void;
3
+
4
+ /**
5
+ * Interface for objects that hold resources and can be disposed.
6
+ * Implement this interface on any object that needs deterministic cleanup.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * class MyResource implements IDisposable {
11
+ * dispose() { /* release resources *\/ }
12
+ * }
13
+ * ```
14
+ *
15
+ * @category Utilities
16
+ * @group Lifecycle
17
+ */
18
+ export interface IDisposable {
19
+ dispose(): void;
20
+ }
21
+
22
+ /**
23
+ * Type guard to check if an object implements {@link IDisposable}.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * if (isDisposable(obj)) {
28
+ * obj.dispose(); // safe to call
29
+ * }
30
+ * ```
31
+ */
32
+ export function isDisposable(value: unknown): value is IDisposable {
33
+ return value !== null
34
+ && typeof value === "object"
35
+ && "dispose" in (value as object)
36
+ && typeof (value as IDisposable).dispose === "function";
37
+ }
38
+
39
+ /**
40
+ * Subscribe to a DOM event on any {@link EventTarget} (window, document, HTML elements, etc.)
41
+ * and return an {@link IDisposable} that removes the listener when disposed.
42
+ *
43
+ * Provides full TypeScript event type inference — the callback parameter
44
+ * is automatically typed based on the event name (e.g. `"resize"` → `UIEvent`,
45
+ * `"click"` → `MouseEvent`).
46
+ *
47
+ * Use with {@link DisposableStore.add} for automatic lifecycle cleanup in components.
48
+ *
49
+ * @param target The EventTarget to listen on (window, document, an element, etc.)
50
+ * @param type The event name (e.g. `"resize"`, `"click"`, `"keydown"`)
51
+ * @param listener The event handler callback
52
+ * @param options Optional addEventListener options (passive, capture, once, signal)
53
+ * @returns An {@link IDisposable} that removes the event listener when disposed
54
+ *
55
+ * @example Standalone usage
56
+ * ```ts
57
+ * import { on } from "@needle-tools/engine";
58
+ *
59
+ * const sub = on(window, "resize", (ev) => {
60
+ * // ev is typed as UIEvent
61
+ * console.log("resized", ev.target);
62
+ * });
63
+ *
64
+ * // Later: clean up
65
+ * sub.dispose();
66
+ * ```
67
+ *
68
+ * @example With autoCleanup in a component
69
+ * ```ts
70
+ * import { Behaviour, on } from "@needle-tools/engine";
71
+ *
72
+ * export class MyComponent extends Behaviour {
73
+ * onEnable() {
74
+ * this.autoCleanup(on(window, "resize", (ev) => { /* UIEvent *\/ }));
75
+ * this.autoCleanup(on(document, "keydown", (ev) => { /* KeyboardEvent *\/ }));
76
+ * this.autoCleanup(on(this.context.domElement, "click", (ev) => { /* MouseEvent *\/ }));
77
+ * }
78
+ * // All listeners removed automatically on disable!
79
+ * }
80
+ * ```
81
+ *
82
+ * @category Utilities
83
+ * @group Lifecycle
84
+ */
85
+ // #region on
86
+ export function on<K extends keyof WindowEventMap>(target: Window, type: K, listener: (ev: WindowEventMap[K]) => void, options?: boolean | AddEventListenerOptions): IDisposable;
87
+ export function on<K extends keyof DocumentEventMap>(target: Document, type: K, listener: (ev: DocumentEventMap[K]) => void, options?: boolean | AddEventListenerOptions): IDisposable;
88
+ export function on<K extends keyof HTMLElementEventMap>(target: HTMLElement, type: K, listener: (ev: HTMLElementEventMap[K]) => void, options?: boolean | AddEventListenerOptions): IDisposable;
89
+ export function on(target: EventTarget, type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): IDisposable;
90
+ export function on(target: EventTarget, type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): IDisposable {
91
+ target.addEventListener(type, listener, options);
92
+ return {
93
+ dispose() {
94
+ target.removeEventListener(type, listener, options);
95
+ }
96
+ };
97
+ }
98
+
99
+
100
+ /**
101
+ * A store for managing disposable resources (event subscriptions, listeners, callbacks)
102
+ * that should be cleaned up together.
103
+ *
104
+ * DisposableStore collects disposables and disposes them all at once when
105
+ * {@link dispose} is called. After disposal, the store can be reused — new items
106
+ * can be added and a subsequent {@link dispose} call will clean those up.
107
+ *
108
+ * This is the same pattern used internally by VSCode for lifecycle-bound resource management.
109
+ *
110
+ * @example Basic usage
111
+ * ```ts
112
+ * import { DisposableStore, on } from "@needle-tools/engine";
113
+ *
114
+ * const store = new DisposableStore();
115
+ *
116
+ * // Register a DOM event listener (typed!)
117
+ * store.add(on(window, "resize", (ev) => console.log(ev)));
118
+ *
119
+ * // Register the return value of EventList.on()
120
+ * store.add(myEventList.on(data => console.log(data)));
121
+ *
122
+ * // Register a raw cleanup function
123
+ * store.add(() => someSDK.off("event", handler));
124
+ *
125
+ * // Later: dispose everything at once
126
+ * store.dispose();
127
+ * ```
128
+ *
129
+ * @example Use with Needle Engine components
130
+ * ```ts
131
+ * import { Behaviour, serializable, EventList, on } from "@needle-tools/engine";
132
+ *
133
+ * export class MyComponent extends Behaviour {
134
+ * @serializable(EventList)
135
+ * onClick?: EventList;
136
+ *
137
+ * onEnable() {
138
+ * // DOM events — fully typed
139
+ * this.autoCleanup(on(window, "resize", (ev) => this.onResize(ev)));
140
+ *
141
+ * // EventList — .on() returns a function, autoCleanup accepts it
142
+ * this.autoCleanup(this.onClick?.on(() => console.log("clicked!")));
143
+ * }
144
+ * // No onDisable needed — cleaned up automatically!
145
+ * }
146
+ * ```
147
+ *
148
+ * @category Utilities
149
+ * @group Lifecycle
150
+ */
151
+ // #region DisposableStore
152
+ export class DisposableStore implements IDisposable {
153
+
154
+ private _disposables: Array<DisposeFn> = [];
155
+
156
+ /** The number of registered disposables */
157
+ get size() { return this._disposables.length; }
158
+
159
+ /**
160
+ * Register a disposable resource. Accepts:
161
+ * - An {@link IDisposable} object (has a `dispose()` method) — e.g. from {@link on}
162
+ * - A cleanup function (e.g. return value of `EventList.on()`)
163
+ * - `null` or `undefined` (safe no-op for conditional subscriptions)
164
+ *
165
+ * When {@link dispose} is called, all registered resources are cleaned up.
166
+ *
167
+ * @param disposable The resource to register for disposal
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const store = new DisposableStore();
172
+ *
173
+ * // IDisposable object from on()
174
+ * store.add(on(window, "resize", handler));
175
+ *
176
+ * // Function returned by EventList.on()
177
+ * store.add(myEvent.on(handler));
178
+ *
179
+ * // Raw cleanup function
180
+ * store.add(() => connection.close());
181
+ *
182
+ * // Conditional — safe with undefined
183
+ * store.add(this.maybeEvent?.on(handler));
184
+ * ```
185
+ */
186
+ add(disposable: IDisposable | DisposeFn | Function | null | undefined): void {
187
+ if (!disposable) return;
188
+ if (typeof disposable === "function") {
189
+ this._disposables.push(disposable as DisposeFn);
190
+ }
191
+ else if (typeof disposable === "object" && "dispose" in disposable) {
192
+ this._disposables.push(() => disposable.dispose());
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Dispose all registered resources. Each registered disposable is cleaned up,
198
+ * then the internal list is cleared. The store can be reused after disposal.
199
+ *
200
+ * Called automatically by the engine when a component's `onDisable` lifecycle fires.
201
+ */
202
+ dispose(): void {
203
+ for (let i = this._disposables.length - 1; i >= 0; i--) {
204
+ try {
205
+ this._disposables[i]();
206
+ }
207
+ catch (err) {
208
+ console.error("Error disposing resource", err);
209
+ }
210
+ }
211
+ this._disposables.length = 0;
212
+ }
213
+ }