@@ -6,11 +6,11 @@
|
|
6
6
|
/**
|
7
7
|
* Injects needle asap script into the index.html for when the main needle engine bundle is still being downloaded
|
8
8
|
* @param {import('../types').userSettings} userSettings
|
9
|
-
* @returns {import('vite').Plugin}
|
9
|
+
* @returns {Promise<import('vite').Plugin | null>}
|
10
10
|
*/
|
11
11
|
export const needleAsap = async (command, config, userSettings) => {
|
12
12
|
|
13
|
-
if (userSettings.noAsap) return;
|
13
|
+
if (userSettings.noAsap) return null;
|
14
14
|
|
15
15
|
fixMainTs();
|
16
16
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
async function generatePoster() {
|
3
|
-
const {
|
3
|
+
const { screenshot2 } = await import("@needle-tools/engine");
|
4
4
|
|
5
5
|
try {
|
6
6
|
const needleEngine = document.querySelector("needle-engine");
|
@@ -17,8 +17,23 @@
|
|
17
17
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
18
18
|
|
19
19
|
const mimeType = "image/webp";
|
20
|
-
const dataUrl = screenshot(context, width, height, mimeType);
|
21
20
|
|
21
|
+
// We're reading back as a blob here because that's async, and doesn't seem
|
22
|
+
// to stress the GPU so much on memory-constrained devices.
|
23
|
+
const blob = await screenshot2({context, width, height, mimeType, type: "blob"});
|
24
|
+
|
25
|
+
// We can only send a DataURL, so we need to convert it back here.
|
26
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
27
|
+
const reader = new FileReader();
|
28
|
+
reader.onload = function() {
|
29
|
+
resolve(reader.result);
|
30
|
+
};
|
31
|
+
reader.onloadend = function() {
|
32
|
+
resolve(null);
|
33
|
+
};
|
34
|
+
reader.readAsDataURL(blob);
|
35
|
+
});
|
36
|
+
|
22
37
|
return dataUrl;
|
23
38
|
}
|
24
39
|
catch (e) {
|
@@ -83,19 +83,69 @@
|
|
83
83
|
apply: 'build',
|
84
84
|
enforce: "post",
|
85
85
|
config(viteConfig) {
|
86
|
-
//
|
87
|
-
|
88
|
-
|
86
|
+
// Move the gzip plugin after PWA bundling
|
87
|
+
let gzipPluginIndex = -1;
|
88
|
+
let pwaPluginIndex = -1;
|
89
|
+
let gzipPlugin = null;
|
90
|
+
if (viteConfig.plugins) {
|
91
|
+
for (let i = viteConfig.plugins.length-1; i >= 0; i--) {
|
89
92
|
const plugin = viteConfig.plugins[i];
|
90
|
-
if(plugin && "name" in plugin && plugin.name === "vite:compression") {
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
93
|
+
if (plugin && "name" in plugin && plugin.name === "vite:compression") {
|
94
|
+
gzipPluginIndex = i;
|
95
|
+
gzipPlugin = plugin;
|
96
|
+
}
|
97
|
+
if (plugin && "name" in plugin && plugin.name === "vite-plugin-pwa") {
|
98
|
+
pwaPluginIndex = i;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
if (gzipPluginIndex >= 0 && gzipPluginIndex < viteConfig.plugins.length - 1) {
|
102
|
+
console.warn("[needle:pwa] vite compression plugin detected after PWA plugin. Moving it after the PWA plugin to avoid errors. Move the entry manually in vite.config to remove this warning.");
|
103
|
+
const gzipPlugin = viteConfig.plugins.splice(gzipPluginIndex, 1)[0];
|
104
|
+
const pwaPlugin = viteConfig.plugins[pwaPluginIndex];
|
105
|
+
const beforePwa = viteConfig.plugins.slice(0, pwaPluginIndex + 1);
|
106
|
+
const afterPwa = viteConfig.plugins.slice(pwaPluginIndex + 1);
|
107
|
+
viteConfig.plugins = [...beforePwa, pwaPlugin, gzipPlugin, ...afterPwa];
|
108
|
+
}
|
109
|
+
|
110
|
+
// Also add a number of filters – we want to avoid gzipping for specific files.
|
111
|
+
// Note: this is current
|
112
|
+
/*
|
113
|
+
if (gzipPlugin) {
|
114
|
+
const filteredFiles = [
|
115
|
+
"sw.js",
|
116
|
+
"needle.buildinfo.json",
|
117
|
+
];
|
118
|
+
const method = gzipPlugin.filter;
|
119
|
+
if (!method) {
|
120
|
+
gzipPlugin.filter = (path) => {
|
121
|
+
console.log("PATH", path);
|
122
|
+
for (const file of filteredFiles) {
|
123
|
+
console.log("comparing ", path, "with", file);
|
124
|
+
if (path.endsWith(file)) return false;
|
125
|
+
}
|
126
|
+
return true;
|
96
127
|
}
|
97
128
|
}
|
129
|
+
else if (typeof method === "function") {
|
130
|
+
gzipPlugin.filter = (path) => {
|
131
|
+
for (const file of filteredFiles)
|
132
|
+
if (path.endsWith(file)) return false;
|
133
|
+
// check original function
|
134
|
+
return method(path);
|
135
|
+
}
|
136
|
+
}
|
137
|
+
else if (typeof method === "string") {
|
138
|
+
gzipPlugin.filter = (path) => {
|
139
|
+
for (const file of filteredFiles)
|
140
|
+
if (path.endsWith(file)) return false;
|
141
|
+
// check original regex
|
142
|
+
return !path.match(new RegExp(method));
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
console.log("[needle:pwa] Added filters to vite-plugin-compression to avoid gzipping service worker and build info files.", gzipPlugin);
|
98
147
|
}
|
148
|
+
*/
|
99
149
|
}
|
100
150
|
},
|
101
151
|
configResolved(config) {
|
@@ -454,6 +504,20 @@
|
|
454
504
|
function processWorkboxConfig(manifest) {
|
455
505
|
|
456
506
|
// Workaround: urlPattern, ignoreSearch und ignoreURLParametersMatching, dontCacheBustURLsMatching are because we currently append ?v=... to loaded GLB files
|
507
|
+
const externalResourceCaching = {
|
508
|
+
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
509
|
+
handler: 'CacheFirst',
|
510
|
+
options: {
|
511
|
+
cacheName: 'google-fonts-cache',
|
512
|
+
expiration: {
|
513
|
+
maxEntries: 10,
|
514
|
+
maxAgeSeconds: 60 * 60 * 24 * 365 // <== 365 days
|
515
|
+
},
|
516
|
+
cacheableResponse: {
|
517
|
+
statuses: [0, 200]
|
518
|
+
}
|
519
|
+
}
|
520
|
+
};
|
457
521
|
|
458
522
|
// this is our default config
|
459
523
|
/** @type {Partial<import("workbox-build").GenerateSWOptions>} */
|
@@ -465,7 +529,47 @@
|
|
465
529
|
maximumFileSizeToCacheInBytes: 50000000,
|
466
530
|
dontCacheBustURLsMatching: /\.[a-f0-9]{8}\./,
|
467
531
|
ignoreURLParametersMatching: [/.*/],
|
532
|
+
additionalManifestEntries: [
|
533
|
+
// Profile list
|
534
|
+
{ url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/profilesList.json", revision: "1" },
|
535
|
+
// Quest 2
|
536
|
+
{ url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/oculus-touch-v3/profile.json", revision: "1" },
|
537
|
+
{ url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/oculus-touch-v3/left.glb", revision: "1" },
|
538
|
+
{ url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/oculus-touch-v3/right.glb", revision: "1" },
|
539
|
+
// Quest 3
|
540
|
+
{ url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/meta-quest-touch-plus/profile.json", revision: "1" },
|
541
|
+
{ url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/meta-quest-touch-plus/left.glb", revision: "1" },
|
542
|
+
{ url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/meta-quest-touch-plus/right.glb", revision: "1" },
|
543
|
+
// Hand tracking
|
544
|
+
{ url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/generic-hand/profile.json", revision: "1" },
|
545
|
+
{ url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/generic-hand/left.glb", revision: "1" },
|
546
|
+
{ url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/generic-hand/right.glb", revision: "1" },
|
547
|
+
],
|
468
548
|
runtimeCaching: [
|
549
|
+
// allow caching Google Fonts
|
550
|
+
{...externalResourceCaching, ...{
|
551
|
+
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
552
|
+
options: { cacheName: 'google-fonts-cache' },
|
553
|
+
}},
|
554
|
+
// allow caching static resources from Google, like CSS
|
555
|
+
{...externalResourceCaching, ...{
|
556
|
+
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
557
|
+
options: { cacheName: 'gstatic-fonts-cache' },
|
558
|
+
}},
|
559
|
+
// allow caching Needle cdn resources
|
560
|
+
{...externalResourceCaching, ...{
|
561
|
+
urlPattern: /^https:\/\/cdn\.needle\.tools\/.*/i,
|
562
|
+
handler: 'NetworkFirst',
|
563
|
+
options: { cacheName: 'needle-cdn-cache' },
|
564
|
+
}},
|
565
|
+
// allow caching controller resources,
|
566
|
+
// https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/
|
567
|
+
{...externalResourceCaching, ...{
|
568
|
+
urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/npm\/@webxr-input-profiles\/assets@1\.0\/dist\/profiles\/.*/i,
|
569
|
+
handler: 'NetworkFirst',
|
570
|
+
options: { cacheName: 'webxr-controller-cache' },
|
571
|
+
}},
|
572
|
+
// allow caching local resources
|
469
573
|
{
|
470
574
|
urlPattern: ({ url }) => url,
|
471
575
|
// Apply a network-first strategy.
|
@@ -48,8 +48,8 @@
|
|
48
48
|
if (listener?.parent) return;
|
49
49
|
|
50
50
|
const cam = this.context.mainCameraComponent || GameObject.getComponentInParent(this.gameObject, Camera);
|
51
|
-
if (cam?.
|
52
|
-
cam.
|
51
|
+
if (cam?.threeCamera) {
|
52
|
+
cam.threeCamera.add(listener);
|
53
53
|
}
|
54
54
|
else {
|
55
55
|
this.gameObject.add(listener);
|
@@ -1,17 +1,16 @@
|
|
1
|
-
import {
|
1
|
+
import { AudioLoader, PositionalAudio } from "three";
|
2
2
|
import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
|
3
3
|
|
4
4
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
5
|
import { Application, ApplicationEvents } from "../engine/engine_application.js";
|
6
6
|
import { Mathf } from "../engine/engine_math.js";
|
7
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
-
import {
|
9
|
-
import * as utils from "../engine/engine_utils.js";
|
8
|
+
import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
|
10
9
|
import { AudioListener } from "./AudioListener.js";
|
11
10
|
import { Behaviour, GameObject } from "./Component.js";
|
12
11
|
|
13
12
|
|
14
|
-
const debug =
|
13
|
+
const debug = getParam("debugaudio");
|
15
14
|
|
16
15
|
/**
|
17
16
|
* The AudioRolloffMode enum describes different ways that audio can attenuate with distance.
|
@@ -207,7 +206,7 @@
|
|
207
206
|
public get Sound(): PositionalAudio | null {
|
208
207
|
if (!this.sound && AudioSource.userInteractionRegistered) {
|
209
208
|
let listener = GameObject.getComponent(this.context.mainCamera, AudioListener) ?? GameObject.findObjectOfType(AudioListener, this.context);
|
210
|
-
if (!listener && this.context.mainCamera) listener = GameObject.
|
209
|
+
if (!listener && this.context.mainCamera) listener = GameObject.addComponent(this.context.mainCamera, AudioListener);
|
211
210
|
if (listener?.listener) {
|
212
211
|
this.sound = new PositionalAudio(listener.listener);
|
213
212
|
this.gameObject?.add(this.sound);
|
@@ -298,7 +297,7 @@
|
|
298
297
|
private onVisibilityChanged = () => {
|
299
298
|
switch (document.visibilityState) {
|
300
299
|
case "hidden":
|
301
|
-
if (this.playInBackground === false ||
|
300
|
+
if (this.playInBackground === false || DeviceUtilities.isMobileDevice()) {
|
302
301
|
this.wasPlaying = this.isPlaying;
|
303
302
|
if (this.isPlaying) {
|
304
303
|
this.pause();
|
@@ -25,7 +25,7 @@
|
|
25
25
|
if (!obj) return;
|
26
26
|
if (!obj.getComponentInParent(Raycaster)) {
|
27
27
|
if (isDevEnvironment()) console.warn("Create Raycaster on " + obj.name + " because no raycaster was found in the hierarchy")
|
28
|
-
obj.
|
28
|
+
obj.addComponent(ObjectRaycaster);
|
29
29
|
}
|
30
30
|
}
|
31
31
|
|
@@ -667,7 +667,7 @@
|
|
667
667
|
|
668
668
|
ensureAudioSource() {
|
669
669
|
if (!this.target) {
|
670
|
-
const newAudioSource = this.gameObject.
|
670
|
+
const newAudioSource = this.gameObject.addComponent(AudioSource);
|
671
671
|
if (newAudioSource) {
|
672
672
|
this.target = newAudioSource;
|
673
673
|
newAudioSource.spatialBlend = 1;
|
@@ -801,6 +801,7 @@
|
|
801
801
|
|
802
802
|
private animationSequence? = new Array<RegisteredAnimationInfo>();
|
803
803
|
private animationLoopAfterSequence? = new Array<RegisteredAnimationInfo>();
|
804
|
+
private randomOffsetNormalized: number = 0;
|
804
805
|
|
805
806
|
createBehaviours(_ext: BehaviorExtension, model: USDObject, _context: USDZExporterContext) {
|
806
807
|
|
@@ -852,7 +853,8 @@
|
|
852
853
|
const sequence = PlayAnimationOnClick.getActionForSequences(
|
853
854
|
model,
|
854
855
|
this.animationSequence,
|
855
|
-
this.animationLoopAfterSequence
|
856
|
+
this.animationLoopAfterSequence,
|
857
|
+
this.randomOffsetNormalized,
|
856
858
|
);
|
857
859
|
|
858
860
|
const playAnimationOnTap = new BehaviorModel(this.trigger + "_" + behaviorName + "_toPlayAnimation_" + this.stateName + "_on_" + this.target?.name,
|
@@ -868,7 +870,7 @@
|
|
868
870
|
});
|
869
871
|
}
|
870
872
|
|
871
|
-
static getActionForSequences(model: USDObject, animationSequence?: Array<RegisteredAnimationInfo>, animationLoopAfterSequence?: Array<RegisteredAnimationInfo
|
873
|
+
static getActionForSequences(model: USDObject, animationSequence?: Array<RegisteredAnimationInfo>, animationLoopAfterSequence?: Array<RegisteredAnimationInfo>, randomOffsetNormalized?: number) {
|
872
874
|
|
873
875
|
const getOrCacheAction = (model: USDObject, anim: RegisteredAnimationInfo) => {
|
874
876
|
let action = PlayAnimationOnClick.animationActions.find(a => a.affectedObjects == model && a.start == anim.start && a.duration == anim.duration && a.animationSpeed == anim.speed);
|
@@ -899,11 +901,19 @@
|
|
899
901
|
sequence.addAction(loopSequence);
|
900
902
|
}
|
901
903
|
|
904
|
+
if (randomOffsetNormalized && randomOffsetNormalized > 0) {
|
905
|
+
sequence.actions.unshift(ActionBuilder.waitAction(randomOffsetNormalized));
|
906
|
+
}
|
907
|
+
|
902
908
|
return sequence;
|
903
909
|
}
|
904
910
|
|
905
911
|
static getAndRegisterAnimationSequences(ext: AnimationExtension, target: GameObject, stateName?: string):
|
906
|
-
{
|
912
|
+
{
|
913
|
+
animationSequence: Array<RegisteredAnimationInfo>,
|
914
|
+
animationLoopAfterSequence: Array<RegisteredAnimationInfo>,
|
915
|
+
randomTimeOffset: number,
|
916
|
+
} | undefined {
|
907
917
|
|
908
918
|
if (!target) return undefined;
|
909
919
|
|
@@ -927,9 +937,18 @@
|
|
927
937
|
else
|
928
938
|
animationSequence.push(anim);
|
929
939
|
}
|
940
|
+
|
941
|
+
let randomTimeOffset = 0;
|
942
|
+
if (animation.minMaxOffsetNormalized) {
|
943
|
+
const from = animation.minMaxOffsetNormalized.x;
|
944
|
+
const to = animation.minMaxOffsetNormalized.y;
|
945
|
+
randomTimeOffset = (animation.clip?.duration || 1) * (from + Math.random() * (to - from));
|
946
|
+
}
|
947
|
+
|
930
948
|
return {
|
931
949
|
animationSequence,
|
932
|
-
animationLoopAfterSequence
|
950
|
+
animationLoopAfterSequence,
|
951
|
+
randomTimeOffset,
|
933
952
|
}
|
934
953
|
}
|
935
954
|
|
@@ -1041,9 +1060,18 @@
|
|
1041
1060
|
}
|
1042
1061
|
}
|
1043
1062
|
|
1063
|
+
let randomTimeOffset = 0;
|
1064
|
+
if (animator && runtimeController && animator.minMaxOffsetNormalized) {
|
1065
|
+
const from = animator.minMaxOffsetNormalized.x;
|
1066
|
+
const to = animator.minMaxOffsetNormalized.y;
|
1067
|
+
// first state in the sequence
|
1068
|
+
const firstState = statesUntilLoop.length ? statesUntilLoop[0] : statesLooping.length ? statesLooping[0] : null;
|
1069
|
+
randomTimeOffset = (firstState?.motion.clip?.duration || 1) * (from + Math.random() * (to - from));
|
1070
|
+
}
|
1044
1071
|
return {
|
1045
1072
|
animationSequence,
|
1046
|
-
animationLoopAfterSequence
|
1073
|
+
animationLoopAfterSequence,
|
1074
|
+
randomTimeOffset,
|
1047
1075
|
}
|
1048
1076
|
}
|
1049
1077
|
|
@@ -1056,6 +1084,7 @@
|
|
1056
1084
|
|
1057
1085
|
this.animationSequence = result.animationSequence;
|
1058
1086
|
this.animationLoopAfterSequence = result.animationLoopAfterSequence;
|
1087
|
+
this.randomOffsetNormalized = result.randomTimeOffset;
|
1059
1088
|
|
1060
1089
|
this.stateAnimationModel = model;
|
1061
1090
|
}
|
@@ -219,7 +219,7 @@
|
|
219
219
|
yield;
|
220
220
|
yield;
|
221
221
|
if (this._requestedAnimatorTrigger == requestedTriggerId) {
|
222
|
-
this.animator?.
|
222
|
+
this.animator?.setTrigger(requestedTriggerId);
|
223
223
|
}
|
224
224
|
}
|
225
225
|
|
@@ -1,12 +1,12 @@
|
|
1
1
|
import { EquirectangularReflectionMapping, Euler, Frustum, Matrix4, OrthographicCamera, PerspectiveCamera, Ray, Vector3 } from "three";
|
2
2
|
import { Texture } from "three";
|
3
3
|
|
4
|
-
import {
|
4
|
+
import { showBalloonMessage } from "../engine/debug/index.js";
|
5
5
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
6
6
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
7
|
import { Context } from "../engine/engine_setup.js";
|
8
8
|
import { RenderTexture } from "../engine/engine_texture.js";
|
9
|
-
import { getTempColor,
|
9
|
+
import { getTempColor, getWorldPosition } from "../engine/engine_three_utils.js";
|
10
10
|
import type { ICamera } from "../engine/engine_types.js"
|
11
11
|
import { getParam } from "../engine/engine_utils.js";
|
12
12
|
import { RGBAColor } from "../engine/js-extensions/index.js";
|
@@ -260,7 +260,7 @@
|
|
260
260
|
private static _origin: Vector3 = new Vector3();
|
261
261
|
private static _direction: Vector3 = new Vector3();
|
262
262
|
public screenPointToRay(x: number, y: number, ray?: Ray): Ray {
|
263
|
-
const cam = this.
|
263
|
+
const cam = this.threeCamera;
|
264
264
|
const origin = Camera._origin;
|
265
265
|
origin.set(x, y, -1);
|
266
266
|
this.context.input.convertScreenspaceToRaycastSpace(origin);
|
@@ -302,7 +302,7 @@
|
|
302
302
|
*/
|
303
303
|
public getProjectionScreenMatrix(target: Matrix4, forceUpdate?: boolean) {
|
304
304
|
if (forceUpdate) {
|
305
|
-
this._projScreenMatrix.multiplyMatrices(this.
|
305
|
+
this._projScreenMatrix.multiplyMatrices(this.threeCamera.projectionMatrix, this.threeCamera.matrixWorldInverse);
|
306
306
|
}
|
307
307
|
if (target === this._projScreenMatrix) return target;
|
308
308
|
return target.copy(this._projScreenMatrix);
|
@@ -8,7 +8,7 @@
|
|
8
8
|
import { getParam } from "../engine/engine_utils.js";
|
9
9
|
import { Animator } from "./Animator.js"
|
10
10
|
import { CapsuleCollider } from "./Collider.js";
|
11
|
-
import { Behaviour
|
11
|
+
import { Behaviour } from "./Component.js";
|
12
12
|
import { Rigidbody } from "./RigidBody.js";
|
13
13
|
|
14
14
|
const debug = getParam("debugcharactercontroller");
|
@@ -30,7 +30,7 @@
|
|
30
30
|
if (this._rigidbody) return this._rigidbody;
|
31
31
|
this._rigidbody = this.gameObject.getComponent(Rigidbody);
|
32
32
|
if (!this._rigidbody)
|
33
|
-
this._rigidbody = this.gameObject.
|
33
|
+
this._rigidbody = this.gameObject.addComponent(Rigidbody) as Rigidbody;
|
34
34
|
return this.rigidbody;
|
35
35
|
}
|
36
36
|
|
@@ -44,7 +44,7 @@
|
|
44
44
|
const rb = this.rigidbody;
|
45
45
|
let collider = this.gameObject.getComponent(CapsuleCollider);
|
46
46
|
if (!collider)
|
47
|
-
collider = this.gameObject.
|
47
|
+
collider = this.gameObject.addComponent(CapsuleCollider) as CapsuleCollider;
|
48
48
|
|
49
49
|
collider.center.copy(this.center);
|
50
50
|
collider.radius = this.radius;
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
-
import { addComponent,
|
4
|
+
import { addComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../engine/engine_components.js";
|
5
5
|
import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
|
6
6
|
import { destroy, findByGuid, foreachComponent, HideFlags, type IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
|
7
7
|
import * as main from "../engine/engine_mainloop_utils.js";
|
@@ -33,10 +33,8 @@
|
|
33
33
|
// these are implemented via threejs object extensions
|
34
34
|
abstract activeSelf: boolean;
|
35
35
|
|
36
|
-
// The actual implementation / prototype of threejs is modified in js-extensions/Object3D
|
37
|
-
abstract get transform(): GameObject;
|
38
|
-
|
39
36
|
/** @deprecated use `addComponent` */
|
37
|
+
// eslint-disable-next-line deprecation/deprecation
|
40
38
|
abstract addNewComponent<T extends IComponent>(type: ConstructorConcrete<T>, init?: ComponentInit<T>): T;
|
41
39
|
/** creates a new component on this gameObject */
|
42
40
|
abstract addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>): T;
|
@@ -209,7 +207,8 @@
|
|
209
207
|
}, children);
|
210
208
|
}
|
211
209
|
|
212
|
-
/** @deprecated
|
210
|
+
/** @deprecated use `addComponent` */
|
211
|
+
// eslint-disable-next-line deprecation/deprecation
|
213
212
|
public static addNewComponent<T extends IComponent>(go: IGameObject | Object3D, type: T | ConstructorConcrete<T>, init?: ComponentInit<T>, callAwake: boolean = true): T {
|
214
213
|
return addComponent(go, type, init, { callAwake });
|
215
214
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { isLocalNetwork } from "../engine_networking_utils.js";
|
2
|
-
import { getParam
|
2
|
+
import { DeviceUtilities,getParam } from "../engine_utils.js";
|
3
3
|
import { isDevEnvironment } from "./debug.js";
|
4
4
|
import { getErrorCount, makeErrorsVisibleForDevelopment } from "./debug_overlay.js";
|
5
5
|
|
@@ -23,7 +23,7 @@
|
|
23
23
|
consoleUrl.searchParams.set("console", "1");
|
24
24
|
console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development. In VR a spatial console will appear.)", "\nOpen this page to get the console: " + consoleUrl.toString());
|
25
25
|
}
|
26
|
-
const enableConsole = isMobileDevice() || (isQuest() && isDevEnvironment());
|
26
|
+
const enableConsole = DeviceUtilities.isMobileDevice() || (DeviceUtilities.isQuest() && isDevEnvironment());
|
27
27
|
if (enableConsole || showConsole) {
|
28
28
|
// we need to invoke this here - otherwise we will miss errors that happen after the console is loaded
|
29
29
|
// and calling the method from the root needle-engine.ts import is evaluated later (if we import the method from the toplevel file and then invoke it)
|
@@ -8,6 +8,7 @@
|
|
8
8
|
clearMessages as clearOverlayMessages,
|
9
9
|
LogType,
|
10
10
|
setAllowBalloonMessages,
|
11
|
+
// eslint-disable-next-line deprecation/deprecation
|
11
12
|
setAllowOverlayMessages,
|
12
13
|
};
|
13
14
|
export { enableSpatialConsole } from "./debug_spatial_console.js";
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
|
-
import {
|
3
|
+
import { DeviceUtilities } from "../engine/engine_utils.js";
|
4
4
|
import { Behaviour, GameObject } from "./Component.js";
|
5
5
|
|
6
6
|
|
@@ -31,7 +31,7 @@
|
|
31
31
|
|
32
32
|
private test() : boolean {
|
33
33
|
if(this.visibleOn < 0) return true;
|
34
|
-
if(isMobileDevice()){
|
34
|
+
if(DeviceUtilities.isMobileDevice()) {
|
35
35
|
return (this.visibleOn & (DeviceType.Mobile)) !== 0;
|
36
36
|
}
|
37
37
|
const allowDesktop = (this.visibleOn & (DeviceType.Desktop)) !== 0;
|
@@ -42,5 +42,5 @@
|
|
42
42
|
|
43
43
|
/**@deprecated use isMobileDevice() */
|
44
44
|
function isMobile() {
|
45
|
-
return isMobileDevice();
|
45
|
+
return DeviceUtilities.isMobileDevice();
|
46
46
|
};
|
@@ -1,11 +1,11 @@
|
|
1
1
|
import { Group, Object3D, Texture, TextureLoader } from "three";
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { getParam, resolveUrl } from "../engine/engine_utils.js";
|
4
4
|
import { destroy, type IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
5
5
|
import { getLoader } from "./engine_gltf.js";
|
6
6
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
7
7
|
import { registerPrefabProvider, syncInstantiate,SyncInstantiateOptions } from "./engine_networking_instantiate.js";
|
8
|
-
import {
|
8
|
+
import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
|
9
9
|
import { Context } from "./engine_setup.js";
|
10
10
|
import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
|
11
11
|
import { download } from "./engine_web_api.js";
|
@@ -57,9 +57,9 @@
|
|
57
57
|
* @internal
|
58
58
|
*/
|
59
59
|
registerAssetReference(ref: AssetReference): AssetReference {
|
60
|
-
if (!ref.
|
61
|
-
if (!this._assetReferences[ref.
|
62
|
-
this._assetReferences[ref.
|
60
|
+
if (!ref.url) return ref;
|
61
|
+
if (!this._assetReferences[ref.url]) {
|
62
|
+
this._assetReferences[ref.url] = ref;
|
63
63
|
}
|
64
64
|
else {
|
65
65
|
|
@@ -70,8 +70,8 @@
|
|
70
70
|
|
71
71
|
/** @internal */
|
72
72
|
unregisterAssetReference(ref: AssetReference) {
|
73
|
-
if (!ref.
|
74
|
-
delete this._assetReferences[ref.
|
73
|
+
if (!ref.url) return;
|
74
|
+
delete this._assetReferences[ref.url];
|
75
75
|
}
|
76
76
|
}
|
77
77
|
|
@@ -198,7 +198,7 @@
|
|
198
198
|
}
|
199
199
|
|
200
200
|
private async onResolvePrefab(url: string): Promise<IGameObject | null> {
|
201
|
-
if (url === this.
|
201
|
+
if (url === this.url) {
|
202
202
|
if (this.mustLoad) await this.loadAssetAsync();
|
203
203
|
if (this.asset) {
|
204
204
|
return this.asset;
|
@@ -254,7 +254,7 @@
|
|
254
254
|
*/
|
255
255
|
async loadAssetAsync(prog?: ProgressCallback | null) {
|
256
256
|
if (debug)
|
257
|
-
console.log("loadAssetAsync", this.
|
257
|
+
console.log("loadAssetAsync", this.url);
|
258
258
|
if (!this.mustLoad) return this.asset;
|
259
259
|
if (prog)
|
260
260
|
this._progressListeners.push(prog);
|
@@ -268,12 +268,12 @@
|
|
268
268
|
// we should "address" (LUL) this
|
269
269
|
// console.log("START LOADING");
|
270
270
|
if (this._rawBinary) {
|
271
|
-
this._loading = getLoader().parseSync(context, this._rawBinary, this.
|
271
|
+
this._loading = getLoader().parseSync(context, this._rawBinary, this.url, null);
|
272
272
|
this.raiseProgressEvent(new ProgressEvent("progress", { loaded: this._rawBinary.byteLength, total: this._rawBinary.byteLength }));
|
273
273
|
}
|
274
274
|
else {
|
275
|
-
if (debug) console.log("Load async", this.
|
276
|
-
this._loading = getLoader().loadSync(context, this._hashedUri, this.
|
275
|
+
if (debug) console.log("Load async", this.url);
|
276
|
+
this._loading = getLoader().loadSync(context, this._hashedUri, this.url, null, prog => {
|
277
277
|
this.raiseProgressEvent(prog);
|
278
278
|
});
|
279
279
|
}
|
@@ -353,25 +353,25 @@
|
|
353
353
|
await this.loadAssetAsync();
|
354
354
|
}
|
355
355
|
if (debug)
|
356
|
-
console.log("Instantiate", this.
|
356
|
+
console.log("Instantiate", this.url, "parent:", opts);
|
357
357
|
|
358
358
|
if (this.asset) {
|
359
359
|
if (debug) console.log("Add to scene", this.asset);
|
360
360
|
|
361
|
-
let count = AssetReference.currentlyInstantiating.get(this.
|
361
|
+
let count = AssetReference.currentlyInstantiating.get(this.url);
|
362
362
|
// allow up to 10000 instantiations of the same prefab in the same frame
|
363
363
|
if (count !== undefined && count >= 10000) {
|
364
|
-
console.error("Recursive or too many instantiations of " + this.
|
364
|
+
console.error("Recursive or too many instantiations of " + this.url + " in the same frame (" + count + ")");
|
365
365
|
return null;
|
366
366
|
}
|
367
367
|
try {
|
368
368
|
if (count === undefined) count = 0;
|
369
369
|
count += 1;
|
370
|
-
AssetReference.currentlyInstantiating.set(this.
|
370
|
+
AssetReference.currentlyInstantiating.set(this.url, count);
|
371
371
|
if (networked) {
|
372
372
|
options.context = context;
|
373
373
|
const prefab = this.asset;
|
374
|
-
prefab.guid = this.
|
374
|
+
prefab.guid = this.url;
|
375
375
|
const instance = syncInstantiate(prefab, options, undefined, saveOnServer);
|
376
376
|
if (instance) {
|
377
377
|
return instance;
|
@@ -388,12 +388,12 @@
|
|
388
388
|
context.post_render_callbacks.push(() => {
|
389
389
|
if (count === undefined || count < 0) count = 0;
|
390
390
|
else count -= 1;
|
391
|
-
AssetReference.currentlyInstantiating.set(this.
|
391
|
+
AssetReference.currentlyInstantiating.set(this.url, count)
|
392
392
|
});
|
393
393
|
}
|
394
394
|
|
395
395
|
}
|
396
|
-
else if (debug) console.warn("Failed to load asset", this.
|
396
|
+
else if (debug) console.warn("Failed to load asset", this.url);
|
397
397
|
return null;
|
398
398
|
}
|
399
399
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { isDevEnvironment } from "./debug/
|
1
|
+
import { isDevEnvironment } from "./debug/index.js";
|
2
2
|
import { Context } from "./engine_setup.js";
|
3
3
|
import { NeedleXRSession } from "./engine_xr.js";
|
4
4
|
|
@@ -6,7 +6,7 @@
|
|
6
6
|
* @internal
|
7
7
|
* Ensure the audio context is resumed if it gets suspended or interrupted */
|
8
8
|
export function ensureAudioContextIsResumed() {
|
9
|
-
Application.
|
9
|
+
Application.registerWaitForInteraction(() => {
|
10
10
|
// this is a fix for https://github.com/mrdoob/three.js/issues/27779 & https://linear.app/needle/issue/NE-4257
|
11
11
|
const ctx = AudioContext.getContext();
|
12
12
|
ctx.addEventListener("statechange", () => {
|
@@ -31,17 +31,16 @@
|
|
31
31
|
import { Time } from './engine_time.js';
|
32
32
|
import { patchTonemapping } from './engine_tonemapping.js';
|
33
33
|
import type { CoroutineData, GLTF, ICamera, IComponent, IContext, ILight, LoadedGLTF, Vec2 } from "./engine_types.js";
|
34
|
-
import
|
35
|
-
import { delay, getParam } from './engine_utils.js';
|
34
|
+
import { deepClone,delay, DeviceUtilities, getParam } from './engine_utils.js';
|
36
35
|
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
|
37
36
|
import { NeedleMenu } from './webcomponents/needle menu/needle-menu.js';
|
38
37
|
|
39
38
|
|
40
|
-
const debug =
|
41
|
-
const stats =
|
42
|
-
const debugActive =
|
43
|
-
const debugframerate =
|
44
|
-
const debugCoroutine =
|
39
|
+
const debug = getParam("debugcontext");
|
40
|
+
const stats = getParam("stats");
|
41
|
+
const debugActive = getParam("debugactive");
|
42
|
+
const debugframerate = getParam("debugframerate");
|
43
|
+
const debugCoroutine = getParam("debugcoroutine");
|
45
44
|
|
46
45
|
// this is where functions that setup unity scenes will be pushed into
|
47
46
|
// those will be accessed from our custom html element to load them into their context
|
@@ -128,11 +127,12 @@
|
|
128
127
|
* @example
|
129
128
|
* ```typescript
|
130
129
|
* import { Behaviour } from "@needle-tools/engine";
|
130
|
+
* import { Mesh, BoxGeometry, MeshBasicMaterial } from "three";
|
131
131
|
* export class MyScript extends Behaviour {
|
132
|
-
*
|
133
|
-
*
|
134
|
-
*
|
135
|
-
*
|
132
|
+
* start() {
|
133
|
+
* console.log("Hello from MyScript");
|
134
|
+
* this.context.scene.add(new Mesh(new BoxGeometry(), new MeshBasicMaterial()));
|
135
|
+
* }
|
136
136
|
* }
|
137
137
|
* ```
|
138
138
|
*/
|
@@ -152,7 +152,7 @@
|
|
152
152
|
antialias: true,
|
153
153
|
alpha: false,
|
154
154
|
// Note: this is due to a bug on OSX devices. See NE-5370
|
155
|
-
powerPreference: (
|
155
|
+
powerPreference: (DeviceUtilities.isiOS() || DeviceUtilities.isMacOS()) ? "default" : "high-performance",
|
156
156
|
};
|
157
157
|
/** The default parameters that will be used when creating a new WebGLRenderer.
|
158
158
|
* Modify in global context to change the default parameters for all new contexts.
|
@@ -354,9 +354,9 @@
|
|
354
354
|
}
|
355
355
|
if (this.mainCameraComponent) {
|
356
356
|
const cam = this.mainCameraComponent as ICamera;
|
357
|
-
if (!cam.
|
357
|
+
if (!cam.threeCamera)
|
358
358
|
cam.buildCamera();
|
359
|
-
return cam.
|
359
|
+
return cam.threeCamera;
|
360
360
|
}
|
361
361
|
if (!this._fallbackCamera) {
|
362
362
|
this._fallbackCamera = new PerspectiveCamera(75, this.domWidth / this.domHeight, 0.1, 1000);
|
@@ -423,6 +423,7 @@
|
|
423
423
|
this.input = new Input(this);
|
424
424
|
this.physics = new Physics(this);
|
425
425
|
this.connection = new NetworkConnection(this);
|
426
|
+
// eslint-disable-next-line deprecation/deprecation
|
426
427
|
this.assets = new AssetDatabase();
|
427
428
|
this.sceneLighting = new SceneLighting(this);
|
428
429
|
this.addressables = new Addressables(this);
|
@@ -563,7 +564,7 @@
|
|
563
564
|
try {
|
564
565
|
this._isCreating = true;
|
565
566
|
if (opts !== this._originalCreationArgs)
|
566
|
-
this._originalCreationArgs =
|
567
|
+
this._originalCreationArgs = deepClone(opts);
|
567
568
|
window.addEventListener("unhandledrejection", this.onUnhandledRejection)
|
568
569
|
const res = await this.internalOnCreate(opts);
|
569
570
|
this._isCreated = res;
|
@@ -687,8 +688,8 @@
|
|
687
688
|
|
688
689
|
setCurrentCamera(cam: ICamera) {
|
689
690
|
if (!cam) return;
|
690
|
-
if (!cam.
|
691
|
-
if (!cam.
|
691
|
+
if (!cam.threeCamera) cam.buildCamera(); // < to build camera
|
692
|
+
if (!cam.threeCamera) {
|
692
693
|
console.warn("Camera component is missing camera", cam)
|
693
694
|
return;
|
694
695
|
}
|
@@ -696,7 +697,7 @@
|
|
696
697
|
if (index >= 0) this._cameraStack.splice(index, 1);
|
697
698
|
this._cameraStack.push(cam);
|
698
699
|
this.mainCameraComponent = cam;
|
699
|
-
const camera = cam.
|
700
|
+
const camera = cam.threeCamera as PerspectiveCamera;
|
700
701
|
if (camera.isPerspectiveCamera)
|
701
702
|
this.updateAspect(camera);
|
702
703
|
(this.mainCameraComponent as ICamera)?.applyClearFlagsIfIsActiveCamera();
|
@@ -1412,7 +1413,7 @@
|
|
1412
1413
|
else if (camera) {
|
1413
1414
|
// Workaround for issue on Vision Pro –
|
1414
1415
|
// depth buffer is not cleared between eye draws, despite the spec...
|
1415
|
-
if (this.isInXR &&
|
1416
|
+
if (this.isInXR && DeviceUtilities.isMacOS())
|
1416
1417
|
this.renderer.clearDepth();
|
1417
1418
|
this.renderer.render(this.scene, camera);
|
1418
1419
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Context } from "./engine_setup.js";
|
2
|
-
import { getParam
|
2
|
+
import { DeviceUtilities,getParam } from "./engine_utils.js";
|
3
3
|
|
4
4
|
const debug = getParam("debugoverlay");
|
5
5
|
export const arContainerClassName = "ar";
|
@@ -28,7 +28,7 @@
|
|
28
28
|
this.currentSession = session;
|
29
29
|
this.arContainer = overlayContainer;
|
30
30
|
|
31
|
-
if (isMozillaXR()) {
|
31
|
+
if (DeviceUtilities.isMozillaXR()) {
|
32
32
|
const arElements = context.domElement!.children;
|
33
33
|
for (let i = 0; i < arElements?.length; i++) {
|
34
34
|
const el = arElements[i];
|
@@ -73,7 +73,7 @@
|
|
73
73
|
this._reparentedObjects.length = 0;
|
74
74
|
|
75
75
|
// mozilla XR exit AR fixes
|
76
|
-
if (isMozillaXR()) {
|
76
|
+
if (DeviceUtilities.isMozillaXR()) {
|
77
77
|
// without the timeout we get errors in mozillas code and can not enter XR again
|
78
78
|
// not sure why we have to wait
|
79
79
|
setTimeout(() => {
|
@@ -107,7 +107,7 @@
|
|
107
107
|
|
108
108
|
const overlaySlot = needleEngineElement.shadowRoot!.querySelector(".overlay-content");
|
109
109
|
if (overlaySlot) contentElement.appendChild(overlaySlot);
|
110
|
-
if (debug && !isMobileDevice()) this.ensureQuitARButton(contentElement);
|
110
|
+
if (debug && !DeviceUtilities.isMobileDevice()) this.ensureQuitARButton(contentElement);
|
111
111
|
return contentElement;
|
112
112
|
}
|
113
113
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { Context } from './engine_setup.js';
|
5
5
|
import { getTempVector, getWorldQuaternion } from './engine_three_utils.js';
|
6
6
|
import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
|
7
|
-
import { type EnumToPrimitiveUnion, getParam
|
7
|
+
import { DeviceUtilities, type EnumToPrimitiveUnion, getParam } from './engine_utils.js';
|
8
8
|
|
9
9
|
const debug = getParam("debuginput");
|
10
10
|
|
@@ -794,7 +794,7 @@
|
|
794
794
|
}
|
795
795
|
|
796
796
|
// looks like in Mozilla WebXR viewer the target element is the body
|
797
|
-
if (this.context.isInAR && evt.target === document.body && isMozillaXR()) return true;
|
797
|
+
if (this.context.isInAR && evt.target === document.body && DeviceUtilities.isMozillaXR()) return true;
|
798
798
|
|
799
799
|
if (debug) console.warn("CanReceiveInput:False for", evt.target);
|
800
800
|
return false;
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { isDevEnvironment } from "./debug/index.js";
|
2
|
-
import { RoomEvents } from "./engine_networking.js";
|
3
2
|
import { INetworkConnection, SendQueue } from "./engine_networking_types.js";
|
4
3
|
import type { IComponent } from "./engine_types.js";
|
5
4
|
import { getParam } from "./engine_utils.js";
|
@@ -1,11 +1,10 @@
|
|
1
|
-
// import { IModel, NetworkConnection } from "./engine_networking.js"
|
2
|
-
import * as THREE from "three";
|
3
1
|
import { Object3D, Quaternion, Vector3 } from "three";
|
4
2
|
// https://github.com/uuidjs/uuid
|
5
3
|
// v5 takes string and namespace
|
6
4
|
import { v5 } from 'uuid';
|
7
5
|
|
8
6
|
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
7
|
+
import { isDevEnvironment } from "./debug/index.js";
|
9
8
|
import { destroy, findByGuid, type IInstantiateOptions, instantiate } from "./engine_gameobject.js";
|
10
9
|
import { InstantiateOptions } from "./engine_gameobject.js";
|
11
10
|
import type { INetworkConnection } from "./engine_networking_types.js";
|
@@ -127,7 +126,8 @@
|
|
127
126
|
}
|
128
127
|
|
129
128
|
if (!con.isConnected) {
|
130
|
-
|
129
|
+
if (isDevEnvironment())
|
130
|
+
console.debug("Can not send destroy: not connected", obj.guid);
|
131
131
|
return;
|
132
132
|
}
|
133
133
|
|
@@ -261,6 +261,9 @@
|
|
261
261
|
}
|
262
262
|
model.hostData = hostData;
|
263
263
|
if (save === false) model.dontSave = true;
|
264
|
+
const con = opts?.context?.connection;
|
265
|
+
if (!con && isDevEnvironment())
|
266
|
+
console.debug("Object will be instantiated but it will not be synced: not connected", obj.guid);
|
264
267
|
opts?.context?.connection.send(InstantiateEvent.NewInstanceCreated, model);
|
265
268
|
}
|
266
269
|
else console.warn("Missing guid, can not send new instance event", go);
|
@@ -5,11 +5,10 @@
|
|
5
5
|
import { type Websocket } from 'websocket-ts';
|
6
6
|
|
7
7
|
import * as schemes from "../engine-schemes/schemes.js";
|
8
|
-
import { isDevEnvironment } from './debug/
|
8
|
+
import { isDevEnvironment } from './debug/index.js';
|
9
9
|
import { PeerNetworking } from './engine_networking_peer.js';
|
10
10
|
import { type IModel, type INetworkConnection, SendQueue } from './engine_networking_types.js';
|
11
11
|
import { isHostedOnGlitch } from './engine_networking_utils.js';
|
12
|
-
// import { Networking } from '../engine-components/Networking.js';
|
13
12
|
import { Context } from './engine_setup.js';
|
14
13
|
import * as utils from "./engine_utils.js";
|
15
14
|
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyDesc, RigidBodyType,
|
1
|
+
import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyDesc, RigidBodyType, ShapeType, World } from '@dimforge/rapier3d-compat';
|
2
2
|
import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'
|
3
3
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
4
4
|
|
5
|
-
import { CollisionDetectionMode,
|
6
|
-
import { isDevEnvironment } from './debug/
|
5
|
+
import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
|
6
|
+
import { isDevEnvironment } from './debug/index.js';
|
7
7
|
import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
|
8
8
|
import { foreachComponent } from './engine_gameobject.js';
|
9
9
|
import { Gizmos } from './engine_gizmos.js';
|
@@ -83,7 +83,7 @@
|
|
83
83
|
/** set the scene lighting from a specific scene. Will disable any previously enabled lighting settings */
|
84
84
|
enable(sourceId: SourceIdentifier | AssetReference) {
|
85
85
|
if(sourceId instanceof AssetReference)
|
86
|
-
sourceId = sourceId.
|
86
|
+
sourceId = sourceId.url;
|
87
87
|
const settings = this._sceneLightSettings?.get(sourceId);
|
88
88
|
if (!settings) {
|
89
89
|
if(debug) console.warn("No light settings found for", sourceId);
|
@@ -101,7 +101,7 @@
|
|
101
101
|
/** disable the lighting of a specific scene, will only have any effect if it is currently active */
|
102
102
|
disable(sourceId: SourceIdentifier | AssetReference) {
|
103
103
|
if(sourceId instanceof AssetReference)
|
104
|
-
sourceId = sourceId.
|
104
|
+
sourceId = sourceId.url;
|
105
105
|
if (sourceId === null || sourceId === undefined) return false;
|
106
106
|
const settings = this._sceneLightSettings?.get(sourceId);
|
107
107
|
if (!settings) {
|
@@ -351,7 +351,7 @@
|
|
351
351
|
}
|
352
352
|
}
|
353
353
|
else if (isDevEnvironment()) {
|
354
|
-
console.warn("[Debug] EventList: Could not find event listener in scene", call);
|
354
|
+
console.warn("[Debug] EventList: Could not find event listener in scene", call, context.object, data);
|
355
355
|
}
|
356
356
|
}
|
357
357
|
}
|
@@ -692,7 +692,7 @@
|
|
692
692
|
|
693
693
|
/**
|
694
694
|
* Place an object on a surface. This will calculate the object bounds which might be an expensive operation for complex objects.
|
695
|
-
* The object will be visually placed on the surface (the object's pivot will be ignored)
|
695
|
+
* The object will be visually placed on the surface (the object's pivot will be ignored).
|
696
696
|
* @param obj the object to place on the surface
|
697
697
|
* @param point the point to place the object on
|
698
698
|
* @returns the offset from the object bounds to the pivot
|
@@ -711,9 +711,10 @@
|
|
711
711
|
}
|
712
712
|
}
|
713
713
|
|
714
|
-
|
715
714
|
/**
|
716
|
-
* Postprocesses the material of an object loaded by
|
715
|
+
* Postprocesses the material of an object loaded by {@link FBXLoader}.
|
716
|
+
* It will apply necessary color conversions, remap shininess to roughness, and turn everything into {@link MeshStandardMaterial} on the object.
|
717
|
+
* This ensures consistent lighting and shading, including environment effects.
|
717
718
|
*/
|
718
719
|
export function postprocessFBXMaterials(obj: Mesh, material: Material | Material[], index?: number, array?: Material[]): boolean {
|
719
720
|
|
@@ -726,8 +727,6 @@
|
|
726
727
|
return success;
|
727
728
|
}
|
728
729
|
|
729
|
-
|
730
|
-
|
731
730
|
// ignore if the material is already a MeshStandardMaterial
|
732
731
|
if (material.type === "MeshStandardMaterial") {
|
733
732
|
return false;
|
@@ -93,12 +93,6 @@
|
|
93
93
|
/** call to destroy this object including all components that are attached to it. Will destroy all children recursively */
|
94
94
|
destroy(): void;
|
95
95
|
|
96
|
-
/** NOTE: this is just a wrapper for devs coming from Unity. Please use this.gameObject instead. In Needle Engine this.gameObject is the same as this.gameObject.transform. See the tutorial link below for more information
|
97
|
-
* @augments Object3D
|
98
|
-
* @tutorial https://fwd.needle.tools/needle-engine/docs/transform
|
99
|
-
* */
|
100
|
-
get transform(): IGameObject;
|
101
|
-
|
102
96
|
/** Add a new component to this object. Expects a component type (e.g. `addNewComponent(Animator)`) */
|
103
97
|
addNewComponent<T extends IComponent>(type: Constructor<T>, init?: ComponentInit<T>): T;
|
104
98
|
/** Remove a component from this object. Expected a component instance
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { isDevEnvironment } from "./debug/
|
1
|
+
import { isDevEnvironment } from "./debug/index.js";
|
2
2
|
import { getParam } from "./engine_utils.js";
|
3
3
|
|
4
4
|
const debug = getParam("debugfileformat");
|
@@ -90,9 +90,19 @@
|
|
90
90
|
target?: Texture,
|
91
91
|
}
|
92
92
|
|
93
|
+
export declare type ScreenshotOptionsBlob = ScreenshotOptions & {
|
94
|
+
type: "blob",
|
95
|
+
}
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Take a screenshot from the current scene and return a {@link Texture}. This can applied to a surface in 3D space.
|
99
|
+
* @param opts Provide `{ type: "texture" }` to get a texture instead of a data url.
|
100
|
+
* @returns The texture of the screenshot. Returns null if the screenshot could not be taken.
|
101
|
+
*/
|
102
|
+
export function screenshot2(opts: ScreenshotOptionsTexture): Texture | null;
|
93
103
|
/**
|
94
104
|
* Take a screenshot from the current scene.
|
95
|
-
* @param
|
105
|
+
* @param opts
|
96
106
|
* @returns The data url of the screenshot. Returns null if the screenshot could not be taken.
|
97
107
|
* ```ts
|
98
108
|
* const res = screenshot2({
|
@@ -105,9 +115,14 @@
|
|
105
115
|
* saveImage(res, "screenshot.webp");
|
106
116
|
* ```
|
107
117
|
*/
|
108
|
-
export function screenshot2(opts: ScreenshotOptionsTexture): Texture | null;
|
109
118
|
export function screenshot2(opts: ScreenshotOptionsDataUrl): string | null;
|
110
|
-
|
119
|
+
/**
|
120
|
+
* Take a screenshot asynchronously from the current scene.
|
121
|
+
* @returns A promise that resolves with the blob of the screenshot. Returns null if the screenshot could not be taken.
|
122
|
+
* @param {ScreenshotOptionsBlob} opts Set `{ type: "blob" }` to get a blob instead of a data url.
|
123
|
+
*/
|
124
|
+
export function screenshot2(opts: ScreenshotOptionsBlob): Promise<Blob | null>;
|
125
|
+
export function screenshot2(opts: ScreenshotOptionsDataUrl | ScreenshotOptionsTexture | ScreenshotOptionsBlob): Texture | string | null | Promise<Blob | null> {
|
111
126
|
|
112
127
|
if (!opts) opts = {}
|
113
128
|
|
@@ -183,13 +198,12 @@
|
|
183
198
|
context.renderer.setClearAlpha(0);
|
184
199
|
}
|
185
200
|
|
186
|
-
|
187
201
|
// set the desired output size
|
188
202
|
context.renderer.setSize(width, height, false);
|
189
203
|
|
190
204
|
// If a camera component was provided
|
191
205
|
if ("cam" in camera) {
|
192
|
-
camera = camera.
|
206
|
+
camera = camera.threeCamera;
|
193
207
|
}
|
194
208
|
// update the camera aspect and matrix
|
195
209
|
if (camera instanceof PerspectiveCamera) {
|
@@ -229,6 +243,14 @@
|
|
229
243
|
targetTexture.texture.needsUpdate = true;
|
230
244
|
return targetTexture.texture;
|
231
245
|
}
|
246
|
+
else if (opts.type === "blob") {
|
247
|
+
const promise = new Promise<Blob | null>((resolve, _) => {
|
248
|
+
canvas.toBlob(blob => {
|
249
|
+
resolve(blob);
|
250
|
+
}, mimeType);
|
251
|
+
});
|
252
|
+
return promise;
|
253
|
+
}
|
232
254
|
}
|
233
255
|
|
234
256
|
const dataUrl = canvas.toDataURL(mimeType);
|
@@ -564,11 +564,12 @@
|
|
564
564
|
}
|
565
565
|
|
566
566
|
/**
|
567
|
-
* Utility
|
567
|
+
* Utility functions to detect certain device types (mobile, desktop), browsers, or capabilities.
|
568
568
|
*/
|
569
569
|
export namespace DeviceUtilities {
|
570
|
+
|
570
571
|
let _isDesktop: boolean | undefined;
|
571
|
-
/**
|
572
|
+
/** @returns `true` for MacOS or Windows devices. `false` for Hololens and other headsets. */
|
572
573
|
export function isDesktop() {
|
573
574
|
if (_isDesktop !== undefined) return _isDesktop;
|
574
575
|
const ua = window.navigator.userAgent;
|
@@ -576,67 +577,95 @@
|
|
576
577
|
const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
|
577
578
|
return _isDesktop = standalone && !isHololens && !isiOS();
|
578
579
|
}
|
580
|
+
|
579
581
|
let _ismobile: boolean | undefined;
|
580
582
|
/** @returns `true` if it's a phone or tablet */
|
581
583
|
export function isMobileDevice() {
|
582
584
|
if (_ismobile !== undefined) return _ismobile;
|
585
|
+
// eslint-disable-next-line deprecation/deprecation
|
583
586
|
if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
|
584
587
|
return _ismobile = true;
|
585
588
|
}
|
586
589
|
return _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
|
587
590
|
}
|
588
|
-
|
589
|
-
|
590
|
-
*/
|
591
|
+
|
592
|
+
/** @deprecated use {@link isiPad} instead */
|
591
593
|
export function isIPad() {
|
592
|
-
return
|
594
|
+
return isiPad();
|
593
595
|
}
|
594
|
-
|
595
|
-
|
596
|
-
|
596
|
+
|
597
|
+
let __isiPad: boolean | undefined;
|
598
|
+
/** @returns `true` if we're currently on an iPad */
|
597
599
|
export function isiPad() {
|
598
|
-
|
600
|
+
if (__isiPad !== undefined) return __isiPad;
|
601
|
+
return __isiPad = /iPad/.test(navigator.userAgent);
|
599
602
|
}
|
603
|
+
|
604
|
+
let __isAndroidDevice: boolean | undefined;
|
605
|
+
/** @returns `true` if we're currently on an Android device */
|
600
606
|
export function isAndroidDevice() {
|
601
|
-
|
607
|
+
if (__isAndroidDevice !== undefined) return __isAndroidDevice;
|
608
|
+
return __isAndroidDevice = /Android/.test(navigator.userAgent);
|
602
609
|
}
|
603
|
-
|
610
|
+
|
611
|
+
let __isMozillaXR: boolean | undefined;
|
612
|
+
/** @returns `true` if we're currently using the Mozilla XR Browser (only available for iOS) */
|
604
613
|
export function isMozillaXR() {
|
605
|
-
|
614
|
+
if (__isMozillaXR !== undefined) return __isMozillaXR;
|
615
|
+
return __isMozillaXR = /WebXRViewer\//i.test(navigator.userAgent);
|
606
616
|
}
|
607
|
-
|
617
|
+
|
618
|
+
let __isMacOS: boolean | undefined;
|
608
619
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
|
620
|
+
/** @returns `true` for MacOS devices */
|
609
621
|
export function isMacOS() {
|
610
|
-
if (
|
622
|
+
if (__isMacOS !== undefined) return __isMacOS;
|
611
623
|
if (navigator.userAgentData) {
|
612
624
|
// Use modern UA Client Hints API if available
|
613
|
-
return
|
625
|
+
return __isMacOS = navigator.userAgentData.platform === 'macOS';
|
614
626
|
} else {
|
615
627
|
// Fallback to user agent string parsing
|
616
628
|
const userAgent = navigator.userAgent.toLowerCase();
|
617
|
-
return
|
629
|
+
return __isMacOS = userAgent.includes('mac os x') || userAgent.includes('macintosh');
|
618
630
|
}
|
619
631
|
}
|
632
|
+
|
620
633
|
let __isiOS: boolean | undefined;
|
621
634
|
const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
|
635
|
+
|
622
636
|
/** @returns `true` for iOS devices like iPad, iPhone, iPod... */
|
623
637
|
export function isiOS() {
|
624
638
|
if (__isiOS !== undefined) return __isiOS;
|
639
|
+
// eslint-disable-next-line deprecation/deprecation
|
625
640
|
return __isiOS = iosDevices.includes(navigator.platform)
|
626
641
|
// iPad on iOS 13 detection
|
627
642
|
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
628
643
|
}
|
629
644
|
|
645
|
+
let __isSafari: boolean | undefined;
|
630
646
|
/** @returns `true` if we're currently on safari */
|
631
647
|
export function isSafari() {
|
632
|
-
|
648
|
+
if (__isSafari !== undefined) return __isSafari;
|
649
|
+
__isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
650
|
+
return __isSafari;
|
633
651
|
}
|
634
652
|
|
635
|
-
|
653
|
+
let __isQuest: boolean | undefined;
|
654
|
+
/** @returns `true` for Meta Quest devices and browser. */
|
636
655
|
export function isQuest() {
|
637
|
-
|
656
|
+
if (__isQuest !== undefined) return __isQuest;
|
657
|
+
return __isQuest = navigator.userAgent.includes("OculusBrowser");
|
638
658
|
}
|
639
659
|
|
660
|
+
let __supportsQuickLookAR: boolean | undefined;
|
661
|
+
/** @returns `true` if the browser has `<a rel="ar">` support, which indicates USDZ QuickLook support. */
|
662
|
+
export function supportsQuickLookAR() {
|
663
|
+
if (__supportsQuickLookAR !== undefined) return __supportsQuickLookAR;
|
664
|
+
const a = document.createElement("a") as HTMLAnchorElement;
|
665
|
+
__supportsQuickLookAR = a.relList.supports("ar");
|
666
|
+
return __supportsQuickLookAR;
|
667
|
+
}
|
668
|
+
|
640
669
|
/** @returns `true` if the user allowed to use the microphone */
|
641
670
|
export async function microphonePermissionsGranted() {
|
642
671
|
try {
|
@@ -653,74 +682,96 @@
|
|
653
682
|
}
|
654
683
|
}
|
655
684
|
|
685
|
+
let __iOSVersion: string | null | undefined;
|
686
|
+
export function getiOSVersion() {
|
687
|
+
if (__iOSVersion !== undefined) return __iOSVersion;
|
688
|
+
const match = navigator.userAgent.match(/iPhone OS (\d+_\d+)/);
|
689
|
+
if (match) __iOSVersion = match[1].replace("_", ".");
|
690
|
+
if (!__iOSVersion) {
|
691
|
+
// Look for "(Macintosh;" or "(iPhone;" or "(iPad;" and then check Version/18.0
|
692
|
+
const match2 = navigator.userAgent.match(/(?:\(Macintosh;|iPhone;|iPad;).*Version\/(\d+\.\d+)/);
|
693
|
+
if (match2) __iOSVersion = match2[1];
|
694
|
+
}
|
695
|
+
// if we dont have any match we set it to null to avoid running the check again
|
696
|
+
if (!__iOSVersion) {
|
697
|
+
__iOSVersion = null;
|
698
|
+
}
|
699
|
+
return __iOSVersion;
|
700
|
+
}
|
701
|
+
|
702
|
+
let __chromeVersion: string | null | undefined;
|
703
|
+
export function getChromeVersion() {
|
704
|
+
if (__chromeVersion !== undefined) return __chromeVersion;
|
705
|
+
const match = navigator.userAgent.match(/(?:CriOS|Chrome)\/(\d+\.\d+\.\d+\.\d+)/);
|
706
|
+
if (match) {
|
707
|
+
const result = match[1].replace("_", ".");
|
708
|
+
__chromeVersion = result;
|
709
|
+
}
|
710
|
+
else __chromeVersion = null;
|
711
|
+
return __chromeVersion;
|
712
|
+
}
|
656
713
|
}
|
657
714
|
|
658
|
-
|
659
|
-
|
660
|
-
|
715
|
+
/**
|
716
|
+
* @deprecated use {@link DeviceUtilities.isDesktop} instead
|
717
|
+
*/
|
661
718
|
export function isDesktop() {
|
662
719
|
return DeviceUtilities.isDesktop();
|
663
720
|
}
|
664
721
|
|
665
|
-
/**
|
722
|
+
/**
|
723
|
+
* @deprecated use {@link DeviceUtilities.isMobileDevice} instead
|
724
|
+
*/
|
666
725
|
export function isMobileDevice() {
|
667
726
|
return DeviceUtilities.isMobileDevice();
|
668
727
|
}
|
669
728
|
|
670
|
-
/** @deprecated use {@link isiPad} instead */
|
729
|
+
/** @deprecated use {@link DeviceUtilities.isiPad} instead */
|
671
730
|
export function isIPad() {
|
672
731
|
return DeviceUtilities.isiPad();
|
673
732
|
}
|
674
733
|
|
734
|
+
/** @deprecated use {@link DeviceUtilities.isiPad} instead */
|
675
735
|
export function isiPad() {
|
676
736
|
return DeviceUtilities.isiPad();
|
677
737
|
}
|
678
738
|
|
739
|
+
/** @deprecated use {@link DeviceUtilities.isAndroidDevice} instead */
|
679
740
|
export function isAndroidDevice() {
|
680
741
|
return DeviceUtilities.isAndroidDevice();
|
681
742
|
}
|
682
743
|
|
683
|
-
/** @
|
744
|
+
/** @deprecated use {@link DeviceUtilities.isMozillaXR} instead */
|
684
745
|
export function isMozillaXR() {
|
685
746
|
return DeviceUtilities.isMozillaXR();
|
686
747
|
}
|
687
748
|
|
688
749
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
|
750
|
+
/** @deprecated use {@link DeviceUtilities.isMacOS} instead */
|
689
751
|
export function isMacOS() {
|
690
752
|
return DeviceUtilities.isMacOS();
|
691
753
|
}
|
692
754
|
|
693
|
-
/** @
|
755
|
+
/** @deprecated use {@link DeviceUtilities.isiOS} instead */
|
694
756
|
export function isiOS() {
|
695
757
|
return DeviceUtilities.isiOS();
|
696
758
|
}
|
697
759
|
|
698
|
-
/** @
|
760
|
+
/** @deprecated use {@link DeviceUtilities.isSafari} instead */
|
699
761
|
export function isSafari() {
|
700
762
|
return DeviceUtilities.isSafari();
|
701
763
|
}
|
702
764
|
|
765
|
+
/** @deprecated use {@link DeviceUtilities.isQuest} instead */
|
703
766
|
export function isQuest() {
|
704
767
|
return DeviceUtilities.isQuest();
|
705
768
|
}
|
706
769
|
|
707
|
-
/** @
|
770
|
+
/** @deprecated use {@link DeviceUtilities.microphonePermissionsGranted} instead */
|
708
771
|
export async function microphonePermissionsGranted() {
|
709
|
-
|
710
|
-
//@ts-ignore
|
711
|
-
const res = await navigator.permissions.query({ name: 'microphone' });
|
712
|
-
if (res.state === "denied") {
|
713
|
-
return false;
|
714
|
-
}
|
715
|
-
return true;
|
716
|
-
}
|
717
|
-
catch (err) {
|
718
|
-
console.error("Error querying `microphone` permissions.", err);
|
719
|
-
return false;
|
720
|
-
}
|
772
|
+
return DeviceUtilities.microphonePermissionsGranted();
|
721
773
|
}
|
722
774
|
|
723
|
-
|
724
775
|
const cloudflareIPRegex = /ip=(?<ip>.+?)\n/s;
|
725
776
|
export async function getIpCloudflare() {
|
726
777
|
const data = await fetch('https://www.cloudflare.com/cdn-cgi/trace');
|
@@ -732,12 +783,20 @@
|
|
732
783
|
return null;
|
733
784
|
}
|
734
785
|
|
786
|
+
/** Gets the public IP address of this device.
|
787
|
+
* @returns IP address, or `undefined` when it can't be determined.
|
788
|
+
*/
|
735
789
|
export async function getIp() {
|
736
|
-
const res = await fetch("https://api.db-ip.com/v2/free/self");
|
737
|
-
|
790
|
+
const res = (await fetch("https://api.db-ip.com/v2/free/self").catch(() => null));
|
791
|
+
if (!res) return undefined;
|
792
|
+
const json = await res.json() as IpAndLocation;
|
738
793
|
return json.ipAddress;
|
739
794
|
}
|
740
795
|
|
796
|
+
/**
|
797
|
+
* Contains information about public IP, continent, country, state, city.
|
798
|
+
* This information may be affected by VPNs, proxies, or other network configurations.
|
799
|
+
*/
|
741
800
|
export type IpAndLocation = {
|
742
801
|
ipAddress: string;
|
743
802
|
continentCode: string;
|
@@ -747,8 +806,13 @@
|
|
747
806
|
stateProv: string;
|
748
807
|
city: string;
|
749
808
|
}
|
750
|
-
|
751
|
-
|
809
|
+
|
810
|
+
/** Gets the public IP address, location, and country data of this device.
|
811
|
+
* @returns an object containing {@link IpAndLocation} data, or `undefined` when it can't be determined.
|
812
|
+
*/
|
813
|
+
export async function getIpAndLocation(): Promise<IpAndLocation | undefined> {
|
814
|
+
const res = (await fetch("https://api.db-ip.com/v2/free/self").catch(() => null));
|
815
|
+
if (!res) return undefined;
|
752
816
|
const json = await res.json() as IpAndLocation;
|
753
817
|
return json;
|
754
818
|
}
|
@@ -760,7 +824,10 @@
|
|
760
824
|
}
|
761
825
|
const mutationObserverMap = new WeakMap<HTMLElement, HtmlElementExtra>();
|
762
826
|
|
763
|
-
/**
|
827
|
+
/**
|
828
|
+
* Register a callback when an {@link HTMLElement} attribute changes.
|
829
|
+
* This is used, for example, by the Skybox component to watch for changes to the environment-* and skybox-* attributes.
|
830
|
+
*/
|
764
831
|
export function addAttributeChangeCallback(domElement: HTMLElement, name: string, callback: AttributeChangeCallback) {
|
765
832
|
if (!mutationObserverMap.get(domElement)) {
|
766
833
|
const observer = new MutationObserver((mutations) => {
|
@@ -779,6 +846,10 @@
|
|
779
846
|
}
|
780
847
|
listeners.get(name)!.push(callback);
|
781
848
|
};
|
849
|
+
|
850
|
+
/**
|
851
|
+
* Unregister a callback previously registered with {@link addAttributeChangeCallback}.
|
852
|
+
*/
|
782
853
|
export function removeAttributeChangeCallback(domElement: HTMLElement, name: string, callback: AttributeChangeCallback) {
|
783
854
|
if (!mutationObserverMap.get(domElement)) return;
|
784
855
|
const listeners = mutationObserverMap.get(domElement)!.attributeChangedListeners;
|
@@ -48,7 +48,7 @@
|
|
48
48
|
if (sys.context === context) return; // exists
|
49
49
|
}
|
50
50
|
const go = new Object3D();
|
51
|
-
GameObject.
|
51
|
+
GameObject.addComponent(go, EventSystem);
|
52
52
|
context.scene.add(go);
|
53
53
|
}
|
54
54
|
|
@@ -10,7 +10,7 @@
|
|
10
10
|
private _controls: ThreeFlyControls | null = null;
|
11
11
|
|
12
12
|
onEnable(): void {
|
13
|
-
const cam = GameObject.getComponent(this.gameObject, Camera)?.
|
13
|
+
const cam = GameObject.getComponent(this.gameObject, Camera)?.threeCamera;
|
14
14
|
if (!cam) {
|
15
15
|
console.warn("FlyControls: Requires a Camera component on the same object as this component.");
|
16
16
|
return;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
2
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
3
|
-
import {
|
3
|
+
import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
4
4
|
import { Behaviour, GameObject } from "../Component.js";
|
5
5
|
import { EventList } from "../EventList.js";
|
6
6
|
import { type IPointerEventHandler } from "./PointerEvents.js";
|
@@ -74,7 +74,7 @@
|
|
74
74
|
if (this.placeholder && this.textComponent?.text.length) {
|
75
75
|
GameObject.setActive(this.placeholder.gameObject, false);
|
76
76
|
}
|
77
|
-
if (isiOS()) {
|
77
|
+
if (DeviceUtilities.isiOS()) {
|
78
78
|
this._iosEventFn = this.processInputOniOS.bind(this);
|
79
79
|
window.addEventListener("click", this._iosEventFn);
|
80
80
|
}
|
@@ -235,7 +235,7 @@
|
|
235
235
|
if (InputField.htmlField) {
|
236
236
|
if (debug) console.log("Focus Inputfield", InputField.htmlFieldFocused)
|
237
237
|
InputField.htmlField.setSelectionRange(InputField.htmlField.value.length, InputField.htmlField.value.length);
|
238
|
-
if (isiOS())
|
238
|
+
if (DeviceUtilities.isiOS())
|
239
239
|
InputField.htmlField.focus({ preventScroll: true });
|
240
240
|
else {
|
241
241
|
// on Andoid if we don't focus in a timeout the keyboard will close the second time we click the InputField
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
|
2
|
-
import { LightProbe } from "three";
|
3
2
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
3
|
|
5
4
|
import { Behaviour, GameObject } from "../../engine-components/Component.js";
|
@@ -51,14 +50,14 @@
|
|
51
50
|
if (_result.scene.children.length === 1) {
|
52
51
|
const obj = _result.scene.children[0];
|
53
52
|
// add a component to the root of the scene
|
54
|
-
settings = GameObject.
|
53
|
+
settings = GameObject.addComponent(obj, SceneLightSettings, {}, { callAwake: false });
|
55
54
|
}
|
56
55
|
// if the scene already has multiple children we add it as a new object
|
57
56
|
else {
|
58
57
|
const lightSettings = new Object3D();
|
59
58
|
lightSettings.name = "LightSettings " + this.sourceId;
|
60
59
|
_result.scene.add(lightSettings);
|
61
|
-
settings = GameObject.
|
60
|
+
settings = GameObject.addComponent(lightSettings, SceneLightSettings, {}, { callAwake: false });
|
62
61
|
}
|
63
62
|
settings.sourceId = this.sourceId;
|
64
63
|
settings.ambientIntensity = ext.ambientIntensity;
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
4
|
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
5
5
|
|
6
|
-
import { isDevEnvironment } from "../debug/
|
6
|
+
import { isDevEnvironment } from "../debug/index.js";
|
7
7
|
import { type ILightDataRegistry } from "../engine_lightdata.js";
|
8
8
|
import { type SourceIdentifier } from "../engine_types.js";
|
9
9
|
import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
|
@@ -15,7 +15,7 @@
|
|
15
15
|
startup();
|
16
16
|
handleSessionGrantedASAP({ debug });
|
17
17
|
|
18
|
-
function startup(iteration: number = 0) {
|
18
|
+
function startup(iteration: number = 0): number | undefined {
|
19
19
|
|
20
20
|
|
21
21
|
needleEngineElement = document.querySelector("needle-engine");
|
@@ -30,7 +30,7 @@
|
|
30
30
|
}
|
31
31
|
}
|
32
32
|
|
33
|
-
return;
|
33
|
+
return undefined;
|
34
34
|
|
35
35
|
// if (needleEngineHasLoaded()) {
|
36
36
|
// if (debug) console.log("Skip asap, needle engine has already loaded.");
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Mesh, Object3D,
|
1
|
+
import { Mesh, Object3D, TextureLoader, Vector4 } from "three";
|
2
2
|
import ThreeMeshUI from "three-mesh-ui";
|
3
3
|
|
4
4
|
import { addNewComponent } from "../../engine_components.js";
|
@@ -8,7 +8,7 @@
|
|
8
8
|
import { lookAtObject } from "../../engine_three_utils.js";
|
9
9
|
import { IComponent, IContext, IGameObject } from "../../engine_types.js";
|
10
10
|
import { TypeStore } from "../../engine_typestore.js";
|
11
|
-
import { getParam
|
11
|
+
import { DeviceUtilities,getParam } from "../../engine_utils.js";
|
12
12
|
import { getIconTexture, isIconElement } from "../icons.js";
|
13
13
|
|
14
14
|
const debug = getParam("debugspatialmenu");
|
@@ -99,7 +99,7 @@
|
|
99
99
|
return;
|
100
100
|
}
|
101
101
|
|
102
|
-
if (debug && isDesktop()) {
|
102
|
+
if (debug && DeviceUtilities.isDesktop()) {
|
103
103
|
this.updateMenu();
|
104
104
|
}
|
105
105
|
|
@@ -170,7 +170,7 @@
|
|
170
170
|
const hideMenuThreshold = fwd.y > .4;
|
171
171
|
const newVisibleState = (menu.visible ? hideMenuThreshold : showMenuThreshold) || this.userRequestedMenu;
|
172
172
|
const becomesVisible = !menu.visible && newVisibleState;
|
173
|
-
menu.visible = newVisibleState || (isDesktop() && debug as boolean);
|
173
|
+
menu.visible = newVisibleState || (DeviceUtilities.isDesktop() && debug as boolean);
|
174
174
|
|
175
175
|
fwd.multiplyScalar(3 * rigScale);
|
176
176
|
menuTargetPosition.add(fwd);
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import type { Context } from "../../engine_context.js";
|
2
2
|
import { hasCommercialLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
|
3
3
|
import { isLocalNetwork } from "../../engine_networking_utils.js";
|
4
|
-
import { getParam
|
4
|
+
import { DeviceUtilities,getParam } from "../../engine_utils.js";
|
5
5
|
import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
|
6
6
|
import { ButtonsFactory } from "../buttons.js";
|
7
7
|
import { ensureFonts, iconFontUrl, loadFont } from "../fonts.js";
|
@@ -193,7 +193,7 @@
|
|
193
193
|
*/
|
194
194
|
showQRCodeButton(enabled: boolean | "desktop-only"): HTMLButtonElement | null {
|
195
195
|
if (enabled === "desktop-only") {
|
196
|
-
enabled = !isMobileDevice();
|
196
|
+
enabled = !DeviceUtilities.isMobileDevice();
|
197
197
|
}
|
198
198
|
if (!enabled) {
|
199
199
|
const button = ButtonsFactory.getOrCreate().qrButton;
|
@@ -434,6 +434,8 @@
|
|
434
434
|
.logo {
|
435
435
|
cursor: pointer;
|
436
436
|
padding-left: 0.6rem;
|
437
|
+
padding-bottom: .02rem;
|
438
|
+
margin-right: 0.5rem;
|
437
439
|
}
|
438
440
|
.logo-hidden {
|
439
441
|
.logo {
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import type { Context } from '../engine/engine_context.js';
|
2
2
|
import { serializable } from '../engine/engine_serialization.js';
|
3
|
-
import {
|
3
|
+
import { DeviceUtilities } from '../engine/engine_utils.js';
|
4
4
|
import { Behaviour } from './Component.js';
|
5
5
|
|
6
6
|
/**
|
@@ -57,7 +57,7 @@
|
|
57
57
|
if (this.showSpatialMenu === true)
|
58
58
|
this.context.menu.showSpatialMenu(this.showSpatialMenu);
|
59
59
|
if (this.createQRCodeButton === true) {
|
60
|
-
if (!isMobileDevice()) {
|
60
|
+
if (!DeviceUtilities.isMobileDevice()) {
|
61
61
|
this.context.menu.showQRCodeButton(true);
|
62
62
|
}
|
63
63
|
}
|
@@ -8,7 +8,7 @@
|
|
8
8
|
import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
|
9
9
|
import { getBoundingBox, 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
|
-
import { delay,
|
11
|
+
import { delay, DeviceUtilities, getParam } from "../engine_utils.js";
|
12
12
|
import { invokeXRSessionEnd, invokeXRSessionStart } from "./events.js"
|
13
13
|
import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
|
14
14
|
import { NeedleXRController } from "./NeedleXRController.js";
|
@@ -177,7 +177,7 @@
|
|
177
177
|
}
|
178
178
|
|
179
179
|
|
180
|
-
if (isDesktop() && isDevEnvironment()) {
|
180
|
+
if (DeviceUtilities.isDesktop() && isDevEnvironment()) {
|
181
181
|
window.addEventListener("keydown", (evt) => {
|
182
182
|
if (evt.key === "x" || evt.key === "Escape") {
|
183
183
|
if (NeedleXRSession.active) {
|
@@ -396,10 +396,11 @@
|
|
396
396
|
|
397
397
|
// handle iOS platform where "immersive-ar" is not supported
|
398
398
|
// TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
|
399
|
-
if (isiOS()) {
|
399
|
+
if (DeviceUtilities.isiOS()) {
|
400
400
|
if (mode === "ar") {
|
401
401
|
const arSupported = await this.isARSupported();
|
402
|
-
if (!arSupported
|
402
|
+
if (!arSupported) {
|
403
|
+
InternalUSDZRegistry.exportAndOpen();
|
403
404
|
return null;
|
404
405
|
}
|
405
406
|
else {
|
@@ -452,7 +453,7 @@
|
|
452
453
|
}
|
453
454
|
const defaultInit: XRSessionInit = this.getDefaultSessionInit(mode);
|
454
455
|
const domOverlayElement = getDOMOverlayElement(context.domElement);
|
455
|
-
if (domOverlayElement && !isQuest()) { // excluding quest since dom-overlay breaks sessiongranted
|
456
|
+
if (domOverlayElement && !DeviceUtilities.isQuest()) { // excluding quest since dom-overlay breaks sessiongranted
|
456
457
|
defaultInit.domOverlay = { root: domOverlayElement };
|
457
458
|
defaultInit.optionalFeatures!.push('dom-overlay');
|
458
459
|
}
|
@@ -633,7 +634,7 @@
|
|
633
634
|
if (this.controllers.some(c => c.inputSource.targetRayMode === "tracked-pointer"))
|
634
635
|
return true;
|
635
636
|
}
|
636
|
-
if (isDevEnvironment() && isDesktop() && this.mode === "immersive-ar") {
|
637
|
+
if (isDevEnvironment() && DeviceUtilities.isDesktop() && this.mode === "immersive-ar") {
|
637
638
|
return true;
|
638
639
|
}
|
639
640
|
return false;
|
@@ -1343,7 +1344,7 @@
|
|
1343
1344
|
|
1344
1345
|
// render spectator view if we're in VR using Link
|
1345
1346
|
// __rendered_once is for when we are on device, but opening the browser should not show a blank space
|
1346
|
-
if (isDesktop() || !this["_renderOnceOnDevice"]) {
|
1347
|
+
if (DeviceUtilities.isDesktop() || !this["_renderOnceOnDevice"]) {
|
1347
1348
|
const renderer = this.context.renderer;
|
1348
1349
|
if (renderer.xr.isPresenting && this.context.mainCamera) {
|
1349
1350
|
this["_renderOnceOnDevice"] = true;
|
@@ -43,9 +43,9 @@
|
|
43
43
|
opts.parent = parent;
|
44
44
|
this.gameObject.updateMatrix();
|
45
45
|
const matrix = this.gameObject.matrix;
|
46
|
-
if (debug) console.log("Load nested:", this.filePath?.
|
46
|
+
if (debug) console.log("Load nested:", this.filePath?.url ?? this.filePath, this.gameObject.position);
|
47
47
|
const res = await this.filePath?.instantiate?.call(this.filePath, opts);
|
48
|
-
if (debug) console.log("Nested loaded:", this.filePath?.
|
48
|
+
if (debug) console.log("Nested loaded:", this.filePath?.url ?? this.filePath, res);
|
49
49
|
if (res) {
|
50
50
|
res.matrixAutoUpdate = false;
|
51
51
|
res.matrix.identity();
|
@@ -56,7 +56,7 @@
|
|
56
56
|
|
57
57
|
this.dispatchEvent(new CustomEvent("loaded", { detail: { instance: res, assetReference: this.filePath } }));
|
58
58
|
}
|
59
|
-
if (debug) console.log("Nested loading done:", this.filePath?.
|
59
|
+
if (debug) console.log("Nested loading done:", this.filePath?.url ?? this.filePath, res);
|
60
60
|
}
|
61
61
|
}
|
62
62
|
|
@@ -23,19 +23,26 @@
|
|
23
23
|
declare module 'three' {
|
24
24
|
export interface Object3D {
|
25
25
|
/**
|
26
|
-
* Add a Needle Engine component to the Object3D.
|
26
|
+
* Add a Needle Engine component to the {@link Object3D}.
|
27
27
|
* @param comp The component instance or constructor to add.
|
28
28
|
* @param init Optional initialization data for the component.
|
29
29
|
* @returns The added component instance.
|
30
|
-
* @example
|
30
|
+
* @example Directly pass in constructor and properties:
|
31
31
|
* ```ts
|
32
|
-
* const obj = new
|
32
|
+
* const obj = new Object3D();
|
33
33
|
* obj.addComponent(MyComponent, { myProperty: 42 });
|
34
34
|
* ```
|
35
|
+
* @example Create a component instance, assign properties and then add it:
|
36
|
+
* ```ts
|
37
|
+
* const obj = new Object3D();
|
38
|
+
* const comp = new MyComponent();
|
39
|
+
* comp.myProperty = 42;
|
40
|
+
* obj.addComponent(comp);
|
41
|
+
* ```
|
35
42
|
*/
|
36
43
|
addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>) : T;
|
37
44
|
/**
|
38
|
-
* Remove a Needle Engine component from the Object3D.
|
45
|
+
* Remove a Needle Engine component from the {@link Object3D}.
|
39
46
|
*/
|
40
47
|
removeComponent(inst: IComponent) : IComponent;
|
41
48
|
/**
|
@@ -47,76 +54,76 @@
|
|
47
54
|
*/
|
48
55
|
getOrAddComponent<T extends IComponent>(typeName: ConstructorConcrete<T>, init?: ComponentInit<T>): T;
|
49
56
|
/**
|
50
|
-
* Get a Needle Engine component from the Object3D.
|
57
|
+
* Get a Needle Engine component from the {@link Object3D}.
|
51
58
|
* @returns The component instance or null if not found.
|
52
59
|
*/
|
53
60
|
getComponent<T extends IComponent>(type: Constructor<T>): T | null;
|
54
61
|
/**
|
55
|
-
* Get all components of a specific type from the Object3D.
|
62
|
+
* Get all components of a specific type from the {@link Object3D}.
|
56
63
|
* @param arr Optional array to fill with the found components.
|
57
64
|
* @returns An array of components.
|
58
65
|
*/
|
59
66
|
getComponents<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
|
60
67
|
/**
|
61
|
-
* Get a Needle Engine component from the Object3D or its children. This will search on the current Object and all its children.
|
68
|
+
* Get a Needle Engine component from the {@link Object3D} or its children. This will search on the current Object and all its children.
|
62
69
|
* @returns The component instance or null if not found.
|
63
70
|
*/
|
64
71
|
getComponentInChildren<T extends IComponent>(type: Constructor<T>): T | null;
|
65
72
|
/**
|
66
|
-
* Get all components of a specific type from the Object3D or its children. This will search on the current Object and all its children.
|
73
|
+
* Get all components of a specific type from the {@link Object3D} or its children. This will search on the current Object and all its children.
|
67
74
|
* @param arr Optional array to fill with the found components.
|
68
75
|
* @returns An array of components.
|
69
76
|
*/
|
70
77
|
getComponentsInChildren<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
|
71
78
|
/**
|
72
|
-
* Get a Needle Engine component from the Object3D or its parents. This will search on the current Object and all its parents.
|
79
|
+
* Get a Needle Engine component from the {@link Object3D} or its parents. This will search on the current Object and all its parents.
|
73
80
|
* @returns The component instance or null if not found.
|
74
81
|
*/
|
75
82
|
getComponentInParent<T extends IComponent>(type: Constructor<T>): T | null;
|
76
83
|
/**
|
77
|
-
* Get all Needle Engine components of a specific type from the Object3D or its parents. This will search on the current Object and all its parents.
|
84
|
+
* Get all Needle Engine components of a specific type from the {@link Object3D} or its parents. This will search on the current Object and all its parents.
|
78
85
|
* @param arr Optional array to fill with the found components.
|
79
86
|
* @returns An array of components.
|
80
87
|
*/
|
81
88
|
getComponentsInParent<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
|
82
89
|
|
83
90
|
/**
|
84
|
-
* Destroys the Object3D and all its Needle Engine components.
|
91
|
+
* Destroys the {@link Object3D} and all its Needle Engine components.
|
85
92
|
*/
|
86
93
|
destroy(): void;
|
87
94
|
|
88
95
|
/**
|
89
|
-
* Get or set the world position of the Object3D.
|
96
|
+
* Get or set the world position of the {@link Object3D}.
|
90
97
|
* Added by Needle Engine.
|
91
98
|
*/
|
92
99
|
worldPosition: Vector3;
|
93
100
|
/**
|
94
|
-
* Get or set the world quaternion of the Object3D.
|
101
|
+
* Get or set the world quaternion of the {@link Object3D}.
|
95
102
|
* Added by Needle Engine.
|
96
103
|
*/
|
97
104
|
worldQuaternion: Quaternion;
|
98
105
|
/**
|
99
|
-
* Get or set the world rotation of the Object3D.
|
106
|
+
* Get or set the world rotation of the {@link Object3D}.
|
100
107
|
* Added by Needle Engine.
|
101
108
|
*/
|
102
109
|
worldRotation: Vector3;
|
103
110
|
/**
|
104
|
-
* Get or set the world scale of the Object3D.
|
111
|
+
* Get or set the world scale of the {@link Object3D}.
|
105
112
|
* Added by Needle Engine.
|
106
113
|
*/
|
107
114
|
worldScale: Vector3;
|
108
115
|
/**
|
109
|
-
* Get the world forward vector of the Object3D.
|
116
|
+
* Get the world forward vector of the {@link Object3D}.
|
110
117
|
* Added by Needle Engine.
|
111
118
|
*/
|
112
119
|
get worldForward(): Vector3;
|
113
120
|
/**
|
114
|
-
* Get the world right vector of the Object3D.
|
121
|
+
* Get the world right vector of the {@link Object3D}.
|
115
122
|
* Added by Needle Engine.
|
116
123
|
*/
|
117
124
|
get worldRight(): Vector3;
|
118
125
|
/**
|
119
|
-
* Get the world up vector of the Object3D.
|
126
|
+
* Get the world up vector of the {@link Object3D}.
|
120
127
|
* Added by Needle Engine.
|
121
128
|
*/
|
122
129
|
get worldUp(): Vector3;
|
@@ -201,17 +208,6 @@
|
|
201
208
|
});
|
202
209
|
}
|
203
210
|
|
204
|
-
if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "transform")) {
|
205
|
-
Object.defineProperty(Object3D.prototype, "transform", {
|
206
|
-
get: function () {
|
207
|
-
return this;
|
208
|
-
}
|
209
|
-
});
|
210
|
-
}
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
211
|
if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldPosition")) {
|
216
212
|
Object.defineProperty(Object3D.prototype, "worldPosition", {
|
217
213
|
get: function () {
|
@@ -1,10 +1,10 @@
|
|
1
1
|
|
2
2
|
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
3
3
|
import { serializable } from "../../engine/engine_serialization.js";
|
4
|
-
import {
|
4
|
+
import { DeviceUtilities } from "../../engine/engine_utils.js";
|
5
5
|
import { Behaviour } from "../Component.js";
|
6
6
|
import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
|
7
|
-
import { ObjectRaycaster
|
7
|
+
import { ObjectRaycaster } from "../ui/Raycaster.js";
|
8
8
|
|
9
9
|
/**
|
10
10
|
* OpenURLMode defines how a URL should be opened.
|
@@ -59,7 +59,7 @@
|
|
59
59
|
|
60
60
|
switch (this.mode) {
|
61
61
|
case OpenURLMode.NewTab:
|
62
|
-
if (isSafari()) {
|
62
|
+
if (DeviceUtilities.isSafari()) {
|
63
63
|
globalThis.open(url, "_blank");
|
64
64
|
}
|
65
65
|
else
|
@@ -67,14 +67,14 @@
|
|
67
67
|
break;
|
68
68
|
case OpenURLMode.SameTab:
|
69
69
|
// TODO: test if "same tab" now also works on iOS
|
70
|
-
if (isSafari() && isiOS()) {
|
70
|
+
if (DeviceUtilities.isSafari() && DeviceUtilities.isiOS()) {
|
71
71
|
globalThis.open(url, "_top");
|
72
72
|
}
|
73
73
|
else
|
74
74
|
globalThis.open(url, "_self");
|
75
75
|
break;
|
76
76
|
case OpenURLMode.NewWindow:
|
77
|
-
if (isSafari()) {
|
77
|
+
if (DeviceUtilities.isSafari()) {
|
78
78
|
globalThis.open(url, "_top");
|
79
79
|
}
|
80
80
|
else globalThis.open(url, "_new");
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { Box3Helper, Object3D, PerspectiveCamera, Ray, Vector2, Vector3, Vector3Like } from "three";
|
2
2
|
import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
3
|
-
import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
|
4
3
|
|
5
4
|
import { setCameraController } from "../engine/engine_camera.js";
|
6
5
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
@@ -10,7 +9,7 @@
|
|
10
9
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
11
10
|
import { getBoundingBox, getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
|
12
11
|
import type { ICameraController } from "../engine/engine_types.js";
|
13
|
-
import {
|
12
|
+
import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
|
14
13
|
import { Camera } from "./Camera.js";
|
15
14
|
import { Behaviour, GameObject } from "./Component.js";
|
16
15
|
import { GroundProjectedEnv } from "./GroundProjection.js";
|
@@ -281,7 +280,7 @@
|
|
281
280
|
this._enableTime = this.context.time.time;
|
282
281
|
const cameraComponent = GameObject.getComponent(this.gameObject, Camera);
|
283
282
|
this._camera = cameraComponent;
|
284
|
-
let cam = cameraComponent?.
|
283
|
+
let cam = cameraComponent?.threeCamera;
|
285
284
|
if (!cam && this.gameObject instanceof PerspectiveCamera) {
|
286
285
|
cam = this.gameObject;
|
287
286
|
}
|
@@ -303,7 +302,7 @@
|
|
303
302
|
this.enablePan = true;
|
304
303
|
this.enableZoom = true;
|
305
304
|
this.middleClickToFocus = true;
|
306
|
-
if (isMobileDevice()) this.doubleClickToFocus = true;
|
305
|
+
if (DeviceUtilities.isMobileDevice()) this.doubleClickToFocus = true;
|
307
306
|
}
|
308
307
|
this._controls.addEventListener("start", this.onControlsChangeStarted);
|
309
308
|
|
@@ -323,8 +322,8 @@
|
|
323
322
|
|
324
323
|
/** @internal */
|
325
324
|
onDisable() {
|
326
|
-
if (this._camera?.
|
327
|
-
setCameraController(this._camera.
|
325
|
+
if (this._camera?.threeCamera) {
|
326
|
+
setCameraController(this._camera.threeCamera, this, false);
|
328
327
|
}
|
329
328
|
if (this._controls) {
|
330
329
|
this._controls.enabled = false;
|
@@ -440,11 +439,11 @@
|
|
440
439
|
if (camGo && !this.setLookTargetFromConstraint()) {
|
441
440
|
if (this.debugLog)
|
442
441
|
console.log("NO TARGET");
|
443
|
-
const worldPosition = getWorldPosition(camGo.
|
442
|
+
const worldPosition = getWorldPosition(camGo.threeCamera);
|
444
443
|
// Handle case where the camera is in 0 0 0 of the scene
|
445
444
|
// if the look at target is set to the camera position we can't move at all anymore
|
446
445
|
const distanceToCenter = Math.max(.01, worldPosition.length());
|
447
|
-
const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.
|
446
|
+
const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.threeCamera.matrixWorld);
|
448
447
|
this.setLookTargetPosition(forward, true);
|
449
448
|
}
|
450
449
|
if (!this.setLookTargetFromConstraint()) {
|
@@ -64,7 +64,9 @@
|
|
64
64
|
writer.appendLine(`double dynamicFriction = ${colliderSource.sharedMaterial?.dynamicFriction}`);
|
65
65
|
if (mat && mat.bounciness !== undefined)
|
66
66
|
writer.appendLine(`double restitution = ${colliderSource.sharedMaterial?.bounciness}`);
|
67
|
+
// eslint-disable-next-line deprecation/deprecation
|
67
68
|
if (mat && mat.staticFriction !== undefined)
|
69
|
+
// eslint-disable-next-line deprecation/deprecation
|
68
70
|
writer.appendLine(`double staticFriction = ${colliderSource.sharedMaterial?.staticFriction}`);
|
69
71
|
writer.closeBlock( "}" );
|
70
72
|
}
|
@@ -620,7 +620,7 @@
|
|
620
620
|
this._audioTracks.push(audio);
|
621
621
|
if (!audioListener) {
|
622
622
|
// If the scene doesnt have an AudioListener we add one to the main camera
|
623
|
-
audioListener = this.context.mainCameraComponent?.gameObject.
|
623
|
+
audioListener = this.context.mainCameraComponent?.gameObject.addComponent(AudioListener)!;
|
624
624
|
}
|
625
625
|
audio.listener = audioListener.listener;
|
626
626
|
for (let i = 0; i < track.clips.length; i++) {
|
@@ -84,9 +84,9 @@
|
|
84
84
|
async getInstance() {
|
85
85
|
if (this._localInstance) return this._localInstance;
|
86
86
|
|
87
|
-
if (debug) console.log("PlayerSync.createInstance", this.asset?.
|
87
|
+
if (debug) console.log("PlayerSync.createInstance", this.asset?.url);
|
88
88
|
|
89
|
-
if (!this.asset?.asset && !this.asset?.
|
89
|
+
if (!this.asset?.asset && !this.asset?.url) {
|
90
90
|
console.error("PlayerSync: can not create an instance because \"asset\" is not set and or has no URL!");
|
91
91
|
return null;
|
92
92
|
}
|
@@ -105,7 +105,7 @@
|
|
105
105
|
}
|
106
106
|
else {
|
107
107
|
this._localInstance = undefined;
|
108
|
-
console.error("<strong>Failed finding PlayerState on " + this.asset?.
|
108
|
+
console.error("<strong>Failed finding PlayerState on " + this.asset?.url + "</strong>: please make sure the asset has a PlayerState component!");
|
109
109
|
GameObject.destroySynced(instance);
|
110
110
|
}
|
111
111
|
}
|
@@ -13,7 +13,7 @@
|
|
13
13
|
import { showBalloonWarning } from "../../engine/debug/index.js";
|
14
14
|
import { Context } from "../../engine/engine_setup.js";
|
15
15
|
import type { Constructor } from "../../engine/engine_types.js";
|
16
|
-
import {
|
16
|
+
import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
17
17
|
import { Camera } from "../Camera.js";
|
18
18
|
import { _SharpeningEffect } from "./Effects/Sharpening.js";
|
19
19
|
import { PostProcessingEffect, PostProcessingEffectContext } from "./PostProcessingEffect.js";
|
@@ -151,7 +151,7 @@
|
|
151
151
|
const camera = context.mainCameraComponent as Camera;
|
152
152
|
const renderer = context.renderer;
|
153
153
|
const scene = context.scene;
|
154
|
-
const cam = camera.
|
154
|
+
const cam = camera.threeCamera;
|
155
155
|
|
156
156
|
// Store the auto clear setting because the postprocessing composer just disables it
|
157
157
|
// and when we disable postprocessing we want to restore the original setting
|
@@ -165,7 +165,7 @@
|
|
165
165
|
this._composer = new EffectComposer(renderer, {
|
166
166
|
frameBufferType: HalfFloatType,
|
167
167
|
stencilBuffer: true,
|
168
|
-
multisampling: Math.min(isMobileDevice() ? 4 : 8, maxSamples),
|
168
|
+
multisampling: Math.min(DeviceUtilities.isMobileDevice() ? 4 : 8, maxSamples),
|
169
169
|
});
|
170
170
|
}
|
171
171
|
if (context.composer && context.composer !== this._composer) {
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import { Context } from "../../../../engine/engine_setup.js";
|
2
2
|
|
3
|
-
|
4
|
-
export function ensureQuicklookLinkIsCreated(context: Context) : HTMLAnchorElement {
|
3
|
+
export function ensureQuicklookLinkIsCreated(context: Context, supportsQuickLook: boolean) : HTMLAnchorElement {
|
5
4
|
const existingLink = context.domElement.shadowRoot!.querySelector("link[rel='ar']");
|
6
5
|
if (existingLink) return existingLink as HTMLAnchorElement;
|
7
6
|
|
@@ -23,7 +22,15 @@
|
|
23
22
|
|
24
23
|
const button = document.createElement("button");
|
25
24
|
button.id = "open-in-ar";
|
26
|
-
|
25
|
+
if (supportsQuickLook) {
|
26
|
+
button.innerText = "View in AR";
|
27
|
+
button.title = "View this scene in AR. The scene will be exported to USDZ and opened with Apple's QuickLook.";
|
28
|
+
}
|
29
|
+
else {
|
30
|
+
button.innerText = "Download for AR";
|
31
|
+
button.title = "Download this scene for AR. Open the downloaded USDZ file to view it in AR using Apple's QuickLook.";
|
32
|
+
}
|
33
|
+
|
27
34
|
div.appendChild(button);
|
28
35
|
|
29
36
|
const link = document.createElement("a");
|
@@ -31,6 +38,7 @@
|
|
31
38
|
link.style.display = "none";
|
32
39
|
link.rel = "ar";
|
33
40
|
link.href = "";
|
41
|
+
link.target = "_blank";
|
34
42
|
div.appendChild(link);
|
35
43
|
|
36
44
|
const img = document.createElement("img");
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -220,7 +220,7 @@
|
|
220
220
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
221
221
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
222
222
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
223
|
-
|
223
|
+
|
224
224
|
// Register types
|
225
225
|
TypeStore.add("__Ignore", __Ignore);
|
226
226
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { BatchedMesh,
|
1
|
+
import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js";
|
4
4
|
import { $instancingAutoUpdateBounds, $instancingRenderer, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
|
@@ -1,12 +1,12 @@
|
|
1
1
|
import { EquirectangularReflectionMapping, Object3D, Scene, Texture } from "three";
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { AssetReference } from "../engine/engine_addressables.js";
|
4
4
|
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
5
5
|
import { destroy } from "../engine/engine_gameobject.js";
|
6
6
|
import { InputEvents } from "../engine/engine_input.js";
|
7
7
|
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
8
8
|
import { serializable } from "../engine/engine_serialization.js";
|
9
|
-
import {
|
9
|
+
import { getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
10
10
|
import { Behaviour, GameObject } from "./Component.js";
|
11
11
|
import { EventList } from "./EventList.js";
|
12
12
|
|
@@ -57,7 +57,7 @@
|
|
57
57
|
* // Add this component to the root object of a scene loaded by a SceneSwitcher or to the same object as the SceneSwitcher
|
58
58
|
* export class MySceneListener implements ISceneEventListener {
|
59
59
|
* async sceneOpened(sceneSwitcher: SceneSwitcher) {
|
60
|
-
* console.log("Scene opened", sceneSwitcher.currentlyLoadedScene?.
|
60
|
+
* console.log("Scene opened", sceneSwitcher.currentlyLoadedScene?.url);
|
61
61
|
* }
|
62
62
|
* }
|
63
63
|
* ```
|
@@ -91,16 +91,16 @@
|
|
91
91
|
* @example
|
92
92
|
* ```ts
|
93
93
|
* sceneSwitcher.addEventListener("loadscene-start", (e) => {
|
94
|
-
* console.log("Loading scene", e.detail.scene.
|
94
|
+
* console.log("Loading scene", e.detail.scene.url);
|
95
95
|
* });
|
96
96
|
* sceneSwitcher.addEventListener("loadscene-finished", (e) => {
|
97
|
-
* console.log("Finished loading scene", e.detail.scene.
|
97
|
+
* console.log("Finished loading scene", e.detail.scene.url);
|
98
98
|
* });
|
99
99
|
* sceneSwitcher.addEventListener("progress", (e) => {
|
100
100
|
* console.log("Loading progress", e.loaded, e.total);
|
101
101
|
* });
|
102
102
|
* sceneSwitcher.addEventListener("scene-opened", (e) => {
|
103
|
-
* console.log("Scene opened", e.detail.scene.
|
103
|
+
* console.log("Scene opened", e.detail.scene.url);
|
104
104
|
* });
|
105
105
|
* ```
|
106
106
|
*
|
@@ -472,7 +472,7 @@
|
|
472
472
|
// If the parameter is a string we try to resolve the scene by its uri
|
473
473
|
// it's either already known in the scenes array
|
474
474
|
// or we create/get a new AssetReference and try to switch to that
|
475
|
-
const scene = this.scenes?.find(s => s.
|
475
|
+
const scene = this.scenes?.find(s => s.url === index);
|
476
476
|
if (!scene) {
|
477
477
|
// Ok the scene is unknown to the scene switcher
|
478
478
|
// we create a new asset reference (or get an existing one)
|
@@ -531,7 +531,7 @@
|
|
531
531
|
}
|
532
532
|
}
|
533
533
|
|
534
|
-
if (scene.
|
534
|
+
if (scene.url === this.sourceId) {
|
535
535
|
console.warn("SceneSwitcher: can't load own scene - prevent recursive loading", this.sourceId);
|
536
536
|
return false;
|
537
537
|
}
|
@@ -598,7 +598,7 @@
|
|
598
598
|
return false;
|
599
599
|
}
|
600
600
|
if (this._currentIndex === index) {
|
601
|
-
if (debug) console.log("ADD", scene.
|
601
|
+
if (debug) console.log("ADD", scene.url);
|
602
602
|
this._currentScene = scene;
|
603
603
|
|
604
604
|
|
@@ -637,7 +637,7 @@
|
|
637
637
|
let queryParameterValue = index.toString();
|
638
638
|
// unless the user defines that he wants to use the scene name
|
639
639
|
if (this.useSceneName) {
|
640
|
-
queryParameterValue = sceneUriToName(scene.
|
640
|
+
queryParameterValue = sceneUriToName(scene.url);
|
641
641
|
}
|
642
642
|
// save the loaded scene as an url parameter
|
643
643
|
if (this.queryParameterName?.length)
|
@@ -702,7 +702,7 @@
|
|
702
702
|
for (let i = 0; i < this.scenes.length; i++) {
|
703
703
|
const scene = this.scenes[i];
|
704
704
|
if (!scene) continue;
|
705
|
-
if (sceneUriToName(scene.
|
705
|
+
if (sceneUriToName(scene.url).toLowerCase().includes(lowerCaseValue)) {
|
706
706
|
return this.select(i);;
|
707
707
|
}
|
708
708
|
}
|
@@ -738,7 +738,7 @@
|
|
738
738
|
}
|
739
739
|
await this._loadingScenePromise;
|
740
740
|
if (this._isCurrentlyLoading && this.loadingScene?.asset) {
|
741
|
-
if (debug) console.log("Add loading scene", this.loadingScene.
|
741
|
+
if (debug) console.log("Add loading scene", this.loadingScene.url, this.loadingScene.asset)
|
742
742
|
const loadingScene = this.loadingScene.asset as any as Object3D;
|
743
743
|
GameObject.add(loadingScene, this.gameObject);
|
744
744
|
const sceneListener = this.tryGetSceneEventListener(loadingScene);
|
@@ -762,7 +762,7 @@
|
|
762
762
|
private async onEndLoading() {
|
763
763
|
this._isCurrentlyLoading = false;
|
764
764
|
if (this.loadingScene?.asset) {
|
765
|
-
if (debug) console.log("Remove loading scene", this.loadingScene.
|
765
|
+
if (debug) console.log("Remove loading scene", this.loadingScene.url);
|
766
766
|
const obj = this.loadingScene.asset as any as Object3D;
|
767
767
|
// try to find an ISceneEventListener component
|
768
768
|
const sceneListener = this.tryGetSceneEventListener(obj);
|
@@ -903,10 +903,10 @@
|
|
903
903
|
private async awaitLoading() {
|
904
904
|
if (this.asset && !this.asset.isLoaded()) {
|
905
905
|
if (debug)
|
906
|
-
console.log("Preload start: " + this.asset.
|
906
|
+
console.log("Preload start: " + this.asset.url, this.index);
|
907
907
|
await this.asset.preload();
|
908
908
|
if (debug)
|
909
|
-
console.log("Preload finished: " + this.asset.
|
909
|
+
console.log("Preload finished: " + this.asset.url, this.index);
|
910
910
|
}
|
911
911
|
|
912
912
|
const i = this.tasks.indexOf(this);
|
@@ -68,7 +68,7 @@
|
|
68
68
|
ssao.ssaoMaterial.radius = newValue * .1;
|
69
69
|
}
|
70
70
|
this.samples.onValueChanged = newValue => {
|
71
|
-
ssao.samples = newValue;
|
71
|
+
ssao.ssaoMaterial.samples = newValue;
|
72
72
|
}
|
73
73
|
this.color.onValueChanged = newValue => {
|
74
74
|
if (!ssao.color) ssao.color = new Color();
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
5
|
import { RGBAColor } from "../engine/js-extensions/index.js";
|
6
|
-
import { Behaviour
|
6
|
+
import { Behaviour } from "./Component.js";
|
7
7
|
|
8
8
|
/**
|
9
9
|
* The mode of the ShadowCatcher.
|
@@ -20,6 +20,8 @@
|
|
20
20
|
/**
|
21
21
|
* ShadowCatcher can be added to an Object3D to make it render shadows (or light) in the scene. It can also be used to create a shadow mask, or to occlude light.
|
22
22
|
* If the GameObject is a Mesh, it will apply a shadow-catching material to it - otherwise it will create a quad with the shadow-catching material.
|
23
|
+
*
|
24
|
+
* Note that ShadowCatcher meshes are not raycastable by default; if you want them to be raycastable, change the layers in `onEnable()`.
|
23
25
|
* @category Rendering
|
24
26
|
*/
|
25
27
|
export class ShadowCatcher extends Behaviour {
|
@@ -102,7 +102,7 @@
|
|
102
102
|
onEnable(): void {
|
103
103
|
SpatialTrigger.triggers.push(this);
|
104
104
|
if (!this.boxHelper) {
|
105
|
-
this.boxHelper = GameObject.
|
105
|
+
this.boxHelper = GameObject.addComponent(this.gameObject, BoxHelperComponent);
|
106
106
|
this.boxHelper?.showHelper(null, debug as boolean);
|
107
107
|
}
|
108
108
|
}
|
@@ -255,7 +255,7 @@
|
|
255
255
|
renderer.setClearColor(new Color(1, 1, 1));
|
256
256
|
renderer.setRenderTarget(null); // null: direct to Canvas
|
257
257
|
renderer.xr.enabled = false;
|
258
|
-
const cam = this.cam?.
|
258
|
+
const cam = this.cam?.threeCamera;
|
259
259
|
this.context.updateAspect(cam as PerspectiveCamera);
|
260
260
|
const wasPresenting = renderer.xr.isPresenting;
|
261
261
|
renderer.xr.isPresenting = false;
|
@@ -352,7 +352,7 @@
|
|
352
352
|
this.currentObject = followObject;
|
353
353
|
this.view = view;
|
354
354
|
if (!this.follow)
|
355
|
-
this.follow = GameObject.
|
355
|
+
this.follow = GameObject.addComponent(this.cam.gameObject, SmoothFollow);
|
356
356
|
if (!this.target)
|
357
357
|
this.target = new Object3D();
|
358
358
|
followObject.add(this.target);
|
@@ -394,10 +394,11 @@
|
|
394
394
|
}
|
395
395
|
const perspectiveCamera = this.context.mainCamera as PerspectiveCamera;
|
396
396
|
if (perspectiveCamera) {
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
397
|
+
const cam = this.cam.threeCamera;
|
398
|
+
if (cam.near !== perspectiveCamera.near || cam.far !== perspectiveCamera.far) {
|
399
|
+
cam.near = perspectiveCamera.near;
|
400
|
+
cam.far = perspectiveCamera.far;
|
401
|
+
cam.updateProjectionMatrix();
|
401
402
|
}
|
402
403
|
}
|
403
404
|
|
@@ -1097,7 +1097,7 @@
|
|
1097
1097
|
|
1098
1098
|
} else {
|
1099
1099
|
|
1100
|
-
console.warn( 'NeedleUSDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)', name );
|
1100
|
+
console.warn( 'NeedleUSDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)', material?.name );
|
1101
1101
|
|
1102
1102
|
}
|
1103
1103
|
|
@@ -1439,6 +1439,19 @@
|
|
1439
1439
|
const isSkinnedMesh = geometry && geometry.isBufferGeometry && geometry.attributes.skinIndex !== undefined && geometry.attributes.skinIndex.count > 0;
|
1440
1440
|
const objType = isSkinnedMesh ? 'SkelRoot' : 'Xform';
|
1441
1441
|
const _apiSchemas = new Array<string>();
|
1442
|
+
|
1443
|
+
// Specific case: the material is white unlit, the mesh has vertex colors, so we can
|
1444
|
+
// export as displayColor directly
|
1445
|
+
const isUnlitDisplayColor =
|
1446
|
+
material && material instanceof MeshBasicMaterial &&
|
1447
|
+
material.color && material.color.r === 1 && material.color.g === 1 && material.color.b === 1 &&
|
1448
|
+
!material.map && material.opacity === 1 &&
|
1449
|
+
geometry?.attributes.color;
|
1450
|
+
|
1451
|
+
if (geometry?.attributes.color && !isUnlitDisplayColor) {
|
1452
|
+
console.warn("NeedleUSDZExporter: Geometry has vertex colors. Vertex colors will only be shown in QuickLook for unlit materials with white color and no texture. Otherwise, they will be ignored.", model.displayName);
|
1453
|
+
}
|
1454
|
+
|
1442
1455
|
writer.appendLine();
|
1443
1456
|
if ( geometry ) {
|
1444
1457
|
writer.beginBlock( `def ${objType} "${name}"`, "(", false );
|
@@ -1448,7 +1461,8 @@
|
|
1448
1461
|
writer.appendLine(`prepend references = @./geometries/${getGeometryName(geometry, name)}.usda@</Geometry_doubleSided>`);
|
1449
1462
|
else
|
1450
1463
|
writer.appendLine(`prepend references = @./geometries/${getGeometryName(geometry, name)}.usda@</Geometry>`);
|
1451
|
-
|
1464
|
+
if (!isUnlitDisplayColor)
|
1465
|
+
_apiSchemas.push("MaterialBindingAPI");
|
1452
1466
|
if (isSkinnedMesh)
|
1453
1467
|
_apiSchemas.push("SkelBindingAPI");
|
1454
1468
|
}
|
@@ -1475,8 +1489,10 @@
|
|
1475
1489
|
}
|
1476
1490
|
|
1477
1491
|
if ( geometry && material ) {
|
1478
|
-
|
1479
|
-
|
1492
|
+
if (!isUnlitDisplayColor) {
|
1493
|
+
const materialName = getMaterialName(material);
|
1494
|
+
writer.appendLine( `rel material:binding = </StageRoot/Materials/${materialName}>` );
|
1495
|
+
}
|
1480
1496
|
|
1481
1497
|
// Turns out QuickLook / RealityKit doesn't support the doubleSided attribute, so we
|
1482
1498
|
// work around that by emitting additional indices above, and then we shouldn't emit the attribute either as geometry is
|
@@ -1741,7 +1757,7 @@
|
|
1741
1757
|
|
1742
1758
|
const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count;
|
1743
1759
|
|
1744
|
-
return Array( count / 3 ).fill( 3 ).join( ', ' );
|
1760
|
+
return Array( Math.floor(count / 3) ).fill( 3 ).join( ', ' );
|
1745
1761
|
|
1746
1762
|
}
|
1747
1763
|
|
@@ -5,9 +5,10 @@
|
|
5
5
|
import { hasProLicense } from "../../../engine/engine_license.js";
|
6
6
|
import { serializable } from "../../../engine/engine_serialization.js";
|
7
7
|
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
|
8
|
-
import {
|
8
|
+
import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
|
9
9
|
import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js";
|
10
10
|
import { InternalUSDZRegistry } from "../../../engine/xr/usdz.js"
|
11
|
+
import { InstancingHandler } from "../../../engine-components/RendererInstancing.js";
|
11
12
|
import { Behaviour, GameObject } from "../../Component.js";
|
12
13
|
import { ContactShadows } from "../../ContactShadows.js";
|
13
14
|
import { GroundProjectedEnv } from "../../GroundProjection.js";
|
@@ -137,15 +138,10 @@
|
|
137
138
|
window.addEventListener("keydown", (evt) => {
|
138
139
|
switch (evt.key) {
|
139
140
|
case "t":
|
140
|
-
this.
|
141
|
+
this.exportAndOpen();
|
141
142
|
break;
|
142
143
|
}
|
143
144
|
});
|
144
|
-
if (isMobileDevice()) {
|
145
|
-
setTimeout(() => {
|
146
|
-
this.exportAsync();
|
147
|
-
}, 2000)
|
148
|
-
}
|
149
145
|
}
|
150
146
|
|
151
147
|
// fall back to this object or to the scene if it's empty and doesn't have a mesh
|
@@ -165,14 +161,14 @@
|
|
165
161
|
|
166
162
|
/** @internal */
|
167
163
|
onEnable() {
|
168
|
-
const
|
169
|
-
const
|
170
|
-
if (debug ||
|
164
|
+
const supportsQuickLook = DeviceUtilities.supportsQuickLookAR();
|
165
|
+
const ios = DeviceUtilities.isiOS() || DeviceUtilities.isiPad();
|
166
|
+
if (!this.button && (debug || supportsQuickLook || ios)) {
|
171
167
|
if (this.allowCreateQuicklookButton)
|
172
168
|
this.button = this.createQuicklookButton();
|
173
169
|
|
174
170
|
this.lastCallback = this.quicklookCallback.bind(this);
|
175
|
-
this.link = ensureQuicklookLinkIsCreated(this.context);
|
171
|
+
this.link = ensureQuicklookLinkIsCreated(this.context, supportsQuickLook);
|
176
172
|
this.link.addEventListener('message', this.lastCallback);
|
177
173
|
}
|
178
174
|
if (debug)
|
@@ -186,11 +182,6 @@
|
|
186
182
|
onDisable() {
|
187
183
|
this.button?.remove();
|
188
184
|
this.link?.removeEventListener('message', this.lastCallback);
|
189
|
-
// const ios = isiOS()
|
190
|
-
// const safari = isSafari();
|
191
|
-
// if (debug || (ios && safari)) {
|
192
|
-
// this.removeQuicklookButton();
|
193
|
-
// }
|
194
185
|
if (debug)
|
195
186
|
showBalloonMessage("USDZ Exporter disabled: " + this.name);
|
196
187
|
|
@@ -226,7 +217,7 @@
|
|
226
217
|
name += "MadeWithNeedle";
|
227
218
|
}
|
228
219
|
|
229
|
-
if (!this.link) this.link = ensureQuicklookLinkIsCreated(this.context);
|
220
|
+
if (!this.link) this.link = ensureQuicklookLinkIsCreated(this.context, DeviceUtilities.supportsQuickLookAR());
|
230
221
|
|
231
222
|
// ability to specify a custom USDZ file to be used instead of a dynamic one
|
232
223
|
if (this.customUsdzFile) {
|
@@ -248,10 +239,14 @@
|
|
248
239
|
|
249
240
|
if (debug) console.log("USDZ generation done. Downloading as " + name);
|
250
241
|
|
251
|
-
// TODO detect QuickLook availability
|
242
|
+
// TODO Potentially we have to detect QuickLook availability here,
|
243
|
+
// and download the file instead. But browsers keep changing how they deal with non-user-initiated downloads...
|
252
244
|
// https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/#:~:text=inside%20the%20anchor.-,Feature%20Detection,-To%20detect%20support
|
253
|
-
|
254
|
-
|
245
|
+
/*
|
246
|
+
if (!DeviceUtilities.supportsQuickLookAR())
|
247
|
+
this.download(blob, name);
|
248
|
+
else
|
249
|
+
*/
|
255
250
|
this.openInQuickLook(blob, name);
|
256
251
|
|
257
252
|
return blob;
|
@@ -389,6 +384,7 @@
|
|
389
384
|
//@ts-ignore
|
390
385
|
exporter.debug = debug;
|
391
386
|
exporter.pruneUnusedNodes = !debugUsdzPruning;
|
387
|
+
const instancedRenderers = InstancingHandler.instance.objs.map(x => x.batchedMesh);
|
392
388
|
exporter.keepObject = (object) => {
|
393
389
|
let keep = true;
|
394
390
|
// This explicitly removes geometry and material data from disabled renderers.
|
@@ -396,6 +392,9 @@
|
|
396
392
|
// here, we have an active object with a disabled renderer.
|
397
393
|
const renderer = GameObject.getComponent(object, Renderer);
|
398
394
|
if (renderer && !renderer.enabled) keep = false;
|
395
|
+
// Check if this is an instancing container.
|
396
|
+
// Instances are already included in the export.
|
397
|
+
if (keep && instancedRenderers.includes(object as any)) keep = false;
|
399
398
|
if (keep && GameObject.getComponentInParent(object, ContactShadows)) keep = false;
|
400
399
|
if (keep && GameObject.getComponentInParent(object, GroundProjectedEnv)) keep = false;
|
401
400
|
if (debug && !keep) console.log("USDZExporter: Discarding object", object);
|
@@ -504,7 +503,7 @@
|
|
504
503
|
this.link.addEventListener('message', this.lastCallback);
|
505
504
|
}
|
506
505
|
|
507
|
-
//
|
506
|
+
// Open QuickLook
|
508
507
|
this.link.download = name + ".usdz";
|
509
508
|
this.link.click();
|
510
509
|
|
@@ -611,6 +610,7 @@
|
|
611
610
|
}
|
612
611
|
else if (sessionRoot) {
|
613
612
|
arScale = sessionRoot.arScale;
|
613
|
+
// eslint-disable-next-line deprecation/deprecation
|
614
614
|
_invertForward = sessionRoot.invertForward;
|
615
615
|
}
|
616
616
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import { RoomEvents } from "../engine/engine_networking.js";
|
4
4
|
import { disposeStream, NetworkedStreamEvents, NetworkedStreams, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js"
|
5
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
-
import {
|
6
|
+
import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
|
7
7
|
import { delay } from "../engine/engine_utils.js";
|
8
8
|
import { getIconElement } from "../engine/webcomponents/icons.js";
|
9
9
|
import { Behaviour } from "./Component.js";
|
@@ -137,7 +137,7 @@
|
|
137
137
|
this.updateButton();
|
138
138
|
return false;
|
139
139
|
}
|
140
|
-
else if (!await microphonePermissionsGranted()) {
|
140
|
+
else if (!await DeviceUtilities.microphonePermissionsGranted()) {
|
141
141
|
console.error("Cannot connect to voice chat - microphone permissions not granted");
|
142
142
|
this.updateButton();
|
143
143
|
return false;
|
@@ -154,7 +154,7 @@
|
|
154
154
|
}
|
155
155
|
else {
|
156
156
|
this.updateButton();
|
157
|
-
if (!await microphonePermissionsGranted()) {
|
157
|
+
if (!await DeviceUtilities.microphonePermissionsGranted()) {
|
158
158
|
showBalloonError("Microphone permissions not granted: Please grant microphone permissions to use voice chat");
|
159
159
|
}
|
160
160
|
else console.error("VOIP: Could not get audio stream - please make sure to connect an audio device and grant microphone permissions");
|
@@ -207,7 +207,7 @@
|
|
207
207
|
this.disconnect({ remember: true });
|
208
208
|
}
|
209
209
|
else this.connect();
|
210
|
-
microphonePermissionsGranted().then(res => {
|
210
|
+
DeviceUtilities.microphonePermissionsGranted().then(res => {
|
211
211
|
if (!res) showBalloonWarning("<strong>Microphone permissions not granted</strong>. Please allow your browser to use the microphone to be able to talk. Click on the button on the left side of your browser's address bar to allow microphone permissions.");
|
212
212
|
})
|
213
213
|
});
|
@@ -223,7 +223,7 @@
|
|
223
223
|
this._menubutton.title = this.isSending ? "Click to disable your microphone" : "Click to enable your microphone";
|
224
224
|
let label = this.isSending ? "" : "";
|
225
225
|
let icon = this.isSending ? "mic" : "mic_off";
|
226
|
-
const hasPermission = await microphonePermissionsGranted();
|
226
|
+
const hasPermission = await DeviceUtilities.microphonePermissionsGranted();
|
227
227
|
if (!hasPermission) {
|
228
228
|
label = "No Permission";
|
229
229
|
icon = "mic_off";
|
@@ -284,7 +284,7 @@
|
|
284
284
|
|
285
285
|
// NE-5445, on iOS after calling `getUserMedia` it automatically switches the audio to the built-in microphone and speakers even if headphones are connected
|
286
286
|
// if there's no device selected explictly we will try to automatically select an external device
|
287
|
-
if (isiOS() && audio?.deviceId === undefined) {
|
287
|
+
if (DeviceUtilities.isiOS() && audio?.deviceId === undefined) {
|
288
288
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
289
289
|
// select anything that doesn't have "iPhone" is likely "AirPods" or other bluetooth headphones
|
290
290
|
const nonBuiltInAudioSource = devices.find((device) => (device.kind === "audioinput" || device.kind === "audiooutput") && !device.label.includes("iPhone"));
|
@@ -4,12 +4,10 @@
|
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
5
|
import { Context } from "../../engine/engine_context.js";
|
6
6
|
import { destroy, instantiate } from "../../engine/engine_gameobject.js";
|
7
|
-
import { Gizmos } from "../../engine/engine_gizmos.js";
|
8
7
|
import { InputEventQueue, NEPointerEvent } from "../../engine/engine_input.js";
|
9
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
10
8
|
import { getBoundingBox, getTempVector } from "../../engine/engine_three_utils.js";
|
11
9
|
import type { IComponent, IGameObject } from "../../engine/engine_types.js";
|
12
|
-
import {
|
10
|
+
import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
13
11
|
import { NeedleXRController, type NeedleXREventArgs, type NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
|
14
12
|
import { Behaviour, GameObject } from "../Component.js";
|
15
13
|
|
@@ -695,7 +693,7 @@
|
|
695
693
|
// if a user starts swiping in the top area of the screen
|
696
694
|
// which might be a gesture to open the menu
|
697
695
|
// we ignore it
|
698
|
-
const ignore = isAndroidDevice() && touch.clientY < window.innerHeight * .1;
|
696
|
+
const ignore = DeviceUtilities.isAndroidDevice() && touch.clientY < window.innerHeight * .1;
|
699
697
|
if (!this.prev.has(touch.identifier))
|
700
698
|
this.prev.set(touch.identifier, {
|
701
699
|
ignore,
|
@@ -4,10 +4,9 @@
|
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
5
|
import { findObjectOfType } from "../../engine/engine_components.js";
|
6
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
7
|
-
import { delayForFrames,
|
7
|
+
import { delayForFrames, DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
8
8
|
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
9
9
|
import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
|
10
|
-
import { getIconElement } from "../../engine/webcomponents/icons.js";
|
11
10
|
import { WebXRButtonFactory } from "../../engine/webcomponents/WebXRButtons.js";
|
12
11
|
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
|
13
12
|
import { Behaviour, GameObject } from "../Component.js";
|
@@ -372,7 +371,7 @@
|
|
372
371
|
|
373
372
|
if (this.createARButton || this.createVRButton || this.useQuicklookExport) {
|
374
373
|
// Quicklook / iOS
|
375
|
-
if ((isiOS() && isSafari()) || debugQuicklook) {
|
374
|
+
if ((DeviceUtilities.isiOS() && DeviceUtilities.isSafari()) || debugQuicklook) {
|
376
375
|
if (this.useQuicklookExport) {
|
377
376
|
const usdzExporter = GameObject.findObjectOfType(USDZExporter);
|
378
377
|
if (!usdzExporter || (usdzExporter && usdzExporter.allowCreateQuicklookButton)) {
|
@@ -392,7 +391,7 @@
|
|
392
391
|
}
|
393
392
|
}
|
394
393
|
|
395
|
-
if (this.createSendToQuestButton && !isQuest()) {
|
394
|
+
if (this.createSendToQuestButton && !DeviceUtilities.isQuest()) {
|
396
395
|
NeedleXRSession.isVRSupported().then(supported => {
|
397
396
|
if (!supported) {
|
398
397
|
const button = this.getButtonsFactory().createSendToQuestButton();
|
@@ -2,7 +2,7 @@
|
|
2
2
|
import { isDevEnvironment, showBalloonMessage } from "../debug/index.js";
|
3
3
|
import { findObjectOfType } from "../engine_components.js";
|
4
4
|
import { Context } from "../engine_setup.js";
|
5
|
-
import {
|
5
|
+
import { DeviceUtilities } from "../engine_utils.js";
|
6
6
|
import { NeedleXRSession } from "../engine_xr.js";
|
7
7
|
import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
|
8
8
|
import { ButtonsFactory } from "./buttons.js";
|
@@ -56,7 +56,14 @@
|
|
56
56
|
const button = document.createElement("button");
|
57
57
|
this._quicklookButton = button;
|
58
58
|
button.dataset["needle"] = "quicklook-button";
|
59
|
-
|
59
|
+
const supportsQuickLook = DeviceUtilities.supportsQuickLookAR();
|
60
|
+
// we can immediately enter this scene, because the platform supports rel="ar" links
|
61
|
+
if (supportsQuickLook) {
|
62
|
+
button.innerText = "View in AR";
|
63
|
+
}
|
64
|
+
else {
|
65
|
+
button.innerText = "Download for AR";
|
66
|
+
}
|
60
67
|
button.prepend(getIconElement("view_in_ar"));
|
61
68
|
|
62
69
|
let createdExporter = false;
|
@@ -118,7 +125,7 @@
|
|
118
125
|
button.title = "WebXR requires a secure connection (HTTPS)";
|
119
126
|
}
|
120
127
|
|
121
|
-
if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
|
128
|
+
if (!DeviceUtilities.isMozillaXR()) // WebXR Viewer can't attach events before session start
|
122
129
|
navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
|
123
130
|
|
124
131
|
return button;
|
@@ -152,7 +159,7 @@
|
|
152
159
|
button.title = "WebXR requires a secure connection (HTTPS)";
|
153
160
|
}
|
154
161
|
|
155
|
-
if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
|
162
|
+
if (!DeviceUtilities.isMozillaXR()) // WebXR Viewer can't attach events before session start
|
156
163
|
navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
|
157
164
|
|
158
165
|
return button;
|
@@ -181,7 +188,7 @@
|
|
181
188
|
this.listenToXRSessionState(button);
|
182
189
|
this.hideElementDuringXRSession(button);
|
183
190
|
// make sure to hide the button when we have VR support directly on the device
|
184
|
-
if (!isMozillaXR()) { // WebXR Viewer can't attach events before session start
|
191
|
+
if (!DeviceUtilities.isMozillaXR()) { // WebXR Viewer can't attach events before session start
|
185
192
|
navigator.xr?.addEventListener("devicechange", () => {
|
186
193
|
if (navigator.xr?.isSessionSupported("immersive-vr")) {
|
187
194
|
button.style.display = "none";
|
@@ -366,6 +366,7 @@
|
|
366
366
|
this.makeOccluder(newPlane, newPlane.material, this.occluder);
|
367
367
|
}
|
368
368
|
else if (newPlane instanceof Group) {
|
369
|
+
// We want to process only one level of children on purpose here
|
369
370
|
for (const ch of newPlane.children) {
|
370
371
|
if (ch instanceof Mesh) {
|
371
372
|
disposeObjectResources(ch.geometry);
|
@@ -404,7 +405,7 @@
|
|
404
405
|
_all.set(data, planeContext);
|
405
406
|
|
406
407
|
if (debug) {
|
407
|
-
console.log("New plane detected, id=" + planeContext.id, planeContext);
|
408
|
+
console.log("New plane detected, id=" + planeContext.id, planeContext, { hasCollider: !!mc, isGroup: newPlane instanceof Group });
|
408
409
|
}
|
409
410
|
|
410
411
|
try {
|
@@ -560,6 +561,10 @@
|
|
560
561
|
geometry.setAttribute('normal', new BufferAttribute(new Float32Array(normals), 3));
|
561
562
|
geometry.setIndex(indices);
|
562
563
|
|
564
|
+
// update bounds
|
565
|
+
geometry.computeBoundingBox();
|
|