@threlte/xr 1.5.4 → 1.6.0

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 (73) hide show
  1. package/dist/components/Controller.svelte +59 -57
  2. package/dist/components/Hand.svelte +24 -29
  3. package/dist/components/XR.svelte +146 -16
  4. package/dist/components/XR.svelte.d.ts +20 -0
  5. package/dist/components/XROrigin.svelte +82 -0
  6. package/dist/components/XROrigin.svelte.d.ts +33 -0
  7. package/dist/components/internal/Cursor.svelte +5 -10
  8. package/dist/components/internal/PointerCursor.svelte +18 -4
  9. package/dist/components/internal/TeleportCursor.svelte +4 -1
  10. package/dist/components/internal/TeleportRay.svelte +15 -3
  11. package/dist/hooks/currentReadable.svelte.d.ts +28 -1
  12. package/dist/hooks/currentReadable.svelte.js +36 -9
  13. package/dist/hooks/useController.svelte.d.ts +3 -3
  14. package/dist/hooks/useController.svelte.js +30 -7
  15. package/dist/hooks/useHand.svelte.d.ts +2 -2
  16. package/dist/hooks/useHand.svelte.js +26 -5
  17. package/dist/hooks/useHandJoint.svelte.js +8 -6
  18. package/dist/hooks/useHitTest.svelte.js +56 -12
  19. package/dist/hooks/useTeleport.d.ts +11 -9
  20. package/dist/hooks/useTeleport.js +62 -14
  21. package/dist/hooks/useXR.js +5 -5
  22. package/dist/hooks/useXROrigin.svelte.d.ts +10 -0
  23. package/dist/hooks/useXROrigin.svelte.js +11 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +3 -0
  26. package/dist/internal/inputSources.svelte.d.ts +84 -0
  27. package/dist/internal/inputSources.svelte.js +91 -0
  28. package/dist/internal/setupHeadset.svelte.js +18 -6
  29. package/dist/internal/setupInputSources.d.ts +4 -0
  30. package/dist/internal/setupInputSources.js +319 -0
  31. package/dist/internal/state.svelte.d.ts +10 -12
  32. package/dist/internal/state.svelte.js +9 -3
  33. package/dist/lib/getXRSessionOptions.d.ts +1 -1
  34. package/dist/lib/getXRSessionOptions.js +8 -7
  35. package/dist/lib/toggleXRSession.d.ts +1 -1
  36. package/dist/lib/toggleXRSession.js +22 -7
  37. package/dist/plugins/pointerControls/compute.js +14 -5
  38. package/dist/plugins/pointerControls/context.d.ts +3 -3
  39. package/dist/plugins/pointerControls/context.js +12 -6
  40. package/dist/plugins/pointerControls/index.d.ts +4 -3
  41. package/dist/plugins/pointerControls/index.js +63 -31
  42. package/dist/plugins/pointerControls/plugin.svelte.js +0 -5
  43. package/dist/plugins/pointerControls/setup.svelte.js +92 -78
  44. package/dist/plugins/pointerControls/types.d.ts +16 -3
  45. package/dist/plugins/pointerControls/types.js +2 -1
  46. package/dist/plugins/teleportControls/compute.d.ts +1 -1
  47. package/dist/plugins/teleportControls/compute.js +11 -4
  48. package/dist/plugins/teleportControls/context.d.ts +4 -4
  49. package/dist/plugins/teleportControls/context.js +1 -4
  50. package/dist/plugins/teleportControls/index.js +8 -8
  51. package/dist/plugins/teleportControls/setup.svelte.js +10 -9
  52. package/dist/plugins/touchControls/compute.d.ts +3 -0
  53. package/dist/plugins/touchControls/compute.js +13 -0
  54. package/dist/plugins/touchControls/context.d.ts +12 -0
  55. package/dist/plugins/touchControls/context.js +27 -0
  56. package/dist/plugins/touchControls/hook.d.ts +5 -0
  57. package/dist/plugins/touchControls/hook.js +26 -0
  58. package/dist/plugins/touchControls/index.d.ts +33 -0
  59. package/dist/plugins/touchControls/index.js +41 -0
  60. package/dist/plugins/touchControls/plugin.svelte.d.ts +1 -0
  61. package/dist/plugins/touchControls/plugin.svelte.js +24 -0
  62. package/dist/plugins/touchControls/setup.svelte.d.ts +2 -0
  63. package/dist/plugins/touchControls/setup.svelte.js +247 -0
  64. package/dist/plugins/touchControls/types.d.ts +62 -0
  65. package/dist/plugins/touchControls/types.js +11 -0
  66. package/dist/types.d.ts +1 -1
  67. package/package.json +3 -2
  68. package/dist/internal/setupControllers.d.ts +0 -2
  69. package/dist/internal/setupControllers.js +0 -68
  70. package/dist/internal/setupHands.d.ts +0 -2
  71. package/dist/internal/setupHands.js +0 -67
  72. package/dist/internal/useHandTrackingState.d.ts +0 -5
  73. package/dist/internal/useHandTrackingState.js +0 -20
