@needle-tools/engine 3.1.0-alpha.2 → 3.2.1-alpha

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 (95) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/needle-engine.js +9332 -9066
  3. package/dist/needle-engine.min.js +264 -264
  4. package/dist/needle-engine.umd.cjs +266 -266
  5. package/lib/engine/codegen/register_types.js +2 -0
  6. package/lib/engine/codegen/register_types.js.map +1 -1
  7. package/lib/engine/debug/debug_overlay.js +2 -0
  8. package/lib/engine/debug/debug_overlay.js.map +1 -1
  9. package/lib/engine/engine_context_registry.d.ts +2 -0
  10. package/lib/engine/engine_context_registry.js +3 -0
  11. package/lib/engine/engine_context_registry.js.map +1 -1
  12. package/lib/engine/engine_gltf.js +4 -0
  13. package/lib/engine/engine_gltf.js.map +1 -1
  14. package/lib/engine/engine_gltf_builtin_components.js +2 -0
  15. package/lib/engine/engine_gltf_builtin_components.js.map +1 -1
  16. package/lib/engine/engine_license.js +1 -1
  17. package/lib/engine/engine_license.js.map +1 -1
  18. package/lib/engine/engine_lightdata.js +1 -1
  19. package/lib/engine/engine_lightdata.js.map +1 -1
  20. package/lib/engine/engine_networking_auto.d.ts +3 -3
  21. package/lib/engine/engine_networking_auto.js +7 -3
  22. package/lib/engine/engine_networking_auto.js.map +1 -1
  23. package/lib/engine/engine_rendererdata.d.ts +1 -0
  24. package/lib/engine/engine_rendererdata.js +39 -15
  25. package/lib/engine/engine_rendererdata.js.map +1 -1
  26. package/lib/engine/engine_serialization_builtin_serializer.js +10 -1
  27. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  28. package/lib/engine/extensions/NEEDLE_lighting_settings.js +14 -11
  29. package/lib/engine/extensions/NEEDLE_lighting_settings.js.map +1 -1
  30. package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
  31. package/lib/engine/extensions/NEEDLE_progressive.js +11 -4
  32. package/lib/engine/extensions/NEEDLE_progressive.js.map +1 -1
  33. package/lib/engine/extensions/extensions.js +4 -0
  34. package/lib/engine/extensions/extensions.js.map +1 -1
  35. package/lib/engine-components/AudioSource.d.ts +1 -0
  36. package/lib/engine-components/AudioSource.js +6 -3
  37. package/lib/engine-components/AudioSource.js.map +1 -1
  38. package/lib/engine-components/Camera.js +1 -1
  39. package/lib/engine-components/Camera.js.map +1 -1
  40. package/lib/engine-components/Component.js.map +1 -1
  41. package/lib/engine-components/Renderer.d.ts +5 -0
  42. package/lib/engine-components/Renderer.js +64 -17
  43. package/lib/engine-components/Renderer.js.map +1 -1
  44. package/lib/engine-components/RendererLightmap.d.ts +1 -2
  45. package/lib/engine-components/RendererLightmap.js +8 -10
  46. package/lib/engine-components/RendererLightmap.js.map +1 -1
  47. package/lib/engine-components/SceneSwitcher.d.ts +33 -0
  48. package/lib/engine-components/SceneSwitcher.js +244 -0
  49. package/lib/engine-components/SceneSwitcher.js.map +1 -0
  50. package/lib/engine-components/ScreenCapture.js +1 -0
  51. package/lib/engine-components/ScreenCapture.js.map +1 -1
  52. package/lib/engine-components/VideoPlayer.d.ts +2 -0
  53. package/lib/engine-components/VideoPlayer.js +7 -3
  54. package/lib/engine-components/VideoPlayer.js.map +1 -1
  55. package/lib/engine-components/XRFlag.js +3 -0
  56. package/lib/engine-components/XRFlag.js.map +1 -1
  57. package/lib/engine-components/codegen/components.d.ts +1 -0
  58. package/lib/engine-components/codegen/components.js +1 -0
  59. package/lib/engine-components/codegen/components.js.map +1 -1
  60. package/lib/engine-components/timeline/PlayableDirector.js +6 -4
  61. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  62. package/lib/engine-components/timeline/SignalAsset.d.ts +5 -7
  63. package/lib/engine-components/timeline/SignalAsset.js +38 -6
  64. package/lib/engine-components/timeline/SignalAsset.js.map +1 -1
  65. package/lib/engine-components/timeline/TimelineTracks.js +40 -12
  66. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  67. package/lib/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +1 -1
  69. package/src/engine/codegen/register_types.js +2 -0
  70. package/src/engine/debug/debug_overlay.ts +1 -0
  71. package/src/engine/engine_context_registry.ts +3 -0
  72. package/src/engine/engine_gltf.ts +6 -2
  73. package/src/engine/engine_gltf_builtin_components.ts +3 -0
  74. package/src/engine/engine_license.ts +1 -1
  75. package/src/engine/engine_lightdata.ts +1 -1
  76. package/src/engine/engine_networking_auto.ts +13 -6
  77. package/src/engine/engine_rendererdata.ts +35 -16
  78. package/src/engine/engine_serialization_builtin_serializer.ts +10 -1
  79. package/src/engine/extensions/NEEDLE_lighting_settings.ts +15 -13
  80. package/src/engine/extensions/NEEDLE_lightmaps.ts +3 -2
  81. package/src/engine/extensions/NEEDLE_progressive.ts +9 -3
  82. package/src/engine/extensions/extensions.ts +6 -0
  83. package/src/engine-components/AudioSource.ts +6 -3
  84. package/src/engine-components/Camera.ts +1 -1
  85. package/src/engine-components/Component.ts +0 -1
  86. package/src/engine-components/Renderer.ts +69 -20
  87. package/src/engine-components/RendererLightmap.ts +7 -11
  88. package/src/engine-components/SceneSwitcher.ts +240 -0
  89. package/src/engine-components/ScreenCapture.ts +1 -0
  90. package/src/engine-components/VideoPlayer.ts +16 -11
  91. package/src/engine-components/XRFlag.ts +3 -0
  92. package/src/engine-components/codegen/components.ts +1 -0
  93. package/src/engine-components/timeline/PlayableDirector.ts +13 -11
  94. package/src/engine-components/timeline/SignalAsset.ts +35 -8
  95. package/src/engine-components/timeline/TimelineTracks.ts +43 -13
