@needle-tools/engine 4.12.0-next.0b39d59 → 4.12.0-next.5d44f6c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/generateMeshBVH.worker-iyfPIK6R.js +21 -0
  2. package/dist/{needle-engine.bundle-BZJhFSnI.umd.cjs → needle-engine.bundle-3hSMBtBM.umd.cjs} +146 -146
  3. package/dist/{needle-engine.bundle-BktxajaZ.js → needle-engine.bundle-By-ZxucN.js} +8209 -7951
  4. package/dist/{needle-engine.bundle-C6Ko3O_C.min.js → needle-engine.bundle-DLa-Vhrd.min.js} +145 -145
  5. package/dist/needle-engine.d.ts +41 -9
  6. package/dist/needle-engine.js +46 -46
  7. package/dist/needle-engine.min.js +1 -1
  8. package/dist/needle-engine.umd.cjs +1 -1
  9. package/dist/{postprocessing-BkY94MUG.min.js → postprocessing-B5ksn9-G.min.js} +61 -61
  10. package/dist/{postprocessing-BWZIqm3N.umd.cjs → postprocessing-DZtb9Nnn.umd.cjs} +5 -5
  11. package/dist/{postprocessing-CLbPDsD8.js → postprocessing-__7s9wON.js} +426 -435
  12. package/dist/{vendor-C_oHRUjX.js → vendor-DMZcbVO1.js} +2644 -2487
  13. package/dist/{vendor-BiJQtqow.min.js → vendor-sURMCFSI.min.js} +41 -41
  14. package/dist/{vendor-DN-NsXVB.umd.cjs → vendor-tyBvnMF-.umd.cjs} +36 -36
  15. package/lib/engine/debug/debug_console.js +403 -1
  16. package/lib/engine/debug/debug_console.js.map +1 -1
  17. package/lib/engine/engine_components.js +3 -3
  18. package/lib/engine/engine_components.js.map +1 -1
  19. package/lib/engine/engine_input.d.ts +5 -0
  20. package/lib/engine/engine_input.js +6 -0
  21. package/lib/engine/engine_input.js.map +1 -1
  22. package/lib/engine/engine_networking.js +5 -5
  23. package/lib/engine/engine_networking.js.map +1 -1
  24. package/lib/engine/engine_physics.js.map +1 -1
  25. package/lib/engine/engine_utils.d.ts +4 -1
  26. package/lib/engine/engine_utils.js +28 -4
  27. package/lib/engine/engine_utils.js.map +1 -1
  28. package/lib/engine/extensions/extensions.d.ts +29 -7
  29. package/lib/engine/extensions/extensions.js.map +1 -1
  30. package/lib/engine/webcomponents/WebXRButtons.js +13 -5
  31. package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
  32. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -5
  33. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  34. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +4 -0
  35. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
  36. package/lib/engine/xr/NeedleXRSession.d.ts +1 -1
  37. package/lib/engine/xr/NeedleXRSession.js +85 -22
  38. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  39. package/lib/engine/xr/TempXRContext.js +12 -2
  40. package/lib/engine/xr/TempXRContext.js.map +1 -1
  41. package/lib/engine/xr/usdz.js +6 -2
  42. package/lib/engine/xr/usdz.js.map +1 -1
  43. package/lib/engine-components/Camera.js +4 -1
  44. package/lib/engine-components/Camera.js.map +1 -1
  45. package/lib/engine-components/Component.d.ts +2 -1
  46. package/lib/engine-components/Component.js +3 -2
  47. package/lib/engine-components/Component.js.map +1 -1
  48. package/lib/engine-components/SpectatorCamera.js +15 -7
  49. package/lib/engine-components/SpectatorCamera.js.map +1 -1
  50. package/lib/engine-components/api.d.ts +1 -1
  51. package/lib/engine-components/api.js +1 -1
  52. package/lib/engine-components/api.js.map +1 -1
  53. package/lib/engine-components/webxr/Avatar.js +2 -0
  54. package/lib/engine-components/webxr/Avatar.js.map +1 -1
  55. package/lib/engine-components/webxr/WebXR.js +18 -12
  56. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  57. package/package.json +3 -3
  58. package/plugins/vite/poster-client.js +8 -1
  59. package/src/engine/debug/debug_console.ts +449 -1
  60. package/src/engine/engine_components.ts +4 -4
  61. package/src/engine/engine_input.ts +7 -0
  62. package/src/engine/engine_networking.ts +5 -5
  63. package/src/engine/engine_physics.ts +3 -3
  64. package/src/engine/engine_utils.ts +23 -4
  65. package/src/engine/extensions/extensions.ts +30 -6
  66. package/src/engine/webcomponents/WebXRButtons.ts +15 -5
  67. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -5
  68. package/src/engine/webcomponents/needle-engine.ar-overlay.ts +6 -0
  69. package/src/engine/xr/NeedleXRSession.ts +96 -25
  70. package/src/engine/xr/TempXRContext.ts +12 -2
  71. package/src/engine/xr/usdz.ts +6 -1
  72. package/src/engine-components/Camera.ts +4 -1
  73. package/src/engine-components/Component.ts +5 -4
  74. package/src/engine-components/SpectatorCamera.ts +21 -10
  75. package/src/engine-components/api.ts +1 -1
  76. package/src/engine-components/webxr/Avatar.ts +4 -0
  77. package/src/engine-components/webxr/WebXR.ts +19 -11
  78. package/dist/generateMeshBVH.worker-BvGEI0r7.js +0 -21
