@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.
- package/dist/components/Controller.svelte +59 -57
- package/dist/components/Hand.svelte +24 -29
- package/dist/components/XR.svelte +146 -16
- package/dist/components/XR.svelte.d.ts +20 -0
- package/dist/components/XROrigin.svelte +82 -0
- package/dist/components/XROrigin.svelte.d.ts +33 -0
- package/dist/components/internal/Cursor.svelte +5 -10
- package/dist/components/internal/PointerCursor.svelte +18 -4
- package/dist/components/internal/TeleportCursor.svelte +4 -1
- package/dist/components/internal/TeleportRay.svelte +15 -3
- package/dist/hooks/currentReadable.svelte.d.ts +28 -1
- package/dist/hooks/currentReadable.svelte.js +36 -9
- package/dist/hooks/useController.svelte.d.ts +3 -3
- package/dist/hooks/useController.svelte.js +30 -7
- package/dist/hooks/useHand.svelte.d.ts +2 -2
- package/dist/hooks/useHand.svelte.js +26 -5
- package/dist/hooks/useHandJoint.svelte.js +8 -6
- package/dist/hooks/useHitTest.svelte.js +56 -12
- package/dist/hooks/useTeleport.d.ts +11 -9
- package/dist/hooks/useTeleport.js +62 -14
- package/dist/hooks/useXR.js +5 -5
- package/dist/hooks/useXROrigin.svelte.d.ts +10 -0
- package/dist/hooks/useXROrigin.svelte.js +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/internal/inputSources.svelte.d.ts +84 -0
- package/dist/internal/inputSources.svelte.js +91 -0
- package/dist/internal/setupHeadset.svelte.js +18 -6
- package/dist/internal/setupInputSources.d.ts +4 -0
- package/dist/internal/setupInputSources.js +319 -0
- package/dist/internal/state.svelte.d.ts +10 -12
- package/dist/internal/state.svelte.js +9 -3
- package/dist/lib/getXRSessionOptions.d.ts +1 -1
- package/dist/lib/getXRSessionOptions.js +8 -7
- package/dist/lib/toggleXRSession.d.ts +1 -1
- package/dist/lib/toggleXRSession.js +22 -7
- package/dist/plugins/pointerControls/compute.js +14 -5
- package/dist/plugins/pointerControls/context.d.ts +3 -3
- package/dist/plugins/pointerControls/context.js +12 -6
- package/dist/plugins/pointerControls/index.d.ts +4 -3
- package/dist/plugins/pointerControls/index.js +63 -31
- package/dist/plugins/pointerControls/plugin.svelte.js +0 -5
- package/dist/plugins/pointerControls/setup.svelte.js +92 -78
- package/dist/plugins/pointerControls/types.d.ts +16 -3
- package/dist/plugins/pointerControls/types.js +2 -1
- package/dist/plugins/teleportControls/compute.d.ts +1 -1
- package/dist/plugins/teleportControls/compute.js +11 -4
- package/dist/plugins/teleportControls/context.d.ts +4 -4
- package/dist/plugins/teleportControls/context.js +1 -4
- package/dist/plugins/teleportControls/index.js +8 -8
- package/dist/plugins/teleportControls/setup.svelte.js +10 -9
- package/dist/plugins/touchControls/compute.d.ts +3 -0
- package/dist/plugins/touchControls/compute.js +13 -0
- package/dist/plugins/touchControls/context.d.ts +12 -0
- package/dist/plugins/touchControls/context.js +27 -0
- package/dist/plugins/touchControls/hook.d.ts +5 -0
- package/dist/plugins/touchControls/hook.js +26 -0
- package/dist/plugins/touchControls/index.d.ts +33 -0
- package/dist/plugins/touchControls/index.js +41 -0
- package/dist/plugins/touchControls/plugin.svelte.d.ts +1 -0
- package/dist/plugins/touchControls/plugin.svelte.js +24 -0
- package/dist/plugins/touchControls/setup.svelte.d.ts +2 -0
- package/dist/plugins/touchControls/setup.svelte.js +247 -0
- package/dist/plugins/touchControls/types.d.ts +62 -0
- package/dist/plugins/touchControls/types.js +11 -0
- package/dist/types.d.ts +1 -1
- package/package.json +3 -2
- package/dist/internal/setupControllers.d.ts +0 -2
- package/dist/internal/setupControllers.js +0 -68
- package/dist/internal/setupHands.d.ts +0 -2
- package/dist/internal/setupHands.js +0 -67
- package/dist/internal/useHandTrackingState.d.ts +0 -5
- 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
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 {
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
37
|
+
return runeToCurrentReadable(() => controllers.left);
|
|
15
38
|
case 'right':
|
|
16
|
-
return
|
|
39
|
+
return runeToCurrentReadable(() => controllers.right);
|
|
17
40
|
case 'none':
|
|
18
|
-
return
|
|
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 {
|
|
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
|
|
4
|
-
|
|
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
|
|
34
|
+
return runeToCurrentReadable(() => hands.left);
|
|
14
35
|
case 'right':
|
|
15
|
-
return
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
11
|
+
const hand = fromStore(useHand(handedness));
|
|
10
12
|
let jointSpace = $state.raw();
|
|
11
13
|
useTask(() => {
|
|
12
|
-
const space =
|
|
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
|
|
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 {
|
|
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
|
|
30
|
-
|
|
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 =
|
|
52
|
+
const controller = fromStore(useController(source === 'leftInput' ? 'left' : 'right'));
|
|
36
53
|
$effect.pre(() => {
|
|
37
|
-
|
|
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,
|
|
1
|
+
import { Quaternion, Vector3, type Vector3Tuple } from 'three';
|
|
2
2
|
/**
|
|
3
|
-
* Returns a callback
|
|
3
|
+
* Returns a callback that teleports the player to a target position and optional orientation.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
17
|
+
* Returns a callback that teleports the player to a target position and optional orientation.
|
|
7
18
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
package/dist/hooks/useXR.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isPresenting, isHandTracking, session, xr } from '../internal/state.svelte.js';
|
|
2
|
-
import {
|
|
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:
|
|
9
|
-
isHandTracking:
|
|
10
|
-
session:
|
|
11
|
-
xr:
|
|
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';
|