@needle-tools/engine 4.12.0-next.de80571 → 4.12.0-next.f0b1535

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 (184) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/components.needle.json +1 -1
  3. package/dist/generateMeshBVH.worker-iyfPIK6R.js +21 -0
  4. package/dist/{gltf-progressive-DZrY8VT6.min.js → gltf-progressive-BmSygnAC.min.js} +2 -2
  5. package/dist/{gltf-progressive-DgYz5BYa.js → gltf-progressive-DnLBuGK5.js} +24 -24
  6. package/dist/{gltf-progressive-DWcmTMCh.umd.cjs → gltf-progressive-Rs-ojtXy.umd.cjs} +1 -1
  7. package/dist/{loader.worker-Dip-PthR.js → loader.worker-DWzfDpAl.js} +4 -4
  8. package/dist/{needle-engine.bundle-CbrIHiN8.js → needle-engine.bundle-CrbGpIHn.js} +8564 -8382
  9. package/dist/{needle-engine.bundle-S3PalR7r.min.js → needle-engine.bundle-DAiK_I7w.min.js} +150 -151
  10. package/dist/needle-engine.bundle-DdJ-HkwM.umd.cjs +1646 -0
  11. package/dist/needle-engine.d.ts +108 -43
  12. package/dist/needle-engine.js +4 -4
  13. package/dist/needle-engine.min.js +1 -1
  14. package/dist/needle-engine.umd.cjs +1 -1
  15. package/dist/{postprocessing-DYDtB188.min.js → postprocessing-B5ksn9-G.min.js} +54 -54
  16. package/dist/{postprocessing-CMgoN5t5.umd.cjs → postprocessing-DZtb9Nnn.umd.cjs} +81 -81
  17. package/dist/{postprocessing-BTW9pD_s.js → postprocessing-__7s9wON.js} +450 -441
  18. package/dist/{three-DfMvBzXi.js → three-BCCkyCA5.js} +1 -7
  19. package/dist/{three-qj71I7J3.umd.cjs → three-Bf2NBxAw.umd.cjs} +2 -2
  20. package/dist/{three-B7CT31Bt.min.js → three-W7zWTcfP.min.js} +1 -1
  21. package/dist/{three-examples-CsW4_6LI.umd.cjs → three-examples-Dho7cuu4.umd.cjs} +4 -4
  22. package/dist/{three-examples-D1P7eEhn.min.js → three-examples-MsJjauyk.min.js} +10 -10
  23. package/dist/{three-examples-D1SK93ek.js → three-examples-y2GeYlze.js} +2 -20
  24. package/dist/{three-mesh-ui-C_uSB5dD.js → three-mesh-ui-3nSSizT4.js} +1 -1
  25. package/dist/{three-mesh-ui-LQ44s0AL.min.js → three-mesh-ui-CIez6qJQ.min.js} +1 -1
  26. package/dist/{three-mesh-ui-DpATDXwU.umd.cjs → three-mesh-ui-zsOOA5Pq.umd.cjs} +1 -1
  27. package/dist/{vendor-D0zoswDa.js → vendor-DMZcbVO1.js} +3707 -3527
  28. package/dist/vendor-sURMCFSI.min.js +1116 -0
  29. package/dist/{vendor-UCpFAwt1.umd.cjs → vendor-tyBvnMF-.umd.cjs} +39 -39
  30. package/lib/engine/codegen/register_types.js +0 -2
  31. package/lib/engine/codegen/register_types.js.map +1 -1
  32. package/lib/engine/debug/debug_console.js +403 -1
  33. package/lib/engine/debug/debug_console.js.map +1 -1
  34. package/lib/engine/engine_components.js +3 -3
  35. package/lib/engine/engine_components.js.map +1 -1
  36. package/lib/engine/engine_input.d.ts +5 -0
  37. package/lib/engine/engine_input.js +6 -0
  38. package/lib/engine/engine_input.js.map +1 -1
  39. package/lib/engine/engine_license.js +3 -9
  40. package/lib/engine/engine_license.js.map +1 -1
  41. package/lib/engine/engine_networking.js +5 -5
  42. package/lib/engine/engine_networking.js.map +1 -1
  43. package/lib/engine/engine_physics.js.map +1 -1
  44. package/lib/engine/engine_physics_rapier.js +1 -1
  45. package/lib/engine/engine_physics_rapier.js.map +1 -1
  46. package/lib/engine/engine_utils.d.ts +4 -1
  47. package/lib/engine/engine_utils.js +28 -4
  48. package/lib/engine/engine_utils.js.map +1 -1
  49. package/lib/engine/extensions/extensions.d.ts +29 -7
  50. package/lib/engine/extensions/extensions.js.map +1 -1
  51. package/lib/engine/webcomponents/WebXRButtons.js +13 -5
  52. package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
  53. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -5
  54. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  55. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +4 -0
  56. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
  57. package/lib/engine/webcomponents/needle-engine.js +1 -1
  58. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  59. package/lib/engine/xr/NeedleXRSession.d.ts +1 -1
  60. package/lib/engine/xr/NeedleXRSession.js +67 -24
  61. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  62. package/lib/engine/xr/TempXRContext.js +12 -2
  63. package/lib/engine/xr/TempXRContext.js.map +1 -1
  64. package/lib/engine/xr/usdz.js +6 -2
  65. package/lib/engine/xr/usdz.js.map +1 -1
  66. package/lib/engine-components/AlignmentConstraint.d.ts +1 -1
  67. package/lib/engine-components/AlignmentConstraint.js +1 -1
  68. package/lib/engine-components/Animation.d.ts +1 -1
  69. package/lib/engine-components/Animation.js +1 -1
  70. package/lib/engine-components/Animator.d.ts +1 -1
  71. package/lib/engine-components/Animator.js +1 -1
  72. package/lib/engine-components/AudioListener.d.ts +1 -1
  73. package/lib/engine-components/AudioListener.js +1 -1
  74. package/lib/engine-components/AudioSource.d.ts +1 -1
  75. package/lib/engine-components/AudioSource.js +1 -1
  76. package/lib/engine-components/Camera.d.ts +1 -1
  77. package/lib/engine-components/Camera.js +1 -1
  78. package/lib/engine-components/CharacterController.d.ts +6 -2
  79. package/lib/engine-components/CharacterController.js +6 -2
  80. package/lib/engine-components/CharacterController.js.map +1 -1
  81. package/lib/engine-components/Collider.d.ts +1 -1
  82. package/lib/engine-components/Collider.js.map +1 -1
  83. package/lib/engine-components/Component.d.ts +2 -1
  84. package/lib/engine-components/Component.js +3 -2
  85. package/lib/engine-components/Component.js.map +1 -1
  86. package/lib/engine-components/DragControls.js +3 -0
  87. package/lib/engine-components/DragControls.js.map +1 -1
  88. package/lib/engine-components/Joints.d.ts +14 -0
  89. package/lib/engine-components/Joints.js +14 -0
  90. package/lib/engine-components/Joints.js.map +1 -1
  91. package/lib/engine-components/LookAtConstraint.d.ts +1 -1
  92. package/lib/engine-components/LookAtConstraint.js +1 -1
  93. package/lib/engine-components/OrbitControls.d.ts +1 -1
  94. package/lib/engine-components/OrbitControls.js +1 -1
  95. package/lib/engine-components/Renderer.d.ts +6 -0
  96. package/lib/engine-components/Renderer.js +6 -0
  97. package/lib/engine-components/Renderer.js.map +1 -1
  98. package/lib/engine-components/RendererInstancing.js +5 -3
  99. package/lib/engine-components/RendererInstancing.js.map +1 -1
  100. package/lib/engine-components/SceneSwitcher.js +18 -14
  101. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  102. package/lib/engine-components/SpectatorCamera.js +15 -7
  103. package/lib/engine-components/SpectatorCamera.js.map +1 -1
  104. package/lib/engine-components/SpriteRenderer.d.ts +2 -1
  105. package/lib/engine-components/SpriteRenderer.js +2 -1
  106. package/lib/engine-components/SpriteRenderer.js.map +1 -1
  107. package/lib/engine-components/api.d.ts +1 -0
  108. package/lib/engine-components/api.js +1 -0
  109. package/lib/engine-components/api.js.map +1 -1
  110. package/lib/engine-components/codegen/components.d.ts +0 -1
  111. package/lib/engine-components/codegen/components.js +0 -1
  112. package/lib/engine-components/codegen/components.js.map +1 -1
  113. package/lib/engine-components/timeline/SignalAsset.d.ts +1 -1
  114. package/lib/engine-components/timeline/SignalAsset.js +1 -1
  115. package/lib/engine-components/ui/Raycaster.d.ts +3 -2
  116. package/lib/engine-components/ui/Raycaster.js +3 -2
  117. package/lib/engine-components/ui/Raycaster.js.map +1 -1
  118. package/lib/engine-components/ui/RectTransform.d.ts +6 -0
  119. package/lib/engine-components/ui/RectTransform.js +6 -0
  120. package/lib/engine-components/ui/RectTransform.js.map +1 -1
  121. package/lib/engine-components/utils/LookAt.d.ts +2 -1
  122. package/lib/engine-components/utils/LookAt.js +2 -1
  123. package/lib/engine-components/utils/LookAt.js.map +1 -1
  124. package/lib/engine-components/web/CursorFollow.d.ts +1 -1
  125. package/lib/engine-components/web/CursorFollow.js +1 -1
  126. package/lib/engine-components/web/HoverAnimation.d.ts +1 -1
  127. package/lib/engine-components/web/HoverAnimation.js +1 -1
  128. package/lib/engine-components/web/ViewBox.d.ts +1 -1
  129. package/lib/engine-components/web/ViewBox.js +1 -1
  130. package/lib/engine-components/webxr/Avatar.js +2 -0
  131. package/lib/engine-components/webxr/Avatar.js.map +1 -1
  132. package/lib/engine-components/webxr/WebXR.js +18 -12
  133. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  134. package/package.json +3 -3
  135. package/plugins/vite/poster-client.js +8 -1
  136. package/src/engine/codegen/register_types.ts +0 -2
  137. package/src/engine/debug/debug_console.ts +449 -1
  138. package/src/engine/engine_components.ts +4 -4
  139. package/src/engine/engine_input.ts +7 -0
  140. package/src/engine/engine_license.ts +3 -8
  141. package/src/engine/engine_networking.ts +5 -5
  142. package/src/engine/engine_physics.ts +3 -3
  143. package/src/engine/engine_physics_rapier.ts +1 -1
  144. package/src/engine/engine_utils.ts +23 -4
  145. package/src/engine/extensions/extensions.ts +30 -6
  146. package/src/engine/webcomponents/WebXRButtons.ts +15 -5
  147. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -5
  148. package/src/engine/webcomponents/needle-engine.ar-overlay.ts +6 -0
  149. package/src/engine/webcomponents/needle-engine.ts +2 -2
  150. package/src/engine/xr/NeedleXRSession.ts +78 -27
  151. package/src/engine/xr/TempXRContext.ts +12 -2
  152. package/src/engine/xr/usdz.ts +6 -1
  153. package/src/engine-components/AlignmentConstraint.ts +1 -1
  154. package/src/engine-components/Animation.ts +1 -1
  155. package/src/engine-components/Animator.ts +1 -1
  156. package/src/engine-components/AudioListener.ts +1 -1
  157. package/src/engine-components/AudioSource.ts +1 -1
  158. package/src/engine-components/Camera.ts +1 -1
  159. package/src/engine-components/CharacterController.ts +6 -2
  160. package/src/engine-components/Collider.ts +1 -1
  161. package/src/engine-components/Component.ts +5 -4
  162. package/src/engine-components/DragControls.ts +4 -0
  163. package/src/engine-components/Joints.ts +14 -0
  164. package/src/engine-components/LookAtConstraint.ts +1 -1
  165. package/src/engine-components/OrbitControls.ts +1 -1
  166. package/src/engine-components/Renderer.ts +6 -0
  167. package/src/engine-components/RendererInstancing.ts +6 -3
  168. package/src/engine-components/SceneSwitcher.ts +17 -17
  169. package/src/engine-components/SpectatorCamera.ts +21 -10
  170. package/src/engine-components/SpriteRenderer.ts +2 -1
  171. package/src/engine-components/api.ts +2 -1
  172. package/src/engine-components/codegen/components.ts +0 -1
  173. package/src/engine-components/timeline/SignalAsset.ts +1 -1
  174. package/src/engine-components/ui/Raycaster.ts +3 -2
  175. package/src/engine-components/ui/RectTransform.ts +6 -0
  176. package/src/engine-components/utils/LookAt.ts +2 -1
  177. package/src/engine-components/web/CursorFollow.ts +1 -1
  178. package/src/engine-components/web/HoverAnimation.ts +1 -1
  179. package/src/engine-components/web/ViewBox.ts +1 -1
  180. package/src/engine-components/webxr/Avatar.ts +4 -0
  181. package/src/engine-components/webxr/WebXR.ts +19 -11
  182. package/dist/generateMeshBVH.worker-mO20N_b8.js +0 -21
  183. package/dist/needle-engine.bundle-BGixCtrn.umd.cjs +0 -1647
  184. package/dist/vendor-BKGa4GE0.min.js +0 -1116
