@threlte/xr 1.5.5 → 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 (68) hide show
  1. package/dist/components/Controller.svelte +59 -60
  2. package/dist/components/Hand.svelte +21 -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/TeleportRay.svelte +15 -3
  8. package/dist/hooks/currentReadable.svelte.d.ts +28 -1
  9. package/dist/hooks/currentReadable.svelte.js +36 -9
  10. package/dist/hooks/useController.svelte.d.ts +3 -3
  11. package/dist/hooks/useController.svelte.js +30 -7
  12. package/dist/hooks/useHand.svelte.d.ts +2 -2
  13. package/dist/hooks/useHand.svelte.js +26 -5
  14. package/dist/hooks/useHandJoint.svelte.js +6 -5
  15. package/dist/hooks/useHitTest.svelte.js +56 -12
  16. package/dist/hooks/useTeleport.d.ts +11 -9
  17. package/dist/hooks/useTeleport.js +62 -14
  18. package/dist/hooks/useXR.js +5 -5
  19. package/dist/hooks/useXROrigin.svelte.d.ts +10 -0
  20. package/dist/hooks/useXROrigin.svelte.js +11 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +3 -0
  23. package/dist/internal/inputSources.svelte.d.ts +84 -0
  24. package/dist/internal/inputSources.svelte.js +91 -0
  25. package/dist/internal/setupHeadset.svelte.js +18 -6
  26. package/dist/internal/setupInputSources.d.ts +4 -0
  27. package/dist/internal/setupInputSources.js +319 -0
  28. package/dist/internal/state.svelte.d.ts +10 -12
  29. package/dist/internal/state.svelte.js +9 -3
  30. package/dist/lib/getXRSessionOptions.d.ts +1 -1
  31. package/dist/lib/getXRSessionOptions.js +8 -7
  32. package/dist/lib/toggleXRSession.d.ts +1 -1
  33. package/dist/lib/toggleXRSession.js +20 -5
  34. package/dist/plugins/pointerControls/compute.js +14 -9
  35. package/dist/plugins/pointerControls/context.d.ts +3 -3
  36. package/dist/plugins/pointerControls/context.js +12 -6
  37. package/dist/plugins/pointerControls/index.d.ts +4 -3
  38. package/dist/plugins/pointerControls/index.js +63 -29
  39. package/dist/plugins/pointerControls/setup.svelte.js +64 -44
  40. package/dist/plugins/pointerControls/types.d.ts +14 -3
  41. package/dist/plugins/teleportControls/compute.d.ts +1 -1
  42. package/dist/plugins/teleportControls/compute.js +11 -8
  43. package/dist/plugins/teleportControls/context.d.ts +4 -4
  44. package/dist/plugins/teleportControls/context.js +1 -4
  45. package/dist/plugins/teleportControls/index.js +7 -4
  46. package/dist/plugins/teleportControls/setup.svelte.js +10 -9
  47. package/dist/plugins/touchControls/compute.d.ts +3 -0
  48. package/dist/plugins/touchControls/compute.js +13 -0
  49. package/dist/plugins/touchControls/context.d.ts +12 -0
  50. package/dist/plugins/touchControls/context.js +27 -0
  51. package/dist/plugins/touchControls/hook.d.ts +5 -0
  52. package/dist/plugins/touchControls/hook.js +26 -0
  53. package/dist/plugins/touchControls/index.d.ts +33 -0
  54. package/dist/plugins/touchControls/index.js +41 -0
  55. package/dist/plugins/touchControls/plugin.svelte.d.ts +1 -0
  56. package/dist/plugins/touchControls/plugin.svelte.js +24 -0
  57. package/dist/plugins/touchControls/setup.svelte.d.ts +2 -0
  58. package/dist/plugins/touchControls/setup.svelte.js +247 -0
  59. package/dist/plugins/touchControls/types.d.ts +62 -0
  60. package/dist/plugins/touchControls/types.js +11 -0
  61. package/dist/types.d.ts +1 -1
  62. package/package.json +3 -3
  63. package/dist/internal/setupControllers.d.ts +0 -2
  64. package/dist/internal/setupControllers.js +0 -73
  65. package/dist/internal/setupHands.d.ts +0 -2
  66. package/dist/internal/setupHands.js +0 -67
  67. package/dist/internal/useHandTrackingState.d.ts +0 -5
  68. package/dist/internal/useHandTrackingState.js +0 -20
@@ -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,16 +1,17 @@
1
1
  import { useTask, useThrelte } from '@threlte/core';
2
- import { hands } from './useHand.svelte.js';
2
+ import { useHand } from './useHand.svelte.js';
3
3
  import { isPresenting } from '../internal/state.svelte.js';
4
- import { toCurrentReadable } from './currentReadable.svelte.js';
4
+ import { runeToCurrentReadable } from './currentReadable.svelte.js';
5
+ import { fromStore } from 'svelte/store';
5
6
  /**
6
7
  * Provides a reference to a requested hand joint, once available.
7
8
  */
