@@ -1,3 +1,4 @@
|
|
1
1
|
|
2
2
|
|
3
|
+
export * from "./icons.js"
|
3
4
|
export { type NeedleMenuPostMessageModel } from "./needle menu/needle-menu.js"
|
@@ -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";
|
@@ -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
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import type { ICameraController } from "./engine_types.js";
|
4
4
|
|
5
5
|
|
6
|
-
const $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
|
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[
|
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[
|
32
|
+
return obj[autofit] !== false;
|
33
33
|
}
|
34
34
|
|
35
35
|
/** @internal */
|
36
36
|
export function setAutoFitEnabled(obj: Object3D, enabled: boolean): void {
|
37
|
-
obj[
|
37
|
+
obj[autofit] = enabled;
|
38
38
|
|
39
39
|
}
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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;
|
@@ -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";
|
@@ -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);
|
@@ -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
|
@@ -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
|
22
|
+
onExportObject?(object: Object3D, model: USDObject, context: USDZExporterContext);
|
11
23
|
onAfterSerialize?(context);
|
12
|
-
onAfterHierarchy?(context, writer
|
24
|
+
onAfterHierarchy?(context, writer: any);
|
13
25
|
}
|
@@ -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<
|
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<
|
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;
|
@@ -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
|
-
|
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: .
|
104
|
+
{ opacity: .5 },
|
98
105
|
{ opacity: 0 }
|
99
106
|
], {
|
100
107
|
duration: 3000,
|
@@ -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
|
}
|
@@ -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
|
}
|
@@ -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
|
-
|
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)
|
580
|
+
if (objects.length <= 0) {
|
581
|
+
console.warn("No objects to fit camera to...");
|
582
|
+
return;
|
583
|
+
}
|
568
584
|
|
569
|
-
if (!camera || !controls)
|
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
|
-
|
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(
|
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
|
}
|
@@ -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
|
-
|
1002
|
-
if (
|
1003
|
-
|
1004
|
-
|
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(
|
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 (!
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
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
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
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
|
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
|
-
|
1112
|
-
this.
|
1113
|
-
|
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.
|
1130
|
-
this.
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
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
|
1141
|
-
const
|
1142
|
-
|
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
|
|
@@ -24,7 +24,7 @@
|
|
24
24
|
|
25
25
|
private targetMesh?: Mesh;
|
26
26
|
|
27
|
-
|
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, {
|
@@ -516,7 +516,7 @@
|
|
516
516
|
|
517
517
|
}
|
518
518
|
|
519
|
-
async parse(
|
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
|
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' );
|
@@ -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
|
-
|
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.
|
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)
|
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
|
-
|
212
|
-
|
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
|
-
|
290
|
-
|
291
|
-
|
292
|
-
const renderer = GameObject.getComponent(
|
293
|
-
|
294
|
-
|
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: {
|
@@ -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;
|
@@ -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.
|
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
|
-
|
203
|
+
onXRSessionEnd(_ => {
|
208
204
|
if (element["previous-display"] != undefined)
|
209
205
|
element.style.display = element["previous-display"];
|
210
206
|
});
|