Needle Engine

Changes between version 3.49.0-beta and 3.50.0-beta
Files changed (64) hide show
  1. plugins/vite/asap.js +2 -2
  2. plugins/vite/poster-client.js +17 -2
  3. plugins/vite/pwa.js +113 -9
  4. src/engine-components/AudioListener.ts +2 -2
  5. src/engine-components/AudioSource.ts +5 -6
  6. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +36 -7
  7. src/engine-components/ui/Button.ts +1 -1
  8. src/engine-components/Camera.ts +4 -4
  9. src/engine-components/CharacterController.ts +3 -3
  10. src/engine-components/Component.ts +4 -5
  11. src/engine/debug/debug_console.ts +2 -2
  12. src/engine/debug/debug.ts +1 -0
  13. src/engine-components/DeviceFlag.ts +3 -3
  14. src/engine/engine_addressables.ts +19 -19
  15. src/engine/engine_application.ts +1 -1
  16. src/engine/engine_audio.ts +1 -1
  17. src/engine/engine_context.ts +20 -19
  18. src/engine/engine_element_overlay.ts +4 -4
  19. src/engine/engine_input.ts +2 -2
  20. src/engine/engine_networking_auto.ts +0 -1
  21. src/engine/engine_networking_instantiate.ts +6 -3
  22. src/engine/engine_networking.ts +1 -2
  23. src/engine/engine_physics_rapier.ts +3 -3
  24. src/engine/engine_scenelighting.ts +2 -2
  25. src/engine/engine_serialization_builtin_serializer.ts +1 -1
  26. src/engine/engine_three_utils.ts +4 -5
  27. src/engine/engine_types.ts +0 -6
  28. src/engine/engine_utils_format.ts +1 -1
  29. src/engine/engine_utils_screenshot.ts +27 -5
  30. src/engine/engine_utils.ts +118 -47
  31. src/engine-components/ui/EventSystem.ts +1 -1
  32. src/engine-components/FlyControls.ts +1 -1
  33. src/engine-components/ui/InputField.ts +3 -3
  34. src/engine/extensions/NEEDLE_lighting_settings.ts +2 -3
  35. src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
  36. src/asap/needle-asap.ts +2 -2
  37. src/engine/webcomponents/needle menu/needle-menu-spatial.ts +4 -4
  38. src/engine/webcomponents/needle menu/needle-menu.ts +4 -2
  39. src/engine-components/NeedleMenu.ts +2 -2
  40. src/engine/xr/NeedleXRSession.ts +8 -7
  41. src/engine-components/NestedGltf.ts +3 -3
  42. src/engine/js-extensions/Object3D.ts +25 -29
  43. src/engine-components/utils/OpenURL.ts +5 -5
  44. src/engine-components/OrbitControls.ts +7 -8
  45. src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +2 -0
  46. src/engine-components/timeline/PlayableDirector.ts +1 -1
  47. src/engine-components-experimental/networking/PlayerSync.ts +3 -3
  48. src/engine-components/postprocessing/PostProcessingHandler.ts +3 -3
  49. src/engine-components/export/usdz/utils/quicklook.ts +11 -3
  50. src/engine/codegen/register_types.ts +2 -2
  51. src/engine-components/RendererInstancing.ts +1 -1
  52. src/engine-components/SceneSwitcher.ts +15 -15
  53. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts +1 -1
  54. src/engine-components/ShadowCatcher.ts +3 -1
  55. src/engine-components/SpatialTrigger.ts +1 -1
  56. src/engine-components/SpectatorCamera.ts +7 -6
  57. src/engine-components/export/usdz/ThreeUSDZExporter.ts +21 -5
  58. src/engine-components/export/usdz/USDZExporter.ts +21 -21
  59. src/engine-components/Voip.ts +6 -6
  60. src/engine-components/webxr/WebARSessionRoot.ts +2 -4
  61. src/engine-components/webxr/WebXR.ts +3 -4
  62. src/engine/webcomponents/WebXRButtons.ts +12 -5
  63. src/engine-components/webxr/WebXRPlaneTracking.ts +6 -1
  64. src/engine-components/webxr/controllers/XRControllerModel.ts +1 -1
plugins/vite/asap.js CHANGED
@@ -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
 