@@ -14,7 +14,7 @@ export class RendererLightmap {
14
14
  set lightmap(tex: Texture | null) {
15
15
  if (tex !== this.lightmapTexture) {
16
16
  this.lightmapTexture = tex;
17
- this.setupLightmap();
17
+ this.applyLightmap();
18
18
  }
19
19
  }
20
20
 
@@ -27,8 +27,6 @@ export class RendererLightmap {
27
27
  private lightmapScaleOffsetUniform = { value: new THREE.Vector4(1, 1, 0, 0) };
28
28
  private lightmapUniform: { value: THREE.Texture | null } = { value: null };
29
29
 
30
- private beforeRenderCallback?: OnBeforeRenderCallback;
31
-
32
30
  constructor(gameObject: GameObject, context: Context) {
33
31
  this.gameObject = gameObject;
34
32
  this.context = context;
@@ -44,20 +42,19 @@ export class RendererLightmap {
44
42
 
45
43
  const debugLightmaps = debug;
46
44
  if (debugLightmaps) this.setLightmapDebugMaterial();
47
- this.setupLightmap();
45
+ this.applyLightmap();
48
46
  }
49
47
 
50
48
  bindOnBeforeRender() {
51
- this.beforeRenderCallback = this.onBeforeRenderThreeComplete.bind(this);
52
- this.context.addBeforeRenderListener(this.gameObject, this.beforeRenderCallback);
53
- // this.gameObject.onBeforeRender = this.onBeforeRenderThreeComplete.bind(this);
49
+ this.context.removeBeforeRenderListener(this.gameObject, this.onBeforeRenderThreeComplete);
50
+ this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThreeComplete);
54
51
  }
55
52
 
56
- private onBeforeRenderThreeComplete(_renderer, _scene, _camera, _geometry, material, _group) {
53
+ private onBeforeRenderThreeComplete = (_renderer, _scene, _camera, _geometry, material, _group) => {
57
54
  this.onBeforeRenderThree(material);
58
55
  }
59
56
 
60
- private setupLightmap() {
57
+ private applyLightmap() {
61
58
 
62
59
  if (this.gameObject.type === "Object3D") {
63
60
  // console.warn("Can not add lightmap. Is this object missing a renderer?");
@@ -65,13 +62,12 @@ export class RendererLightmap {
65
62
  }
66
63
 
67
64
  if (this.gameObject.type === "Group") {
68
- console.warn("Lightmap on multimaterial object is not supported yet... please ask kindly for implementation.");
65
+ console.warn("Lightmap on multimaterial object is not supported yet... please open a feature request on https://github.com/needle-tools/needle-engine-support if your project requires it");
69
66
  return;
70
67
  }
71
68
 
72
69
  console.assert(this.gameObject.type === "Mesh", "Lightmap only works on meshes", this);
73
70
 
74
-
75
71
  const mesh = this.gameObject as unknown as THREE.Mesh;
76
72
  // TODO: ensure uv2 exists
77
73
  if (!mesh.geometry.getAttribute("uv2"))
@@ -0,0 +1,240 @@
1
+ import { AssetReference } from "../engine/engine_addressables";
2
+ import { NeedleEngineHTMLElement } from "../engine/engine_element";
3
+ import { InputEvents } from "../engine/engine_input";
4
+ import { isLocalNetwork } from "../engine/engine_networking_utils";
5
+ import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry";
6
+ import { getParam, isMobileDevice, setParamWithoutReload } from "../engine/engine_utils";
7
+ import { serializable } from "../engine/engine_serialization";
8
+ import { Behaviour, GameObject } from "./Component";
9
+
10
+ const debug = getParam("debugsceneswitcher");
11
+
12
+ const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
13
+
14
+ ContextRegistry.registerCallback(ContextEvent.ContextRegistered, _ => {
15
+ if (!NeedleEngineHTMLElement.observedAttributes.includes(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME))
16
+ NeedleEngineHTMLElement.observedAttributes.push(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
17
+ });
18
+
19
+ const couldNotLoadScenePromise = Promise.resolve(false);
20
+
21
+ export class SceneSwitcher extends Behaviour {
22
+
23
+ @serializable(AssetReference)
24
+ scenes!: AssetReference[];
25
+
26
+ /** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable */
27
+ @serializable()
28
+ queryParameterName: string = "scene";
29
+
30
+ @serializable()
31
+ clamp: boolean = true;
32
+
33
+ /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set */
34
+ @serializable()
35
+ useHistory: boolean = true;
36
+
37
+ /** when enabled you can switch between scenes using keyboard left, right, A and D or number keys */
38
+ @serializable()
39
+ useKeyboard: boolean = true;
40
+
41
+ /** when enabled you can switch between scenes using swipe (mobile only) */
42
+ @serializable()
43
+ useSwipe: boolean = true;
44
+
45
+
46
+ private _currentIndex: number = -1;
47
+ private _currentScene: AssetReference | undefined = undefined;
48
+ private _engineElementOverserver: MutationObserver | undefined = undefined;
49
+
50
+ start() {
51
+ if (!this.tryLoadFromQueryParam()) {
52
+ const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
53
+ // let locked = this.lock;
54
+ try {
55
+ // this.lock = false;
56
+ if (value === null || !this.trySelectSceneFromValue(value))
57
+ this.select(0);
58
+ }
59
+ finally {
60
+ // this.lock = locked;
61
+ }
62
+ }
63
+ }
64
+
65
+ onEnable(): void {
66
+ globalThis.addEventListener("popstate", this.onPopState);
67
+ this.context.input.addEventListener(InputEvents.KeyDown, this.onKeyDown);
68
+ this.context.input.addEventListener(InputEvents.PointerMove, this.onPointerMove);
69
+ this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerUp);
70
+
71
+ if (!this._engineElementOverserver) {
72
+ this._engineElementOverserver = new MutationObserver((mutations) => {
73
+ for (const mut of mutations) {
74
+ if (mut.type === "attributes" && mut.attributeName === ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME) {
75
+ const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
76
+ if (value !== null) {
77
+ this.trySelectSceneFromValue(value);
78
+ }
79
+ }
80
+ }
81
+ });
82
+ }
83
+
84
+ this._engineElementOverserver.observe(this.context.domElement, {
85
+ attributes: true
86
+ });
87
+ }
88
+
89
+ onDisable(): void {
90
+ globalThis.removeEventListener("popstate", this.onPopState);
91
+ this.context.input.removeEventListener(InputEvents.KeyDown, this.onKeyDown);
92
+ this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerMove);
93
+ this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerUp);
94
+ }
95
+
96
+ private onPopState = async (_state: PopStateEvent) => {
97
+ if (!this.useHistory) return;
98
+ let wasUsingHistory = this.useHistory;
99
+ try {
100
+ this.useHistory = false;
101
+ await this.tryLoadFromQueryParam();
102
+ }
103
+ finally {
104
+ this.useHistory = wasUsingHistory;
105
+ }
106
+ }
107
+
108
+ private normalizedSwipeThresholdX = 0.1;
109
+ private _didSwipe: boolean = false;
110
+ private onPointerMove = (e: any) => {
111
+ if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
112
+ const delta = this.context.input.getPointerPositionDelta(e.button);
113
+ if (delta) {
114
+ const normalizedX = delta.x / this.context.domWidth;
115
+ if (normalizedX >= this.normalizedSwipeThresholdX) {
116
+ this._didSwipe = true;
117
+ this.selectPrev();
118
+ }
119
+ else if (normalizedX <= -this.normalizedSwipeThresholdX) {
120
+ this._didSwipe = true;
121
+ this.selectNext();
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ private onPointerUp = (e: any) => {
128
+ if (e.button === 0) {
129
+ this._didSwipe = false;
130
+ }
131
+ };
132
+
133
+ private onKeyDown = (e: any) => {
134
+ if (!this.useKeyboard) return;
135
+ if (!this.scenes) return;
136
+ const key = e.key.toLowerCase();
137
+ if (!key) return;
138
+ const index = parseInt(key) - 1;
139
+ if (index >= 0) {
140
+ this.trySelectSceneFromValue(index);
141
+ return;
142
+ }
143
+ switch (key) {
144
+ case "arrowright":
145
+ case "d":
146
+ this.selectNext();
147
+ break;
148
+ case "arrowleft":
149
+ case "a":
150
+ this.selectPrev();
151
+ break;
152
+ }
153
+ }
154
+
155
+ selectNext(): Promise<boolean> {
156
+ return this.select(this._currentIndex + 1);
157
+ }
158
+
159
+ selectPrev(): Promise<boolean> {
160
+ return this.select(this._currentIndex - 1);
161
+ }
162
+
163
+ select(index: number): Promise<boolean> {
164
+ if (debug) console.log("select", index)
165
+ if (!this.scenes?.length) return couldNotLoadScenePromise;
166
+ if (index < 0) {
167
+ if (this.clamp) return couldNotLoadScenePromise;
168
+ index = this.scenes.length - 1;
169
+ }
170
+ else if (index >= this.scenes.length) {
171
+ if (this.clamp) return couldNotLoadScenePromise;
172
+ index = 0;
173
+ }
174
+ const scene = this.scenes[index];
175
+ return this.switchScene(scene);
176
+ }
177
+
178
+ async switchScene(scene: AssetReference): Promise<boolean> {
179
+ if (scene === this._currentScene) return true;
180
+ if (this._currentScene)
181
+ this._currentScene.unload();
182
+ const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
183
+ this._currentScene = scene;
184
+ try {
185
+ await scene.loadAssetAsync();
186
+ if (!scene.asset) {
187
+ if (debug) console.warn("Failed loading scene:", scene);
188
+ return false;
189
+ }
190
+ if (this._currentIndex === index) {
191
+ GameObject.add(scene.asset, this.gameObject);
192
+ // save the loaded scene as an url parameter
193
+ if (this.queryParameterName?.length)
194
+ setParamWithoutReload(this.queryParameterName, index.toString(), this.useHistory);
195
+ return true;
196
+ }
197
+ }
198
+ catch (err) {
199
+ console.error(err);
200
+ }
201
+ return false;
202
+ }
203
+
204
+ private tryLoadFromQueryParam() {
205
+ if (!this.queryParameterName?.length) return false;
206
+ // try restore the scene from the url
207
+ const value = getParam(this.queryParameterName);
208
+ if (typeof value === "boolean") return false;
209
+ return this.trySelectSceneFromValue(value);
210
+ }
211
+
212
+ /** try to select a scene from a string or index */
213
+ private trySelectSceneFromValue(value: string | number) {
214
+
215
+ if (typeof value === "string") {
216
+ const index = parseInt(value as string);
217
+ if (index >= 0 && index < this.scenes.length) {
218
+ return this.select(index);;
219
+ }
220
+ else {
221
+ // Try to find a scene with a matching name
222
+ for (let i = 0; i < this.scenes.length; i++) {
223
+ const scene = this.scenes[i];
224
+ if (scene.uri.toLowerCase().includes(value)) {
225
+ return this.select(i);;
226
+ }
227
+ }
228
+ }
229
+ }
230
+ else if (typeof value === "number") {
231
+ if (value >= 0 && value < this.scenes.length) {
232
+ return this.select(value);;
233
+ }
234
+ }
235
+
236
+ if (isLocalNetwork()) console.warn("Unknown scene value or index: \"" + value + "\"", this)
237
+
238
+ return couldNotLoadScenePromise;
239
+ }
240
+ }
@@ -96,6 +96,7 @@ export class ScreenCapture extends Behaviour implements IPointerClickHandler {
96
96
  console.log("Screensharing", this.name, this);
97
97
  AudioSource.registerWaitForAllowAudio(() => {
98
98
  if (this.videoPlayer && this._currentStream && this._currentMode === ScreenCaptureMode.Receiving) {
99
+ this.videoPlayer.playInBackground = true;
99
100
  this.videoPlayer.setVideo(this._currentStream);
100
101
  }
101
102
  });
@@ -52,7 +52,7 @@ export class VideoPlayer extends Behaviour {
52
52
 
53
53
  @serializable()
54
54
  aspectMode: AspectMode = AspectMode.None;
55
-
55
+
56
56
  @serializable(URL)
57
57
  private clip?: string | MediaStream | null = null;
58
58
 
@@ -106,12 +106,12 @@ export class VideoPlayer extends Behaviour {
106
106
  get isPlaying(): boolean {
107
107
  const video = this._videoElement;
108
108
  if (video) {
109
- if(video.currentTime > 0 && !video.paused && !video.ended
109
+ if (video.currentTime > 0 && !video.paused && !video.ended
110
110
  && video.readyState > video.HAVE_CURRENT_DATA)
111
111
  return true;
112
- else if(video.srcObject){
112
+ else if (video.srcObject) {
113
113
  const stream = video.srcObject as MediaStream;
114
- if(stream.active) return true;
114
+ if (stream.active) return true;
115
115
  }
116
116
  }
117
117
  return false;
@@ -149,6 +149,9 @@ export class VideoPlayer extends Behaviour {
149
149
  }
150
150
  private _muted: boolean = false;
151
151
 
152
+ /** Set this to false to pause video playback while the tab is not active */
153
+ playInBackground: boolean = true;
154
+
152
155
  private _crossOrigin: string | null = "anonymous";
153
156
 
154
157
  private audioOutputMode: VideoAudioOutputMode = VideoAudioOutputMode.AudioSource;
@@ -198,11 +201,13 @@ export class VideoPlayer extends Behaviour {
198
201
  window.addEventListener('visibilitychange', _evt => {
199
202
  switch (document.visibilityState) {
200
203
  case "hidden":
201
- this.wasPlaying = this._isPlaying;
202
- this.pause();
204
+ if(!this.playInBackground){
205
+ this.wasPlaying = this._isPlaying;
206
+ this.pause();
207
+ }
203
208
  break;
204
209
  case "visible":
205
- if (this.wasPlaying) this.play();
210
+ if (this.wasPlaying && !this._isPlaying) this.play();
206
211
  break;
207
212
  }
208
213
  });
@@ -260,7 +265,7 @@ export class VideoPlayer extends Behaviour {
260
265
  this.updateVideoElementSettings();
261
266
  this._videoElement?.play().catch(err => {
262
267
  // https://developer.chrome.com/blog/play-request-was-interrupted/
263
- if(debug)
268
+ if (debug)
264
269
  console.error("Error playing video", err, "CODE=" + err.code, this.videoElement?.src, this);
265
270
  setTimeout(() => {
266
271
  if (this._isPlaying && !this.destroyed && this.activeAndEnabled)
@@ -318,7 +323,7 @@ export class VideoPlayer extends Behaviour {
318
323
  this._videoTexture.flipY = false;
319
324
  this._videoTexture.encoding = THREE.sRGBEncoding;
320
325
  this.handleBeginPlaying(playAutomatically);
321
- if(debug)
326
+ if (debug)
322
327
  console.log(this);
323
328
  }
324
329
 
@@ -420,7 +425,7 @@ export class VideoPlayer extends Behaviour {
420
425
  // dont open in fullscreen on ios
421
426
  this._videoElement.playsInline = true;
422
427
  let muted = !this._receivedInput && this.audioOutputMode !== VideoAudioOutputMode.None;
423
- if(!muted && this._muted) muted = true;
428
+ if (!muted && this._muted) muted = true;
424
429
  this._videoElement.muted = muted;
425
430
  if (this.playOnAwake)
426
431
  this._videoElement.autoplay = true;
@@ -540,7 +545,7 @@ class VideoOverlay {
540
545
  if (!this._screenspaceModeQuad) return;
541
546
  this._screenspaceModeQuad.geometry.scale(2, 2, 2);
542
547
  }
543
-
548
+
544
549
  const quad = this._screenspaceModeQuad;
545
550
  this.context.scene.add(quad);
546
551
  this.updateScreenspaceMaterialUniforms();
@@ -79,6 +79,9 @@ export class XRFlag extends Behaviour {
79
79
  XRFlag.firstApply = true;
80
80
  XRFlag.Apply();
81
81
  }
82
+ else {
83
+ this.UpdateVisible(XRState.Global);
84
+ }
82
85
  }
83
86
 
84
87
  onDestroy(): void {
@@ -114,6 +114,7 @@ export { RGBAColor } from "../js-extensions/RGBAColor";
114
114
  export { Rigidbody } from "../RigidBody";
115
115
  export { RotationBySpeedModule } from "../ParticleSystemModules";
116
116
  export { RotationOverLifetimeModule } from "../ParticleSystemModules";
117
+ export { SceneSwitcher } from "../SceneSwitcher";
117
118
  export { ScreenCapture } from "../ScreenCapture";
118
119
  export { ScreenSpaceAmbientOcclusion } from "../postprocessing/Effects/ScreenspaceAmbientOcclusion";
119
120
  export { ShadowCatcher } from "../ShadowCatcher";
@@ -172,7 +172,7 @@ export class PlayableDirector extends Behaviour {
172
172
  await Promise.all(promises);
173
173
  if (!this._isPlaying) return;
174
174
  }
175
- while(this._audioTracks.length > 0 && this._isPlaying && !AudioSource.userInteractionRegistered && this.waitForAudio)
175
+ while (this._audioTracks.length > 0 && this._isPlaying && !AudioSource.userInteractionRegistered && this.waitForAudio)
176
176
  await delay(200);
177
177
  }
178
178
  this._internalUpdateRoutine = this.startCoroutine(this.internalUpdate());
@@ -211,9 +211,9 @@ export class PlayableDirector extends Behaviour {
211
211
  let t = this._time;
212
212
  switch (this.extrapolationMode) {
213
213
  case DirectorWrapMode.Hold:
214
- if(this._speed > 0)
214
+ if (this._speed > 0)
215
215
  t = Math.min(t, this._duration);
216
- else if(this._speed < 0)
216
+ else if (this._speed < 0)
217
217
  t = Math.max(t, 0);
218
218
  this._time = t;
219
219
  break;
@@ -242,7 +242,7 @@ export class PlayableDirector extends Behaviour {
242
242
  }
243
243
  }
244
244
 
245
- get audioTracks() : Tracks.AudioTrackHandler[] {
245
+ get audioTracks(): Tracks.AudioTrackHandler[] {
246
246
  return this._audioTracks;
247
247
  }
248
248
 
@@ -362,7 +362,7 @@ export class PlayableDirector extends Behaviour {
362
362
  if (debug)
363
363
  console.log("Resolved binding", binding, "to", obj);
364
364
  track.outputs[i] = obj;
365
- if(obj instanceof Animator) {
365
+ if (obj instanceof Animator) {
366
366
  // TODO: should disable? animator but this is not the animator that is currently on the object? needs investigation
367
367
  // console.log("DISABLE ANIMATOR", obj, obj.name, binding, this._guidsMap);
368
368
  // obj.enabled = false;
@@ -376,7 +376,7 @@ export class PlayableDirector extends Behaviour {
376
376
  continue;
377
377
  }
378
378
  // if the binding is missing remove it to avoid unnecessary loops
379
- if (track.type !== Models.TrackType.Audio && track.type !== Models.TrackType.Control && track.type !== Models.TrackType.Marker)
379
+ if (track.type !== Models.TrackType.Audio && track.type !== Models.TrackType.Control && track.type !== Models.TrackType.Marker && track.type !== Models.TrackType.Signal)
380
380
  console.warn("Missing binding", binding, track.name, track.type, this.name, this.playableAsset.name);
381
381
  }
382
382
  }
@@ -416,8 +416,11 @@ export class PlayableDirector extends Behaviour {
416
416
  for (const clip of track.clips) {
417
417
  if (clip.end > this._duration) this._duration = clip.end;
418
418
  }
419
+ for(const marker of track.markers){
420
+ if (marker.time > this._duration) this._duration = marker.time + .001;
421
+ }
419
422
  }
420
- // console.log("timeline duration", this._duration);
423
+ // console.log("timeline duration", this._duration, this.playableAsset);
421
424
  }
422
425
 
423
426
  private setupAndCreateTrackHandlers() {
@@ -442,17 +445,16 @@ export class PlayableDirector extends Behaviour {
442
445
  // only handle animation tracks
443
446
  if (track.type === Models.TrackType.Animation) {
444
447
  if (track.clips.length <= 0) {
445
- if(debug) console.warn("Animation track has no clips", track);
448
+ if (debug) console.warn("Animation track has no clips", track);
446
449
  continue;
447
450
  }
448
451
  // loop outputs / bindings, they should contain animator references
449
452
  for (let i = track.outputs.length - 1; i >= 0; i--) {
450
453
  let binding = track.outputs[i] as Animator;
451
- if(binding instanceof Object3D){
454
+ if (binding instanceof Object3D) {
452
455
  const anim = GameObject.getOrAddComponent(binding, Animator);
453
- if(anim) binding = anim;
456
+ if (anim) binding = anim;
454
457
  }
455
- if (typeof binding.enabled === "boolean") binding.enabled = false;
456
458
  const animationClips = binding?.gameObject?.animations;
457
459
  if (animationClips) {
458
460
  const handler = new Tracks.AnimationTrackHandler();
@@ -7,28 +7,55 @@ import { serializable } from "../../engine/engine_serialization_decorator";
7
7
 
8
8
 
9
9
  export class SignalAsset {
10
+ @serializable()
10
11
  guid!: string;
11
12
  }
12
13
 
13
- export class SignalReceiverEvent implements ISerializable {
14
+ export class SignalReceiverEvent {
15
+ @serializable(SignalAsset)
14
16
  signal!: SignalAsset;
17
+ @serializable(EventList)
15
18
  reaction!: EventList;
16
-
17
- $serializedTypes = {
18
- signal: SignalAsset,
19
- reaction: EventList
20
- }
21
19
  }
22
20
 
23
21
  export class SignalReceiver extends Behaviour {
24
22
 
23
+ private static receivers: { [key: string]: SignalReceiver[] } = {};
24
+
25
+ static invoke(guid: string) {
26
+ if (SignalReceiver.receivers[guid]) {
27
+ const receivers = SignalReceiver.receivers[guid];
28
+ if (!receivers) return;
29
+ for (const rec of receivers)
30
+ rec.invoke(guid);
31
+ }
32
+ }
33
+
34
+
25
35
 
26
36
  @serializable(SignalReceiverEvent)
27
37
  events?: SignalReceiverEvent[];
28
38
 
39
+ onEnable(): void {
40
+ if (this.events) {
41
+ for (const evt of this.events) {
42
+ if (!SignalReceiver.receivers[evt.signal.guid])
43
+ SignalReceiver.receivers[evt.signal.guid] = [];
44
+ SignalReceiver.receivers[evt.signal.guid].push(this);
45
+ }
46
+ }
47
+ }
29
48
 
30
- start() {
31
- console.log(this);
49
+ onDisable(): void {
50
+ if (this.events) {
51
+ for (const evt of this.events) {
52
+ if (SignalReceiver.receivers[evt.signal.guid]) {
53
+ const idx = SignalReceiver.receivers[evt.signal.guid].indexOf(this);
54
+ if (idx >= 0)
55
+ SignalReceiver.receivers[evt.signal.guid].splice(idx, 1);
56
+ }
57
+ }
58
+ }
32
59
  }
33
60
 
34
61
  invoke(sig: SignalAsset | string) {
@@ -145,7 +145,7 @@ export class AnimationTrackHandler extends TrackHandler {
145
145
  onPauseChanged() {
146
146
  // When the timeline is paused the original animator will be enabled again if it was before
147
147
  if (this._animator && this._animatorWasEnabled !== undefined) {
148
- this._animator.enabled = this.director.isPaused ? this._animatorWasEnabled : false;
148
+ this._animator.enabled = this.director.isPlaying === false ? this._animatorWasEnabled : false;
149
149
  }
150
150
  }
151
151
 
@@ -225,7 +225,11 @@ export class AnimationTrackHandler extends TrackHandler {
225
225
  // We need to disable the animator component in case it also animates
226
226
  // which overrides the timeline
227
227
  this._animator = GameObject.getComponent(this.target, Animator) ?? null;
228
- this._animatorWasEnabled = this._animator?.enabled;
228
+ if (this._animator) {
229
+ this._animatorWasEnabled = this._animator.enabled;
230
+ this._animator.enabled = false;
231
+ }
232
+ else this._animatorWasEnabled = false;
229
233
  }
230
234
 
231
235
  // Clip Offsets
@@ -323,9 +327,10 @@ export class AnimationTrackHandler extends TrackHandler {
323
327
  // const clip = this.clips[i];
324
328
  let weight = 1;
325
329
  weight *= this.evaluateWeight(time, i, this.models, isActive);
330
+ weight *= this.director.weight;
326
331
 
327
332
  let handleLoop = isInTimeRange;
328
- if(doPreExtrapolate){
333
+ if (doPreExtrapolate) {
329
334
  if (preExtrapolation !== Models.ClipExtrapolation.Hold) {
330
335
  time += model.start;
331
336
  handleLoop = true;
@@ -370,7 +375,7 @@ export class AnimationTrackHandler extends TrackHandler {
370
375
  else action.time = t;
371
376
 
372
377
  action.timeScale = 0;
373
- const effectiveWeight = weight * this.director.weight;
378
+ const effectiveWeight = weight;
374
379
  action.weight = effectiveWeight;
375
380
  action.clampWhenFinished = true;
376
381
  if (!action.isRunning())
@@ -660,7 +665,7 @@ export class AudioTrackHandler extends TrackHandler {
660
665
  public static dispose() {
661
666
  AudioTrackHandler._currentlyLoading.clear();
662
667
  }
663
-
668
+
664
669
  private handleAudioLoading(model: Models.ClipModel, audio: THREE.Audio): Promise<AudioBuffer | null> | null {
665
670
  if (!this._audioLoader) {
666
671
  this._audioLoader = new THREE.AudioLoader();
@@ -675,7 +680,7 @@ export class AudioTrackHandler extends TrackHandler {
675
680
  });
676
681
  return promise;
677
682
  }
678
-
683
+
679
684
  if (debug) console.warn("LOAD audio track", path, this.director.sourceId);
680
685
  const loadingPromise = new Promise<AudioBuffer | null>((resolve, _reject) => {
681
686
  this._audioLoader!.load(path,
@@ -699,23 +704,48 @@ export class SignalTrackHandler extends TrackHandler {
699
704
  didTrigger: boolean[] = [];
700
705
  receivers: Array<SignalReceiver | null> = [];
701
706
 
707
+ // private _lastTime: number = -1;
708
+
702
709
  evaluate(time: number) {
703
- if (this.receivers.length <= 0) return;
704
710
  if (this.track.muted) return;
711
+
712
+ // let lastTime = this._lastTime;
713
+ // if (lastTime === -1) lastTime = time;
714
+ // this._lastTime = time;
715
+
705
716
  for (let i = 0; i < this.models.length; i++) {
706
717
  const model = this.models[i];
707
718
  const wasTriggered = this.didTrigger[i];
708
719
  const td = model.time - time;
709
- let isActive = model.retroActive ? td < 0 : (td < 0 && Math.abs(td) < .1);
720
+ let isActive = false;
721
+ if (model.retroActive) {
722
+ isActive = td <= 0.000001;
723
+ }
724
+ // TODO: handle signal asset at time 0 (only trigger when time is 0)
725
+ // else if (model.time < .001) {
726
+ // if (td <= 0.000001 && lastTime > time)
727
+ // isActive = true;
728
+ // }
729
+ else {
730
+ const abs = Math.abs(td);
731
+ if (abs >= .00001 && abs < .1) {
732
+ isActive = true;
733
+ }
734
+ }
735
+ // console.log(time, td, isActive);
710
736
  if (isActive) {
711
737
  if (!wasTriggered) {
712
738
  this.didTrigger[i] = true;
713
- for (const rec of this.receivers) {
714
- if (!rec) continue;
715
- rec.invoke(model.asset);
739
+ // If a signal doesnt have any explicit receivers it will invoke the signal globally
740
+ if (this.receivers?.length <= 0) {
741
+ SignalReceiver.invoke(model.asset);
742
+ }
743
+ else {
744
+ for (const rec of this.receivers) {
745
+ if (!rec) continue;
746
+ rec.invoke(model.asset);
747
+ }
716
748
  }
717
- // console.log("TRIGGER " + model.asset);
718
- // TimelineSignals.invoke(model.asset);
719
749
  }
720
750
  }
721
751
  else {