@@ -11,6 +11,7 @@
11
11
 
12
12
  <script lang="ts">
13
13
  import { T, useTask, useThrelte } from '@threlte/core'
14
+ import { untrack } from 'svelte'
14
15
  import { pointerIntersection, pointerState } from '../../internal/state.svelte.js'
15
16
  import Cursor from './Cursor.svelte'
16
17
  import type { Snippet } from 'svelte'
@@ -28,6 +29,8 @@
28
29
 
29
30
  const ref = new Group()
30
31
 
32
+ const SURFACE_OFFSET = 0.002
33
+
31
34
  useTask(
32
35
  () => {
33
36
  if (intersection === undefined) {
@@ -43,17 +46,28 @@
43
46
 
44
47
  normalMatrix.getNormalMatrix(object.matrixWorld)
45
48
  worldNormal.copy(face.normal).applyMatrix3(normalMatrix).normalize()
46
- ref.lookAt(vec3.addVectors(point, worldNormal))
49
+
50
+ // Float the reticle just above the surface so it doesn't z-fight
51
+ // with the coplanar face underneath.
52
+ ref.position.addScaledVector(worldNormal, SURFACE_OFFSET)
53
+
54
+ ref.lookAt(vec3.addVectors(ref.position, worldNormal))
47
55
  },
48
56
  {
49
57
  running: () => hovering && intersection !== undefined
50
58
  }
51
59
  )
52
60
 
61
+ // Snap to the hit point on hover entry so the reticle doesn't visibly
62
+ // fly in from its previous location. `intersection` is read untracked
63
+ // so this only reruns on hover transitions, not every frame.
53
64
  $effect.pre(() => {
54
- if (hovering && intersection) {
55
- ref.position.copy(intersection.point)
56
- }
65
+ if (!hovering) return
66
+ untrack(() => {
67
+ if (intersection) {
68
+ ref.position.copy(intersection.point)
69
+ }
70
+ })
57
71
  })
58
72
  </script>
59
73
 
@@ -40,7 +40,10 @@
40
40
  if (face) {
41
41
  normalMatrix.getNormalMatrix(object.matrixWorld)
42
42
  worldNormal.copy(face.normal).applyMatrix3(normalMatrix).normalize()
43
- ref.lookAt(vec3.addVectors(point, worldNormal))
43
+ // lookAt from the lerped position (matches PointerCursor) — using the
44
+ // raw hit point here causes orientation to wobble while position is
45
+ // still easing toward the target.
46
+ ref.lookAt(vec3.addVectors(ref.position, worldNormal))
44
47
  }
45
48
  },