plugins/vite/poster-client.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  async function generatePoster() {
3
- const { screenshot } = await import("@needle-tools/engine");
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) {
plugins/vite/pwa.js CHANGED
@@ -83,19 +83,69 @@
83
83
  apply: 'build',
84
84
  enforce: "post",
85
85
  config(viteConfig) {
86
- // Remove the gzip plugin
87
- if(viteConfig.plugins){
88
- for(let i = viteConfig.plugins.length-1; i >= 0; i--) {
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
- // override the closeBundle method so gzip is not be applied
92
- // The PWA files should not have gzip compression – this will mess up with the precache and the service worker.
93
- // If gzip is wanted, the server should serve files with gzip compression on the fly.
94
- plugin.closeBundle = () => {
95
- console.log("[needle-pwa] gzip compression is disabled because you're building a PWA. This will otherwise mess up with the precache and the service worker.");
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.
src/engine-components/AudioListener.ts CHANGED
@@ -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?.cam) {
52
- cam.cam.add(listener);
51
+ if (cam?.threeCamera) {
52
+ cam.threeCamera.add(listener);
53
53
  }
54
54
  else {
55
55
  this.gameObject.add(listener);
src/engine-components/AudioSource.ts CHANGED
@@ -1,17 +1,16 @@
1
- import { Audio, AudioContext, AudioLoader, PositionalAudio, Vector3 } from "three";
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 { getTempVector } from "../engine/engine_three_utils.js";
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 = utils.getParam("debugaudio");
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.addNewComponent(this.context.mainCamera, AudioListener);
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 || utils.isMobileDevice()) {
300
+ if (this.playInBackground === false || DeviceUtilities.isMobileDevice()) {
302
301
  this.wasPlaying = this.isPlaying;
303
302
  if (this.isPlaying) {
304
303
  this.pause();
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -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.addNewComponent(ObjectRaycaster);
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.addNewComponent(AudioSource);
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
- { animationSequence: Array<RegisteredAnimationInfo>, animationLoopAfterSequence: Array<RegisteredAnimationInfo> } | undefined {
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
  }
src/engine-components/ui/Button.ts CHANGED
@@ -219,7 +219,7 @@
219
219
  yield;
220
220
  yield;
221
221
  if (this._requestedAnimatorTrigger == requestedTriggerId) {
222
- this.animator?.SetTrigger(requestedTriggerId);
222
+ this.animator?.setTrigger(requestedTriggerId);
223
223
  }
224
224
  }
225
225
 
src/engine-components/Camera.ts CHANGED
@@ -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 { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
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, getTempVector, getWorldPosition } from "../engine/engine_three_utils.js";
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.cam;
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.cam.projectionMatrix, this.cam.matrixWorldInverse);
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);
src/engine-components/CharacterController.ts CHANGED
@@ -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, GameObject } from "./Component.js";
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.addNewComponent(Rigidbody) as Rigidbody;
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.addNewComponent(CapsuleCollider) as CapsuleCollider;
47
+ collider = this.gameObject.addComponent(CapsuleCollider) as CapsuleCollider;
48
48
 
49
49
  collider.center.copy(this.center);
50
50
  collider.radius = this.radius;
src/engine-components/Component.ts CHANGED
@@ -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, addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../engine/engine_components.js";
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 Use `addComponent` */
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
  }
src/engine/debug/debug_console.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isLocalNetwork } from "../engine_networking_utils.js";
2
- import { getParam, isMobileDevice, isQuest } from "../engine_utils.js";
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)
src/engine/debug/debug.ts CHANGED
@@ -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";
src/engine-components/DeviceFlag.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { serializable } from "../engine/engine_serialization_decorator.js";
3
- import { isMobileDevice } from "../engine/engine_utils.js";
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
  };
src/engine/engine_addressables.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Group, Object3D, Texture, TextureLoader } from "three";
2
2
 
3
- import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
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 { assign, SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
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.uri) return ref;
61
- if (!this._assetReferences[ref.uri]) {
62
- this._assetReferences[ref.uri] = 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.uri) return;
74
- delete this._assetReferences[ref.uri];
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.uri) {
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.uri);
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.uri, null);
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.uri);
276
- this._loading = getLoader().loadSync(context, this._hashedUri, this.uri, null, prog => {
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.uri, "parent:", opts);
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.uri);
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.uri + " in the same frame (" + count + ")");
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.uri, count);
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.uri;
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.uri, count)
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.uri);
396
+ else if (debug) console.warn("Failed to load asset", this.url);
397
397
  return null;
398
398
  }
399
399
 
src/engine/engine_application.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isDevEnvironment } from "./debug/debug.js";
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
 
