Needle Engine

Changes between version 3.36.2-beta and 3.36.3-beta
Files changed (20) hide show
  1. src/engine/webcomponents/api.ts +1 -0
  2. src/engine/xr/api.ts +2 -1
  3. src/engine-components/ContactShadows.ts +8 -3
  4. src/engine/engine_camera.ts +5 -5
  5. src/engine/engine_components.ts +95 -4
  6. src/engine/engine_element_loading.ts +0 -1
  7. src/engine/engine_utils.ts +4 -0
  8. src/engine/xr/events.ts +3 -3
  9. src/engine-components/export/usdz/Extension.ts +14 -2
  10. src/engine/extensions/NEEDLE_progressive.ts +35 -4
  11. src/asap/needle-asap.ts +10 -3
  12. src/engine/webcomponents/needle menu/needle-menu.ts +7 -0
  13. src/engine/xr/NeedleXRSession.ts +3 -1
  14. src/engine-components/OrbitControls.ts +41 -13
  15. src/engine-components/Renderer.ts +53 -48
  16. src/engine-components/ShadowCatcher.ts +1 -1
  17. src/engine-components/export/usdz/ThreeUSDZExporter.ts +4 -5
  18. src/engine-components/export/usdz/USDZExporter.ts +87 -18
  19. src/engine-components/webxr/WebARSessionRoot.ts +12 -3
  20. src/engine-components/webxr/WebXRButtons.ts +3 -7
src/engine/webcomponents/api.ts CHANGED
@@ -1,3 +1,4 @@
1
1
 
2
2
 
3
+ export * from "./icons.js"
3
4
  export { type NeedleMenuPostMessageModel } from "./needle menu/needle-menu.js"
src/engine/xr/api.ts CHANGED
@@ -1,5 +1,6 @@
1
+ export * from "./events.js";
1
2
  export * from "./NeedleXRController.js";
2
3
  export * from "./NeedleXRSession.js";
3
4
  export * from "./NeedleXRSync.js"
4
5
  export * from "./utils.js"
5
- export * from "./XRRig.js";
6
+ export * from "./XRRig.js";
src/engine-components/ContactShadows.ts CHANGED
@@ -171,9 +171,6 @@
171
171
 
172
172
  /** @internal */
173
173
  onBeforeRender(_frame: XRFrame | null): void {
174
- const scene = this.context.scene;
175
- const renderer = this.context.renderer;
176
- const initialRenderTarget = renderer.getRenderTarget();
177
174
 
178
175
  if (!this.renderTarget || !this.renderTargetBlur ||
179
176
  !this.depthMaterial || !this.shadowCamera ||
@@ -184,6 +181,10 @@
184
181
  return;
185
182
  }
186
183
 
184
+ const scene = this.context.scene;
185
+ const renderer = this.context.renderer;
186
+ const initialRenderTarget = renderer.getRenderTarget();
187
+
187
188
  // Idea: shear the shadowCamera matrix to add some light direction to the ground shadows
188
189
  /*
189
190
  const mat = this.shadowCamera.projectionMatrix.clone();
@@ -211,6 +212,9 @@
211
212
  const initialClearAlpha = renderer.getClearAlpha();
212
213
  renderer.setClearAlpha(0);
213
214
 
215
+ const prevXRState = renderer.xr.enabled;
216
+ renderer.xr.enabled = false;
217
+
214
218
  // render to the render target to get the depths
215
219
  renderer.setRenderTarget(this.renderTarget);
216
220
  renderer.render(scene, this.shadowCamera);
@@ -235,6 +239,7 @@
235
239
  renderer.setRenderTarget(initialRenderTarget);
236
240
  renderer.setClearAlpha(initialClearAlpha);
237
241
  scene.background = initialBackground;
242
+ renderer.xr.enabled = prevXRState;
238
243
  }
239
244
 
240
245
  // renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget
src/engine/engine_camera.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import type { ICameraController } from "./engine_types.js";
4
4
 
5
5
 
6
- const $cameraController = Symbol("cameraController");
6
+ const $cameraController = "needle:cameraController";
7
7
 
8
8
  /** Get the camera controller for the given camera (if any)
9
9
  */
@@ -22,18 +22,18 @@
22
22
  }
23
23
 
24
24
 
25
- const $autofit = Symbol("camera autofit");
25
+ const autofit = "needle:autofit";
26
26
 
27
27
  /** @internal */
28
28
  export function useForAutoFit(obj: Object3D): boolean {
29
29
  // if autofit is not defined we assume it may be included
30
- if (obj[$autofit] === undefined) return true;
30
+ if (obj[autofit] === undefined) return true;
31
31
  // otherwise if anything is set except false we assume it should be included
32
- return obj[$autofit] !== false;
32
+ return obj[autofit] !== false;
33
33
  }
34
34
 
35
35
  /** @internal */
36
36
  export function setAutoFitEnabled(obj: Object3D, enabled: boolean): void {
37
- obj[$autofit] = enabled;
37
+ obj[autofit] = enabled;
38
38
 
39
39
  }
src/engine/engine_components.ts CHANGED
@@ -169,6 +169,16 @@
169
169
  return arr;
170
170
  }
171
171
 
172
+ /**
173
+ * Searches for a given component type in the given object.
174
+ * @param obj The object to search in.
175
+ * @param componentType The type of the component to search for.
176
+ * @returns The first component of the given type found in the given object.
177
+ * @example
178
+ * ```typescript
179
+ * const myComponent = getComponent(myObject, MyComponent);
180
+ * ```
181
+ */
172
182
  export function getComponent<T extends IComponent>(obj: Object3D, componentType: Constructor<T>): T | null {
173
183
  const result = onGetComponent(obj, componentType);
174
184
  if (!result) return null;
@@ -176,6 +186,18 @@
176
186
  return result;
177
187
  }
178
188
 