46
49
  {
@@ -33,6 +33,7 @@
33
33
  const positions = new Float32Array(rayDivisions * 3)
34
34
  const lineGeometry = new LineGeometry()
35
35
  const intersection = $derived(teleportIntersection[handedness])
36
+ let firstRender = true
36
37
 
37
38
  const setCurvePoints = (alpha = 0.3) => {
38
39
  if (intersection === undefined) return
@@ -51,9 +52,16 @@
51
52
  // Create an arc
52
53
  rayMidpoint.y += arc
53
54
 
54
- curve.v0.lerp(rayStart, alpha)
55
- curve.v1.lerp(rayMidpoint, alpha)
56
- curve.v2.lerp(rayEnd, alpha)
55
+ if (firstRender) {
56
+ curve.v0.copy(rayStart)
57
+ curve.v1.copy(rayMidpoint)
58
+ curve.v2.copy(rayEnd)
59
+ firstRender = false
60
+ } else {
61
+ curve.v0.lerp(rayStart, alpha)
62
+ curve.v1.lerp(rayMidpoint, alpha)
63
+ curve.v2.lerp(rayEnd, alpha)
64
+ }
57
65
 
58
66
  for (let i = 0, j = 0; i < rayDivisions; i += 1, j += 3) {
59
67
  const t = i / rayDivisions
@@ -66,6 +74,10 @@
66
74
  lineGeometry.setPositions(positions)
67
75
  }
68
76
 
77
+ $effect(() => {
78
+ if (intersection === undefined) firstRender = true
79
+ })
80
+
69
81
  useTask(
70
82
  () => {
71
83
  setCurvePoints()
@@ -2,4 +2,31 @@ import { type Readable } from 'svelte/store';
2
2
  export type CurrentReadable<T> = Readable<T> & {
3
3
  current: T;
4
4
  };
5
- export declare const toCurrentReadable: <T>(getter: () => T) => CurrentReadable<T>;
5
+ /**
6
+ * ### `runeToCurrentReadable`
7
+ *
8
+ * Bridges a Svelte 5 `$state` or `$derived` rune into the `CurrentReadable`
9
+ * interface, allowing rune-backed reactive values to be consumed as read-only
10
+ * stores by legacy or store-aware code.
11
+ *
12
+ * Pass the getter of a `$state` or `$derived` variable:
13
+ *
14
+ * ```ts
15
+ * let count = $state(0)
16
+ * const doubled = $derived(count * 2)
17
+ *
18
+ * const store = runeToCurrentReadable(() => doubled)
19
+ *
20
+ * store.subscribe((v) => console.log(v)) // reacts to rune changes
21
+ * console.log(store.current) // synchronous read, no overhead
22
+ * ```
23
+ *
24
+ * Use this over `runeToCurrentWritable` when the value is derived or when
25
+ * external writes should not be allowed — for example, renderer settings
26
+ * that are owned by `<Canvas>` props.
27
+ *
28
+ * The `.current` property reads the rune directly and is always in sync.
29
+ * `.subscribe` is powered by `toStore`, which tracks the getter reactively
30
+ * via `$effect`.
31
+ */
32
+ export declare const runeToCurrentReadable: <T>(get: () => T) => CurrentReadable<T>;
@@ -1,11 +1,38 @@
1
+ import { untrack } from 'svelte';
1
2
  import { toStore } from 'svelte/store';
2
- export const toCurrentReadable = (getter) => {
3
- const store = toStore(getter);
4
- store.current = getter();
5
- $effect.pre(() => {
6
- return store.subscribe((value) => {
7
- store.current = value;
8
- });
9
- });
10
- return store;
3
+ /**
4
+ * ### `runeToCurrentReadable`
5
+ *
6
+ * Bridges a Svelte 5 `$state` or `$derived` rune into the `CurrentReadable`
7
+ * interface, allowing rune-backed reactive values to be consumed as read-only
8
+ * stores by legacy or store-aware code.
9
+ *
10
+ * Pass the getter of a `$state` or `$derived` variable:
11
+ *
12
+ * ```ts
13
+ * let count = $state(0)
14
+ * const doubled = $derived(count * 2)
15
+ *
16
+ * const store = runeToCurrentReadable(() => doubled)
17
+ *
18
+ * store.subscribe((v) => console.log(v)) // reacts to rune changes
19
+ * console.log(store.current) // synchronous read, no overhead
20
+ * ```
21
+ *
22
+ * Use this over `runeToCurrentWritable` when the value is derived or when
23
+ * external writes should not be allowed — for example, renderer settings
24
+ * that are owned by `<Canvas>` props.
25
+ *
26
+ * The `.current` property reads the rune directly and is always in sync.
27
+ * `.subscribe` is powered by `toStore`, which tracks the getter reactively
28
+ * via `$effect`.
29
+ */
30
+ export const runeToCurrentReadable = (get) => {
31
+ const { subscribe } = toStore(get);
32
+ return {
33
+ subscribe,
34
+ get current() {
35
+ return untrack(get);
36
+ }
37
+ };
11
38
  };
@@ -1,9 +1,9 @@
1
1
  import type { XRController } from '../types.js';
2
2
  import { type CurrentReadable } from './currentReadable.svelte.js';
3
3
  declare class Controllers {
4
- left: XRController | undefined;
5
- right: XRController | undefined;
6
- none: XRController | undefined;
4
+ get left(): XRController | undefined;
5
+ get right(): XRController | undefined;
6
+ get none(): XRController | undefined;
7
7
  }
8
8
  export declare const controllers: Controllers;
9
9
  /**
@@ -1,8 +1,31 @@
1
- import { toCurrentReadable } from './currentReadable.svelte.js';
1
+ import { getControllerState } from '../internal/inputSources.svelte.js';
2
+ import { runeToCurrentReadable } from './currentReadable.svelte.js';
3
+ const controllerObjects = new WeakMap();
4
+ const toXRController = (state) => {
5
+ if (state === undefined)
6
+ return undefined;
7
+ let controller = controllerObjects.get(state);
8
+ if (controller !== undefined)
9
+ return controller;
10
+ controller = {
11
+ inputSource: state.inputSource,
12
+ targetRay: state.targetRay,
13
+ grip: state.grip,
14
+ model: state.model
15
+ };
16
+ controllerObjects.set(state, controller);
17
+ return controller;
18
+ };
2
19
  class Controllers {
3
- left = $state.raw();
4
- right = $state.raw();
5
- none = $state.raw();
20
+ get left() {
21
+ return toXRController(getControllerState('left'));
22
+ }
23
+ get right() {
24
+ return toXRController(getControllerState('right'));
25
+ }
26
+ get none() {
27
+ return toXRController(getControllerState('none'));
28
+ }
6
29
  }
7
30
  export const controllers = new Controllers();
8
31
  /**
@@ -11,11 +34,11 @@ export const controllers = new Controllers();
11
34
  export const useController = (handedness) => {
12
35
  switch (handedness) {
13
36
  case 'left':
14
- return toCurrentReadable(() => controllers.left);
37
+ return runeToCurrentReadable(() => controllers.left);
15
38
  case 'right':
16
- return toCurrentReadable(() => controllers.right);
39
+ return runeToCurrentReadable(() => controllers.right);
17
40
  case 'none':
18
- return toCurrentReadable(() => controllers.none);
41
+ return runeToCurrentReadable(() => controllers.none);
19
42
  default:
20
43
  throw new Error('useController handedness must be left, right, or none.');
21
44
  }
@@ -1,8 +1,8 @@
1
1
  import type { XRHandObject } from '../types.js';
2
2
  import { type CurrentReadable } from './currentReadable.svelte.js';
3
3
  declare class Hands {
4
- left: XRHandObject | undefined;
5
- right: XRHandObject | undefined;
4
+ get left(): XRHandObject | undefined;
5
+ get right(): XRHandObject | undefined;
6
6
  }
7
7
  export declare const hands: Hands;
8
8
  /**
@@ -1,7 +1,28 @@
1
- import { toCurrentReadable } from './currentReadable.svelte.js';
1
+ import { getHandState } from '../internal/inputSources.svelte.js';
2
+ import { runeToCurrentReadable } from './currentReadable.svelte.js';
3
+ const handObjects = new WeakMap();
4
+ const toXRHandObject = (state) => {
5
+ if (state === undefined)
6
+ return undefined;
7
+ let hand = handObjects.get(state);
8
+ if (hand !== undefined)
9
+ return hand;
10
+ hand = {
11
+ targetRay: state.targetRay,
12
+ hand: state.hand,
13
+ model: state.model,
14
+ inputSource: state.inputSource.hand
15
+ };
16
+ handObjects.set(state, hand);
17
+ return hand;
18
+ };
2
19
  class Hands {
3
- left = $state.raw();
4
- right = $state.raw();
20
+ get left() {
21
+ return toXRHandObject(getHandState('left'));
22
+ }
23
+ get right() {
24
+ return toXRHandObject(getHandState('right'));
25
+ }
5
26
  }
6
27
  export const hands = new Hands();
7
28
  /**
@@ -10,9 +31,9 @@ export const hands = new Hands();
10
31
  export const useHand = (handedness) => {
11
32
  switch (handedness) {
12
33
  case 'left':
13
- return toCurrentReadable(() => hands.left);
34
+ return runeToCurrentReadable(() => hands.left);
14
35
  case 'right':
15
- return toCurrentReadable(() => hands.right);
36
+ return runeToCurrentReadable(() => hands.right);
16
37
  default:
17
38
  throw new Error('useHand handedness must be left or right.');
18
39
  }
@@ -1,15 +1,17 @@
1
1
  import { useTask, useThrelte } from '@threlte/core';
2
- import { hands } from './useHand.svelte.js';
3
- import { toCurrentReadable } from './currentReadable.svelte.js';
2
+ import { useHand } from './useHand.svelte.js';
3
+ import { isPresenting } from '../internal/state.svelte.js';
4
+ import { runeToCurrentReadable } from './currentReadable.svelte.js';
5
+ import { fromStore } from 'svelte/store';
4
6
  /**
5
7
  * Provides a reference to a requested hand joint, once available.
6
8
  */
7
9
  export const useHandJoint = (handedness, joint) => {
8
10
  const { invalidate } = useThrelte();
9
- const xrhand = $derived(hands[handedness]);
11
+ const hand = fromStore(useHand(handedness));
10
12
  let jointSpace = $state.raw();
11
13
  useTask(() => {
12
- const space = xrhand?.hand.joints[joint];
14
+ const space = hand.current?.hand.joints[joint];
13
15
  // The joint radius is a good indicator that the joint is ready.
14
16
  // Re-check each frame so we pick up reconnects and clear on disconnect.
15
17
  if (space?.jointRadius !== undefined) {
@@ -22,6 +24,6 @@ export const useHandJoint = (handedness, joint) => {
22
24
  jointSpace = undefined;
23
25
  invalidate();
24
26
  }
25
- });
26
- return toCurrentReadable(() => jointSpace);
27
+ }, { running: () => isPresenting.current });
28
+ return runeToCurrentReadable(() => jointSpace);
27
29
  };
@@ -1,7 +1,9 @@
1
1
  import { Matrix4 } from 'three';
2
2
  import { useThrelte, useTask } from '@threlte/core';
3
- import { controllers } from './useController.svelte.js';
3
+ import { useController } from './useController.svelte.js';
4
+ import { useXROrigin } from './useXROrigin.svelte.js';
4
5
  import { isPresenting, session } from '../internal/state.svelte.js';
6
+ import { fromStore } from 'svelte/store';
5
7
  /**
6
8
  * Use this hook to perform a hit test per frame in an AR environment.
7
9
  *
@@ -16,25 +18,62 @@ import { isPresenting, session } from '../internal/state.svelte.js';
16
18
  export const useHitTest = (hitTestCallback, options = {}) => {
17
19
  const source = options.source ?? 'viewer';
18
20
  const { xr } = useThrelte().renderer;
21
+ const xrOrigin = useXROrigin();
19
22
  const hitMatrix = new Matrix4();
20
23
  let hitTestSource = $state.raw();
21
- const getHitTestSource = async (space) => {
22
- if (space === undefined) {
23
- return;
24
- }
25
- hitTestSource = await session.current?.requestHitTestSource?.({ space });
26
- };
27
24
  if (source === 'viewer') {
28
25
  $effect.pre(() => {
29
- session.current?.requestReferenceSpace('viewer').then((space) => {
30
- getHitTestSource(space);
31
- });
26
+ const currentSession = session.current;
27
+ if (currentSession === undefined)
28
+ return;
29
+ let cancelled = false;
30
+ let created;
31
+ currentSession
32
+ .requestReferenceSpace('viewer')
33
+ .then((space) => currentSession.requestHitTestSource?.({ space }))
34
+ .then((src) => {
35
+ if (cancelled || src === undefined) {
36
+ src?.cancel();
37
+ return;
38
+ }
39
+ created = src;
40
+ hitTestSource = src;
41
+ })
42
+ .catch(console.error);
43
+ return () => {
44
+ cancelled = true;
45
+ created?.cancel();
46
+ if (hitTestSource === created)
47
+ hitTestSource = undefined;
48
+ };
32
49
  });
33
50
  }
34
51
  else {
35
- const controller = $derived(controllers[source === 'leftInput' ? 'left' : 'right']);
52
+ const controller = fromStore(useController(source === 'leftInput' ? 'left' : 'right'));
36
53
  $effect.pre(() => {
37
- getHitTestSource(controller?.inputSource.targetRaySpace);
54
+ const currentSession = session.current;
55
+ const space = controller.current?.inputSource.targetRaySpace;
56
+ if (currentSession === undefined || space === undefined)
57
+ return;
58
+ let cancelled = false;
59
+ let created;
60
+ currentSession
61
+ .requestHitTestSource?.({ space })
62
+ ?.then((src) => {
63
+ if (cancelled || src === undefined) {
64
+ src?.cancel();
65
+ return;
66
+ }
67
+ created = src;
68
+ hitTestSource = src;
69
+ })
70
+ .catch(console.error);
71
+ return () => {
72
+ cancelled = true;
73
+ created?.cancel();
74
+ if (hitTestSource === created)
75
+ hitTestSource = undefined;
76
+ };
38
77
  });
39
78
  }
40
79
  useTask(() => {
@@ -48,6 +87,11 @@ export const useHitTest = (hitTestCallback, options = {}) => {
48
87
  return hitTestCallback(hitMatrix, undefined);
49
88
  }
50
89
  hitMatrix.fromArray(pose.transform.matrix);
90
+ const currentOrigin = xrOrigin.current;
91
+ if (currentOrigin !== undefined) {
92
+ currentOrigin.updateWorldMatrix(true, false);
93
+ hitMatrix.premultiply(currentOrigin.matrixWorld);
94
+ }
51
95
  hitTestCallback(hitMatrix, hit);
52
96
  }, { running: () => isPresenting.current && hitTestSource !== undefined });
53
97
  $effect.pre(() => {
@@ -1,17 +1,19 @@
1
- import { Quaternion, type Vector3, type Vector3Tuple } from 'three';
1
+ import { Quaternion, Vector3, type Vector3Tuple } from 'three';
2
2
  /**
3
- * Returns a callback to teleport the player from the world origin to a position and optional orientation.
3
+ * Returns a callback that teleports the player to a target position and optional orientation.
4
4
  *
5
- * @example
6
- * const teleport = useTeleport()
7
- * const vec3 = new THREE.Vector3()
5
+ * When used inside `<XROrigin>`, the origin group is translated directly — the
6
+ * user's feet end up at the target, and their room-scale offset from the origin is preserved.
8
7
  *
9
- * vec3.set(5, 0, 5)
8
+ * When used outside `<XROrigin>`, the underlying `XRReferenceSpace` is mutated to compensate
9
+ * for the viewer's current position so the feet end up at the target regardless of where the
10
+ * user has walked in their physical space.
10
11
  *
11
- * teleport(vec3)
12
+ * @example
13
+ * const teleport = useTeleport()
14
+ * teleport([5, 0, 5])
12
15
  *
13
16
  * const quat = new THREE.Quaternion()
14
- *
15
- * teleport(vec3, quat)
17
+ * teleport(new THREE.Vector3(5, 0, 5), quat)
16
18
  */
17
19
  export declare const useTeleport: () => (position: Vector3 | Vector3Tuple, orientation?: Quaternion) => void;
@@ -1,28 +1,76 @@
1
- import { Quaternion } from 'three';
1
+ import { Matrix4, Quaternion, Vector3 } from 'three';
2
2
  import { useThrelte } from '@threlte/core';
3
- const quaternion = new Quaternion();
3
+ import { useXROrigin } from './useXROrigin.svelte.js';
4
+ const defaultOrientation = new Quaternion();
4
5
  const offset = { x: 0, y: 0, z: 0 };
6
+ const targetPosition = new Vector3();
7
+ const localOffset = new Vector3();
8
+ const worldOffset = new Vector3();
9
+ const originWorldPosition = new Vector3();
10
+ const parentWorldQuaternion = new Quaternion();
11
+ const localQuaternion = new Quaternion();
12
+ const inverseParentQuaternion = new Quaternion();
13
+ const localBasis = new Matrix4();
14
+ const worldBasis = new Matrix4();
15
+ const inverseParentMatrix = new Matrix4();
5
16
  /**
6
- * Returns a callback to teleport the player from the world origin to a position and optional orientation.
17
+ * Returns a callback that teleports the player to a target position and optional orientation.
7
18
  *
8
- * @example
9
- * const teleport = useTeleport()
10
- * const vec3 = new THREE.Vector3()
19
+ * When used inside `<XROrigin>`, the origin group is translated directly — the
20
+ * user's feet end up at the target, and their room-scale offset from the origin is preserved.
11
21
  *
12
- * vec3.set(5, 0, 5)
22
+ * When used outside `<XROrigin>`, the underlying `XRReferenceSpace` is mutated to compensate
23
+ * for the viewer's current position so the feet end up at the target regardless of where the
24
+ * user has walked in their physical space.
13
25
  *
14
- * teleport(vec3)
26
+ * @example
27
+ * const teleport = useTeleport()
28
+ * teleport([5, 0, 5])
15
29
  *
16
30
  * const quat = new THREE.Quaternion()
17
- *
18
- * teleport(vec3, quat)
31
+ * teleport(new THREE.Vector3(5, 0, 5), quat)
19
32
  */
20
33
  export const useTeleport = () => {
21
34
  const { xr } = useThrelte().renderer;
22
- /**
23
- * Teleports a player from the world origin to a position and optional orientation.
24
- */
25
- return (position, orientation = quaternion) => {
35
+ const xrOrigin = useXROrigin();
36
+ return (position, orientation = defaultOrientation) => {
37
+ const currentOrigin = xrOrigin.current;
38
+ if (currentOrigin !== undefined) {
39
+ if (Array.isArray(position)) {
40
+ targetPosition.set(position[0], position[1], position[2]);
41
+ }
42
+ else {
43
+ targetPosition.copy(position);
44
+ }
45
+ const parent = currentOrigin.parent;
46
+ const space = xr.getReferenceSpace();
47
+ const pose = space === null ? undefined : xr.getFrame()?.getViewerPose(space);
48
+ localOffset.set(0, 0, 0);
49
+ if (pose !== undefined && pose !== null) {
50
+ localOffset.set(pose.transform.position.x, 0, pose.transform.position.z);
51
+ }
52
+ if (parent === null) {
53
+ localQuaternion.copy(orientation);
54
+ worldBasis.compose(originWorldPosition.set(0, 0, 0), localQuaternion, currentOrigin.scale);
55
+ worldOffset.copy(localOffset).applyMatrix4(worldBasis);
56
+ currentOrigin.position.copy(targetPosition).sub(worldOffset);
57
+ currentOrigin.quaternion.copy(localQuaternion);
58
+ }
59
+ else {
60
+ parent.updateWorldMatrix(true, false);
61
+ parent.getWorldQuaternion(parentWorldQuaternion);
62
+ inverseParentQuaternion.copy(parentWorldQuaternion).invert();
63
+ localQuaternion.copy(inverseParentQuaternion).multiply(orientation);
64
+ localBasis.compose(originWorldPosition.set(0, 0, 0), localQuaternion, currentOrigin.scale);
65
+ worldBasis.copy(parent.matrixWorld).multiply(localBasis);
66
+ worldOffset.copy(localOffset).applyMatrix4(worldBasis);
67
+ originWorldPosition.copy(targetPosition).sub(worldOffset);
68
+ inverseParentMatrix.copy(parent.matrixWorld).invert();
69
+ currentOrigin.position.copy(originWorldPosition.applyMatrix4(inverseParentMatrix));
70
+ currentOrigin.quaternion.copy(localQuaternion);
71
+ }
72
+ return;
73
+ }
26
74
  const space = xr.getReferenceSpace();
27
75
  if (space === null)
28
76
  return;
@@ -1,13 +1,13 @@
1
1
  import { isPresenting, isHandTracking, session, xr } from '../internal/state.svelte.js';
2
- import { toCurrentReadable } from './currentReadable.svelte.js';
2
+ import { runeToCurrentReadable } from './currentReadable.svelte.js';
3
3
  /**
4
4
  * Provides access to context related to `<XR />`.
5
5
  */
6
6
  export const useXR = () => {
7
7
  return {
8
- isPresenting: toCurrentReadable(() => isPresenting.current),
9
- isHandTracking: toCurrentReadable(() => isHandTracking.current),
10
- session: toCurrentReadable(() => session.current),
11
- xr: toCurrentReadable(() => xr.current)
8
+ isPresenting: runeToCurrentReadable(() => isPresenting.current),
9
+ isHandTracking: runeToCurrentReadable(() => isHandTracking.current),
10
+ session: runeToCurrentReadable(() => session.current),
11
+ xr: runeToCurrentReadable(() => xr.current)
12
12
  };
13
13
  };
@@ -0,0 +1,10 @@
1
+ import type { Group } from 'three';
2
+ interface Context {
3
+ current: Group | undefined;
4
+ }
5
+ /**
6
+ * Returns XR-scoped origin state for the current `<XR>` tree. `current` is the
7
+ * mounted `<XROrigin>` group when present, otherwise `undefined`.
8
+ */
9
+ export declare const useXROrigin: () => Context;
10
+ export {};
@@ -0,0 +1,11 @@
1
+ class XROriginState {
2
+ current = $state.raw();
3
+ }
4
+ const origin = new XROriginState();
5
+ /**
6
+ * Returns XR-scoped origin state for the current `<XR>` tree. `current` is the
7
+ * mounted `<XROrigin>` group when present, otherwise `undefined`.
8
+ */
9
+ export const useXROrigin = () => {
10
+ return origin;
11
+ };
package/dist/index.d.ts CHANGED
@@ -5,11 +5,13 @@ export { default as Controller } from './components/Controller.svelte';
5
5
  export { default as Hand } from './components/Hand.svelte';
6
6
  export { default as Headset } from './components/Headset.svelte';
7
7
  export { default as XR } from './components/XR.svelte';
8
+ export { default as XROrigin } from './components/XROrigin.svelte';
8
9
  export { getXRSupportState } from './lib/getXRSupportState.js';
9
10
  export { toggleXRSession } from './lib/toggleXRSession.js';
10
11
  export { handJoints } from './lib/handJoints.js';
11
12
  export { pointerControls } from './plugins/pointerControls/index.js';
12
13
  export { teleportControls } from './plugins/teleportControls/index.js';
14
+ export { touchControls } from './plugins/touchControls/index.js';
13
15
  export { useController } from './hooks/useController.svelte.js';
14
16
  export { useHand } from './hooks/useHand.svelte.js';
15
17
  export { useHandJoint } from './hooks/useHandJoint.svelte.js';
@@ -17,4 +19,6 @@ export { useHeadset } from './hooks/useHeadset.js';
17
19
  export { useHitTest } from './hooks/useHitTest.svelte.js';
18
20
  export { useTeleport } from './hooks/useTeleport.js';
19
21
  export { useXR } from './hooks/useXR.js';
22
+ export { useXROrigin } from './hooks/useXROrigin.svelte.js';
20
23
  export type { XRSessionEventType, XRControllerEventType, XRHandEventType, XRControllerEvent, XRController, XRHandObject, XRHandEvent } from './types.js';
24
+ export type { HandJoints } from './lib/handJoints.js';
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { default as Controller } from './components/Controller.svelte';
6
6
  export { default as Hand } from './components/Hand.svelte';
7
7
  export { default as Headset } from './components/Headset.svelte';
8
8
  export { default as XR } from './components/XR.svelte';
9
+ export { default as XROrigin } from './components/XROrigin.svelte';
9
10
  // Utilities
10
11
  export { getXRSupportState } from './lib/getXRSupportState.js';
11
12
  export { toggleXRSession } from './lib/toggleXRSession.js';
@@ -13,6 +14,7 @@ export { handJoints } from './lib/handJoints.js';
13
14
  // Plugins
14
15
  export { pointerControls } from './plugins/pointerControls/index.js';
15
16
  export { teleportControls } from './plugins/teleportControls/index.js';
17
+ export { touchControls } from './plugins/touchControls/index.js';
16
18
  // Hooks
17
19
  export { useController } from './hooks/useController.svelte.js';
18
20
  export { useHand } from './hooks/useHand.svelte.js';
@@ -21,3 +23,4 @@ export { useHeadset } from './hooks/useHeadset.js';
21
23
  export { useHitTest } from './hooks/useHitTest.svelte.js';
22
24
  export { useTeleport } from './hooks/useTeleport.js';
23
25
  export { useXR } from './hooks/useXR.js';
26
+ export { useXROrigin } from './hooks/useXROrigin.svelte.js';