src/engine/engine_audio.ts CHANGED
@@ -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.registerWaitForAllowAudio(() => {
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", () => {
src/engine/engine_context.ts CHANGED
@@ -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 * as utils from "./engine_utils.js";
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 = utils.getParam("debugcontext");
41
- const stats = utils.getParam("stats");
42
- const debugActive = utils.getParam("debugactive");
43
- const debugframerate = utils.getParam("debugframerate");
44
- const debugCoroutine = utils.getParam("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
- * start() {
133
- * console.log("Hello from MyScript");
134
- * this.context.scene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial()));
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: (utils.isiOS() || utils.isMacOS()) ? "default" : "high-performance",
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.cam)
357
+ if (!cam.threeCamera)
358
358
  cam.buildCamera();
359
- return cam.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 = utils.deepClone(opts);
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.cam) cam.buildCamera(); // < to build camera
691
- if (!cam.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.cam as PerspectiveCamera;
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 && utils.isMacOS())
1416
+ if (this.isInXR && DeviceUtilities.isMacOS())
1416
1417
  this.renderer.clearDepth();
1417
1418
  this.renderer.render(this.scene, camera);
1418
1419
  }
src/engine/engine_element_overlay.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context } from "./engine_setup.js";
2
- import { getParam, isMobileDevice, isMozillaXR } from "./engine_utils.js";
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
 
src/engine/engine_input.ts CHANGED
@@ -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, isMozillaXR } from './engine_utils.js';
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;
src/engine/engine_networking_auto.ts CHANGED
@@ -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";
src/engine/engine_networking_instantiate.ts CHANGED
@@ -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
- console.warn("Can not send destroy: not connected", obj.guid);
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);
src/engine/engine_networking.ts CHANGED
@@ -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/debug.js';
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
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyDesc, RigidBodyType, ShapeColliderTOI, ShapeType, World } from '@dimforge/rapier3d-compat';
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, type PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
6
- import { isDevEnvironment } from './debug/debug.js';
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';
src/engine/engine_scenelighting.ts CHANGED
@@ -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.uri;
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.uri;
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) {
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -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
  }
src/engine/engine_three_utils.ts CHANGED
@@ -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 THREE.FBXLoader. It will apply some conversions to the material and will assign a MeshStandardMaterial to the object.
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;
src/engine/engine_types.ts CHANGED
@@ -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
src/engine/engine_utils_format.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isDevEnvironment } from "./debug/debug.js";
1
+ import { isDevEnvironment } from "./debug/index.js";
2
2
  import { getParam } from "./engine_utils.js";
3
3
 
4
4
  const debug = getParam("debugfileformat");
src/engine/engine_utils_screenshot.ts CHANGED
@@ -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 {ScreenshotOptions} opts
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
- export function screenshot2(opts: ScreenshotOptionsDataUrl | ScreenshotOptionsTexture): Texture | string | null {
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.cam;
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);
src/engine/engine_utils.ts CHANGED
@@ -564,11 +564,12 @@
564
564
  }
565
565
 
566
566
  /**
567
- * Utility function to detect certain device types (mobile, desktop) or browsers
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
- /** Is MacOS or Windows (and not hololens) */
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
- * @deprecated use {@link isiPad} instead
590
- */
591
+
592
+ /** @deprecated use {@link isiPad} instead */
591
593
  export function isIPad() {
592
- return /iPad/.test(navigator.userAgent);
594
+ return isiPad();
593
595
  }
594
- /**
595
- * @returns `true` if it's an iPad by checking the navigator.userAgent
596
- */
596
+
597
+ let __isiPad: boolean | undefined;
598
+ /** @returns `true` if we're currently on an iPad */
597
599
  export function isiPad() {
598
- return /iPad/.test(navigator.userAgent);
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
- return /Android/.test(navigator.userAgent);
607
+ if (__isAndroidDevice !== undefined) return __isAndroidDevice;
608
+ return __isAndroidDevice = /Android/.test(navigator.userAgent);
602
609
  }
603
- /** @returns `true` if we're currently using the mozilla XR browser */
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
- return /WebXRViewer\//i.test(navigator.userAgent);
614
+ if (__isMozillaXR !== undefined) return __isMozillaXR;
615
+ return __isMozillaXR = /WebXRViewer\//i.test(navigator.userAgent);
606
616
  }