@@ -150,6 +150,12 @@ export class NEPointerEvent extends PointerEvent {
150
150
  override get pointerType(): PointerTypeNames { return this._pointerType; }
151
151
  private readonly _pointerType: PointerTypeNames;
152
152
 
153
+ /**
154
+ * The button name that raised this event (e.g. for mouse events "left", "right", "middle" or for XRTrigger "xr-standard-trigger" or "xr-standard-thumbstick")
155
+ * Use {@link button} to get the numeric button index (e.g. 0, 1, 2...) on the controller or mouse.
156
+ */
157
+ readonly buttonName?: ButtonName | "none" = undefined;
158
+
153
159
  // this is set via the init arguments (we override it here for intellisense to show the string options)
154
160
  /** The input that raised this event like `pointerdown` */
155
161
  override get type(): InputEventNames { return this._type; }
@@ -176,6 +182,7 @@ export class NEPointerEvent extends PointerEvent {
176
182
  this.mode = init.mode;
177
183
  this._ray = init.ray;
178
184
  this.space = init.device;
185
+ this.buttonName = init.buttonName;
179
186
  }
180
187
 
181
188
  private _immediatePropagationStopped = false;
@@ -81,7 +81,7 @@ async function checkLicense() {
81
81
  if (runtimeLicenseCheckPromise) return runtimeLicenseCheckPromise;
82
82
  if (NEEDLE_ENGINE_LICENSE_TYPE === "basic") {
83
83
  try {
84
- const licenseUrl = "https://engine.needle.tools/licensing/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
84
+ const licenseUrl = "https://needle.tools/api/v1/needle-engine/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
85
85
  const res = await fetch(licenseUrl, {
86
86
  method: "GET",
87
87
  }).catch(_err => {
@@ -352,17 +352,12 @@ async function sendUsageMessageToAnalyticsBackend(context: IContext) {
352
352
  }
353
353
 
354
354
  try {
355
- const analyticsUrl = "htt" + "ps://nee" + "dle.tools/api/v1/ana" + "lytics";
355
+ const analyticsUrl = "htt" + "ps://" + "needle" + ".tools/" + "api/v1/needle-engine/ping";
356
356
  if (analyticsUrl) {
357
357
 
358
358
  // current url without query parameters
359
359
  const currentUrl = window.location.href.split("?")[0];
360
-
361
- let endpoint = "api/v2/new/request";
362
- if (!analyticsUrl.endsWith("/")) endpoint = "/" + endpoint;
363
360
  const license = NEEDLE_ENGINE_LICENSE_TYPE;
364
- const finalUrl = `${analyticsUrl}${endpoint}`;
365
- if (debug) console.debug("Sending beacon");
366
361
 
367
362
  const beaconData = {
368
363
  license,
@@ -376,7 +371,7 @@ async function sendUsageMessageToAnalyticsBackend(context: IContext) {
376
371
  build_time: BUILD_TIME,
377
372
  public_key: PUBLIC_KEY,
378
373
  };
379
- const res = navigator.sendBeacon?.(finalUrl, JSON.stringify(beaconData));
374
+ const res = navigator.sendBeacon?.(analyticsUrl, JSON.stringify(beaconData));
380
375
  if (debug) console.debug("Sent beacon: " + res);
381
376
  }
382
377
  }
@@ -629,7 +629,7 @@ export class NetworkConnection implements INetworkConnection {
629
629
  return;
630
630
  }
631
631
 
632
- console.debug("Connecting to networking backend on\n" + networkingServerUrl)
632
+ console.debug("Connecting to networking backend on\n" + networkingServerUrl)
633
633
  const pkg = await import('websocket-ts');
634
634
  // @ts-ignore
635
635
  const WebsocketBuilder = pkg.default?.WebsocketBuilder ?? pkg.WebsocketBuilder;
@@ -641,8 +641,8 @@ export class NetworkConnection implements INetworkConnection {
641
641
  this._connectingToWebsocketPromise = null;
642
642
  this._ws = ws;
643
643
  this.connected = true;
644
- if (isDevEnvironment() || debugNet) console.log("Connected to networking backend\n" + networkingServerUrl);
645
- else console.debug("Connected to networking backend", networkingServerUrl);
644
+ if (isDevEnvironment() || debugNet) console.log("Connected to networking backend\n" + networkingServerUrl);
645
+ else console.debug("Connected to networking backend", networkingServerUrl);
646
646
  resolve(true);
647
647
  this.onSendQueued(SendQueue.OnConnection);
648
648
  })
@@ -656,10 +656,10 @@ export class NetworkConnection implements INetworkConnection {
656
656
  console.error(msg);
657
657
  })
658
658
  .onError((_e) => {
659
- console.error("Websocket connection failed...");
659
+ console.error("Websocket connection failed...");
660
660
  resolve(false);
661
661
  })
662
- .onRetry(() => { console.log("Retry connecting to networking websocket") })
662
+ .onRetry(() => { console.log("Retry connecting to networking websocket") })
663
663
  .build();
664
664
  ws.addEventListener(pkg.WebsocketEvent.message, (socket, msg) => {
665
665
  this.onMessage(socket, msg);
@@ -1,7 +1,7 @@
1
1
  import { getRaycastMesh } from '@needle-tools/gltf-progressive';
2
2
  import { ArrayCamera, Box3, BufferGeometry, Camera, type Intersection, Layers, Line, Matrix3, Matrix4, Mesh, Object3D, PerspectiveCamera, Plane, Ray, Raycaster, SkinnedMesh, Sphere, SphereGeometry, Vector2, Vector3 } from 'three'
3
3
  import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js';
4
- import type { MeshBVH, MeshBVHOptions, StaticGeometryGenerator } from 'three-mesh-bvh';
4
+ import type { ComputeBVHOptions, GeometryBVH, MeshBVH, StaticGeometryGenerator } from 'three-mesh-bvh';
5
5
  import type { GenerateMeshBVHWorker } from 'three-mesh-bvh/src/workers/GenerateMeshBVHWorker.js';
6
6
 
7
7
  import { isDevEnvironment } from './debug/index.js';
@@ -762,7 +762,7 @@ namespace NEMeshBVH {
762
762
  return true;
763
763
  }
764
764
  else if (method instanceof Sphere) {
765
- const bvh = geom.boundsTree;
765
+ const bvh = geom.boundsTree as MeshBVH | undefined;
766
766
  if (bvh) {
767
767
  const sphere = method;
768
768
  // Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0xdddd00, 1, false);
@@ -789,7 +789,7 @@ namespace NEMeshBVH {
789
789
  let _acceleratedRaycast: Function | null = null;
790
790
  let _MeshBVH: ConstructorConcrete<MeshBVH> | null = null;
791
791
  let _StaticGeometryGenerator: ConstructorConcrete<StaticGeometryGenerator> | null = null;
792
- let _computeBoundsTree: ((_opt?: MeshBVHOptions) => MeshBVH) | null = null;
792
+ let _computeBoundsTree: (( options?: ComputeBVHOptions ) => GeometryBVH) | null = null;
793
793
 
794
794
  function loadMeshBVHLibrary() {
795
795
  if (didLoadMeshBVHLibrary) return;
@@ -273,7 +273,7 @@ export class RapierPhysics implements IPhysicsEngine {
273
273
  if (MODULES.RAPIER_PHYSICS.MAYBEMODULE == undefined) {
274
274
  if (debugPhysics) console.trace("Loading rapier physics engine");
275
275
  const module = await MODULES.RAPIER_PHYSICS.load();
276
- // https://github.com/dimforge/rapier/issues/811
276
+ // https://github.com/dimforge/rapier/issues/811 < but single object parameter still warns
277
277
  await module.init();
278
278
  }
279
279
  if (debugPhysics) console.log("Physics engine initialized, creating world...");
@@ -606,7 +606,8 @@ export namespace DeviceUtilities {
606
606
  /** @returns `true` if we're currently on an iPad */
607
607
  export function isiPad() {
608
608
  if (__isiPad !== undefined) return __isiPad;
609
- return __isiPad = /iPad/.test(navigator.userAgent);
609
+ const userAgent = navigator.userAgent.toLowerCase();
610
+ return __isiPad = /iPad/.test(navigator.userAgent) || userAgent.includes("macintosh") && "ontouchend" in document;
610
611
  }
611
612
 
612
613
  let __isAndroidDevice: boolean | undefined;
@@ -623,17 +624,25 @@ export namespace DeviceUtilities {
623
624
  return __isMozillaXR = /WebXRViewer\//i.test(navigator.userAgent);
624
625
  }
625
626
 
627
+ let __isNeedleAppClip: boolean | undefined;
628
+ /** @returns `true` if we're currently in the Needle App Clip */
629
+ export function isNeedleAppClip() {
630
+ if (__isNeedleAppClip !== undefined) return __isNeedleAppClip;
631
+ return __isNeedleAppClip = /NeedleAppClip\//i.test(navigator.userAgent);
632
+ }
633
+
626
634
  let __isMacOS: boolean | undefined;
627
635
  // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
628
636
  /** @returns `true` for MacOS devices */
629
637
  export function isMacOS() {
630
638
  if (__isMacOS !== undefined) return __isMacOS;
639
+ if (isiOS() || isiPad()) return __isMacOS = false;
640
+ const userAgent = navigator.userAgent.toLowerCase();
631
641
  if (navigator.userAgentData) {
632
642
  // Use modern UA Client Hints API if available
633
643
  return __isMacOS = navigator.userAgentData.platform === 'macOS';
634
644
  } else {
635
645
  // Fallback to user agent string parsing
636
- const userAgent = navigator.userAgent.toLowerCase();
637
646
  return __isMacOS = userAgent.includes('mac os x') || userAgent.includes('macintosh');
638
647
  }
639
648
  }
@@ -642,13 +651,13 @@ export namespace DeviceUtilities {
642
651
  /** @returns `true` for VisionOS devices */
643
652
  export function isVisionOS() {
644
653
  if (__isVisionOS !== undefined) return __isVisionOS;
645
- return __isVisionOS = (isMacOS() && "xr" in navigator);
654
+ return __isVisionOS = (isiPad() && "xr" in navigator && supportsQuickLookAR());
646
655
  }
647
656
 
648
657
  let __isiOS: boolean | undefined;
649
658
  const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
650
659
 
651
- /** @returns `true` for iOS devices like iPad, iPhone, iPod... */
660
+ /** @returns `true` for mobile Apple devices like iPad, iPhone, iPod, Vision Pro, ... */
652
661
  export function isiOS() {
653
662
  if (__isiOS !== undefined) return __isiOS;
654
663
  // eslint-disable-next-line deprecation/deprecation
@@ -725,6 +734,16 @@ export namespace DeviceUtilities {
725
734
  else __chromeVersion = null;
726
735
  return __chromeVersion;
727
736
  }
737
+
738
+ let __safariVersion: string | null | undefined;
739
+ export function getSafariVersion() {
740
+ if (__safariVersion !== undefined) return __safariVersion;
741
+ const match = navigator.userAgent.match(/Version\/(\d+\.\d+)/);
742
+ if (match && isSafari()) {
743
+ __safariVersion = match[1];
744
+ } else __safariVersion = null;
745
+ return __safariVersion;
746
+ }
728
747
  }
729
748
 
730
749
  /**
@@ -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);
@@ -356,7 +356,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
356
356
  case "loadfinished":
357
357
  if (typeof newValue === "string" && newValue.length > 0) {
358
358
  if (debug) console.log(name + " attribute changed", newValue);
359
- this.registerEventFromAttribute("loadfinished", newValue);
359
+ this.registerEventFromAttribute(name, newValue);
360
360
  }
361
361
  break;
362
362
  case "dracoDecoderPath":
@@ -708,7 +708,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
708
708
  if (attributeValue)
709
709
  this.registerEventFromAttribute("loadstart", attributeValue);
710
710
  }
711
- private registerEventFromAttribute(eventName: 'loadfinished' | 'loadstart', code: string) {
711
+ private registerEventFromAttribute(eventName: 'loadfinished' | 'loadstart' | 'progress', code: string) {
712
712
  const prev = this._previouslyRegisteredMap.get(eventName);
713
713
  if (prev) {
714
714
  this._previouslyRegisteredMap.delete(eventName);
@@ -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,28 +450,48 @@ 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;
432
- }
433
- else {
434
- mode = "immersive-ar";
435
- }
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;
436
471
  }
472
+
473
+ if (!arSupported && (mode === "immersive-ar" || mode === "ar")) {
474
+ // Forward to the AppClip experience
475
+ // const url = `https://appclip.apple.com/id?p=tools.needle.launch-app.Clip&url=${encodeURIComponent(location.href)}`
476
+ const url =`https://appclip.needle.tools/ar?url=${encodeURIComponent(location.href)}`;
477
+ console.debug("iOS device detected - opening Needle App Clip for AR experience", url, { mode, init });
478
+ // navigate to app clip url but keep the current url in history, open in same tab
479
+ window.location.href = url;
480
+
481
+ return null;
482
+ }
483
+ }
484
+
485
+ if (mode === "quicklook") {
486
+ console.warn("QuickLook mode is only supported on iOS devices");
487
+ return null;
437
488
  }
438
- else if (mode == "ar") {
489
+
490
+ // Since we now know we are not on iOS, ar mode becomes "immersive-ar"
491
+ if (mode == "ar") {
439
492
  mode = "immersive-ar";
440
493
  }
441
494
 
442
-
443
-
444
495
  if (isDevEnvironment() && getParam("debugxrpreroom")) {
445
496
  console.warn("Debug: Starting temporary XR session");
446
497
  await TemporaryXRContext.start(mode, init || NeedleXRSession.getDefaultSessionInit(mode));
@@ -539,8 +590,8 @@ export class NeedleXRSession implements INeedleXRSession {
539
590
  this._currentSessionRequestMode = mode;
540
591
  /**@type {XRSystem} */
541
592
  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)")
593
+ console.error(e, "Code: " + e?.code);
594
+ if (e?.code === 9) showBalloonWarning("Couldn't start XR session. Make sure you allow the required permissions.")
544
595
  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
596
  const notSecure = location.protocol === 'http:';
546
597
  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
  }
@@ -9,7 +9,7 @@ import { Behaviour, GameObject } from "./Component.js";
9
9
  * You can use this to create dynamic beams or connectors between objects.
10
10
  *
11
11
  * @summary Aligns and scales the object between two target GameObjects
12
- * @category Utility
12
+ * @category Constraints
13
13
  * @group Components
14
14
  **/
15
15
  export class AlignmentConstraint extends Behaviour {
@@ -60,7 +60,7 @@ class Vec2 { x!: number; y!: number }
60
60
  /**
61
61
  * Animation component to play animations on a GameObject.
62
62
  *
63
- * @summary Play animations
63
+ * @summary Plays animations from AnimationClips
64
64
  * @category Animation and Sequencing
65
65
  * @group Components
66
66
  */
@@ -43,7 +43,7 @@ export declare class PlayOptions {
43
43
  * It works with an {@link AnimatorController} to handle state transitions and animation blending.
44
44
  * A new AnimatorController can be created from code via `AnimatorController.createFromClips`.
45
45
  *
46
- * @summary Animation controller and playback component
46
+ * @summary Plays and manages animations on a GameObject based on an AnimatorController
47
47
  * @category Animation and Sequencing
48
48
  * @group Components
49
49
  */
@@ -9,7 +9,7 @@ import { Behaviour, GameObject } from "./Component.js";
9
9
  * This component creates and manages a Three.js {@link three#AudioListener}, automatically connecting it
10
10
  * to the main camera or a Camera in the parent hierarchy.
11
11
  *
12
- * @summary Audio listener for 3D audio capture
12
+ * @summary Receives audio in the scene and outputs it to speakers
13
13
  * @category Multimedia
14
14
  * @group Components
15
15
  */
@@ -52,7 +52,7 @@ export enum AudioRolloffMode {
52
52
  * is muted, the volume is set to 0. When unmuted, the volume
53
53
  * returns to its previous value.
54
54
  *
55
- * @summary 3D audio source with spatial positioning and playback controls
55
+ * @summary Plays audio clips from files or media streams
56
56
  * @category Multimedia
57
57
  * @group Components
58
58
  */
@@ -37,7 +37,7 @@ const debugscreenpointtoray = getParam("debugscreenpointtoray");
37
37
  * Internally, this component uses {@link PerspectiveCamera} and {@link OrthographicCamera} three.js objects.
38
38
  *
39
39
  * @summary Rendering scenes from a specific viewpoint
40
- * @category Camera
40
+ * @category Camera and Controls
41
41
  * @group Components
42
42
  */
43
43
  export class Camera extends Behaviour implements ICamera {