@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
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
-->
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import { T, useThrelte } from '@threlte/core'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
teleportState,
|
|
11
|
-
controllerEvents
|
|
12
|
-
} from '../internal/state.svelte.js'
|
|
6
|
+
import { useController } from '../hooks/useController.svelte.js'
|
|
7
|
+
import { pointerState, teleportState } from '../internal/state.svelte.js'
|
|
8
|
+
import { addSubscriber } from '../internal/inputSources.svelte.js'
|
|
9
|
+
import { useXROrigin } from '../hooks/useXROrigin.svelte.js'
|
|
13
10
|
import type { XRControllerEvents } from '../types.js'
|
|
14
11
|
import PointerCursor from './internal/PointerCursor.svelte'
|
|
15
12
|
import ShortRay from './internal/ShortRay.svelte'
|
|
@@ -71,63 +68,68 @@
|
|
|
71
68
|
}: Props = $props()
|
|
72
69
|
|
|
73
70
|
const { scene } = useThrelte()
|
|
74
|
-
|
|
75
|
-
const
|
|
71
|
+
const xrOrigin = useXROrigin()
|
|
72
|
+
const attachTarget = $derived(xrOrigin.current ?? scene)
|
|
73
|
+
const handedness: 'left' | 'right' = left ? 'left' : right ? 'right' : (hand ?? 'left')
|
|
74
|
+
const controller = useController(handedness)
|
|
76
75
|
|
|
77
76
|
$effect.pre(() => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
77
|
+
return addSubscriber({
|
|
78
|
+
type: 'controller',
|
|
79
|
+
handedness,
|
|
80
|
+
callbacks: {
|
|
81
|
+
onconnected,
|
|
82
|
+
ondisconnected,
|
|
83
|
+
onselect,
|
|
84
|
+
onselectend,
|
|
85
|
+
onselectstart,
|
|
86
|
+
onsqueeze,
|
|
87
|
+
onsqueezeend,
|
|
88
|
+
onsqueezestart
|
|
89
|
+
}
|
|
90
|
+
})
|
|
93
91
|
})
|
|
94
92
|
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
const grip = $derived($controller?.grip)
|
|
94
|
+
const targetRay = $derived($controller?.targetRay)
|
|
95
|
+
const model = $derived($controller?.model)
|
|
96
|
+
const hasPointerControls = $derived.by(() =>
|
|
97
|
+
handedness === 'left' ? pointerState.left.enabled : pointerState.right.enabled
|
|
98
|
+
)
|
|
99
|
+
const hasTeleportControls = $derived.by(() =>
|
|
100
|
+
handedness === 'left' ? teleportState.left.enabled : teleportState.right.enabled
|
|
101
|
+
)
|
|
101
102
|
</script>
|
|
102
103
|
|
|
103
|
-
{#if
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{/if}
|
|
118
|
-
|
|
119
|
-
{#if targetRay}
|
|
120
|
-
<T is={targetRay}>
|
|
121
|
-
{@render targetRaySnippet?.()}
|
|
104
|
+
{#if grip}
|
|
105
|
+
<T
|
|
106
|
+
is={grip}
|
|
107
|
+
attach={attachTarget}
|
|
108
|
+
>
|
|
109
|
+
{#if children}
|
|
110
|
+
{@render children?.()}
|
|
111
|
+
{:else}
|
|
112
|
+
<T is={model} />
|
|
113
|
+
{/if}
|
|
114
|
+
|
|
115
|
+
{@render gripSnippet?.()}
|
|
116
|
+
</T>
|
|
117
|
+
{/if}
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
{#if targetRay}
|
|
120
|
+
<T
|
|
121
|
+
is={targetRay}
|
|
122
|
+
attach={attachTarget}
|
|
123
|
+
>
|
|
124
|
+
{@render targetRaySnippet?.()}
|
|
125
|
+
|
|
126
|
+
{#if hasPointerControls || hasTeleportControls}
|
|
127
|
+
<ShortRay
|
|
128
|
+
{handedness}
|
|
129
|
+
children={pointerRaySnippet}
|
|
130
|
+
/>
|
|
131
|
+
{/if}
|
|
132
|
+
</T>
|
|
131
133
|
{/if}
|
|
132
134
|
|
|
133
135
|
{#if hasPointerControls}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { Group } from 'three'
|
|
3
3
|
import { T, useThrelte, useTask, useStage } from '@threlte/core'
|
|
4
4
|
import type { XRHandEvents } from '../types.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { addSubscriber } from '../internal/inputSources.svelte.js'
|
|
6
|
+
import { useHand } from '../hooks/useHand.svelte.js'
|
|
7
|
+
import { useXROrigin } from '../hooks/useXROrigin.svelte.js'
|
|
7
8
|
import type { Snippet } from 'svelte'
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
@@ -46,23 +47,23 @@
|
|
|
46
47
|
}: Props = $props()
|
|
47
48
|
|
|
48
49
|
const { scene, renderer, renderStage } = useThrelte()
|
|
49
|
-
|
|
50
|
-
const
|
|
50
|
+
const xrOrigin = useXROrigin()
|
|
51
|
+
const attachTarget = $derived(xrOrigin.current ?? scene)
|
|
52
|
+
const handedness: 'left' | 'right' = left ? 'left' : right ? 'right' : (hand ?? 'left')
|
|
53
|
+
const handStore = useHand(handedness)
|
|
51
54
|
|
|
52
55
|
$effect.pre(() => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
ondisconnected,
|
|
57
|
-
|
|
58
|
-
onpinchstart
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return () => {
|
|
62
|
-
handEvents[key] = undefined
|
|
63
|
-
}
|
|
56
|
+
return addSubscriber({
|
|
57
|
+
type: 'hand',
|
|
58
|
+
handedness,
|
|
59
|
+
callbacks: { onconnected, ondisconnected, onpinchend, onpinchstart }
|
|
60
|
+
})
|
|
64
61
|
})
|
|
65
62
|
|
|
63
|
+
const xrHand = $derived($handStore)
|
|
64
|
+
const inputSource = $derived(xrHand?.inputSource)
|
|
65
|
+
const model = $derived(xrHand?.model)
|
|
66
|
+
|
|
66
67
|
const stage = useStage(Symbol('xr-hand-stage'), { before: renderStage })
|
|
67
68
|
|
|
68
69
|
const group = new Group()
|
|
@@ -94,22 +95,15 @@
|
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
97
|
stage,
|
|
97
|
-
running: () =>
|
|
98
|
-
isHandTracking.current &&
|
|
99
|
-
(wrist !== undefined || children !== undefined) &&
|
|
100
|
-
inputSource !== undefined
|
|
98
|
+
running: () => inputSource !== undefined && (wrist !== undefined || children !== undefined)
|
|
101
99
|
}
|
|
102
100
|
)
|
|
103
|
-
|
|
104
|
-
const xrHand = $derived(hands[handedness])
|
|
105
|
-
const inputSource = $derived(xrHand?.inputSource)
|
|
106
|
-
const model = $derived(xrHand?.model)
|
|
107
101
|
</script>
|
|
108
102
|
|
|
109
|
-
{#if xrHand?.hand
|
|
103
|
+
{#if xrHand?.hand}
|
|
110
104
|
<T
|
|
111
105
|
is={xrHand.hand}
|
|
112
|
-
attach={
|
|
106
|
+
attach={attachTarget}
|
|
113
107
|
>
|
|
114
108
|
{#if children === undefined}
|
|
115
109
|
<T is={model} />
|
|
@@ -117,16 +111,17 @@
|
|
|
117
111
|
</T>
|
|
118
112
|
|
|
119
113
|
{#if targetRay !== undefined}
|
|
120
|
-
<T
|
|
114
|
+
<T
|
|
115
|
+
is={xrHand.targetRay}
|
|
116
|
+
attach={attachTarget}
|
|
117
|
+
>
|
|
121
118
|
{@render targetRay()}
|
|
122
119
|
</T>
|
|
123
120
|
{/if}
|
|
124
|
-
{/if}
|
|
125
121
|
|
|
126
|
-
{#if isHandTracking.current}
|
|
127
122
|
<T
|
|
128
123
|
is={group}
|
|
129
|
-
attach={
|
|
124
|
+
attach={attachTarget}
|
|
130
125
|
>
|
|
131
126
|
{@render wrist?.()}
|
|
132
127
|
{@render children?.()}
|
|
@@ -17,6 +17,17 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
-->
|
|
20
|
+
<script
|
|
21
|
+
module
|
|
22
|
+
lang="ts"
|
|
23
|
+
>
|
|
24
|
+
declare global {
|
|
25
|
+
interface XRSystem {
|
|
26
|
+
offerSession?: XRSystem['requestSession']
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
20
31
|
<script lang="ts">
|
|
21
32
|
import type { EventListener, WebXRManager, Event as ThreeEvent } from 'three'
|
|
22
33
|
import type { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js'
|
|
@@ -24,16 +35,30 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
24
35
|
import { untrack, type Snippet } from 'svelte'
|
|
25
36
|
import { useThrelte } from '@threlte/core'
|
|
26
37
|
import {
|
|
27
|
-
isHandTracking,
|
|
28
38
|
isPresenting,
|
|
39
|
+
lastSessionRequest,
|
|
40
|
+
pointerIntersection,
|
|
29
41
|
referenceSpaceType,
|
|
30
42
|
session,
|
|
43
|
+
teleportIntersection,
|
|
31
44
|
xr
|
|
32
45
|
} from '../internal/state.svelte.js'
|
|
33
46
|
import { setupRaf } from '../internal/setupRaf.svelte.js'
|
|
34
47
|
import { setupHeadset } from '../internal/setupHeadset.svelte.js'
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
48
|
+
import { setupInputSources } from '../internal/setupInputSources.js'
|
|
49
|
+
import { dispatchXRInputSourceEvent } from '../internal/inputSources.svelte.js'
|
|
50
|
+
import { defaultFeatures } from '../internal/defaultFeatures.js'
|
|
51
|
+
import { getXRSessionOptions } from '../lib/getXRSessionOptions.js'
|
|
52
|
+
import { toggleXRSession } from '../lib/toggleXRSession.js'
|
|
53
|
+
|
|
54
|
+
const INPUT_SOURCE_EVENTS = [
|
|
55
|
+
'select',
|
|
56
|
+
'selectstart',
|
|
57
|
+
'selectend',
|
|
58
|
+
'squeeze',
|
|
59
|
+
'squeezestart',
|
|
60
|
+
'squeezeend'
|
|
61
|
+
] as const
|
|
37
62
|
|
|
38
63
|
interface Props {
|
|
39
64
|
/**
|
|
@@ -81,6 +106,23 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
81
106
|
|
|
82
107
|
/** Called when the session frame rate changes. */
|
|
83
108
|
onframeratechange?: (event: XRSessionEvent) => void
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Auto-enter a session when the OS grants one without an explicit request
|
|
112
|
+
* (e.g. when the user puts on a headset). Pass `false` to disable, or an
|
|
113
|
+
* array of modes to restrict which modes are eligible.
|
|
114
|
+
* @default true
|
|
115
|
+
*/
|
|
116
|
+
enterGrantedSession?: boolean | XRSessionMode[]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pre-offer a session via `navigator.xr.offerSession` so the browser can
|
|
120
|
+
* show its own entry UI (e.g. Vision Pro). When `true`, offers AR if
|
|
121
|
+
* supported, otherwise VR. Pass a specific mode to restrict. Pass `false`
|
|
122
|
+
* to disable.
|
|
123
|
+
* @default true
|
|
124
|
+
*/
|
|
125
|
+
offerSession?: boolean | XRSessionMode
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
let {
|
|
@@ -92,6 +134,8 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
92
134
|
onvisibilitychange,
|
|
93
135
|
oninputsourceschange,
|
|
94
136
|
onframeratechange,
|
|
137
|
+
enterGrantedSession = true,
|
|
138
|
+
offerSession = true,
|
|
95
139
|
fallback,
|
|
96
140
|
children,
|
|
97
141
|
handFactory,
|
|
@@ -102,25 +146,21 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
102
146
|
|
|
103
147
|
setupRaf()
|
|
104
148
|
setupHeadset()
|
|
105
|
-
|
|
106
|
-
setupHands(handFactory)
|
|
149
|
+
const bindInputSources = setupInputSources(controllerFactory, handFactory)
|
|
107
150
|
|
|
108
151
|
const handleSessionStart: EventListener<object, 'sessionstart', WebXRManager> = (event) => {
|
|
109
152
|
isPresenting.current = true
|
|
110
|
-
const currentSession = renderer.xr.getSession()
|
|
111
|
-
if (currentSession !== null) {
|
|
112
|
-
isHandTracking.current = Array.from(currentSession.inputSources).some(
|
|
113
|
-
(source) => source.hand !== undefined
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
153
|
onsessionstart?.(event)
|
|
117
154
|
}
|
|
118
155
|
|
|
119
156
|
const handleSessionEnd = (event: XRSessionEvent) => {
|
|
120
157
|
onsessionend?.(event)
|
|
121
158
|
isPresenting.current = false
|
|
122
|
-
isHandTracking.current = false
|
|
123
159
|
session.current = undefined
|
|
160
|
+
pointerIntersection.left = undefined
|
|
161
|
+
pointerIntersection.right = undefined
|
|
162
|
+
teleportIntersection.left = undefined
|
|
163
|
+
teleportIntersection.right = undefined
|
|
124
164
|
}
|
|
125
165
|
|
|
126
166
|
const handleVisibilityChange = (event: XRSessionEvent) => {
|
|
@@ -128,7 +168,6 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
128
168
|
}
|
|
129
169
|
|
|
130
170
|
const handleInputSourcesChange = (event: XRInputSourcesChangeEvent) => {
|
|
131
|
-
isHandTracking.current = Object.values(event.session.inputSources).some((source) => source.hand)
|
|
132
171
|
oninputsourceschange?.(event)
|
|
133
172
|
}
|
|
134
173
|
|
|
@@ -136,23 +175,37 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
136
175
|
onframeratechange?.(event)
|
|
137
176
|
}
|
|
138
177
|
|
|
178
|
+
const handleXRInputEvent = (event: XRInputSourceEvent) => {
|
|
179
|
+
dispatchXRInputSourceEvent(event)
|
|
180
|
+
}
|
|
181
|
+
|
|
139
182
|
$effect(() => {
|
|
140
183
|
const currentSession = session.current
|
|
141
184
|
|
|
142
185
|
if (currentSession === undefined) {
|
|
186
|
+
bindInputSources(undefined)
|
|
143
187
|
return
|
|
144
188
|
}
|
|
145
189
|
|
|
190
|
+
bindInputSources(currentSession)
|
|
191
|
+
|
|
146
192
|
currentSession.addEventListener('visibilitychange', handleVisibilityChange)
|
|
147
193
|
currentSession.addEventListener('inputsourceschange', handleInputSourcesChange)
|
|
148
194
|
currentSession.addEventListener('frameratechange', handleFramerateChange)
|
|
149
195
|
currentSession.addEventListener('end', handleSessionEnd)
|
|
196
|
+
for (const type of INPUT_SOURCE_EVENTS) {
|
|
197
|
+
currentSession.addEventListener(type, handleXRInputEvent)
|
|
198
|
+
}
|
|
150
199
|
|
|
151
200
|
return () => {
|
|
152
201
|
currentSession.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
153
202
|
currentSession.removeEventListener('inputsourceschange', handleInputSourcesChange)
|
|
154
203
|
currentSession.removeEventListener('frameratechange', handleFramerateChange)
|
|
155
204
|
currentSession.removeEventListener('end', handleSessionEnd)
|
|
205
|
+
for (const type of INPUT_SOURCE_EVENTS) {
|
|
206
|
+
currentSession.removeEventListener(type, handleXRInputEvent)
|
|
207
|
+
}
|
|
208
|
+
bindInputSources(undefined)
|
|
156
209
|
}
|
|
157
210
|
})
|
|
158
211
|
|
|
@@ -170,8 +223,6 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
170
223
|
})
|
|
171
224
|
|
|
172
225
|
$effect.pre(() => {
|
|
173
|
-
const currentSession = session.current
|
|
174
|
-
|
|
175
226
|
xr.current = renderer.xr
|
|
176
227
|
renderer.xr.enabled = true
|
|
177
228
|
renderer.xr.addEventListener('sessionstart', handleSessionStart)
|
|
@@ -182,7 +233,9 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
182
233
|
renderer.xr.removeEventListener('sessionstart', handleSessionStart)
|
|
183
234
|
|
|
184
235
|
// if unmounted while presenting (e.g. due to sveltekit navigation), end the session
|
|
185
|
-
|
|
236
|
+
untrack(() => session.current)
|
|
237
|
+
?.end()
|
|
238
|
+
.catch(() => {})
|
|
186
239
|
}
|
|
187
240
|
})
|
|
188
241
|
|
|
@@ -204,6 +257,83 @@ This should be placed within a Threlte `<Canvas />`.
|
|
|
204
257
|
renderer.xr.setReferenceSpaceType(referenceSpace)
|
|
205
258
|
referenceSpaceType.current = referenceSpace
|
|
206
259
|
})
|
|
260
|
+
|
|
261
|
+
$effect.pre(() => {
|
|
262
|
+
if (enterGrantedSession === false) return
|
|
263
|
+
|
|
264
|
+
const allowed: XRSessionMode[] = Array.isArray(enterGrantedSession)
|
|
265
|
+
? enterGrantedSession
|
|
266
|
+
: ['immersive-ar', 'immersive-vr']
|
|
267
|
+
|
|
268
|
+
const listener = async () => {
|
|
269
|
+
// Prefer to replay whatever mode + sessionInit the app entered with last.
|
|
270
|
+
if (lastSessionRequest.mode !== undefined && allowed.includes(lastSessionRequest.mode)) {
|
|
271
|
+
toggleXRSession(lastSessionRequest.mode, lastSessionRequest.sessionInit, 'enter').catch(
|
|
272
|
+
() => {}
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const mode of allowed) {
|
|
278
|
+
if (await navigator.xr?.isSessionSupported(mode).catch(() => false)) {
|
|
279
|
+
toggleXRSession(mode, { ...defaultFeatures }, 'enter').catch(() => {})
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
navigator.xr?.addEventListener('sessiongranted', listener)
|
|
286
|
+
return () => {
|
|
287
|
+
navigator.xr?.removeEventListener('sessiongranted', listener)
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
$effect.pre(() => {
|
|
292
|
+
if (navigator.xr === undefined) return
|
|
293
|
+
if (offerSession === false) return
|
|
294
|
+
if (!('offerSession' in navigator.xr)) return
|
|
295
|
+
if (session.current !== undefined) return
|
|
296
|
+
const manager = xr.current
|
|
297
|
+
if (manager === undefined) return
|
|
298
|
+
|
|
299
|
+
let cancelled = false
|
|
300
|
+
|
|
301
|
+
const run = async () => {
|
|
302
|
+
let mode: XRSessionMode
|
|
303
|
+
if (offerSession === true) {
|
|
304
|
+
const arSupported = await navigator.xr
|
|
305
|
+
?.isSessionSupported('immersive-ar')
|
|
306
|
+
.catch(() => false)
|
|
307
|
+
mode = arSupported ? 'immersive-ar' : 'immersive-vr'
|
|
308
|
+
} else {
|
|
309
|
+
mode = offerSession
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const init = getXRSessionOptions(
|
|
313
|
+
referenceSpaceType.current,
|
|
314
|
+
lastSessionRequest.sessionInit,
|
|
315
|
+
defaultFeatures
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const nextSession = await navigator.xr?.offerSession?.(mode, init)
|
|
320
|
+
if (!nextSession || cancelled) return
|
|
321
|
+
await manager.setSession(nextSession)
|
|
322
|
+
if (cancelled) return
|
|
323
|
+
lastSessionRequest.mode = mode
|
|
324
|
+
lastSessionRequest.sessionInit = init
|
|
325
|
+
session.current = nextSession
|
|
326
|
+
} catch {
|
|
327
|
+
// user declined or offer was rejected
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
run()
|
|
332
|
+
|
|
333
|
+
return () => {
|
|
334
|
+
cancelled = true
|
|
335
|
+
}
|
|
336
|
+
})
|
|
207
337
|
</script>
|
|
208
338
|
|
|
209
339
|
{#if isPresenting.current}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface XRSystem {
|
|
3
|
+
offerSession?: XRSystem['requestSession'];
|
|
4
|
+
}
|
|
5
|
+
}
|
|
1
6
|
import type { WebXRManager, Event as ThreeEvent } from 'three';
|
|
2
7
|
import type { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
|
|
3
8
|
import type { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
|
|
@@ -38,6 +43,21 @@ interface Props {
|
|
|
38
43
|
oninputsourceschange?: (event: XRSessionEvent) => void;
|
|
39
44
|
/** Called when the session frame rate changes. */
|
|
40
45
|
onframeratechange?: (event: XRSessionEvent) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Auto-enter a session when the OS grants one without an explicit request
|
|
48
|
+
* (e.g. when the user puts on a headset). Pass `false` to disable, or an
|
|
49
|
+
* array of modes to restrict which modes are eligible.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
enterGrantedSession?: boolean | XRSessionMode[];
|
|
53
|
+
/**
|
|
54
|
+
* Pre-offer a session via `navigator.xr.offerSession` so the browser can
|
|
55
|
+
* show its own entry UI (e.g. Vision Pro). When `true`, offers AR if
|
|
56
|
+
* supported, otherwise VR. Pass a specific mode to restrict. Pass `false`
|
|
57
|
+
* to disable.
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
offerSession?: boolean | XRSessionMode;
|
|
41
61
|
}
|
|
42
62
|
/**
|
|
43
63
|
* `<XR />` is a WebXR manager that configures your scene for XR rendering and interaction.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component `<XROrigin />` represents the position of the XR user's feet. The XR
|
|
3
|
+
camera (headset) is parented to this group, and any `<Controller>` / `<Hand>`
|
|
4
|
+
components nested inside attach here instead of the scene root. Transforming
|
|
5
|
+
the origin (position, rotation, scale) transforms the user in the scene —
|
|
6
|
+
useful for teleportation, dolly rigs, and resizing the user.
|
|
7
|
+
|
|
8
|
+
Only one `<XROrigin>` may be mounted within a given `<XR>`.
|
|
9
|
+
|
|
10
|
+
```svelte
|
|
11
|
+
<XROrigin position={[0, 0, 5]} rotation.y={Math.PI}>
|
|
12
|
+
<Controller left />
|
|
13
|
+
<Controller right />
|
|
14
|
+
<Hand left />
|
|
15
|
+
<Hand right />
|
|
16
|
+
</XROrigin>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Without an `<XROrigin>`, controllers and hands continue to attach to the
|
|
20
|
+
scene root (existing behaviour, unchanged).
|
|
21
|
+
-->
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import { Group } from 'three'
|
|
24
|
+
import { T, useThrelte, type Props } from '@threlte/core'
|
|
25
|
+
import type { Snippet } from 'svelte'
|
|
26
|
+
import { useXROrigin } from '../hooks/useXROrigin.svelte.js'
|
|
27
|
+
import { isPresenting } from '../internal/state.svelte.js'
|
|
28
|
+
|
|
29
|
+
interface XROriginProps extends Props<Group> {
|
|
30
|
+
ref?: Group
|
|
31
|
+
children?: Snippet<[{ ref: Group }]>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let { ref = $bindable(), children, ...rest }: XROriginProps = $props()
|
|
35
|
+
|
|
36
|
+
const { camera, scene } = useThrelte()
|
|
37
|
+
|
|
38
|
+
const group = new Group()
|
|
39
|
+
const origin = useXROrigin()
|
|
40
|
+
|
|
41
|
+
$effect.pre(() => {
|
|
42
|
+
if (origin.current !== undefined && origin.current !== group) {
|
|
43
|
+
console.warn(
|
|
44
|
+
'Only one <XROrigin> may be mounted within a single <XR>. The newer instance will take over.'
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
origin.current = group
|
|
49
|
+
return () => {
|
|
50
|
+
if (origin.current === group) {
|
|
51
|
+
origin.current = undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Parent the active scene camera to this group so its `parent.matrixWorld`
|
|
57
|
+
// reflects our transform. Three's `WebXRManager` reads `camera.parent` (where
|
|
58
|
+
// `camera` is the camera passed to `renderer.render(scene, camera)`) — NOT
|
|
59
|
+
// `renderer.xr.getCamera().parent` — when composing the XR view matrices, so
|
|
60
|
+
// reparenting the XR camera itself has no effect. When this component
|
|
61
|
+
// unmounts (or the active camera changes) the camera returns to its previous
|
|
62
|
+
// parent so non-XR rendering keeps working.
|
|
63
|
+
$effect.pre(() => {
|
|
64
|
+
if (!isPresenting.current) return
|
|
65
|
+
|
|
66
|
+
const userCamera = $camera
|
|
67
|
+
const previousParent = userCamera.parent ?? scene
|
|
68
|
+
group.add(userCamera)
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
previousParent.add(userCamera)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<T
|
|
77
|
+
is={group}
|
|
78
|
+
bind:ref
|
|
79
|
+
{...rest}
|
|
80
|
+
>
|
|
81
|
+
{@render children?.({ ref: group })}
|
|
82
|
+
</T>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Group } from 'three';
|
|
2
|
+
import { type Props } from '@threlte/core';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
interface XROriginProps extends Props<Group> {
|
|
5
|
+
ref?: Group;
|
|
6
|
+
children?: Snippet<[{
|
|
7
|
+
ref: Group;
|
|
8
|
+
}]>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `<XROrigin />` represents the position of the XR user's feet. The XR
|
|
12
|
+
* camera (headset) is parented to this group, and any `<Controller>` / `<Hand>`
|
|
13
|
+
* components nested inside attach here instead of the scene root. Transforming
|
|
14
|
+
* the origin (position, rotation, scale) transforms the user in the scene —
|
|
15
|
+
* useful for teleportation, dolly rigs, and resizing the user.
|
|
16
|
+
*
|
|
17
|
+
* Only one `<XROrigin>` may be mounted within a given `<XR>`.
|
|
18
|
+
*
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <XROrigin position={[0, 0, 5]} rotation.y={Math.PI}>
|
|
21
|
+
* <Controller left />
|
|
22
|
+
* <Controller right />
|
|
23
|
+
* <Hand left />
|
|
24
|
+
* <Hand right />
|
|
25
|
+
* </XROrigin>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Without an `<XROrigin>`, controllers and hands continue to attach to the
|
|
29
|
+
* scene root (existing behaviour, unchanged).
|
|
30
|
+
*/
|
|
31
|
+
declare const XrOrigin: import("svelte").Component<XROriginProps, {}, "ref">;
|
|
32
|
+
type XrOrigin = ReturnType<typeof XrOrigin>;
|
|
33
|
+
export default XrOrigin;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Color, DoubleSide,
|
|
2
|
+
import { Color, DoubleSide, ShaderMaterial, type ColorRepresentation } from 'three'
|
|
3
3
|
import { T } from '@threlte/core'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
const { color = new Color('white'), size = 0.03, thickness = 0.035 }: Props = $props()
|
|
12
12
|
|
|
13
13
|
const vertexShader = `
|
|
14
|
-
uniform mat4 projectionMatrix;
|
|
15
|
-
uniform mat4 modelViewMatrix;
|
|
16
|
-
attribute vec2 uv;
|
|
17
|
-
attribute vec3 position;
|
|
18
14
|
varying vec2 vUv;
|
|
19
15
|
void main() {
|
|
20
16
|
vUv = uv;
|
|
@@ -23,14 +19,13 @@
|
|
|
23
19
|
`
|
|
24
20
|
|
|
25
21
|
const fragmentShader = `
|
|
26
|
-
precision mediump float;
|
|
27
22
|
uniform float thickness;
|
|
28
23
|
uniform vec3 color;
|
|
29
24
|
varying vec2 vUv;
|
|
30
25
|
void main() {
|
|
31
|
-
float
|
|
32
|
-
float
|
|
33
|
-
float alpha = 1.0 -
|
|
26
|
+
float d = abs(distance(vUv, vec2(0.5)) - 0.25);
|
|
27
|
+
float edge = fwidth(d);
|
|
28
|
+
float alpha = 1.0 - smoothstep(thickness - edge, thickness + edge, d);
|
|
34
29
|
gl_FragColor = vec4(color, alpha);
|
|
35
30
|
}
|
|
36
31
|
`
|
|
@@ -40,7 +35,7 @@
|
|
|
40
35
|
color: { value: color }
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
const shaderMaterial = new
|
|
38
|
+
const shaderMaterial = new ShaderMaterial({
|
|
44
39
|
vertexShader,
|
|
45
40
|
fragmentShader,
|
|
46
41
|
uniforms,
|