607
- let __isMacOs: boolean | undefined;
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 (__isMacOs !== undefined) return __isMacOs;
622
+ if (__isMacOS !== undefined) return __isMacOS;
611
623
  if (navigator.userAgentData) {
612
624
  // Use modern UA Client Hints API if available
613
- return __isMacOs = navigator.userAgentData.platform === 'macOS';
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 __isMacOs = userAgent.includes('mac os x') || userAgent.includes('macintosh');
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
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
648
+ if (__isSafari !== undefined) return __isSafari;
649
+ __isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
650
+ return __isSafari;
633
651
  }
634
652
 
635
- /** @returns true if we're currently running on quest */
653
+ let __isQuest: boolean | undefined;
654
+ /** @returns `true` for Meta Quest devices and browser. */
636
655
  export function isQuest() {
637
- return navigator.userAgent.includes("OculusBrowser");
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
- /** Is MacOS or Windows (and not hololens) */
715
+ /**
716
+ * @deprecated use {@link DeviceUtilities.isDesktop} instead
717
+ */
661
718
  export function isDesktop() {
662
719
  return DeviceUtilities.isDesktop();
663
720
  }
664
721
 
665
- /** @returns `true` if it's a phone or tablet */
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
- /** @returns `true` if we're currently using the mozilla XR browser */
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
- /** @returns `true` for iOS devices like iPad, iPhone, iPod... */
755
+ /** @deprecated use {@link DeviceUtilities.isiOS} instead */
694
756
  export function isiOS() {
695
757
  return DeviceUtilities.isiOS();
696
758
  }
697
759
 
698
- /** @returns `true` if we're currently on safari */
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
- /** @returns `true` if the user allowed to use the microphone */
770
+ /** @deprecated use {@link DeviceUtilities.microphonePermissionsGranted} instead */
708
771
  export async function microphonePermissionsGranted() {
709
- try {
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
- const json = await res.json();
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
- export async function getIpAndLocation(): Promise<IpAndLocation> {
751
- const res = (await fetch("https://api.db-ip.com/v2/free/self").catch(() => null))!;
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
- /** Register a callback when a attribute changes */
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;
src/engine-components/ui/EventSystem.ts CHANGED
@@ -48,7 +48,7 @@
48
48
  if (sys.context === context) return; // exists
49
49
  }
50
50
  const go = new Object3D();
51
- GameObject.addNewComponent(go, EventSystem);
51
+ GameObject.addComponent(go, EventSystem);
52
52
  context.scene.add(go);
53
53
  }
54
54
 
src/engine-components/FlyControls.ts CHANGED
@@ -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)?.cam;
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;
src/engine-components/ui/InputField.ts CHANGED
@@ -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 { getParam, isiOS } from "../../engine/engine_utils.js";
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
src/engine/extensions/NEEDLE_lighting_settings.ts CHANGED
@@ -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.addNewComponent(obj, SceneLightSettings, {}, false);
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.addNewComponent(lightSettings, SceneLightSettings, {}, false);
60
+ settings = GameObject.addComponent(lightSettings, SceneLightSettings, {}, { callAwake: false });
62
61
  }
63
62
  settings.sourceId = this.sourceId;
64
63
  settings.ambientIntensity = ext.ambientIntensity;
src/engine/extensions/NEEDLE_lightmaps.ts CHANGED
@@ -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/debug.js";
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";
src/asap/needle-asap.ts CHANGED
@@ -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.");
src/engine/webcomponents/needle menu/needle-menu-spatial.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Mesh, Object3D, Quaternion, TextureLoader, Vector3, Vector4 } from "three";
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, isDesktop } from "../../engine_utils.js";
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);
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -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, isMobileDevice } from "../../engine_utils.js";
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 {
src/engine-components/NeedleMenu.ts CHANGED
@@ -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 { isMobileDevice } from '../engine/engine_utils.js';
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
  }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -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, getParam, isDesktop, isiOS, isQuest } from "../engine_utils.js";
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 && InternalUSDZRegistry.exportAndOpen()) {
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;
src/engine-components/NestedGltf.ts CHANGED
@@ -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?.uri ?? this.filePath, this.gameObject.position);
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?.uri ?? this.filePath, res);
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?.uri ?? this.filePath, res);
59
+ if (debug) console.log("Nested loading done:", this.filePath?.url ?? this.filePath, res);
60
60
  }
61
61
  }
62
62
 