189
+ /**
190
+ * Searches for a given component type in the children of the given object.
191
+ * @param obj The object to start the search from - this object is also included in the search.
192
+ * @param componentType The type of the component to search for.
193
+ * @param arr An optional array to store the found components in. If not provided, a new array is created.
194
+ * @param clearArray If true, the array is cleared before storing the found components. Default is true.
195
+ * @returns An array of components of the given type found in the children of the given object.
196
+ * @example
197
+ * ```typescript
198
+ * const myComponents = getComponents(myObject, MyComponent);
199
+ * ```
200
+ */
179
201
  export function getComponents<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null, clearArray: boolean = true): T[] {
180
202
  if (!arr) arr = [];
181
203
  if (clearArray) arr.length = 0;
@@ -183,6 +205,17 @@
183
205
  return arr;
184
206
  }
185
207
 
208
+ /**
209
+ * Searches for a given component type in the children of the given object.
210
+ * @param obj The object to start the search from - this object is also included in the search.
211
+ * @param componentType The type of the component to search for.
212
+ * @param includeInactive If true, also inactive components are returned. Default is true.
213
+ * @returns The first component of the given type found in the children of the given object.
214
+ * @example
215
+ * ```typescript
216
+ * const myComponent = getComponentInChildren(myObject, MyComponent);
217
+ * ```
218
+ */
186
219
  export function getComponentInChildren<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, includeInactive?: boolean): T | null {
187
220
  const res = getComponent(obj, componentType) as IComponent | null;
188
221
  if (includeInactive === false && res?.enabled === false) return null;
@@ -194,6 +227,18 @@
194
227
  return null;
195
228
  }
196
229
 
230
+ /**
231
+ * Searches for a given component type in the children of the given object.
232
+ * @param obj The object to start the search from - this object is also included in the search.
233
+ * @param componentType The type of the component to search for.
234
+ * @param arr An optional array to store the found components in. If not provided, a new array is created.
235
+ * @param clearArray If true, the array is cleared before storing the found components. Default is true.
236
+ * @returns An array of components of the given type found in the children of the given object.
237
+ * @example
238
+ * ```typescript
239
+ * const myComponents = getComponentsInChildren(myObject, MyComponent);
240
+ * ```
241
+ */
197
242
  export function getComponentsInChildren<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, arr?: T[], clearArray: boolean = true): T[] | null {
198
243
  if (!arr) arr = [];
199
244
  if (clearArray) arr.length = 0;
@@ -205,6 +250,16 @@
205
250
  return arr;
206
251
  }
207
252
 
253
+ /**
254
+ * Searches for a given component type in the parent hierarchy of the given object.
255
+ * @param obj The object to start the search from - this object is also included in the search.
256
+ * @param componentType The type of the component to search for.
257
+ * @returns The first component of the given type found in the parent hierarchy of the given object.
258
+ * @example
259
+ * ```typescript
260
+ * const myComponent = getComponentInParent(myObject, MyComponent);
261
+ * ```
262
+ */
208
263
  export function getComponentInParent<T extends IComponent>(obj: Object3D, componentType: Constructor<T>): T | null {
209
264
  if (!obj) return null;
210
265
  if (Array.isArray(obj)) {
@@ -223,6 +278,18 @@
223
278
  return null;
224
279
  }
225
280
 
281
+ /**
282
+ * Searches for a given component type in the parent hierarchy of the given object.
283
+ * @param obj The object to start the search from - this object is also included in the search.
284
+ * @param componentType The type of the component to search for.
285
+ * @param arr An optional array to store the found components in. If not provided, a new array is created.
286
+ * @param clearArray If true, the array is cleared before storing the found components. Default is true.
287
+ * @returns An array of components of the given type found in the parent hierarchy of the given object.
288
+ * @example
289
+ * ```typescript
290
+ * const myComponents = getComponentsInParent(myObject, MyComponent);
291
+ * ```
292
+ */
226
293
  export function getComponentsInParent<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null, clearArray: boolean = true): T[] {
227
294
  if (!arr) arr = [];
228
295
  if (clearArray) arr.length = 0;
@@ -234,7 +301,19 @@
234
301
  return arr;
235
302
  }
236
303
 
237
- export function findObjectOfType<T extends IComponent>(type: Constructor<T>, contextOrScene: Object3D | { scene: Scene }, includeInactive): T | null {
304
+ /**
305
+ * Searches the the scene for a component of the given type.
306
+ * If the contextOrScene is not provided, the current context is used.
307
+ * @param type The type of the component to search for.
308
+ * @param contextOrScene The context or scene to search in. If not provided, the current context is used.
309
+ * @param includeInactive If true, also inactive components are returned. Default is true.
310
+ * @returns The first component of the given type found in the scene or null if none was found.
311
+ * @example
312
+ * ```typescript
313
+ * const myComponent = findObjectOfType(MyComponent);
314
+ * ```
315
+ */
316
+ export function findObjectOfType<T extends IComponent>(type: Constructor<T>, contextOrScene: undefined | Object3D | { scene: Scene } = undefined, includeInactive: boolean = true): T | null {
238
317
  if (!type) return null;
239
318
  if (!contextOrScene) {
240
319
  contextOrScene = Context.Current;
@@ -259,7 +338,17 @@
259
338
  return null;
260
339
  }
261
340
 
262
- export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array: T[], contextOrScene): T[] {
341
+ /**
342
+ * Searches the the scene for all components of the given type.
343
+ * If the contextOrScene is not provided, the current context is used.
344
+ * @param type The type of the component to search for.
345
+ * @param contextOrScene The context or scene to search in. If not provided, the current context is used.
346
+ * @example
347
+ * ```typescript
348
+ * const myComponents = findObjectsOfType(MyComponent);
349
+ * ```
350
+ */
351
+ export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array: T[], contextOrScene: undefined | Object3D | { scene: Scene } = undefined): T[] {
263
352
  if (!type) return array;
264
353
  if (!array) array = [];
265
354
  array.length = 0;
@@ -271,11 +360,13 @@
271
360
  return array;
272
361
  }
273
362
  }
274
- const scene = contextOrScene.isScene === true || contextOrScene.isObject3D === true ? contextOrScene : contextOrScene?.scene;
363
+
364
+ if ("scene" in contextOrScene) contextOrScene = (contextOrScene as { scene: Scene }).scene;
365
+
366
+ const scene = contextOrScene;
275
367
  if (!scene) return array;
