@needle-tools/engine 3.2.0-alpha → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "3.2.0-alpha",
3
+ "version": "3.2.1-alpha",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in",
5
5
  "main": "dist/needle-engine.umd.cjs",
6
6
  "type": "module",
@@ -8,8 +8,8 @@ import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
8
8
  export interface INeedleGltfLoader {
9
9
  createBuiltinComponents(context: Context, gltfId: SourceIdentifier, gltf, seed: number | null | UIDProvider, extension?: NEEDLE_components): Promise<void>
10
10
  writeBuiltinComponentData(comp: object, context: SerializationContext);
11
- parseSync(context: Context, data : string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined>;
12
- loadSync(context: Context, url: string, sourceId:string, seed: number | UIDProvider | null, prog?: (prog : ProgressEvent) => void): Promise<GLTF | undefined>
11
+ parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined>;
12
+ loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (prog: ProgressEvent) => void): Promise<GLTF | undefined>
13
13
  }
14
14
 
15
15
  let gltfLoader: INeedleGltfLoader;
@@ -20,6 +20,10 @@ export function getLoader(): INeedleGltfLoader {
20
20
  }
21
21
 
22
22
  export function registerLoader<T extends INeedleGltfLoader>(loader: ConstructorConcrete<T>) {
23
+ if (loader === null || loader === undefined) {
24
+ console.warn("Oh no: someone tried registering a non-existend gltf-loader. When you see this log it might mean that needle-engine is being imported multiple times. Please check your project setup.");
25
+ return;
26
+ }
23
27
  if (gltfLoaderType !== loader) {
24
28
  gltfLoaderType = loader;
25
29
  gltfLoader = new loader();
@@ -7,6 +7,8 @@ import { getParam, isMobileDevice, setParamWithoutReload } from "../engine/engin
7
7
  import { serializable } from "../engine/engine_serialization";
8
8
  import { Behaviour, GameObject } from "./Component";
9
9
 
10
+ const debug = getParam("debugsceneswitcher");
11
+
10
12
  const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
11
13
 
12
14
  ContextRegistry.registerCallback(ContextEvent.ContextRegistered, _ => {
@@ -25,6 +27,9 @@ export class SceneSwitcher extends Behaviour {
25
27
  @serializable()
26
28
  queryParameterName: string = "scene";
27
29
 
30
+ @serializable()
31
+ clamp: boolean = true;
32
+
28
33
  /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set */
29
34
  @serializable()
30
35
  useHistory: boolean = true;
@@ -37,6 +42,7 @@ export class SceneSwitcher extends Behaviour {
37
42
  @serializable()
38
43
  useSwipe: boolean = true;
39
44
 
45
+
40
46
  private _currentIndex: number = -1;
41
47
  private _currentScene: AssetReference | undefined = undefined;
42
48
  private _engineElementOverserver: MutationObserver | undefined = undefined;
@@ -146,18 +152,25 @@ export class SceneSwitcher extends Behaviour {
146
152
  }
147
153
  }
148
154
 
149
- selectNext() {
155
+ selectNext(): Promise<boolean> {
150
156
  return this.select(this._currentIndex + 1);
151
157
  }
152
158
 
153
- selectPrev() {
159
+ selectPrev(): Promise<boolean> {
154
160
  return this.select(this._currentIndex - 1);
155
161
  }
156
162
 
157
- select(index: number) {
163
+ select(index: number): Promise<boolean> {
164
+ if (debug) console.log("select", index)
158
165
  if (!this.scenes?.length) return couldNotLoadScenePromise;
159
- if (index < 0) index = this.scenes.length - 1;
160
- if (index >= this.scenes.length) index = 0;
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
+ }
161
174
  const scene = this.scenes[index];
162
175
  return this.switchScene(scene);
163
176
  }
@@ -165,12 +178,15 @@ export class SceneSwitcher extends Behaviour {
165
178
  async switchScene(scene: AssetReference): Promise<boolean> {
166
179
  if (scene === this._currentScene) return true;
167
180
  if (this._currentScene)
168
- GameObject.remove(this._currentScene.asset);
181
+ this._currentScene.unload();
169
182
  const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
170
183
  this._currentScene = scene;
171
184
  try {
172
185
  await scene.loadAssetAsync();
173
- if (!scene.asset) return false;
186
+ if (!scene.asset) {
187
+ if (debug) console.warn("Failed loading scene:", scene);
188
+ return false;
189
+ }
174
190
  if (this._currentIndex === index) {
175
191
  GameObject.add(scene.asset, this.gameObject);
176
192
  // save the loaded scene as an url parameter
@@ -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,15 +445,15 @@ 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
458
  const animationClips = binding?.gameObject?.animations;
456
459
  if (animationClips) {
@@ -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) {
@@ -225,8 +225,7 @@ 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
- if (this._animator)
229
- {
228
+ if (this._animator) {
230
229
  this._animatorWasEnabled = this._animator.enabled;
231
230
  this._animator.enabled = false;
232
231
  }
@@ -331,7 +330,7 @@ export class AnimationTrackHandler extends TrackHandler {
331
330
  weight *= this.director.weight;
332
331
 
333
332
  let handleLoop = isInTimeRange;
334
- if(doPreExtrapolate){
333
+ if (doPreExtrapolate) {
335
334
  if (preExtrapolation !== Models.ClipExtrapolation.Hold) {
336
335
  time += model.start;
337
336
  handleLoop = true;
@@ -666,7 +665,7 @@ export class AudioTrackHandler extends TrackHandler {
666
665
  public static dispose() {
667
666
  AudioTrackHandler._currentlyLoading.clear();
668
667
  }
669
-
668
+
670
669
  private handleAudioLoading(model: Models.ClipModel, audio: THREE.Audio): Promise<AudioBuffer | null> | null {
671
670
  if (!this._audioLoader) {
672
671
  this._audioLoader = new THREE.AudioLoader();
@@ -681,7 +680,7 @@ export class AudioTrackHandler extends TrackHandler {
681
680
  });
682
681
  return promise;
683
682
  }
684
-
683
+
685
684
  if (debug) console.warn("LOAD audio track", path, this.director.sourceId);
686
685
  const loadingPromise = new Promise<AudioBuffer | null>((resolve, _reject) => {
687
686
  this._audioLoader!.load(path,
@@ -705,23 +704,48 @@ export class SignalTrackHandler extends TrackHandler {
705
704
  didTrigger: boolean[] = [];
706
705
  receivers: Array<SignalReceiver | null> = [];
707
706
 
707
+ // private _lastTime: number = -1;
708
+
708
709
  evaluate(time: number) {
709
- if (this.receivers.length <= 0) return;
710
710
  if (this.track.muted) return;
711
+
712
+ // let lastTime = this._lastTime;
713
+ // if (lastTime === -1) lastTime = time;
714
+ // this._lastTime = time;
715
+
711
716
  for (let i = 0; i < this.models.length; i++) {
712
717
  const model = this.models[i];
713
718
  const wasTriggered = this.didTrigger[i];
714
719
  const td = model.time - time;
715
- 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);
716
736
  if (isActive) {
717
737
  if (!wasTriggered) {
718
738
  this.didTrigger[i] = true;
719
- for (const rec of this.receivers) {
720
- if (!rec) continue;
721
- 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
+ }
722
748
  }
723
- // console.log("TRIGGER " + model.asset);
724
- // TimelineSignals.invoke(model.asset);
725
749
  }
726
750
  }
727
751
  else {