Needle Engine

Changes between version 3.14.0-beta and 3.15.0-beta
Files changed (33) hide show
  1. src/engine-components/Animation.ts +2 -2
  2. src/engine-components/Camera.ts +1 -1
  3. src/engine-components/Component.ts +1 -1
  4. src/engine-components/DeviceFlag.ts +5 -8
  5. src/engine-components/DropListener.ts +5 -1
  6. src/engine/engine_addressables.ts +1 -0
  7. src/engine/engine_context_registry.ts +10 -2
  8. src/engine/engine_context.ts +17 -11
  9. src/engine/engine_element.ts +1 -2
  10. src/engine/engine_networking_auto.ts +4 -1
  11. src/engine/engine_scenelighting.ts +1 -1
  12. src/engine/engine_serialization_decorator.ts +10 -1
  13. src/engine/engine_util_decorator.ts +14 -5
  14. src/engine/engine_utils.ts +31 -0
  15. src/engine-components/FlyControls.ts +6 -1
  16. src/engine-components/export/gltf/GltfExport.ts +11 -5
  17. src/engine-components/ui/Graphic.ts +1 -0
  18. src/engine/extensions/NEEDLE_components.ts +25 -22
  19. src/engine/extensions/NEEDLE_gameobject_data.ts +1 -1
  20. src/engine-components/utils/OpenURL.ts +1 -1
  21. src/engine-components/OrbitControls.ts +4 -1
  22. src/engine-components/ParticleSystem.ts +6 -1
  23. src/engine-components/ui/RectTransform.ts +8 -4
  24. src/engine-components/SceneSwitcher.ts +28 -8
  25. src/engine-components/ScreenCapture.ts +1 -1
  26. src/engine-components/Skybox.ts +3 -2
  27. src/engine-components/SpectatorCamera.ts +1 -1
  28. src/engine-components/Voip.ts +29 -9
  29. src/engine-components/webxr/WebARSessionRoot.ts +1 -0
  30. src/engine-components/webxr/WebXRAvatar.ts +2 -2
  31. src/engine-components/webxr/WebXRController.ts +26 -16
  32. src/engine-components/webxr/WebXRImageTracking.ts +13 -11
  33. src/engine-components/webxr/WebXRSync.ts +3 -3
src/engine-components/Animation.ts CHANGED
@@ -136,8 +136,8 @@
136
136
  }
137
137
 