@@ -30,20 +30,44 @@ const KHR_ANIMATIONPOINTER_IMPORT = import("@needle-tools/three-animation-pointe
30
30
  });
31
31
 
32
32
 
33
- declare type OnImportCallback = (loader: GLTFLoader, url: string, context: Context) => void;
34
- declare type OnExportCallback = (exporter: GLTFExporter, context: Context) => void;
33
+ /**
34
+ * Callback type for glTF import plugins. See {@link INeedleGLTFExtensionPlugin.onImport}
35
+ */
36
+ export type OnImportCallback = (loader: GLTFLoader, url: string, context: Context) => void;
37
+
38
+ /**
39
+ * Callback type for glTF export plugins. See {@link INeedleGLTFExtensionPlugin.onExport}
40
+ */
41
+ export type OnExportCallback = (exporter: GLTFExporter, context: Context) => void;
35
42
 
36
43
  /**
37
- * Interface for registering custom glTF extensions to the Needle Engine GLTFLoaders. Register your plugin via {@link addCustomExtensionPlugin}
44
+ * Interface for registering custom glTF extensions to the Needle Engine GLTFLoaders.
45
+ * Register your plugin using the {@link addCustomExtensionPlugin} method
38
46
  */
39
47
  export interface INeedleGLTFExtensionPlugin {
40
48
  /** The Name of your plugin */
41
49
  name: string;
42
- /** Called before starting to load a glTF file. This callback can be used to add custom extensions to the GLTFLoader */
50
+ /** Called before starting to load a glTF file. This callback can be used to add custom extensions to the [GLTFLoader](https://threejs.org/docs/#GLTFLoader.register)
51
+ *
52
+ * @example Add a custom extension to the GLTFloader
53
+ * ```ts
54
+ * onImport: (loader, url, context) => {
55
+ * loader.register((parser) => new MyCustomExtension(parser));
56
+ * }
57
+ * ```
58
+ */
43
59
  onImport?: OnImportCallback;
44
- /** Called after the glTF has been loaded */
60
+ /** Called after a glTF file has been loaded */
45
61
  onLoaded?: (url: string, gltf: GLTF, context: Context) => void;
46
- /** Called before starting to export a glTF file. This callback can be used to add custom extensions to the GLTFExporter */
62
+ /** Called before starting to export a glTF file. This callback can be used to add custom extensions to the [GLTFExporter](https://threejs.org/docs/#examples/en/exporters/GLTFExporter.register)
63
+ *
64
+ * @example Add a custom extension to the GLTFExporter
65
+ * ```ts
66
+ * onExport: (exporter, context) => {
67
+ * exporter.register((writer) => new MyCustomExportExtension(writer));
68
+ * }
69
+ *
70
+ */
47
71
  onExport?: OnExportCallback;
48
72
  }
49
73
 