src/engine/js-extensions/Object3D.ts CHANGED
@@ -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 THREE.Object3D();
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 () {
src/engine-components/utils/OpenURL.ts CHANGED
@@ -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 { isiOS, isSafari } from "../../engine/engine_utils.js";
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, Raycaster } from "../ui/Raycaster.js";
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");
src/engine-components/OrbitControls.ts CHANGED
@@ -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 { getParam, isMobileDevice } from "../engine/engine_utils.js";
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?.cam;
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?.cam) {
327
- setCameraController(this._camera.cam, this, false);
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.cam);
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.cam.matrixWorld);
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()) {
src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts CHANGED
@@ -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
  }
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -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.addNewComponent(AudioListener)!;
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++) {
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -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?.uri);
87
+ if (debug) console.log("PlayerSync.createInstance", this.asset?.url);
88
88
 
89
- if (!this.asset?.asset && !this.asset?.uri) {
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?.uri + "</strong>: please make sure the asset has a PlayerState component!");
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
  }
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -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 { getParam, isMobileDevice } from "../../engine/engine_utils.js";
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.cam;
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) {
src/engine-components/export/usdz/utils/quicklook.ts CHANGED
@@ -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
- button.innerText = "Open in QuickLook";
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");
src/engine/codegen/register_types.ts CHANGED
@@ -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);
src/engine-components/RendererInstancing.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BatchedMesh, BufferAttribute, BufferGeometry, Color, InstancedMesh, InterleavedBuffer, InterleavedBufferAttribute, Material, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, Object3D, RawShaderMaterial, StaticDrawUsage, Texture, Vector3 } from "three";
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";
src/engine-components/SceneSwitcher.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { EquirectangularReflectionMapping, Object3D, Scene, Texture } from "three";
2
2
 
3
- import { Addressables, AssetReference } from "../engine/engine_addressables.js";
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 { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
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?.uri);
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.uri);
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.uri);
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.uri);
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.uri === index);
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.uri === this.sourceId) {
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.uri);
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.uri);
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.uri).toLowerCase().includes(lowerCaseValue)) {
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.uri, this.loadingScene.asset)
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.uri);
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.uri, this.index);
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.uri, this.index);
909
+ console.log("Preload finished: " + this.asset.url, this.index);
910
910
  }
911
911
 
912
912
  const i = this.tasks.indexOf(this);
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts CHANGED
@@ -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();
src/engine-components/ShadowCatcher.ts CHANGED
@@ -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, GameObject } from "./Component.js";
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 {
src/engine-components/SpatialTrigger.ts CHANGED
@@ -102,7 +102,7 @@
102
102
  onEnable(): void {
103
103
  SpatialTrigger.triggers.push(this);
104
104
  if (!this.boxHelper) {
105
- this.boxHelper = GameObject.addNewComponent(this.gameObject, BoxHelperComponent);
105
+ this.boxHelper = GameObject.addComponent(this.gameObject, BoxHelperComponent);
106
106
  this.boxHelper?.showHelper(null, debug as boolean);
107
107
  }
108
108
  }
src/engine-components/SpectatorCamera.ts CHANGED
@@ -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?.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.addNewComponent(this.cam.gameObject, SmoothFollow);
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
- if (this.cam.cam.near !== perspectiveCamera.near || this.cam.cam.far !== perspectiveCamera.far) {
398
- this.cam.cam.near = perspectiveCamera.near;
399
- this.cam.cam.far = perspectiveCamera.far;
400
- this.cam.cam.updateProjectionMatrix();
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
 
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -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
- _apiSchemas.push("MaterialBindingAPI");
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
- const materialName = getMaterialName(material);
1479
- writer.appendLine( `rel material:binding = </StageRoot/Materials/${materialName}>` );
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
 
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -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 { getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
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.exportAsync();
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 ios = isiOS()
169
- const safari = isSafari();
170
- if (debug || (ios && safari)) {
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
- // open quicklook
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
 
src/engine-components/Voip.ts CHANGED
@@ -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 { getParam, isiOS, microphonePermissionsGranted } from "../engine/engine_utils.js";
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"));
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -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 { getParam, isAndroidDevice } from "../../engine/engine_utils.js";
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,
src/engine-components/webxr/WebXR.ts CHANGED
@@ -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, getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
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();
src/engine/webcomponents/WebXRButtons.ts CHANGED
@@ -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 { isMozillaXR } from "../engine_utils.js";
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
- button.innerText = "Open in Quicklook";
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";
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -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();