8
9
  export const useHandJoint = (handedness, joint) => {
9
10
  const { invalidate } = useThrelte();
10
- const xrhand = $derived(hands[handedness]);
11
+ const hand = fromStore(useHand(handedness));
11
12
  let jointSpace = $state.raw();
12
13
  useTask(() => {
13
- const space = xrhand?.hand.joints[joint];
14
+ const space = hand.current?.hand.joints[joint];
14
15
  // The joint radius is a good indicator that the joint is ready.
15
16
  // Re-check each frame so we pick up reconnects and clear on disconnect.
16
17
  if (space?.jointRadius !== undefined) {
@@ -24,5 +25,5 @@ export const useHandJoint = (handedness, joint) => {
24
25
  invalidate();
25
26
  }
26
27
  }, { running: () => isPresenting.current });
27
- return toCurrentReadable(() => jointSpace);
28
+ return runeToCurrentReadable(() => jointSpace);
28
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';
@@ -0,0 +1,84 @@
1
+ import type { XRGripSpace, XRHandSpace, XRTargetRaySpace } from 'three';
2
+ import type { XRControllerModel } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
3
+ import type { XRHandModel } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
4
+ import type { XRControllerEvents, XRHandEvents } from '../types.js';
5
+ export type XRHandInputSource = XRInputSource & {
6
+ hand: XRHand;
7
+ };
8
+ export type XRInputSourceStateBase = {
9
+ id: string;
10
+ inputSource: XRInputSource;
11
+ handedness: XRHandedness;
12
+ isPrimary: boolean;
13
+ targetRay: XRTargetRaySpace;
14
+ };
15
+ export type XRControllerSourceState = XRInputSourceStateBase & {
16
+ type: 'controller';
17
+ grip: XRGripSpace;
18
+ model: XRControllerModel;
19
+ };
20
+ export type XRHandSourceState = Omit<XRInputSourceStateBase, 'inputSource'> & {
21
+ type: 'hand';
22
+ inputSource: XRHandInputSource;
23
+ hand: XRHandSpace;
24
+ model: XRHandModel;
25
+ };
26
+ export type XRGazeSourceState = XRInputSourceStateBase & {
27
+ type: 'gaze';
28
+ };
29
+ export type XRTransientPointerSourceState = XRInputSourceStateBase & {
30
+ type: 'transientPointer';
31
+ };
32
+ export type XRScreenInputSourceState = XRInputSourceStateBase & {
33
+ type: 'screenInput';
34
+ };
35
+ export type XRInputSourceState = XRControllerSourceState | XRHandSourceState | XRGazeSourceState | XRTransientPointerSourceState | XRScreenInputSourceState;
36
+ declare class InputSourcesState {
37
+ current: readonly XRInputSourceState[];
38
+ }
39
+ export declare const inputSources: InputSourcesState;
40
+ export type ControllerSubscriber = {
41
+ type: 'controller';
42
+ handedness: XRHandedness;
43
+ callbacks: XRControllerEvents;
44
+ };
45
+ export type HandSubscriber = {
46
+ type: 'hand';
47
+ handedness: 'left' | 'right';
48
+ callbacks: XRHandEvents;
49
+ };
50
+ export type Subscriber = ControllerSubscriber | HandSubscriber;
51
+ /**
52
+ * Registers callbacks with the module-level XR input-source dispatcher.
53
+ *
54
+ * This does not subscribe to a specific `XRInputSource`, `XRSession`, or
55
+ * Three.js object. Instead, the subscriber is stored in the internal
56
+ * `subscribers` set and receives events for whichever current input-source
57
+ * state matches its `type` and `handedness`.
58
+ *
59
+ * For example, a `{ type: 'controller', handedness: 'left' }` subscriber will
60
+ * receive forwarded events for the current left controller, even if the
61
+ * underlying `XRInputSource` instance disconnects and reconnects.
62
+ *
63
+ * Returns a cleanup function that removes the subscriber from the dispatcher.
64
+ */
65
+ export declare const addSubscriber: (sub: Subscriber) => () => void;
66
+ export declare const dispatchEvent: (state: XRInputSourceState, eventType: string, event: unknown) => void;
67
+ export declare const createInputSourceEvent: (state: XRInputSourceState, type: string, extra?: Record<string, unknown>) => {
68
+ type: string;
69
+ data: XRInputSource | XRHandInputSource;
70
+ inputSource: XRInputSource | XRHandInputSource;
71
+ target: XRTargetRaySpace | XRHandSpace;
72
+ };
73
+ export declare const dispatchInputSourceStateEvent: (state: XRInputSourceState, eventType: string, event: unknown, options?: {
74
+ dispatchSpaces?: boolean;
75
+ }) => void;
76
+ type ResolveOptions = {
77
+ isPrimary?: boolean;
78
+ };
79
+ export declare const getInputSourceState: (inputSource: XRInputSource, options?: ResolveOptions) => XRInputSourceState | undefined;
80
+ export declare const getControllerState: (handedness: XRHandedness, options?: ResolveOptions) => XRControllerSourceState | undefined;
81
+ export declare const getHandState: (handedness: "left" | "right", options?: ResolveOptions) => XRHandSourceState | undefined;
82
+ export declare const dispatchInputSourceEvent: (inputSource: XRInputSource, eventType: string, event: unknown) => void;
83
+ export declare const dispatchXRInputSourceEvent: (event: XRInputSourceEvent) => void;
84
+ export {};