276
368
  for (const i in scene.children) {
277
369
  const child = scene.children[i];
278
- if (child.constructor == type) return child;
279
370
  getComponentsInChildren(child, type, array, false);
280
371
  }
281
372
  return array;
src/engine/engine_element_loading.ts CHANGED
@@ -240,7 +240,6 @@
240
240
  const logoSize = 120;
241
241
  logo.style.width = `${logoSize}px`;
242
242
  logo.style.height = `${logoSize}px`;
243
- logo.style.borderRadius = "80px";
244
243
  logo.style.padding = "20px";
245
244
  logo.style.margin = "-20px";
246
245
  logo.style.marginBottom = "-10px";
src/engine/engine_utils.ts CHANGED
@@ -515,6 +515,10 @@
515
515
  return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
516
516
  }
517
517
 
518
+ export function isAndroidDevice() {
519
+ return /Android/.test(navigator.userAgent);
520
+ }
521
+
518
522
  /** @returns `true` if we're currently using the mozilla XR browser */
519
523
  export function isMozillaXR() {
520
524
  return /WebXRViewer\//i.test(navigator.userAgent);
src/engine/xr/events.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  /**
8
8
  * Add a listener for when an XR session starts
9
- * This event is triggered when the XR session is started, either by the user or by the application
9
+ * This event is triggered when the XR session is started, either by the user or by the application before all other XR start events
10
10
  * @param fn The function to call when the XR session starts
11
11
  * @example
12
12
  * ```js
@@ -42,8 +42,8 @@
42
42
  const onXRSessionEndListeners: ((evt: XRSessionEventArgs) => void)[] = [];
43
43
 
44
44
  /**
45
- * Add a listener for when an XR session ends
46
- * This event is triggered when the XR session is ended, either by the user or by the application
45
+ * Add a listener for when an XR session ends
46
+ * This event is triggered when the XR session is ended, either by the user or by the application before all other XR end events
47
47
  * @param fn The function to call when the XR session ends
48
48
  * @example
49
49
  * ```js
src/engine-components/export/usdz/Extension.ts CHANGED
@@ -2,12 +2,24 @@
2
2
 
3
3
  import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
4
4
 
5
+ /**
6
+ * Interface for USDZ Exporter Extensions used by {@link USDZExporter}
7
+ */
5
8
  export interface IUSDExporterExtension {
6
9
 
10
+ /**
11
+ * The name of the extension
12
+ */
7
13
  get extensionName(): string;
14
+ /**
15
+ * Called before the document is built
16
+ */
8
17
  onBeforeBuildDocument?(context);
18
+ /**
19
+ * Called after the document is built
20
+ */
9
21
  onAfterBuildDocument?(context);
10
- onExportObject?(object: Object3D, model : USDObject, context: USDZExporterContext);
22
+ onExportObject?(object: Object3D, model: USDObject, context: USDZExporterContext);
11
23
  onAfterSerialize?(context);
12
- onAfterHierarchy?(context, writer : any);
24
+ onAfterHierarchy?(context, writer: any);
13
25
  }
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -92,6 +92,36 @@
92
92
  }>
93
93
  }
94
94
 
95
+ /**
96
+ * This is the result of a progressive texture loading event for a material's texture slot in {@link NEEDLE_progressive.assignTextureLOD}
97
+ * @internal
98
+ */
99
+ export declare type ProgressiveMaterialTextureLoadingResult = {
100
+ /** the material the progressive texture was loaded for */
101
+ material: Material,
102
+ /** the slot in the material where the texture was loaded */
103
+ slot: string,
104
+ /** the texture that was loaded (if any) */
105
+ texture: Texture | null;
106
+ /** the level of detail that was loaded */
107
+ level: number;
108
+ }
109
+
110
+ /**
111
+ * The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
112
+ * This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
113
+ * @example
114
+ * ```javascript
115
+ * const loader = new GLTFLoader();
116
+ * loader.register(new NEEDLE_progressive());
117
+ * loader.load("model.glb", (gltf) => {
118
+ * const mesh = gltf.scene.children[0] as Mesh;
119
+ * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
120
+ * console.log("Mesh with LOD level 1 loaded", mesh);
121
+ * });
122
+ * });
123
+ * ```
124
+ */
95
125
  export class NEEDLE_progressive implements GLTFLoaderPlugin {
96
126
 
97
127
  /** The name of the extension */
@@ -210,6 +240,7 @@
210
240
  }
211
241
  this.onProgressiveLoadEnd(info);
212
242
  return geo;
