@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
@@ -3,13 +3,10 @@
3
3
  -->
4
4
  <script lang="ts">
5
5
  import { T, useThrelte } from '@threlte/core'
6
- import { controllers } from '../hooks/useController.svelte.js'
7
- import {
8
- isHandTracking,
9
- pointerState,
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 handedness = $derived<'left' | 'right'>(left ? 'left' : right ? 'right' : (hand ?? 'left'))
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
- const key = handedness
79
- controllerEvents[key] = {
80
- onconnected,
81
- ondisconnected,
82
- onselect,
83
- onselectend,
84
- onselectstart,
85
- onsqueeze,
86
- onsqueezeend,
87
- onsqueezestart
88
- }
89
-
90
- return () => {
91
- controllerEvents[key] = undefined
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 xrController = $derived(controllers[handedness])
96
- const grip = $derived(xrController?.grip)
97
- const targetRay = $derived(xrController?.targetRay)
98
- const model = $derived(xrController?.model)
99
- const hasPointerControls = $derived(pointerState[handedness].enabled)
100
- const hasTeleportControls = $derived(teleportState[handedness].enabled)
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 !isHandTracking.current}
104
- {#if grip}
105
- <T
106
- is={grip}
107
- attach={scene}
108
- >
109
- {#if children}
110
- {@render children?.()}
111
- {:else}
112
- <T is={model} />
113
- {/if}
114
-
115
- {@render gripSnippet?.()}
116
- </T>
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
- {#if hasPointerControls || hasTeleportControls}
124
- <ShortRay
125
- {handedness}
126
- children={pointerRaySnippet}
127
- />
128
- {/if}
129
- </T>
130
- {/if}
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 { isHandTracking, handEvents } from '../internal/state.svelte.js'
6
- import { hands } from '../hooks/useHand.svelte.js'
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 handedness = $derived<'left' | 'right'>(left ? 'left' : right ? 'right' : (hand ?? 'left'))
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
- const key = handedness
54
- handEvents[key] = {
55
- onconnected,
56
- ondisconnected,
57
- onpinchend,
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 && isHandTracking.current}
103
+ {#if xrHand?.hand}
110
104
  <T
111
105
  is={xrHand.hand}
112
- attach={scene}
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 is={xrHand.targetRay}>
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={scene}
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 { setupControllers } from '../internal/setupControllers.js'
36
- import { setupHands } from '../internal/setupHands.js'
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
- setupControllers(controllerFactory)
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
- currentSession?.end()
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, RawShaderMaterial, type ColorRepresentation } from 'three'
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 radius = 0.1;
32
- float dist = length(vUv - vec2(0.5));
33
- float alpha = 1.0 - step(thickness, abs(distance(vUv, vec2(0.5)) - 0.25));
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 RawShaderMaterial({
38
+ const shaderMaterial = new ShaderMaterial({
44
39
  vertexShader,
45
40
  fragmentShader,
46
41
  uniforms,