138
138
  get isPlaying() {
139
- for (let i = 0; i < this._currentActions.length; i++) {
140
- if (this._currentActions[i].isRunning())
139
+ for (let i = 0; i < this.actions.length; i++) {
140
+ if (this.actions[i].isRunning())
141
141
  return true;
142
142
  }
143
143
  return false;
src/engine-components/Camera.ts CHANGED
@@ -45,7 +45,7 @@
45
45
  if (changed && this._cam) {
46
46
  if (this._cam instanceof PerspectiveCamera) {
47
47
  if (this._fov === undefined) {
48
- console.error("Can not set undefined fov on PerspectiveCamera");
48
+ console.warn("Can not set undefined fov on PerspectiveCamera");
49
49
  return;
50
50
  }
51
51
  this._cam.fov = this._fov;
src/engine-components/Component.ts CHANGED
@@ -671,7 +671,7 @@
671
671
  }
672
672
 
673
673
  dispatchEvent(evt: Event): boolean {
674
- if (!this._eventListeners[evt.type]) return false;
674
+ if (!evt || !this._eventListeners[evt.type]) return false;
675
675
  const listeners = this._eventListeners[evt.type];
676
676
  for (let i = 0; i < listeners.length; i++) {
677
677
  listeners[i](evt);
src/engine-components/DeviceFlag.ts CHANGED
@@ -1,3 +1,5 @@
1
+
2
+ import { isMobileDevice } from "../engine/engine_utils.js";
1
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
2
4
  import { Behaviour, GameObject } from "./Component.js";
3
5
 
@@ -26,7 +28,7 @@
26
28
 
27
29
  private test() : boolean {
28
30
  if(this.visibleOn < 0) return true;
29
- if(isMobile()){
31
+ if(isMobileDevice()){
30
32
  return (this.visibleOn & (DeviceType.Mobile)) !== 0;
31
33
  }
32
34
  const allowDesktop = (this.visibleOn & (DeviceType.Desktop)) !== 0;
@@ -35,12 +37,7 @@
35
37
 
36
38
  }
37
39
 
38
- // shamelessly taken from https://stackoverflow.com/a/11381730
39
- let _isMobile: boolean | undefined = undefined;
40
+ /**@deprecated use isMobileDevice() */
40
41
  function isMobile() {
41
- if (_isMobile === true || _isMobile === false) return _isMobile;
42
- let check = false;
43
- (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window["opera"]);
44
- _isMobile = check;
45
- return check;
42
+ return isMobileDevice();
46
43
  };
src/engine-components/DropListener.ts CHANGED
@@ -42,7 +42,7 @@
42
42
 
43
43
  private onDrop = async (evt: DragEvent) => {
44
44
  if (debug) console.log(evt);
45
- if (!evt.dataTransfer) return;
45
+ if (!evt?.dataTransfer) return;
46
46
  evt.preventDefault();
47
47
  const items = evt.dataTransfer.items;
48
48
  if (!items) return;
@@ -89,6 +89,10 @@
89
89
 
90
90
  private async addObject(evt: DragEvent | undefined, gltf: GLTF) {
91
91
  if (debug) console.log("Dropped", gltf);
92
+ if (!gltf?.scene) {
93
+ console.warn("No object specified to add to scene", gltf);
94
+ return;
95
+ }
92
96
 
93
97
  const obj = gltf.scene;
94
98
  if (evt !== undefined) {
src/engine/engine_addressables.ts CHANGED
@@ -178,6 +178,7 @@
178
178
  return this._rawBinary;
179
179
  }
180
180
 
181
+ // TODO: we need a way to abort loading a resource
181
182
  async loadAssetAsync(prog?: ProgressCallback | null) {
182
183
  if (debug)
183
184
  console.log("loadAssetAsync", this.uri);
src/engine/engine_context_registry.ts CHANGED
@@ -5,12 +5,15 @@
5
5
  ContextRegistered = "ContextRegistered",
6
6
  /** called before the first glb is loaded, can be used to initialize physics engine, is awaited */
7
7
  ContextCreationStart = "ContextCreationStart",
8
+ /** Called when the context has been created, before the first frame */
8
9
  ContextCreated = "ContextCreated",
10
+ /** Called when the context has been destroyed */
9
11
  ContextDestroyed = "ContextDestroyed",
12
+ /** Called when the context could not find a camera during creation */
10
13
  MissingCamera = "MissingCamera",
11
- /** Called before the context is being cleared */
14
+ /** Called before the context is being cleared (all objects in the scene are being destroyed and state is reset) */
12
15
  ContextClearing = "ContextClearing",
13
- /** Called after the context has been cleared */
16
+ /** Called after the context has been cleared (all objects in the scene have been destroyed and state has been reset) */
14
17
  ContextCleared = "ContextCleared",
15
18
  }
16
19
 
@@ -36,6 +39,11 @@
36
39
  globalThis["NeedleEngine.Context.Current"] = ctx;
37
40
  }
38
41
 
42
+ /** Returns the array of all registered Needle Engine contexts. Do not modify */
43
+ static get All() {
44
+ return this.Registered;
45
+ }
46
+
39
47
  /** All currently registered Needle Engine contexts. Do not modify */
40
48
  static Registered: IContext[] = [];
41
49
 
src/engine/engine_context.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  import { LightDataRegistry, ILightDataRegistry } from './engine_lightdata.js';
26
26
  import { PlayerViewManager } from './engine_playerview.js';
27
27
 
28
- import { CoroutineData, GLTF, ICamera, IComponent, IContext, ILight } from "./engine_types.js"
28
+ import { CoroutineData, GLTF, ICamera, IComponent, IContext, ILight, LoadedGLTF } from "./engine_types.js"
29
29
  import { destroy, foreachComponent } from './engine_gameobject.js';
30
30
  import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
31
31
  import { delay, getParam } from './engine_utils.js';
@@ -666,7 +666,7 @@
666
666
 
667
667
  // load and create scene
668
668
  let prepare_succeeded = true;
669
- let loadedFiles!: Array<GLTF | null>;
669
+ let loadedFiles!: Array<LoadedGLTF | null>;
670
670
  try {
671
671
  Context.Current = this;
672
672
  if (opts) {
@@ -789,15 +789,18 @@
789
789
  }
790
790
  else if (debug) console.log("Target framerate set to", this.targetFrameRate);
791
791
 
792
+ this._dispatchReadyAfterFrame = true;
793
+ const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
794
+ if (res) await res;
795
+
792
796
  this._isCreating = false;
793
797
  if (!this.isManagedExternally)
794
798
  this.restartRenderLoop();
795
- this._dispatchReadyAfterFrame = true;
796
- return ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
799
+ return res;
797
800
  }
798
801
 
799
- private async internalLoadInitialContent(createId: number, args: ContextCreateArgs): Promise<Array<GLTF>> {
800
- const results = new Array<GLTF>();
802
+ private async internalLoadInitialContent(createId: number, args: ContextCreateArgs): Promise<Array<LoadedGLTF>> {
803
+ const results = new Array<LoadedGLTF>();
801
804
  // early out if we dont have any files to load
802
805
  if (args.files.length === 0) return results;
803
806
 
@@ -837,7 +840,10 @@
837
840
  });
838
841
  args?.onLoadingFinished?.call(this, i, file, res ?? null);
839
842
  if (res) {
840
- results.push(res);
843
+ results.push({
844
+ src: file,
845
+ file: res
846
+ });
841
847
  }
842
848
  else {
843
849
  // a file could not be loaded
@@ -849,8 +855,8 @@
849
855
  // then we want to cleanup/destroy previously loaded files
850
856
  if (createId !== this._createId) {
851
857
  for (const res of results) {
852
- if (res) {
853
- for (const scene of res.scenes)
858
+ if (res && res.file) {
859
+ for (const scene of res.file.scenes)
854
860
  destroy(scene, true, true);
855
861
  }
856
862
  }
@@ -858,8 +864,8 @@
858
864
  // otherwise we want to add the loaded files to the current scene
859
865
  else {
860
866
  for (const res of results) {
861
- if (res) {
862
- this.scene.add(res.scene);
867
+ if (res && res.file) {
868
+ this.scene.add(res.file.scene);
863
869
  }
864
870
  }
865
871
  }
src/engine/engine_element.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "./engine_loaders.js";
7
7
  import { getLoader, registerLoader } from "../engine/engine_gltf.js";
8
8
  import { NeedleGltfLoader } from "./engine_scenetools.js";
9
- import { GLTF, INeedleEngineComponent, LoadedGLTF } from "./engine_types.js";
9
+ import { INeedleEngineComponent, LoadedGLTF } from "./engine_types.js";
10
10
  import { isLocalNetwork } from "./engine_networking_utils.js";
11
11
  import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
12
12
  import { destroy } from "./engine_gameobject.js";
@@ -323,7 +323,6 @@
323
323
  files: filesToLoad,
324
324
  onLoadingProgress: evt => {
325
325
  evt.name = getNameFromUrl(evt.name);
326
- console.log({...evt})
327
326
  if (useDefaultLoading) this._loadingView?.onLoadingUpdate(evt);
328
327
  progressEventDetail.name = evt.name;
329
328
  progressEventDetail.progress = evt.progress;
src/engine/engine_networking_auto.ts CHANGED
@@ -218,7 +218,10 @@
218
218
  */
219
219
  export const syncField = function (onFieldChanged?: string | FieldChangedCallbackFn) {
220
220
 
221
- return function (target: any, propertyKey: string) {
221
+ return function (target: any, _propertyKey: string | { name: string }) {
222
+ let propertyKey = "";
223
+ if (typeof _propertyKey === "string") propertyKey = _propertyKey;
224
+ else propertyKey = _propertyKey.name;
222
225
 
223
226
  let syncer: ComponentPropertiesSyncer | null = null;
224
227
 
src/engine/engine_scenelighting.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Vector4, EquirectangularReflectionMapping, sRGBEncoding, WebGLCubeRenderTarget, Texture, LightProbe, Color, SphericalHarmonics3, SRGBColorSpace } from "three";
1
+ import { Vector4, EquirectangularReflectionMapping, WebGLCubeRenderTarget, Texture, LightProbe, SphericalHarmonics3, SRGBColorSpace } from "three";
2
2
  import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator.js"
3
3
  import { Context } from "./engine_setup.js";
4
4
  import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
src/engine/engine_serialization_decorator.ts CHANGED
@@ -21,12 +21,21 @@
21
21
  }
22
22
  }
23
23
 
24
- return function (_target: any, _propertyKey: string) {
24
+ return function (_target: any, _propertyKey: string | { name: string }) {
25
+ // The _propertyKey parameter is a string in TS4 with experimentalDecorators
26
+ // but a ClassFieldDecoratorContext in TS5 without.
27
+ // Seems when a different TS version is used in VSCode for editor checking, we get errors here
28
+ // if we don't also check for any. See https://github.com/needle-tools/needle-engine-support/issues/179
29
+ if (typeof _propertyKey !== 'string') {
30
+ _propertyKey = _propertyKey.name;
31
+ }
32
+
25
33
  // this is important so objects with inheritance dont override their serialized type
26
34
  // info if e.g. multiple classes inheriting from the same type implement a member with the same name
27
35
  // and both use @serializable() with different types
28
36
  if (!Object.getOwnPropertyDescriptor(_target, '$serializedTypes'))
29
37
  _target["$serializedTypes"] = {};
38
+
30
39
  const types = _target["$serializedTypes"] = _target["$serializedTypes"] || {}
31
40
  types[_propertyKey] = type;
32
41
  }
src/engine/engine_util_decorator.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
 
18
- function createPropertyWrapper(target: IComponent | any, propertyKey: string, descriptor?: PropertyDescriptor,
18
+ function createPropertyWrapper(target: IComponent | any, _propertyKey: string | { name: string }, descriptor?: PropertyDescriptor,
19
19
  set?: setter,
20
20
  get?: getter) {
21
21
 
@@ -24,11 +24,15 @@
24
24
  // this is not undefined when its a property getter or setter already and not just a field
25
25
  // we currently only support validation of fields
26
26
  if (descriptor !== undefined) {
27
- console.error("Invalid usage of validate decorator. Only fields can be validated.", target, propertyKey, descriptor);
28
- showBalloonMessage("Invalid usage of validate decorator. Only fields can be validated. Property: " + propertyKey, LogType.Error);
27
+ console.error("Invalid usage of validate decorator. Only fields can be validated.", target, _propertyKey, descriptor);
28
+ showBalloonMessage("Invalid usage of validate decorator. Only fields can be validated. Property: " + _propertyKey, LogType.Error);
29
29
  return;
30
30
  }
31
31
 
32
+ let propertyKey: string = "";
33
+ if (typeof _propertyKey === "string") propertyKey = _propertyKey;
34
+ else propertyKey = _propertyKey.name;
35
+
32
36
  if (target.__internalAwake) {
33
37
  // this is the hidden key we save the original property to
34
38
  const $prop = Symbol(propertyKey);
@@ -75,14 +79,19 @@
75
79
  * Return false to prevent the original method from running.
76
80
  */
77
81
  export const prefix = function <T>(type: Constructor<T>) {
78
- return function (target: IComponent | any, propertyKey: string, _PropertyDescriptor: PropertyDescriptor) {
82
+ return function (target: IComponent | any, _propertyKey: string | { name: string }, _PropertyDescriptor: PropertyDescriptor) {
79
83
 
84
+ let propertyKey: string = "";
85
+ if (typeof _propertyKey === "string") propertyKey = _propertyKey;
86
+ else propertyKey = _propertyKey.name;
87
+
80
88
  const targetType = type.prototype;
81
89
  const originalProp = Object.getOwnPropertyDescriptor(targetType, propertyKey);
82
90
  if (!originalProp?.value) {
83
- console.warn("Can not apply prefix: type does not have method named", propertyKey, type);
91
+ console.warn("Can not apply prefix: type does not have method named", _propertyKey, type);
84
92
  return;
85
93
  }
94
+
86
95
  const originalValue = originalProp.value;
87
96
  const prefix = target[propertyKey];
88
97
  Object.defineProperty(targetType, propertyKey, {
src/engine/engine_utils.ts CHANGED
@@ -512,3 +512,34 @@
512
512
  }
513
513
 
514
514
  }
515
+
516
+
517
+ export class PromiseErrorResult {
518
+ readonly reason: string;
519
+ constructor(reason: string) {
520
+ this.reason = reason;
521
+ }
522
+ }
523
+
524
+ /** Can be used to simplify Promise error handling and if errors are acceptable.
525
+ * Promise.all will just fail if any of the provided promises fails and not return or cancel pending promises or partial results
526
+ * Using Promise.allSettled (or this method) instead will return a result for each promise and not automatically fail if any of the promises fails.
527
+ * Instead it will return a promise containing information if any of the promises failed
528
+ * and the actual results will be available as `results` array
529
+ **/
530
+ export async function PromiseAllWithErrors<T>(promise: Promise<T>[]): Promise<{
531
+ anyFailed: boolean,
532
+ results: Array<T | PromiseErrorResult>
533
+ }> {
534
+ const results = await Promise.allSettled(promise);
535
+ let anyFailed: boolean = false;
536
+ const res = results.map(x => {
537
+ if ("value" in x) return x.value;
538
+ anyFailed = true;
539
+ return new PromiseErrorResult(x.reason);
540
+ });
541
+ return {
542
+ anyFailed: anyFailed,
543
+ results: res,
544
+ };
545
+ }
src/engine-components/FlyControls.ts CHANGED
@@ -7,7 +7,12 @@
7
7
 
8
8
  onEnable(): void {
9
9
  const cam = GameObject.getComponent(this.gameObject, Camera)?.cam;
10
- this._controls = new ThreeFlyControls(cam!, this.context.renderer.domElement);
10
+ if (!cam) {
11
+ console.warn("FlyControls: Requires a Camera component on the same object as this component.");
12
+ return;
13
+ }
14
+
15
+ this._controls = new ThreeFlyControls(cam, this.context.renderer.domElement);
11
16
  this._controls.rollSpeed = .5;
12
17
  this._controls.movementSpeed = 3;
13
18
  this._controls.dragToLook = true;
src/engine-components/export/gltf/GltfExport.ts CHANGED
@@ -8,7 +8,9 @@
8
8
  import { getWorldPosition } from "../../../engine/engine_three_utils.js";
9
9
  import { BoxHelperComponent } from "../../BoxHelperComponent.js";
10
10
  import { AnimationClip } from "three";
11
+ import { getParam } from "../../../engine/engine_utils.js";
11
12
 
13
+ const debugExport = getParam("debuggltfexport");
12
14
 
13
15
  declare type ExportOptions = {
14
16
  binary: boolean,
@@ -41,7 +43,11 @@
41
43
  private ext?: NEEDLE_components;
42
44
 
43
45
  async exportNow(name: string) {
44
- console.log("DO EXPORT", this.objects);
46
+ if (debugExport) console.log("Exporting objects as glTF", this.objects);
47
+ if (!name) name = "scene";
48
+ if (!this.objects || this.objects.length <= 0)
49
+ this.objects = [this.context.scene];
50
+
45
51
  const opts = { binary: this.binary, pivot: GltfExport.calculateCenter(this.objects) };
46
52
  const res = await this.export(this.objects, opts);
47
53
 
@@ -59,8 +65,8 @@
59
65
 
60
66
  async export(objectsToExport: Object3D[], opts?: ExportOptions): Promise<any> {
61
67
 
62
- if (objectsToExport === null || objectsToExport.length <= 0) {
63
- console.log("no objects set to export");
68
+ if (!objectsToExport || objectsToExport.length <= 0) {
69
+ console.warn("No objects set to export");
64
70
  return;
65
71
  }
66
72
 
@@ -100,7 +106,7 @@
100
106
  // console.log(exportScene.position);
101
107
 
102
108
  // add objects for export
103
- console.log("EXPORT", objectsToExport);
109
+ if (debugExport) console.log("EXPORT", objectsToExport);
104
110
  objectsToExport.forEach(obj => {
105
111
  if (obj) {
106
112
  // adding directly does not require us to change parents and mess with the hierarchy actually
@@ -143,7 +149,7 @@
143
149
  reject(err);
144
150
  }
145
151
  finally {
146
- console.log("FINALLY");
152
+ if (debugExport) console.log("Finished glTF export.");
147
153
  }
148
154
  });
149
155
 
src/engine-components/ui/Graphic.ts CHANGED
@@ -73,6 +73,7 @@
73
73
  if (!this._rect) {
74
74
  this._rect = GameObject.getComponent(this.gameObject, RectTransform);
75
75
  }
76
+ if (!this._rect) throw new Error("Not Supported: Make sure to add a RectTransform component before adding a UI Graphic component.");
76
77
  return this._rect!;
77
78
  }
78
79
 
src/engine/extensions/NEEDLE_components.ts CHANGED
@@ -54,7 +54,7 @@
54
54
  // the write node callback is called after user data is serialized
55
55
  // we could also traverse everything before export and remove components
56
56
  // but doing it like that we avoid traversing multiple times
57
- if("serializeUserData" in writer){
57
+ if ("serializeUserData" in writer) {
58
58
  //@ts-ignore
59
59
  const originalFunction = writer.serializeUserData.bind(writer);
60
60
  this.writer = writer;
@@ -105,7 +105,8 @@
105
105
 
106
106
  writeNode(node: Object3D, nodeDef) {
107
107
  const nodeIndex = this.writer.json.nodes.length;
108
- console.log(node.name, nodeIndex, node.uuid);
108
+ if (debug)
109
+ console.log(node.name, nodeIndex, node.uuid);
109
110
  const context = new ExportData(node, nodeIndex, nodeDef);
110
111
  this.exportContext[nodeIndex] = context;
111
112
  this.objectToNodeMap[node.uuid] = nodeIndex;
@@ -171,29 +172,31 @@
171
172
  const loadComponents: Array<Promise<void>> = [];
172
173
  if (hasExtension === true) {
173
174
  const nodes = parser.json.nodes;
174
- for (let i = 0; i < nodes.length; i++) {
175
- const obj = await parser.getDependency('node', i);
176
- this.nodeToObjectMap[i] = obj;
177
- }
178
-
179
- for (let i = 0; i < nodes.length; i++) {
180
- const node = nodes[i];
181
- const index = i;// node.mesh;
182
- const ext = node.extensions;
183
- if (!ext) continue;
184
- const data = ext[this.name];
185
- if (!data) continue;
186
- if (debug)
187
- console.log("NODE", node);
188
- const obj = this.nodeToObjectMap[index];
189
- if (!obj) {
190
- console.error("Could not find object for node index: " + index, node, parser);
191
- continue;
175
+ if (nodes) {
176
+ for (let i = 0; i < nodes.length; i++) {
177
+ const obj = await parser.getDependency('node', i);
178
+ this.nodeToObjectMap[i] = obj;
192
179
  }
193
180
 
194
- apply(obj);
181
+ for (let i = 0; i < nodes.length; i++) {
182
+ const node = nodes[i];
183
+ const index = i;// node.mesh;
184
+ const ext = node.extensions;
185
+ if (!ext) continue;
186
+ const data = ext[this.name];
187
+ if (!data) continue;
188
+ if (debug)
189
+ console.log("NODE", node);
190
+ const obj = this.nodeToObjectMap[index];
191
+ if (!obj) {
192
+ console.error("Could not find object for node index: " + index, node, parser);
193
+ continue;
194
+ }
195
195
 
196
- loadComponents.push(this.createComponents(obj, data));
196
+ apply(obj);
197
+
198
+ loadComponents.push(this.createComponents(obj, data));
199
+ }
197
200
  }
198
201
  }
199
202
  await Promise.all(loadComponents);
src/engine/extensions/NEEDLE_gameobject_data.ts CHANGED
@@ -42,7 +42,7 @@
42
42
  afterRoot(_result: GLTF): Promise<void> | null {
43
43
  // console.log("AFTER ROOT", _result);
44
44
  const promises: Promise<void>[] = [];
45
- for (let index = 0; index < this.parser.json.nodes.length; index++) {
45
+ for (let index = 0; index < this.parser.json.nodes?.length; index++) {
46
46
  const node = this.parser.json.nodes[index];
47
47
  if (node && node.extensions) {
48
48
  const ext = node.extensions[EXTENSION_NAME];
src/engine-components/utils/OpenURL.ts CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  async open() {
28
28
  if (!this.url) {
29
- console.error("URL is not set", this);
29
+ console.warn("OpenURL: URL is not set, can't open.", this);
30
30
  return;
31
31
  }
32
32
 
src/engine-components/OrbitControls.ts CHANGED
@@ -121,7 +121,10 @@
121
121
  const cam = cameraComponent?.cam;
122
122
  if (cam) setCameraController(cam, this, true);
123
123
  if (!this._controls) {
124
- console.assert(cam !== null && cam !== undefined, "Missing camera", this);
124
+ if (!cam) {
125
+ console.warn("OrbitControls: Requires a Camera component on the same object as this component.");
126
+ return;
127
+ }
125
128
  if (cam)
126
129
  this._cameraObject = cam;
127
130
  // Using the parent if possible to make it possible to disable input on the canvas
src/engine-components/ParticleSystem.ts CHANGED
@@ -882,6 +882,10 @@
882
882
  awake(): void {
883
883
  this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as ParticleSystemRenderer;
884
884
 
885
+ if (!this.main) {
886
+ throw new Error("Not Supported: ParticleSystem needs a serialized MainModule. Creating new particle systems at runtime is currently not supported.");
887
+ }
888
+
885
889
  this._container = new Object3D();
886
890
  this._container.matrixAutoUpdate = false;
887
891
  // if (this.main.simulationSpace == ParticleSystemSimulationSpace.Local) {
@@ -928,6 +932,7 @@
928
932
  }
929
933
 
930
934
  onEnable() {
935
+ if (!this.main) return;
931
936
  if (this.inheritVelocity)
932
937
  this.inheritVelocity.system = this;
933
938
  if (this._batchSystem)
@@ -943,6 +948,7 @@
943
948
  }
944
949
 
945
950
  onBeforeRender() {
951
+ if (!this.main) return;
946
952
  if (this._didPreWarm === false && this.main?.prewarm === true) {
947
953
  this._didPreWarm = true;
948
954
  this.preWarm();
@@ -1009,7 +1015,6 @@
1009
1015
 
1010
1016
  private lastMaterialVersion: number = -1;
1011
1017
  private onUpdate() {
1012
-
1013
1018
  const mat = this.renderer.getMaterial(this.trails.enabled);
1014
1019
  if (mat && mat.version != this.lastMaterialVersion && this._particleSystem) {
1015
1020
  this.lastMaterialVersion = mat.version;
src/engine-components/ui/RectTransform.ts CHANGED
@@ -94,19 +94,23 @@
94
94
  }
95
95
 
96
96
  // private lastMatrixWorld!: Matrix4;
97
- private lastMatrix!: Matrix4;
98
- private rectBlock!: Object3D;
97
+ private lastMatrix: Matrix4;
98
+ private rectBlock: Object3D;
99
99
  private _transformNeedsUpdate: boolean = false;
100
100
  private _initialPosition!: Vector3;
101
101
 
102
+ constructor() {
103
+ super();
104
+ this.lastMatrix = new Matrix4();
105
+ this.rectBlock = new Object3D();
106
+ }
107
+
102
108
  awake() {
103
109
  super.awake();
104
110
  // this is required if an animator animated the transform anchoring
105
111
  if (!this._anchoredPosition)
106
112
  this._anchoredPosition = new Vector2();
107
113
 
108
- this.lastMatrix = new Matrix4();
109
- this.rectBlock = new Object3D();
110
114
  this.rectBlock.name = this.name;
111
115
 
112
116
  // TODO: get rid of the initial position
src/engine-components/SceneSwitcher.ts CHANGED
@@ -154,10 +154,10 @@
154
154
  if (this.queryParameterName)
155
155
  didResolve = await this.tryLoadFromQueryParam();
156
156
  if (!didResolve) {
157
- const state = _state.state;
158
- if (state !== null && state.startsWith(this.guid)) {
157
+ const state = _state?.state;
158
+ if (state && state.startsWith(this.guid)) {
159
159
  const value = state.substr(this.guid.length + 2);
160
- console.log(value);
160
+ if(debug) console.log("PopState", value);
161
161
  await this.trySelectSceneFromValue(value);
162
162
  }
163
163
  }
@@ -263,6 +263,11 @@
263
263
  return this.switchScene(scene);
264
264
  }
265
265
 
266
+
267
+ // this is the scene that was requested last
268
+ private __lastSwitchScene?: AssetReference;
269
+ private __lastSwitchScenePromise?: Promise<boolean>;
270
+
266
271
  async switchScene(scene: AssetReference): Promise<boolean> {
267
272
  if (!(scene instanceof AssetReference)) {
268
273
  const type = typeof scene;
@@ -273,15 +278,28 @@
273
278
  return this.select(scene);
274
279
  }
275
280
  else {
276
- console.error("Cannot switch to scene", scene, "of type", type);
281
+ console.warn("SceneSwitcher: Can't switch to scene", scene, "of type", type);
277
282
  return false;
278
283
  }
279
284
  }
280
- if (scene === this._currentScene) return true;
281
- if (this._currentScene)
285
+
286
+ // ensure that we never run the same scene switch multiple times (at the same time) for the same requested scene
287
+ if (this.__lastSwitchScene === scene && this.__lastSwitchScenePromise) {
288
+ return this.__lastSwitchScenePromise;
289
+ }
290
+ this.__lastSwitchScene = scene;
291
+ this.__lastSwitchScenePromise = this.__internalSwitchScene(scene);
292
+ const res = await this.__lastSwitchScenePromise;
293
+ return res;
294
+ }
295
+ async __internalSwitchScene(scene: AssetReference): Promise<boolean> {
296
+ if (this._currentScene) {
297
+ if(debug) console.log("UNLOAD", scene.uri)
282
298
  this._currentScene.unload();
299
+ }
300
+ this._currentScene = undefined;
301
+
283
302
  const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
284
- this._currentScene = scene;
285
303
  try {
286
304
  const loadStartEvt = new CustomEvent<LoadSceneEvent>("loadscene-start", { detail: { scene: scene, switcher: this, index: index } })
287
305
  this.dispatchEvent(loadStartEvt);
@@ -297,6 +315,8 @@
297
315
  return false;
298
316
  }
299
317
  if (this._currentIndex === index) {
318
+ if(debug) console.log("ADD", scene.uri)
319
+ this._currentScene = scene;
300
320
  GameObject.add(scene.asset, this.gameObject);
301
321
  if (this.useSceneLighting)
302
322
  this.context.sceneLighting.enable(scene)
@@ -474,7 +494,7 @@
474
494
  }
475
495
 
476
496
  private async awaitLoading() {
477
- if (!this.asset.isLoaded()) {
497
+ if (this.asset && !this.asset.isLoaded()) {
478
498
  if (debug)
479
499
  console.log("Preload start: " + this.asset.uri, this.index);
480
500
  await this.asset.preload();
src/engine-components/ScreenCapture.ts CHANGED
@@ -128,7 +128,7 @@
128
128
  this.videoPlayer = GameObject.getComponent(this.gameObject, VideoPlayer) ?? undefined;
129
129
  }
130
130
  if (!this.videoPlayer) {
131
- console.error("Screencapture did not find a VideoPlayer component");
131
+ console.warn("ScreenCapture: Requires an assigned VideoPlayer or a VideoPlayer component on the same object as this component.");
132
132
  return;
133
133
  }
134
134
  const handle = PeerHandle.getOrCreate(this.context, this.guid);
src/engine-components/Skybox.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { EquirectangularRefractionMapping, NeverDepth, SRGBColorSpace, sRGBEncoding, Texture, TextureLoader } from "three"
6
6
  import { syncField } from "../engine/engine_networking_auto.js";
7
7
  import { Camera } from "./Camera.js";
8
- import { addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils.js";
8
+ import { PromiseAllWithErrors, addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils.js";
9
9
  import { ContextRegistry } from "../engine/engine_context_registry.js";
10
10
  import { registerObservableAttribute } from "../engine/engine_element_extras.js";
11
11
  import { type IContext } from "../engine/engine_types.js";
@@ -53,7 +53,7 @@
53
53
  promises.push(promise);
54
54
  }
55
55
  if (promises.length > 0) {
56
- return Promise.all(promises);
56
+ return PromiseAllWithErrors(promises);
57
57
  }
58
58
  return Promise.resolve();
59
59
  });
@@ -171,6 +171,7 @@
171
171
  }
172
172
 
173
173
  private async loadTexture(url: string) {
174
+ if (!url) return Promise.resolve(null);
174
175
  const cached = tryGetPreviouslyLoadedTexture(url);
175
176
  if (cached) {
176
177
  const res = await cached;
src/engine-components/SpectatorCamera.ts CHANGED
@@ -137,7 +137,7 @@
137
137
 
138
138
  this.cam = GameObject.getComponent(this.gameObject, Camera);
139
139
  if (!this.cam) {
140
- console.error("Spectator camera needs camera component", this);
140
+ console.warn("SpectatorCamera: Spectator camera needs camera component on the same object.", this);
141
141
  return;
142
142
  }
143
143
 
src/engine-components/Voip.ts CHANGED
@@ -83,7 +83,13 @@
83
83
 
84
84
  if (debug)
85
85
  console.log("start voip call");
86
- this.stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
86
+ try {
87
+ this.stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
88
+ }
89
+ catch (err) {
90
+ console.error(err);
91
+ return;
92
+ }
87
93
  this.updateMute(this.voip.muteOutput);
88
94
  if (debug)
89
95
  console.log(this.stream)
@@ -321,7 +327,9 @@
321
327
 
322
328
  this.context.connection.beginListen(RoomEvents.JoinedRoom, _evt => {
323
329
  // request mic once
324
- navigator.mediaDevices.getUserMedia({ audio: true, video: false });
330
+ navigator.mediaDevices.getUserMedia({ audio: true, video: false }).catch(err => {
331
+ console.error("Error initializing VoIP connection.", err);
332
+ });
325
333
  });
326
334
 
327
335
  this.context.connection.beginListen(PeerMessage.Update_ID, (cb: IPeerUpdateResponse) => {
@@ -414,7 +422,8 @@
414
422
  }
415
423
 
416
424
  private async onReceiveCall(call) {
417
-
425
+ if (!call) return;
426
+
418
427
  const { metadata } = call;
419
428
  console.assert(metadata.userId);
420
429
  const { userId } = metadata;
@@ -428,8 +437,13 @@
428
437
 
429
438
  // if we have mic permissions we can answer with our own mic
430
439
  if (await Voip.HasMicrophonePermissions()) {
431
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
432
- call.answer(stream);
440
+ try {
441
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
442
+ call.answer(stream);
443
+ }
444
+ catch (err) {
445
+ console.error("Error initializing VoIP connection.", err);
446
+ }
433
447
  }
434
448
  // otherwise take the call but dont send any audio ourselves
435
449
  else call.answer(null);
@@ -457,11 +471,17 @@
457
471
  // }
458
472
 
459
473
  public static async HasMicrophonePermissions(): Promise<boolean> {
460
- //@ts-ignore
461
- const res = await navigator.permissions.query({ name: 'microphone' });
462
- if (res.state === "denied") {
474
+ try {
475
+ //@ts-ignore
476
+ const res = await navigator.permissions.query({ name: 'microphone' });
477
+ if (res.state === "denied") {
478
+ return false;
479
+ }
480
+ return true;
481
+ }
482
+ catch (err) {
483
+ console.error("Error querying `microphone` permissions.", err);
463
484
  return false;
464
485
  }
465
- return true;
466
486
  }
467
487
  }
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -145,6 +145,7 @@
145
145
  InstancingUtil.markDirty(this.gameObject, true);
146
146
  // HACK to fix physics being not in correct place after exiting AR
147
147
  setTimeout(() => {
148
+ if (!this.gameObject) return;
148
149
  this.gameObject.matrixAutoUpdate = true;
149
150
  this.gameObject.visible = true;
150
151
  }, 100);
src/engine-components/webxr/WebXRAvatar.ts CHANGED
@@ -117,7 +117,7 @@
117
117
  this.context = context;
118
118
  this.guid = guid;
119
119
  this.webxr = webXR;
120
- this.setupCustomAvatar(this.webxr.defaultAvatar as AssetReference);
120
+ this.setupCustomAvatar(this.webxr.defaultAvatar);
121
121
  }
122
122
 
123
123
  public updateFlags() {
@@ -251,7 +251,7 @@
251
251
  }
252
252
  }
253
253
 
254
- private async setupCustomAvatar(avatarId: string | Object3D | AssetReference): Promise<boolean> {
254
+ private async setupCustomAvatar(avatarId: string | Object3D | AssetReference | undefined): Promise<boolean> {
255
255
  if (debug)
256
256
  console.log("LOAD", avatarId, this);
257
257
 
src/engine-components/webxr/WebXRController.ts CHANGED
@@ -156,7 +156,7 @@
156
156
 
157
157
  private static eventSubs: { [key: string]: Function[] } = {};
158
158
 
159
- public webXR!: WebXR;
159
+ public webXR?: WebXR;
160
160
  public index: number = -1;
161
161
  public controllerModel!: XRControllerModel;
162
162
  public controller!: Group;
@@ -223,7 +223,7 @@
223
223
 
224
224
  awake(): void {
225
225
  if (!this.controller) {
226
- console.warn("Missing Controller!!!", this);
226
+ console.warn("WebXRController: Missing controller object.", this);
227
227
  return;
228
228
  }
229
229
  this._connnectedCallback = this.onSourceConnected.bind(this);
@@ -260,6 +260,11 @@
260
260
  }
261
261
 
262
262
  public onEnable(): void {
263
+ if (!this.webXR) {
264
+ console.warn("No WebXR component assigned to WebXRController.");
265
+ return;
266
+ }
267
+
263
268
  if (this.hand)
264
269
  this.hand.name = "Hand";
265
270
  if (this.controllerGrip)
@@ -301,9 +306,11 @@
301
306
  // this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
302
307
  // this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
303
308
 
304
- const i = this.webXR.Controllers.indexOf(this);
305
- if (i >= 0)
306
- this.webXR.Controllers.splice(i, 1);
309
+ if (this.webXR) {
310
+ const i = this.webXR.Controllers.indexOf(this);
311
+ if (i >= 0)
312
+ this.webXR.Controllers.splice(i, 1);
313
+ }
307
314
  }
308
315
 
309
316
  // onDestroy(): void {
@@ -382,6 +389,7 @@
382
389
  }
383
390
 
384
391
  update(): void {
392
+ if (!this.webXR) return;
385
393
 
386
394
  // TODO: we should wait until we actually have models, this is just a workaround
387
395
  if (this.context.time.frameCount % 60 === 0) {
@@ -435,7 +443,7 @@
435
443
  if (this.type === ControllerType.PhysicalDevice)
436
444
  this.input = session.inputSources[this.index];
437
445
  if (!this.input) return;
438
- const rig = this.webXR.Rig;
446
+ const rig = this.webXR!.Rig;
439
447
  if (!rig) return;
440
448
 
441
449
  if (this._didNotEndSelection && !this.handPointerModel.pinched) {
@@ -477,7 +485,7 @@
477
485
 
478
486
  rig.getWorldQuaternion(this.worldRot);
479
487
  this.movementVector.set(side, 0, forward);
480
- this.movementVector.applyQuaternion(this.webXR.TransformOrientation);
488
+ this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
481
489
  this.movementVector.y = 0;
482
490
  this.movementVector.applyQuaternion(this.worldRot);
483
491
  this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
@@ -524,8 +532,10 @@
524
532
  }
525
533
  else this._pinchStartTime = undefined;
526
534
 
527
- let doTeleport = teleport > .5 && this.webXR.IsInVR;
528
- let isInMiniatureMode = this.webXR.Rig ? this.webXR.Rig?.scale?.x < .999 : false;
535
+ const inVR = this.webXR!.IsInVR;
536
+ const xrRig = this.webXR!.Rig;
537
+ let doTeleport = teleport > .5 && inVR;
538
+ let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
529
539
  let newRigScale: number | null = null;
530
540
 
531
541
  if (buttons && this.input && !this.input.hand) {
@@ -534,9 +544,9 @@
534
544
  // button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
535
545
  // see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
536
546
  if (i === 4) {
537
- if (btn.pressed && !this.didChangeScale && this.webXR.IsInVR) {
547
+ if (btn.pressed && !this.didChangeScale && inVR) {
538
548
  this.didChangeScale = true;
539
- const rig = this.webXR.Rig;
549
+ const rig = xrRig;
540
550
  if (rig) {
541
551
  const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
542
552
  doTeleport = args.doTeleport;
@@ -611,8 +621,8 @@
611
621
  const hit = rc ? rc[0] : null;
612
622
  this.lastHit = hit;
613
623
  let factor = 1;
614
- if (this.webXR.Rig) {
615
- factor /= this.webXR.Rig.scale.x;
624
+ if (this.webXR!.Rig) {
625
+ factor /= this.webXR!.Rig.scale.x;
616
626
  }
617
627
  // if (!hit) factor = 0;
618
628
 
@@ -961,7 +971,7 @@
961
971
  const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
962
972
  getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
963
973
 
964
- const rig = this.controller.webXR.Rig;
974
+ const rig = this.controller.webXR!.Rig;
965
975
  if (rig)
966
976
  this.rigPositionLastFrame.copy(getWorldPosition(rig))
967
977
 
@@ -1056,7 +1066,7 @@
1056
1066
  this.controllerPosDelta.copy(this.controllerWorldPos);
1057
1067
  this.controllerPosDelta.sub(this.lastControllerWorldPos);
1058
1068
  this.lastControllerWorldPos.copy(this.controllerWorldPos);
1059
- const rig = this.controller.webXR.Rig;
1069
+ const rig = this.controller.webXR!.Rig;
1060
1070
  if (rig) {
1061
1071
  const rigPos = getWorldPosition(rig);
1062
1072
  const rigDelta = this.rigPositionLastFrame.sub(rigPos);
@@ -1094,7 +1104,7 @@
1094
1104
 
1095
1105
  if (!this.didReparent && this.selected && this.controller) {
1096
1106
 
1097
- const rigScale = this.controller.webXR.Rig?.scale.x ?? 1.0;
1107
+ const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
1098
1108
 
1099
1109
  this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
1100
1110
  // console.log(this.totalChangeAlongDirection);
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -138,13 +138,14 @@
138
138
  export class WebXRImageTracking extends Behaviour {
139
139
 
140
140
  @serializable(WebXRImageTrackingModel)
141
- trackedImages!: WebXRImageTrackingModel[];
141
+ trackedImages?: WebXRImageTrackingModel[];
142
142
 
143
143
  private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
144
144
 
145
145
  private static _imageElements: Map<string, ImageBitmap | null> = new Map();
146
146
 
147
147
  awake(): void {
148
+ if (!this.trackedImages) return;
148
149
  for (const trackedImage of this.trackedImages) {
149
150
  if (trackedImage.image) {
150
151
  if (WebXRImageTracking._imageElements.has(trackedImage.image)) {
@@ -160,19 +161,19 @@
160
161
 
161
162
  // read back Uint8Array to use in USDZ -
162
163
  // TODO better would be to do that once we actually need it
163
- const canvas = await imageToCanvas( img );
164
+ const canvas = await imageToCanvas(img);
164
165
  if (canvas) {
165
- const blob = await new Promise( resolve => canvas.toBlob( resolve, 'image/png', 1 ) ) as any;
166
+ const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png', 1)) as any;
166
167
  const arrayBuffer = await blob.arrayBuffer();
167
-
168
+
168
169
  const exporter = GameObject.findObjectOfType(USDZExporter);
169
- if (exporter) {
170
+ if (exporter && this.trackedImages) {
170
171
  exporter.extensions.push(
171
- new ImageTrackingExtension("marker.png", new Uint8Array(arrayBuffer), this.trackedImages[0].widthInMeters)
172
- );
172
+ new ImageTrackingExtension("marker.png", new Uint8Array(arrayBuffer), this.trackedImages[0].widthInMeters)
173
+ );
173
174
  exporter.anchoringType = "image";
174
175
  }
175
- }
176
+ }
176
177
  });
177
178
  }
178
179
  }
@@ -197,6 +198,7 @@
197
198
  }
198
199
 
199
200
  private onModifyAROptions = (event: any) => {
201
+ if (!this.trackedImages) return;
200
202
  const options = event.detail;
201
203
  const features = options.optionalFeatures || [];
202
204
  if (!features.includes("image-tracking"))
@@ -254,7 +256,7 @@
254
256
 
255
257
  if (asset) {
256
258
  trackedData!.object = asset;
257
-
259
+
258
260
  // make sure to parent to the WebXR.rig
259
261
  if (this.xr) {
260
262
  this.xr.Rig.add(asset);
@@ -273,9 +275,9 @@
273
275
  // to improve the tracking quality a bit.
274
276
  if (model.imageDoesNotMove && trackedData.frames > 10)
275
277
  continue;
276
-
278
+
277
279
  if (!trackedData.object) continue;
278
-
280
+
279
281
  if (this.xr) {
280
282
  this.xr.Rig.add(trackedData.object);
281
283
  }
src/engine-components/webxr/WebXRSync.ts CHANGED
@@ -187,10 +187,9 @@
187
187
 
188
188
  if(!this.webXR)
189
189
  {
190
- console.log("Missing webxr component");
191
190
  this.webXR = GameObject.findObjectOfType(WebXR, this.context);
192
191
  if(!this.webXR) {
193
- console.error("Could not find webxr component");
192
+ console.warn("WebXRSync: Could not find WebXR component, won't sync.");
194
193
  return;
195
194
  }
196
195
  }
@@ -282,11 +281,12 @@
282
281
  console.log("old data", timeDiff, guid)
283
282
  return null;
284
283
  }
284
+ if (!this.webXR) return null;
285
285
  let user = this.avatars[guid];
286
286
  if (user === undefined) {
287
287
  try {
288
288
  console.log("create new avatar");
289
- const newUser = new WebXRAvatar(this.context, guid, this.webXR!);
289
+ const newUser = new WebXRAvatar(this.context, guid, this.webXR);
290
290
  user = newUser;
291
291
  this.avatars[guid] = newUser;
292
292
  } catch (err) {