243
+
213
244
  }).catch(err => {
214
245
  this.onProgressiveLoadEnd(info);
215
246
  console.error("Error loading mesh LOD", mesh, err);
@@ -231,7 +262,7 @@
231
262
  * @returns a promise that resolves to the material or texture with the requested LOD level
232
263
  */
233
264
  static assignTextureLOD(context: Context, source: SourceIdentifier, materialOrTexture: Material | Texture, level: number = 0)
234
- : Promise<Array<{ slot: string, texture: Texture | null }> | Texture | null> {
265
+ : Promise<Array<ProgressiveMaterialTextureLoadingResult> | Texture | null> {
235
266
 
236
267
  if (!materialOrTexture) return Promise.resolve(null);
237
268
 
@@ -262,15 +293,15 @@
262
293
  }
263
294
  }
264
295
  return PromiseAllWithErrors(promises).then(res => {
265
- const textures = new Array<{ slot: string, texture: Texture | null }>();
296
+ const textures = new Array<ProgressiveMaterialTextureLoadingResult>();
266
297
  for (let i = 0; i < res.results.length; i++) {
267
298
  const tex = res.results[i];
268
299
  const slot = slots[i];
269
300
  if (tex instanceof Texture) {
270
- textures.push({ slot, texture: tex });
301
+ textures.push({ material, slot, texture: tex, level });
271
302
  }
272
303
  else {
273
- textures.push({ slot, texture: null });
304
+ textures.push({ material, slot, texture: null, level });
274
305
  }
275
306
  }
276
307
  return textures;
src/asap/needle-asap.ts CHANGED
@@ -73,11 +73,18 @@
73
73
  }
74
74
 
75
75
 
76
- function insertTemporaryContentWhileEngineHasntLoaded(needleEngineElement) {
76
+ function insertTemporaryContentWhileEngineHasntLoaded(needleEngineElement: HTMLElement) {
77
77
  if (needleEngineHasLoaded()) return;
78
78
 
79
79
  const img = document.createElement("img");
80
- img.src = needleLogoOnlySVG;
80
+
81
+ // if a custom logo is assigned we should use this here
82
+ // technically we should check if the user has a license
83
+ const customLogoUrl = needleEngineElement?.getAttribute("loading-logo-src");
84
+ if (customLogoUrl && customLogoUrl?.length > 0)
85
+ img.src = customLogoUrl;
86
+ else
87
+ img.src = needleLogoOnlySVG;
81
88
  img.style.position = "absolute";
82
89
  img.style.top = "50%";
83
90
  img.style.left = "50%";
@@ -94,7 +101,7 @@
94
101
  // animation to pulsate
95
102
  img.animate([
96
103
  { opacity: 0 },
97
- { opacity: .3 },
104
+ { opacity: .5 },
98
105
  { opacity: 0 }
99
106
  ], {
100
107
  duration: 3000,
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -231,6 +231,7 @@
231
231
  display: flex;
232
232
  justify-content: center;
233
233
  align-items: center;
234
+ max-height: 2.3rem;
234
235
 
235
236
  /** basic font settings for all entries **/
236
237
  font-size: 1rem;
@@ -265,6 +266,12 @@
265
266
  outline: rgba(0,0,0,.05) 1px solid;
266
267
  }
267
268
 
269
+ :host .options > *:disabled {
270
+ background: rgba(0,0,0,.05);
271
+ color: rgba(60,60,60,.7);
272
+ pointer-events: none;
273
+ }
274
+
268
275
  button {
269
276
  gap: 0.3rem;
270
277
  }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
10
10
  import type { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
11
11
  import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
12
- import { invokeXRSessionStart } from "./events.js"
12
+ import { invokeXRSessionEnd, invokeXRSessionStart } from "./events.js"
13
13
  import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
14
14
  import { NeedleXRController } from "./NeedleXRController.js";
15
15
  import { NeedleXRSync } from "./NeedleXRSync.js";
@@ -952,6 +952,8 @@
952
952
  this.context.mainCameraComponent?.applyClearFlags()
953
953
  });
954
954
 
955
+ invokeXRSessionEnd({ session: this });
956
+
955
957
  for (const listener of NeedleXRSession._xrEndListeners) {
956
958
  listener({ xr: this });
957
959
  }
src/engine-components/OrbitControls.ts CHANGED
@@ -559,14 +559,33 @@
559
559
 
560
560
  // Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
561
561
  // Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
562
- /** Fits the camera to show the objects provided (defaults to the scene if no objects are passed in) */
563
- fitCamera(objects?: Array<Object3D>, fitOffset: number = 1.1, immediate: boolean = true) {
562
+ /** Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
563
+ * @param objects The objects to fit the camera to - defaults to the scene if not provided
564
+ * @param fitOffset A factor to multiply the distance to the objects by. Default is 1.1
565
+ * @param immediate If true the camera will move immediately to the new position, otherwise it will lerp
566
+ */
567
+ fitCamera(objects?: Array<Object3D>, fitOffset: undefined | number = undefined, immediate: boolean = true) {
568
+
569
+ if (this.context.isInXR) {
570
+ // camera fitting in XR is not supported
571
+ return;
572
+ }
573
+
574
+ if (fitOffset == undefined) {
575
+ fitOffset = 1.1;
576
+ }
564
577
  const camera = this._cameraObject as PerspectiveCamera;
565
578
  const controls = this._controls as ThreeOrbitControls | null;
566
579
  if (!objects?.length) objects = this.context.scene.children;
567
- if (objects.length <= 0) return;
580
+ if (objects.length <= 0) {
581
+ console.warn("No objects to fit camera to...");
582
+ return;
583
+ }
568
584
 
569
- if (!camera || !controls) return;
585
+ if (!camera || !controls) {
586
+ console.warn("No camera or controls found to fit camera to objects...");
587
+ return;
588
+ }
570
589
 
571
590
  const size = new Vector3();
572
591
  const center = new Vector3();
@@ -654,16 +673,14 @@
654
673
  console.log("Fit camera to objects", fitHeightDistance, fitWidthDistance, "distance", distance);
655
674
  }
656
675
 
657
- const cameraWp = getWorldPosition(camera);
658
- const direction = controls.target.clone()
659
- .sub(cameraWp)
660
- .normalize()
661
- .multiplyScalar(distance);
662
-
663
676
  controls.maxDistance = distance * 10;
664
677
  controls.minDistance = distance * 0.01;
665
678
 
666
- this.setLookTargetPosition(center, immediate);
679
+ const verticalOffset = 0.05;
680
+
681
+ const lookAt = center.clone();
682
+ lookAt.y -= size.y * verticalOffset;
683
+ this.setLookTargetPosition(lookAt, immediate);
667
684
  this.autoTarget = false;
668
685
 
669
686
  // TODO: this doesnt take the Camera component nearClipPlane into account
@@ -673,8 +690,16 @@
673
690
  camera.updateMatrixWorld();
674
691
  camera.updateProjectionMatrix();
675
692
 
693
+ const cameraWp = getWorldPosition(camera);
694
+ const direction = center.clone();
695
+ direction.sub(cameraWp);
696
+ direction.y = 0;
697
+ direction.normalize();
698
+ direction.multiplyScalar(distance);
699
+ direction.y += -verticalOffset * 4 * size.y;
700
+
676
701
  if (camera.parent) {
677
- const cameraLocalPosition = camera.parent!.worldToLocal(controls.target.clone().sub(direction));
702
+ const cameraLocalPosition = camera.parent!.worldToLocal(center.clone().sub(direction));
678
703
  this.setCameraTargetPosition(cameraLocalPosition, immediate);
679
704
  }
680
705
  else console.error(`Can not fit camera ${camera.name} because it has no parent`)
@@ -685,12 +710,15 @@
685
710
  const helper = new Box3Helper(box);
686
711
  this.context.scene.add(helper);
687
712
  setWorldRotation(helper, getWorldRotation(camera));
713
+ setTimeout(() => {
714
+ this.context.scene.remove(helper);
715
+ }, 10000);
688
716
 
689
717
  if (!this._haveAttachedKeyboardEvents) {
690
718
  this._haveAttachedKeyboardEvents = true;
691
719
  document.body.addEventListener("keydown", (e) => {
692
720
  if (e.code === "KeyF") {
693
- this.fitCamera(objects);
721
+ this.fitCamera(objects, fitOffset, immediate);
694
722
  }
695
723
  });
696
724
  }
src/engine-components/Renderer.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import { getTempVector, getWorldDirection, getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
12
12
  import type { IGameObject, IRenderer, ISharedMaterials } from "../engine/engine_types.js";
13
13
  import { getParam, isMobileDevice } from "../engine/engine_utils.js";
14
- import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
14
+ import { NEEDLE_progressive, ProgressiveMaterialTextureLoadingResult } from "../engine/extensions/NEEDLE_progressive.js";
15
15
  import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
16
16
  import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
17
17
  import { Behaviour, GameObject } from "./Component.js";
@@ -681,7 +681,7 @@
681
681
  }
682
682
  }
683
683
 
684
- if (this._wasVisible && this.allowProgressiveLoading) {
684
+ if (this._wasVisible && this.allowProgressiveLoading && !suppressProgressiveLoading) {
685
685
  if (this.automaticallyUpdateLODLevel) {
686
686
  this.updateLODs();
687
687
  }
@@ -868,7 +868,7 @@
868
868
  return this._lastLodLevel = mesh["DEBUG:LOD"];
869
869
  }
870
870
 
871
- let meshDensity = 0;
871
+ let meshDensity = 0;
872
872
  // TODO: the mesh info contains also the density for all available LOD level so we can use this for selecting which level to show
873
873
  const lodsInfo = NEEDLE_progressive.getMeshLODInformation(mesh.geometry);
874
874
  // TODO: the substraction here is not clear - it should be some sort of mesh density value
@@ -997,18 +997,13 @@
997
997
  * @param level the LOD level to load. Level 0 is the best quality, higher levels are lower quality
998
998
  * @returns Promise with true if the LOD was loaded, false if not
999
999
  */
1000
- loadProgressiveTextures(material: Material, level: number) {
1001
- // progressive load before rendering so we only load textures for visible materials
1002
- if (!suppressProgressiveLoading && material) {
1003
- if (this.allowProgressiveLoading) {
1004
- if (!material.userData) material.userData = {};
1005
- if (material.userData.LOD !== level) {
1006
- material.userData.LOD = level;
1007
- return NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material, level);
1008
- }
1009
- }
1000
+ loadProgressiveTextures(material: Material, level: number): Promise<ProgressiveMaterialTextureLoadingResult[] | Texture | null> {
1001
+ if (!material) return Promise.resolve(null);
1002
+ if (material.userData && material.userData.LOD !== level) {
1003
+ material.userData.LOD = level;
1004
+ return NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material, level);
1010
1005
  }
1011
- return Promise.resolve(true);
1006
+ return Promise.resolve(null);
1012
1007
  }
1013
1008
 
1014
1009
  /** Load progressive meshes for the given mesh
@@ -1017,31 +1012,29 @@
1017
1012
  * @param level the LOD level to load. Level 0 is the best quality, higher levels are lower quality
1018
1013
  * @returns Promise with true if the LOD was loaded, false if not
1019
1014
  */
1020
- loadProgressiveMeshes(mesh: Mesh, level: number) {
1021
- if (!suppressProgressiveLoading && mesh) {
1022
- if (this.allowProgressiveLoading) {
1023
- if (!mesh.userData) mesh.userData = {};
1024
- if (mesh.userData.LOD !== level) {
1025
- mesh.userData.LOD = level;
1026
- const originalGeometry = mesh.geometry;
1027
- return NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, mesh, level).then(res => {
1028
- if (res && mesh.userData.LOD == level && originalGeometry != mesh.geometry) {
1029
- // update the lightmap
1030
- this.applyLightmapping();
1015
+ loadProgressiveMeshes(mesh: Mesh, level: number) : Promise<BufferGeometry | null> {
1016
+ if (!mesh) return Promise.resolve(null);
1017
+ if (!mesh.userData) mesh.userData = {};
1018
+ if (mesh.userData.LOD !== level) {
1019
+ mesh.userData.LOD = level;
1020
+ const originalGeometry = mesh.geometry;
1021
+ return NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, mesh, level).then(res => {
1022
+ if (res && mesh.userData.LOD == level && originalGeometry != mesh.geometry) {
1023
+ // update the lightmap
1024
+ this.applyLightmapping();
1031
1025
 
1032
- if (this.handles) {
1033
- for (const inst of this.handles) {
1034
- // if (inst["LOD"] < level) continue;
1035
- // inst["LOD"] = level;
1036
- inst.setGeometry(mesh.geometry);
1037
- }
1038
- }
1026
+ if (this.handles) {
1027
+ for (const inst of this.handles) {
1028
+ // if (inst["LOD"] < level) continue;
1029
+ // inst["LOD"] = level;
1030
+ inst.setGeometry(mesh.geometry);
1039
1031
  }
1040
- })
1032
+ }
1041
1033
  }
1042
- }
1034
+ return res;
1035
+ })
1043
1036
  }
1044
- return true;
1037
+ return Promise.resolve(null);
1045
1038
  }
1046
1039
 
1047
1040
  /** Apply the settings of this renderer to the given object
@@ -1106,11 +1099,15 @@
1106
1099
 
1107
1100
  awake() {
1108
1101
  super.awake();
1102
+ if (debugskinnedmesh) console.log("SkinnedMeshRenderer for \"" + this.name + "\"", this);
1109
1103
  // disable skinned mesh occlusion because of https://github.com/mrdoob/js/issues/14499
1110
1104
  this.allowOcclusionWhenDynamic = false;
1111
- // If we don't do that here the bounding sphere matrix used for raycasts will be wrong. Not sure *why* this is necessary
1112
- this.gameObject.parent?.updateWorldMatrix(false, true);
1113
- this.markBoundsDirty();
1105
+
1106
+ for (const mesh of this.sharedMeshes) {
1107
+ // If we don't do that here the bounding sphere matrix used for raycasts will be wrong. Not sure *why* this is necessary
1108
+ mesh.parent?.updateWorldMatrix(false, true);
1109
+ this.markBoundsDirty();
1110
+ }
1114
1111
  }
1115
1112
  onAfterRender(): void {
1116
1113
  super.onAfterRender();
@@ -1126,20 +1123,28 @@
1126
1123
  // };
1127
1124
  // }
1128
1125
 
1129
- if (this.gameObject instanceof SkinnedMesh && this._needUpdateBoundingSphere) {
1130
- this._needUpdateBoundingSphere = false;
1131
- const geometry = this.gameObject.geometry;
1132
- const raycastmesh = getRaycastMesh(this.gameObject);
1133
- if (raycastmesh) this.gameObject.geometry = raycastmesh;
1134
- this.gameObject.computeBoundingSphere();
1135
- this.gameObject.geometry = geometry;
1126
+ if (this._needUpdateBoundingSphere) {
1127
+ for (const mesh of this.sharedMeshes) {
1128
+ if (mesh instanceof SkinnedMesh) {
1129
+ this._needUpdateBoundingSphere = false;
1130
+ const geometry = mesh.geometry;
1131
+ const raycastmesh = getRaycastMesh(mesh);
1132
+ if (raycastmesh) mesh.geometry = raycastmesh;
1133
+ mesh.computeBoundingSphere();
1134
+ mesh.geometry = geometry;
1135
+ }
1136
+ }
1136
1137
  }
1137
1138
 
1138
1139
  // if (this.context.time.frame % 30 === 0) this.markBoundsDirty();
1139
1140
 
1140
- if (debugskinnedmesh && this.gameObject instanceof SkinnedMesh && this.gameObject.boundingSphere) {
1141
- const tempCenter = getTempVector(this.gameObject.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld);
1142
- Gizmos.DrawWireSphere(tempCenter, this.gameObject.boundingSphere.radius, "red");
1141
+ if (debugskinnedmesh) {
1142
+ for (const mesh of this.sharedMeshes) {
1143
+ if (mesh instanceof SkinnedMesh && mesh.boundingSphere) {
1144
+ const tempCenter = getTempVector(mesh.boundingSphere.center).applyMatrix4(mesh.matrixWorld);
1145
+ Gizmos.DrawWireSphere(tempCenter, mesh.boundingSphere.radius, "red");
1146
+ }
1147
+ }
1143
1148
  }
1144
1149
  }
1145
1150
 
src/engine-components/ShadowCatcher.ts CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  private targetMesh?: Mesh;
26
26
 
27
- awake() {
27
+ start() {
28
28
  // if there's no geometry, make a basic quad
29
29
  if (!(this.gameObject instanceof Mesh)) {
30
30
  const quad = ObjectUtils.createPrimitive(PrimitiveType.Quad, {
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -516,7 +516,7 @@
516
516
 
517
517
  }
518
518
 
519
- async parse( scene, options: USDZExporterOptions = new USDZExporterOptions() ) {
519
+ async parse(scene: Object3D | undefined, options: USDZExporterOptions = new USDZExporterOptions()) {
520
520
 
521
521
  options = Object.assign( new USDZExporterOptions(), options );
522
522
 
@@ -540,9 +540,8 @@
540
540
  Progress.report('export-usdz', "Reparent bones to common ancestor");
541
541
  // HACK let's find all skeletons and reparent them to their skelroot / armature / uppermost bone parent
542
542
  const reparentings: Array<any> = [];
543
- scene.traverseVisible(object => {
544
-
545
- if (object.isSkinnedMesh) {
543
+ scene?.traverseVisible(object => {
544
+ if (object instanceof SkinnedMesh) {
546
545
  const bones = object.skeleton.bones as Bone[];
547
546
 
548
547
  const commonAncestor = findCommonAncestor(bones);
@@ -559,7 +558,7 @@
559
558
  }
560
559
 
561
560
  Progress.report('export-usdz', "Traversing hierarchy");
562
- traverseVisible( scene, context.document, context, this.keepObject);
561
+ if(scene) traverseVisible( scene, context.document, context, this.keepObject);
563
562
 
564
563
  Progress.report('export-usdz', "Invoking onAfterBuildDocument");
565
564
  await invokeAll( context, 'onAfterBuildDocument' );
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -22,11 +22,17 @@
22
22
 
23
23
  const debug = getParam("debugusdz");
24
24
 
25
+ /**
26
+ * Custom branding for the QuickLook overlay, used by {@link USDZExporter}.
27
+ */
25
28
  export class CustomBranding {
29
+ /** The call to action button text. If not set, the button will close the QuickLook overlay. */
26
30
  @serializable()
27
31
  callToAction?: string;
32
+ /** The title of the overlay. */
28
33
  @serializable()
29
34
  checkoutTitle?: string;
35
+ /** The subtitle of the overlay. */
30
36
  @serializable()
31
37
  checkoutSubtitle?: string;
32
38
 
@@ -35,6 +41,21 @@
35
41
  callToActionURL?: string;
36
42
  }
37
43
 
44
+ /**
45
+ * Exports the current scene or a specific object as USDZ file and opens it in QuickLook on iOS/iPadOS/visionOS.
46
+ * The USDZ file is generated using the Needle Engine ThreeUSDZExporter.
47
+ * The exporter supports various extensions to add custom behaviors and interactions to the USDZ file.
48
+ * The exporter can automatically collect Animations and AudioSources and export them as playing at the start.
49
+ * The exporter can also add a custom QuickLook overlay with a call to action button and custom branding.
50
+ * @example
51
+ * ```typescript
52
+ * const usdz = new USDZExporter();
53
+ * usdz.objectToExport = myObject;
54
+ * usdz.autoExportAnimations = true;
55
+ * usdz.autoExportAudioSources = true;
56
+ * usdz.exportAsync();
57
+ * ```
58
+ */
38
59
  export class USDZExporter extends Behaviour {
39
60
 
40
61
  @serializable(Object3D)
@@ -87,12 +108,16 @@
87
108
  @serializable()
88
109
  quickLookCompatible: boolean = true;
89
110
 
90
- // Registered extensions. Add your own extensions here
111
+ /**
112
+ * Extensions to add custom behaviors and interactions to the USDZ file.
113
+ * You can add your own extensions here by extending {@link IUSDExporterExtension}.
114
+ */
91
115
  extensions: IUSDExporterExtension[] = [];
92
116
 
93
117
  private link!: HTMLAnchorElement;
94
118
  private button?: HTMLButtonElement;
95
119
 
120
+ /** @internal */
96
121
  start() {
97
122
  if (debug) {
98
123
  console.log("USDZExporter", this);
@@ -125,6 +150,7 @@
125
150
  }
126
151
  }
127
152
 
153
+ /** @internal */
128
154
  onEnable() {
129
155
  const ios = isiOS()
130
156
  const safari = isSafari();
@@ -142,6 +168,7 @@
142
168
  document.getElementById("open-in-ar")?.addEventListener("click", this.onClickedOpenInARElement);
143
169
  }
144
170
 
171
+ /** @internal */
145
172
  onDisable() {
146
173
  this.button?.remove();
147
174
  this.link?.removeEventListener('message', this.lastCallback);
@@ -158,15 +185,24 @@
158
185
 
159
186
  private onClickedOpenInARElement = (evt) => {
160
187
  evt.preventDefault();
161
- this.exportAsync();
188
+ this.exportAndOpen();
162
189
  }
163
190
 
164
191
  /**
165
192
  * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook.
166
193
  * Use the various public properties of USDZExporter to customize export behaviour.
194
+ * @deprecated use {@link exportAndOpen} instead
167
195
  */
168
196
  async exportAsync() {
197
+ return this.exportAndOpen();
198
+ }
169
199
 
200
+ /**
201
+ * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook.
202
+ * @returns a Promise<Blob> containing the USDZ file
203
+ */
204
+ async exportAndOpen() : Promise<Blob | null> {
205
+
170
206
  let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
171
207
  name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
172
208
 
@@ -181,13 +217,13 @@
181
217
  if (this.customUsdzFile) {
182
218
  if (debug) console.log("Exporting custom usdz", this.customUsdzFile)
183
219
  this.openInQuickLook(this.customUsdzFile, name);
184
- return;
220
+ return null;
185
221
  }
186
222
 
187
223
  const blob = await this.export(this.objectToExport);
188
224
  if (!blob) {
189
225
  console.warn("No object to export", this);
190
- return;
226
+ return null;
191
227
  }
192
228
 
193
229
  if (debug) console.log("USDZ generation done. Downloading as " + name);
@@ -197,20 +233,52 @@
197
233
 
198
234
 
199
235
  this.openInQuickLook(blob, name);
236
+
237
+ return blob;
200
238
  }
201
239
 
202
240
  /**
203
241
  * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook.
204
242
  * @returns a Promise<Blob> containing the USDZ file
205
243
  */
206
- async export(objectToExport: Object3D | undefined) : Promise<Blob | null> {
207
-
208
- if (!objectToExport)
244
+ async export(objectToExport: Object3D | undefined): Promise<Blob | null> {
245
+ // make sure we have an object to export
246
+ if (!objectToExport) {
247
+ console.warn("No object to export");
209
248
  return null;
249
+ }
210
250
 
211
- Progress.start("export-usdz", { onProgress: (progress) => {
212
- this.dispatchEvent(new CustomEvent("export-progress", { detail: { progress } }));
213
- }});
251
+ // if we are already exporting, wait for the current export to finish
252
+ const taskForThisObject = this._currentExportTasks.get(objectToExport);
253
+ if (taskForThisObject) {
254
+ return taskForThisObject;
255
+ }
256
+ // start the export
257
+ const task = this.internalExport(objectToExport);
258
+ // store the task
259
+ if (task instanceof Promise) {
260
+ this._currentExportTasks.set(objectToExport, task);
261
+ return task.then((blob) => {
262
+ this._currentExportTasks.delete(objectToExport);
263
+ return blob;
264
+ }).catch((_) => {
265
+ this._currentExportTasks.delete(objectToExport);
266
+ return null;
267
+ });
268
+ }
269
+
270
+ return task;
271
+ }
272
+
273
+ private readonly _currentExportTasks = new Map<Object3D, Promise<Blob | null>>();
274
+
275
+ private async internalExport(objectToExport: Object3D): Promise<Blob | null> {
276
+
277
+ Progress.start("export-usdz", {
278
+ onProgress: (progress) => {
279
+ this.dispatchEvent(new CustomEvent("export-progress", { detail: { progress } }));
280
+ }
281
+ });
214
282
  Progress.report("export-usdz", { message: "Starting export", totalSteps: 40, currentStep: 0 });
215
283
  Progress.report("export-usdz", { message: "Load progressive textures", autoStep: 5 });
216
284
  Progress.start("export-usdz-textures", "export-usdz");
@@ -218,6 +286,7 @@
218
286
  const renderers = GameObject.getComponentsInChildren(objectToExport, Renderer);
219
287
  const progressiveLoading = new Array<Promise<any>>();
220
288
  let progressiveTasks = 0;
289
+ // TODO: it would be better to directly integrate this into the exporter and *on export* request the correct LOD level for textures and meshes instead of relying on the renderer etc
221
290
  for (const rend of renderers) {
222
291
  rend["didAutomaticallyUpdateLODLevel"] = rend.automaticallyUpdateLODLevel;
223
292
  rend.automaticallyUpdateLODLevel = false;
@@ -268,7 +337,7 @@
268
337
  extensions.push(animExt);
269
338
 
270
339
  const eventArgs = { self: this, exporter: exporter, extensions: extensions, object: objectToExport };
271
- Progress.report("export-usdz", "Invoking before-export" );
340
+ Progress.report("export-usdz", "Invoking before-export");
272
341
  this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }))
273
342
 
274
343
  // Implicit registration and actions for Animators and Animation components
@@ -286,12 +355,12 @@
286
355
  //@ts-ignore
287
356
  exporter.debug = debug;
288
357
  exporter.keepObject = (object) => {
289
- // TODO We need to take more care with disabled renderers. This currently breaks when any renderer is disabled
290
- // and then enabled at runtime by e.g. SetActiveOnClick, requiring extra work to enable them before export,
291
- // cache their state, and then reset their state after export. See
292
- const renderer = GameObject.getComponent( object, Renderer )
293
- if (renderer && !renderer.enabled) return false;
294
- return true;
358
+ // TODO We need to take more care with disabled renderers. This currently breaks when any renderer is disabled
359
+ // and then enabled at runtime by e.g. SetActiveOnClick, requiring extra work to enable them before export,
360
+ // cache their state, and then reset their state after export. See
361
+ const renderer = GameObject.getComponent(object, Renderer)
362
+ if (renderer && !renderer.enabled) return false;
363
+ return true;
295
364
  }
296
365
 
297
366
  // sanitize anchoring types
@@ -300,7 +369,7 @@
300
369
  if (this.planeAnchoringAlignment !== "horizontal" && this.planeAnchoringAlignment !== "vertical" && this.planeAnchoringAlignment !== "any")
301
370
  this.planeAnchoringAlignment = "horizontal";
302
371
 
303
- Progress.report("export-usdz", "Invoking exporter.parse" );
372
+ Progress.report("export-usdz", "Invoking exporter.parse");
304
373
  //@ts-ignore
305
374
  const arraybuffer = await exporter.parse(this.objectToExport, {
306
375
  ar: {
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { InputEventQueue, NEPointerEvent } from "../../engine/engine_input.js";
9
9
  import { serializable } from "../../engine/engine_serialization_decorator.js";
10
10
  import type { IComponent, IGameObject } from "../../engine/engine_types.js";
11
- import { getParam } from "../../engine/engine_utils.js";
11
+ import { getParam, isAndroidDevice, isMobileDevice } from "../../engine/engine_utils.js";
12
12
  import { NeedleXRController, type NeedleXREventArgs, type NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
13
13
  import { Behaviour, GameObject } from "../Component.js";
14
14
 
@@ -552,14 +552,19 @@
552
552
  // // this.offset.premultiply(this._tempMatrix.makeScale(s, s, s));
553
553
  // }
554
554
 
555
- private prev: Map<number, { x: number, z: number, screenx: number, screeny: number }> = new Map();
555
+ private prev: Map<number, { ignore: boolean, x: number, z: number, screenx: number, screeny: number }> = new Map();
556
556
  private _didMultitouch: boolean = false;
557
557
 
558
558
  private touchStart = (evt: TouchEvent) => {
559
559
  for (let i = 0; i < evt.changedTouches.length; i++) {
560
560
  const touch = evt.changedTouches[i];
561
+ // if a user starts swiping in the top area of the screen
562
+ // which might be a gesture to open the menu
563
+ // we ignore it
564
+ const ignore = isAndroidDevice() && touch.clientY < window.innerHeight * .1;
561
565
  if (!this.prev.has(touch.identifier))
562
566
  this.prev.set(touch.identifier, {
567
+ ignore,
563
568
  x: 0,
564
569
  z: 0,
565
570
  screenx: 0,
@@ -579,6 +584,10 @@
579
584
  if (evt.touches.length <= 0) {
580
585
  this._didMultitouch = false;
581
586
  }
587
+ for (let i = 0; i < evt.changedTouches.length; i++) {
588
+ const touch = evt.changedTouches[i];
589
+ this.prev.delete(touch.identifier);
590
+ }
582
591
  }
583
592
  private touchMove = (evt: TouchEvent) => {
584
593
  if (evt.defaultPrevented) return;
@@ -593,7 +602,7 @@
593
602
  }
594
603
  const touch = evt.touches[0];
595
604
  const prev = this.prev.get(touch.identifier);
596
- if (!prev) return;
605
+ if (!prev || prev.ignore) return;
597
606
  const pos = this.getPositionOnPlane(touch.clientX, touch.clientY);
598
607
  const dx = pos.x - prev.x;
599
608
  const dy = pos.z - prev.z;
src/engine-components/webxr/WebXRButtons.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { isDevEnvironment } from "../../engine/debug/index.js";
2
- import { generateQRCode } from "../../engine/engine_utils.js";
3
2
  import { isMozillaXR } from "../../engine/engine_utils.js";
4
3
  import { NeedleXRSession } from "../../engine/engine_xr.js";
5
4
  import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
6
5
  import { getIconElement } from "../../engine/webcomponents/icons.js";
7
- import { onXRSessionStart } from "../../engine/xr/events.js";
6
+ import { onXRSessionEnd, onXRSessionStart } from "../../engine/xr/events.js";
8
7
  import { GameObject } from "../Component.js";
9
8
  import { USDZExporter } from "../export/usdz/USDZExporter.js";
10
9
 
@@ -62,7 +61,7 @@
62
61
  const usdzExporter = GameObject.findObjectOfType(USDZExporter);
63
62
  if (usdzExporter) {
64
63
  button.classList.add("this-mode-is-requested");
65
- usdzExporter.exportAsync().then(() => {
64
+ usdzExporter.exportAndOpen().then(() => {
66
65
  button.classList.remove("this-mode-is-requested");
67
66
  }).catch(err => {
68
67
  button.classList.remove("this-mode-is-requested");
@@ -198,13 +197,10 @@
198
197
 
199
198
  private hideElementDuringXRSession(element: HTMLElement) {
200
199
  onXRSessionStart(_ => {
201
- })
202
- onXRSessionStart
203
- NeedleXRSession.onXRSessionStart(_ => {
204
200
  element["previous-display"] = element.style.display;
205
201
  element.style.display = "none";
206
202
  });
207
- NeedleXRSession.onXRSessionEnd(_ => {
203
+ onXRSessionEnd(_ => {
208
204
  if (element["previous-display"] != undefined)
209
205
  element.style.display = element["previous-display"];
210
206
  });