@@ -57,13 +57,16 @@ export class WebXRButtonFactory {
57
57
  this._quicklookButton = button;
58
58
  button.dataset["needle"] = "quicklook-button";
59
59
  const supportsQuickLook = DeviceUtilities.supportsQuickLookAR();
60
+ let buttonText = "View in AR";
60
61
  // we can immediately enter this scene, because the platform supports rel="ar" links
61
- if (supportsQuickLook) {
62
- button.innerText = "View in AR";
62
+ if (DeviceUtilities.isVisionOS()) {
63
+ buttonText = "View in AR";
63
64
  }
64
- else {
65
- button.innerText = "View in AR";
65
+ else if (supportsQuickLook || DeviceUtilities.isiOS()) {
66
+ buttonText = "Open in Quicklook";
66
67
  }
68
+
69
+ button.innerText = buttonText;
67
70
  button.prepend(getIconElement("view_in_ar"));
68
71
 
69
72
  let createdExporter = false;
@@ -209,13 +212,20 @@ export class WebXRButtonFactory {
209
212
  }
210
213
 
211
214
  private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
215
+ if (mode === "immersive-ar" && DeviceUtilities.isiOS() && !DeviceUtilities.isVisionOS()) {
216
+ // on iOS, we can forward to the AppClip experience from the same button,
217
+ // so we always show the button. No AppClip support on VisionOS for now, so
218
+ // button state depends on WebXR support there.
219
+ return;
220
+ }
221
+
212
222
  if (!("xr" in navigator)) {
213
223
  button.style.display = "none";
214
224
  return;
215
225
  }
216
226
  NeedleXRSession.isSessionSupported(mode).then(supported => {
217
227
  button.style.display = !supported ? "none" : "";
218
- if (isDevEnvironment() && !supported) console.log("[WebXR] \"" + mode + "\" is not supported on this device make sure your server runs using HTTPS and you have a device connected that supports " + mode);
228
+ if (isDevEnvironment() && !supported) console.log("[WebXR] \"" + mode + "\" is not supported on this device. Make sure your server runs using HTTPS and you have a device connected that supports " + mode);
219
229
  });
220
230
  }
221
231
 
@@ -601,16 +601,15 @@ export class NeedleMenuElement extends HTMLElement {
601
601
  bottom: calc(100% + 5px);
602
602
  z-index: 100;
603
603
  width: auto;
604
- left: .2rem;
605
- right: .2rem;
606
- padding: .2rem;
604
+ max-width: 90vw;
605
+ left: 50%;
606
+ transform: translateX(-50%);
607
+ padding: .2rem 1em;
607
608
 
608
609
  }
609
610
  .compact.logo-hidden .foldout {
610
611
  /** for when there's no logo we want to center the foldout **/
611
612
  min-width: 24ch;
612
- margin-left: 50%;
613
- transform: translateX(calc(-50% - .2rem));
614
613
  }
615
614
 
616
615
  .compact.top .foldout {
@@ -118,6 +118,12 @@ export class AROverlayHandler {
118
118
  }
119
119
 
120
120
  private ensureQuitARButton(element: HTMLElement) {
121
+
122
+ // No quit button in app clips, we provide one via the native UI
123
+ if (DeviceUtilities.isNeedleAppClip()) {
124
+ return;
125
+ }
126
+
121
127
  const quitARSlot = document.createElement("slot");
122
128
  quitARSlot.setAttribute("name", "quit-ar");
123
129
  this.appendElement(quitARSlot, element);
@@ -82,6 +82,34 @@ function getDOMOverlayElement(domElement: HTMLElement) {
82
82
  handleSessionGranted();
83
83
  async function handleSessionGranted() {
84
84
 
85
+ // await delay(400);
86
+
87
+
88
+ let defaultMode: XRSessionMode = "immersive-vr";
89
+
90
+ try {
91
+ // In app clips we default to AR
92
+ if (DeviceUtilities.isNeedleAppClip()) {
93
+ defaultMode = "immersive-ar";
94
+ }
95
+ // Check if VR is even supported, otherwise try AR
96
+ else if (!(await navigator.xr?.isSessionSupported("immersive-vr"))) {
97
+ defaultMode = "immersive-ar";
98
+ }
99
+ // Check if AR is supported, otherwise we can't do anything
100
+ if (!(await navigator.xr?.isSessionSupported("immersive-ar")) && defaultMode === "immersive-ar") {
101
+ console.warn("[NeedleXRSession:granted] Neither VR nor AR supported, aborting session start.");
102
+ // showBalloonMessage("NeidleXRSession: Neither VR nor AR supported, aborting session start.");
103
+ return;
104
+ }
105
+ } catch (e) {
106
+ console.error("[NeedleXRSession:granted] Error while checking XR support:", e);
107
+ // showBalloonWarning("NeedleXRSession: Error while checking XR support: " + (e as Error).message);
108
+ return;
109
+ }
110
+
111
+ // showBalloonMessage("sessiongranted: " + defaultMode);
112
+
85
113
  // TODO: asap session granted doesnt handle the pre-room yet
86
114
  if (getParam("debugasap")) {
87
115
  let asapSession = globalThis["needle:XRSession"] as XRSession | undefined | Promise<XRSession>;
@@ -94,11 +122,11 @@ async function handleSessionGranted() {
94
122
  enableSpatialConsole(true);
95
123
  const session = await asapSession;
96
124
  if (session) {
97
- const sessionInit = NeedleXRSession.getDefaultSessionInit("immersive-vr");
98
- NeedleXRSession.setSession("immersive-vr", session, sessionInit, cb.context);
125
+ const sessionInit = NeedleXRSession.getDefaultSessionInit(defaultMode);
126
+ NeedleXRSession.setSession(defaultMode, session, sessionInit, cb.context);
99
127
  }
100
128
  else {
101
- console.error("NeedleXRSession: ASAP session was rejected");
129
+ console.error("[NeedleXRSession:granted] ASAP session was rejected");
102
130
  }
103
131
  asapSession = undefined;
104
132
  });
@@ -106,6 +134,9 @@ async function handleSessionGranted() {
106
134
  }
107
135
  }
108
136
 
137
+ // console.log("Attaching sessiongranted handler...", { haveXR: 'xr' in navigator });
138
+ // setTimeout(() => console.log("Session Granted handler attached.", { haveXR: 'xr' in navigator }), 1000);
139
+
109
140
  if ('xr' in navigator) {
110
141
  // WebXRViewer (based on Firefox) has a bug where addEventListener
111
142
  // throws a silent exception and aborts execution entirely.
@@ -115,10 +146,7 @@ async function handleSessionGranted() {
115
146
  }
116
147
 
117
148
  navigator.xr?.addEventListener('sessiongranted', async () => {
118
- enableSpatialConsole(true);
119
-
120
- console.log("Received Session Granted...")
121
- await delay(100);
149
+ // enableSpatialConsole(true);
122
150
 
123
151
  const lastSessionMode = sessionStorage.getItem("needle_xr_session_mode") as XRSessionMode;
124
152
  const lastSessionInit = sessionStorage.getItem("needle_xr_session_init") ?? null;
@@ -126,7 +154,8 @@ async function handleSessionGranted() {
126
154
 
127
155
  let info: SessionInfo | null = null;
128
156
  if (contextIsLoading()) {
129
- await TemporaryXRContext.start(lastSessionMode || "immersive-vr", init || NeedleXRSession.getDefaultSessionInit("immersive-vr"));
157
+ await TemporaryXRContext.start(lastSessionMode || defaultMode, init || NeedleXRSession.getDefaultSessionInit(defaultMode))
158
+ .catch(e => console.warn("[NeedleXRSession:granted] TemporaryXRContext start failed:", e));
130
159
  await waitForContextLoadingFinished();
131
160
  info = await TemporaryXRContext.handoff();
132
161
  }
@@ -134,17 +163,19 @@ async function handleSessionGranted() {
134
163
  NeedleXRSession.setSession(info.mode, info.session, info.init, Context.Current);
135
164
  }
136
165
  else if (lastSessionMode && lastSessionInit) {
137
- console.log("Session Granted: Restore last session")
166
+ console.log("[NeedleXRSession:granted] Restore last session")
138
167
  const init = JSON.parse(lastSessionInit);
139
168
  NeedleXRSession.start(lastSessionMode as XRSessionMode, init).catch(e => console.warn(e));
140
169
  }
141
170
  else {
142
171
  // if no session was found we start VR by default
143
- NeedleXRSession.start("immersive-vr").catch(e => console.warn("Session Granted failed:", e));
172
+ NeedleXRSession.start(defaultMode).catch(e => console.warn("[NeedleXRSession:granted] failed:", e));
144
173
  }
145
174
  // make sure we only subscribe to the event once
146
175
  }, { once: true });
147
-
176
+ }
177
+ else {
178
+ // showBalloonWarning("NeedleXRSession: WebXR not available in this browser.");
148
179
  }
149
180
  }
150
181
  function saveSessionInfo(mode: XRSessionMode, init: XRSessionInit) {
@@ -419,27 +450,67 @@ export class NeedleXRSession implements INeedleXRSession {
419
450
  * @param init The XRSessionInit to use (optional), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit
420
451
  * @param context The Needle Engine context to use
421
452
  */
422
- static async start(mode: XRSessionMode | "ar", init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
453
+ static async start(mode: XRSessionMode | "ar" | "quicklook", init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
423
454
 
424
- // handle iOS platform where "immersive-ar" is not supported
455
+ // handle iOS platform where "immersive-ar" is special:
456
+ // - we either launch QuickLook
457
+ // - or forward to the Needle App Clip experience for WebXR AR
425
458
  // TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
426
459
  if (DeviceUtilities.isiOS()) {
427
- if (mode === "ar") {
428
- const arSupported = await this.isARSupported();
429
- if (!arSupported) {
430
- InternalUSDZRegistry.exportAndOpen();
431
- return null;
460
+
461
+ const arSupported = await this.isARSupported().catch(() => false);
462
+
463
+ // On VisionOS, we use QuickLook for AR experiences; no AppClip support for now.
464
+ if (DeviceUtilities.isVisionOS() && !arSupported && (mode === "ar" || mode === "immersive-ar")) {
465
+ mode = "quicklook";
466
+ }
467
+
468
+ if (mode === "quicklook") {
469
+ InternalUSDZRegistry.exportAndOpen();
470
+ return null;
471
+ }
472
+
473
+ if (!arSupported && (mode === "immersive-ar" || mode === "ar")) {
474
+ // const debugAppClip = getParam("debugappclip")
475
+ // Forward to the AppClip experience (Using the apple.com url the appclip overlay shows immediately)
476
+ // const url =`https://appclip.needle.tools/ar?url=${(location.href)}`;
477
+ const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
478
+ url.searchParams.set("url", location.href);
479
+
480
+ const urlStr = url.toString();
481
+ // if we are in an iframe, we need to navigate the top window
482
+ const topWindow = window.top || window;
483
+ try {
484
+ console.debug("iOS device detected - opening Needle App Clip for AR experience", { mode, init, url });
485
+ // navigate to app clip url but keep the current url in history, open in same tab
486
+ // eslint-disable-next-line xss/no-location-href-assign
487
+ topWindow.location.href = urlStr;
432
488
  }
433
- else {
434
- mode = "immersive-ar";
489
+ catch (e) {
490
+ console.warn("Error navigating to AppClip " + urlStr + "\n", e);
491
+ // if top window navigation fails and we are in an iframe, we try to navigate the top window directly
492
+ const weAreInIframe = window !== window.top;
493
+ if (weAreInIframe) {
494
+ // we can try to open a new tab as a fallback
495
+ window.open(urlStr, "_blank");
496
+ }
497
+ // eslint-disable-next-line xss/no-location-href-assign
498
+ else window.location.href = urlStr;
435
499
  }
500
+
501
+ return null;
436
502
  }
437
503
  }
438
- else if (mode == "ar") {
439
- mode = "immersive-ar";
440
- }
441
504
 
505
+ if (mode === "quicklook") {
506
+ console.warn("QuickLook mode is only supported on iOS devices");
507
+ return null;
508
+ }
442
509
 
510
+ // Since we now know we are not on iOS, ar mode becomes "immersive-ar"
511
+ if (mode == "ar") {
512
+ mode = "immersive-ar";
513
+ }
443
514
 
444
515
  if (isDevEnvironment() && getParam("debugxrpreroom")) {
445
516
  console.warn("Debug: Starting temporary XR session");
@@ -539,8 +610,8 @@ export class NeedleXRSession implements INeedleXRSession {
539
610
  this._currentSessionRequestMode = mode;
540
611
  /**@type {XRSystem} */
541
612
  const newSession = await (this._currentSessionRequest)?.catch(e => {
542
- console.error(e, "Code: " + e.code);
543
- if (e.code === 9) showBalloonWarning("Make sure your device has the required permissions (e.g. camera access)")
613
+ console.error(e, "Code: " + e?.code);
614
+ if (e?.code === 9) showBalloonWarning("Couldn't start XR session. Make sure you allow the required permissions.")
544
615
  console.log("If the specified XR configuration is not supported (e.g. entering AR doesnt work) - make sure you access the website on a secure connection (HTTPS) and your device has the required permissions (e.g. camera access)");
545
616
  const notSecure = location.protocol === 'http:';
546
617
  if (notSecure) showBalloonWarning("XR requires a secure connection (HTTPS)");
@@ -32,7 +32,13 @@ export class TemporaryXRContext {
32
32
  return null;
33
33
  }
34
34
  this._requestInFlight = true;
35
- const session = await navigator.xr.requestSession(mode, init);
35
+ const session = await navigator.xr.requestSession(mode, init).catch(err => {
36
+ console.error("Failed to start temporary XR session:", err);
37
+ });
38
+ if (!session) {
39
+ this._requestInFlight = false;
40
+ return null;
41
+ }
36
42
  session.addEventListener("end", () => {
37
43
  this._active = null;
38
44
  });
@@ -82,10 +88,14 @@ export class TemporaryXRContext {
82
88
  this._session = session;
83
89
  this._session.addEventListener("end", this.onEnd);
84
90
 
85
- this._renderer = new WebGLRenderer({ alpha: true });
91
+ this._renderer = new WebGLRenderer({ alpha: true, antialias: true });
92
+ this._renderer.outputColorSpace = 'srgb';
86
93
  this._renderer.setAnimationLoop(this.onFrame);
87
94
  this._renderer.xr.setSession(session);
88
95
  this._renderer.xr.enabled = true;
96
+ // Set pixel ratio and size
97
+ this._renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
98
+ this._renderer.setSize(window.innerWidth, window.innerHeight);
89
99
  this._camera = new PerspectiveCamera();
90
100
  this._scene = new Scene();
91
101
  this._scene.fog = new Fog(0x444444, 10, 250);
@@ -1,3 +1,4 @@
1
+ import { isDevEnvironment } from "../debug/index.js";
1
2
 
2
3
  declare type USDZExporter = {
3
4
  exportAndOpen(): Promise<any>,
@@ -11,7 +12,11 @@ export namespace InternalUSDZRegistry {
11
12
  const usdzExporter: USDZExporter[] = [];
12
13
 
13
14
  export function exportAndOpen(): boolean {
14
- if (!usdzExporter?.length) return false;
15
+ if (!usdzExporter?.length) {
16
+ if (isDevEnvironment()) {
17
+ console.warn("No USDZ exporters found – cannot export USDZ for QuickLook.");
18
+ }
19
+ }
15
20
  for (const exp of usdzExporter) {
16
21
  exp.exportAndOpen();
17
22
  }
@@ -8,7 +8,7 @@ import { Context } from "../engine/engine_setup.js";
8
8
  import { RenderTexture } from "../engine/engine_texture.js";
9
9
  import { getTempColor, getWorldPosition } from "../engine/engine_three_utils.js";
10
10
  import type { ICamera } from "../engine/engine_types.js"
11
- import { getParam } from "../engine/engine_utils.js";
11
+ import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
12
12
  import { NeedleXREventArgs } from "../engine/engine_xr.js";
13
13
  import { RGBAColor } from "../engine/js-extensions/index.js";
14
14
  import { Behaviour, GameObject } from "./Component.js";
@@ -664,6 +664,9 @@ export class Camera extends Behaviour implements ICamera {
664
664
  else if (navigator.userAgent?.includes("Mozilla") && navigator.userAgent?.includes("Mobile WebXRViewer/v2")) {
665
665
  transparent = true;
666
666
  }
667
+ else if(DeviceUtilities.isNeedleAppClip()) {
668
+ return true;
669
+ }
667
670
  }
668
671
  }
669
672
 
@@ -476,10 +476,11 @@ export abstract class GameObject extends Object3D implements Object3D, IGameObje
476
476
  * Gets a component of the specified type in the gameObject's children hierarchy
477
477
  * @param go GameObject to search in
478
478
  * @param typeName Constructor of the component type
479
+ * @param includeInactive Whether to include inactive objects in the search
479
480
  * @returns The first matching component if found, otherwise null
480
481
  */
481
- public static getComponentInChildren<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>): T | null {
482
- return getComponentInChildren(go, typeName);
482
+ public static getComponentInChildren<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>, includeInactive: boolean = false): T | null {
483
+ return getComponentInChildren(go, typeName, includeInactive);
483
484
  }
484
485
 
485
486
  /**
@@ -588,7 +589,7 @@ export abstract class Component implements IComponent, EventTarget,
588
589
  */
589
590
  get [$componentName]() { return TypeStore.getKey(this.constructor as any) || undefined; }
590
591
 
591
-
592
+
592
593
  private __context: Context | undefined;
593
594
 
594
595
  /**
@@ -1106,7 +1107,7 @@ export abstract class Component implements IComponent, EventTarget,
1106
1107
  this.dispatchEvent(new CustomEvent("destroyed", { detail: this }));
1107
1108
  }
1108
1109
  destroyComponentInstance(this as any);
1109
- if(isHotReloadEnabled()) unregisterHotReloadType(this);
1110
+ if (isHotReloadEnabled()) unregisterHotReloadType(this);
1110
1111
  }
1111
1112
 
1112
1113
  /**
@@ -8,7 +8,8 @@ import { PlayerView, ViewDevice } from "../engine/engine_playerview.js";
8
8
  import { serializable } from "../engine/engine_serialization.js";
9
9
  import { Context } from "../engine/engine_setup.js";
10
10
  import type { ICamera } from "../engine/engine_types.js";
11
- import { getParam } from "../engine/engine_utils.js";
11
+ import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
12
+ import { PlayerState } from "../engine-components-experimental/networking/PlayerSync.js";
12
13
  import { Camera } from "./Camera.js";
13
14
  import { Behaviour, Component, GameObject } from "./Component.js";
14
15
  import { OrbitControls } from "./OrbitControls.js";
@@ -98,7 +99,7 @@ export class SpectatorCamera extends Behaviour {
98
99
  }
99
100
 
100
101
  /** Gets the local player's connection ID */
101
- private get localId() : string {
102
+ private get localId(): string {
102
103
  return this.context.connection.connectionId ?? "local";
103
104
  }
104
105
 
@@ -196,9 +197,8 @@ export class SpectatorCamera extends Behaviour {
196
197
  */
197
198
  private isSupportedPlatform() {
198
199
  const ua = window.navigator.userAgent;
199
- const standalone = /Windows|MacOS/.test(ua);
200
200
  const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
201
- return standalone && !isHololens;
201
+ return DeviceUtilities.isDesktop() && !DeviceUtilities.isMobileDevice() && !isHololens;
202
202
  }
203
203
 
204
204
  /**
@@ -268,11 +268,16 @@ export class SpectatorCamera extends Behaviour {
268
268
  const previousRenderTarget = renderer.getRenderTarget();
269
269
  let oldFramebuffer: WebGLFramebuffer | null = null;
270
270
 
271
+
271
272
  const webglState = renderer.state as WebGLState & { bindXRFramebuffer?: Function };
272
273
 
274
+
273
275
  // seems that in some cases, renderer.getRenderTarget returns null
274
276
  // even when we're rendering to a headset.
275
- if (!previousRenderTarget) {
277
+ if (!previousRenderTarget ||
278
+ // Prevent rendering if in XR - @TODO: check if we need to allow this for VR?
279
+ previousRenderTarget["isXRRenderTarget"] === true)
280
+ {
276
281
  if (!renderer.state.bindFramebuffer || !webglState.bindXRFramebuffer)
277
282
  return;
278
283
 
@@ -557,12 +562,18 @@ class SpectatorSelectionController {
557
562
  for (const hit of hits) {
558
563
  if (hit.distance < .2) continue;
559
564
  const obj = hit.object;
560
- const avatar = GameObject.getComponentInParent(obj, AvatarMarker);
561
- const id = avatar?.connectionId;
565
+ // For WebXR
566
+ const state = PlayerState.getFor(obj);
567
+ let id = state?.owner;
568
+ // for SpectatorCamera
569
+ if (!id) {
570
+ const avatar = GameObject.getComponentInParent(obj, AvatarMarker);
571
+ id = avatar?.connectionId;
572
+ }
562
573
  if (id) {
563
574
  const view = this.context.players.getPlayerView(id);
564
575
  this.spectator.target = view;
565
- if (debug) console.log("spectate", id, avatar);
576
+ if (debug) console.log("spectate", id, state);
566
577
  break;
567
578
  }
568
579
  }
@@ -633,7 +644,7 @@ class SpectatorCamNetworking {
633
644
  this.context.connection.beginListen("spectator-request-follow", this._requestFollowMethod);
634
645
  this.context.connection.beginListen(RoomEvents.JoinedRoom, this._joinedRoomMethod);
635
646
  this.context.domElement.addEventListener("keydown", evt => {
636
- if(!this.spectator.useKeys) return;
647
+ if (!this.spectator.useKeys) return;
637
648
  if (evt.key === "f") {
638
649
  this.onRequestFollowMe();
639
650
  }
@@ -767,7 +778,7 @@ class SpectatorCamNetworking {
767
778
  }
768
779
 
769
780
  private _enforceFollowInterval: any;
770
-
781
+
771
782
  /**
772
783
  * Periodically retries following a user if the initial attempt failed
773
784
  */
@@ -35,8 +35,8 @@
35
35
  */
36
36
 
37
37
  export * from "./codegen/components.js";
38
- export { Behaviour, Component, GameObject } from "./Component.js";
39
38
  export { Collider } from "./Collider.js"; // export abstract type
39
+ export { Behaviour, Component, GameObject } from "./Component.js";
40
40
 
41
41
  // We dont want to export everything in the extensions
42
42
  export { ClearFlags } from "./Camera.js"
@@ -2,6 +2,7 @@ import { Mesh, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { AssetReference } from "../../engine/engine_addressables.js";
4
4
  import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
5
+ import { ViewDevice } from "../../engine/engine_playerview.js";
5
6
  import { serializable } from "../../engine/engine_serialization_decorator.js";
6
7
  import type { IGameObject } from "../../engine/engine_types.js";
7
8
  import { getParam, PromiseAllWithErrors } from "../../engine/engine_utils.js";
@@ -55,10 +56,13 @@ export class Avatar extends Behaviour {
55
56
  const marker = this.gameObject.addComponent(AvatarMarker)!;
56
57
  marker.avatar = this.gameObject;
57
58
  marker.connectionId = playerstate.owner;
59
+
60
+ this.context.players.setPlayerView(playerstate.owner, this.head?.asset, ViewDevice.Headset);
58
61
  }
59
62
  else if (this.context.connection.isConnected) console.error("No player state found for avatar", this);
60
63
  // don't destroy the avatar when entering XR and not connected to a networking backend
61
64
  else if (playerstate && !this.context.connection.isConnected) playerstate.dontDestroy = true;
65
+
62
66
  }
63
67
 
64
68
  onLeaveXR(_args: NeedleXREventArgs): void {
@@ -210,17 +210,25 @@ export class WebXR extends Behaviour {
210
210
  showBalloonWarning("<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API\" target=\"_blank\">WebXR</a> only works on secure connections (https).");
211
211
  }
212
212
 
213
- if (this.useQuicklookExport) {
214
- const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
215
- if (!existingUSDZExporter) {
216
- // if no USDZ Exporter is found we add one and assign the scene to be exported
217
- if (debug) console.log("WebXR: Adding USDZExporter");
218
- this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
219
- this._usdzExporter.objectToExport = this.context.scene;
220
- this._usdzExporter.autoExportAnimations = true;
221
- this._usdzExporter.autoExportAudioSources = true;
213
+ // Showing the QuickLook button depends on whether we're on iOS or visionOS –
214
+ // on iOS we have AppClip support, so we don't need QuickLook button there,
215
+ // while on visionOS we use QuickLook for AR experiences for now (unless it happens to have WebXR support by then).
216
+ navigator.xr?.isSessionSupported("immersive-ar").catch(() => false).then((arSupported) => {
217
+
218
+ const isVisionOSFallback = DeviceUtilities.isVisionOS() && !arSupported;
219
+
220
+ if (this.useQuicklookExport || isVisionOSFallback) {
221
+ const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
222
+ if (!existingUSDZExporter) {
223
+ // if no USDZ Exporter is found we add one and assign the scene to be exported
224
+ if (debug) console.log("WebXR: Adding USDZExporter");
225
+ this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
226
+ this._usdzExporter.objectToExport = this.context.scene;
227
+ this._usdzExporter.autoExportAnimations = true;
228
+ this._usdzExporter.autoExportAudioSources = true;
229
+ }
222
230
  }
223
- }
231
+ });
224
232
 
225
233
  this.handleCreatingHTML();
226
234
  this.handleOfferSession();
@@ -360,7 +368,7 @@ export class WebXR extends Behaviour {
360
368
 
361
369
  // Handle AR session root
362
370
  if (args.xr.isAR) {
363
- let sessionroot = GameObject.findObjectOfType(WebARSessionRoot);
371
+ let sessionroot = GameObject.findObjectOfType(WebARSessionRoot, this.context, false);
364
372
  // Only create a WebARSessionRoot if none is in the scene already
365
373
  if (!sessionroot) {
366
374
  if (this.usePlacementReticle) {