@threlte/xr 1.5.3 → 1.5.4

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.
@@ -75,7 +75,8 @@
75
75
  const handedness = $derived<'left' | 'right'>(left ? 'left' : right ? 'right' : (hand ?? 'left'))
76
76
 
77
77
  $effect.pre(() => {
78
- controllerEvents[handedness] = {
78
+ const key = handedness
79
+ controllerEvents[key] = {
79
80
  onconnected,
80
81
  ondisconnected,
81
82
  onselect,
@@ -87,7 +88,7 @@
87
88
  }
88
89
 
89
90
  return () => {
90
- controllerEvents[handedness] = undefined
91
+ controllerEvents[key] = undefined
91
92
  }
92
93
  })
93
94
 
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Group } from 'three'
3
- import { T, useThrelte, useTask } from '@threlte/core'
3
+ import { T, useThrelte, useTask, useStage } from '@threlte/core'
4
4
  import type { XRHandEvents } from '../types.js'
5
5
  import { isHandTracking, handEvents } from '../internal/state.svelte.js'
6
6
  import { hands } from '../hooks/useHand.svelte.js'
@@ -45,12 +45,13 @@
45
45
  wrist
46
46
  }: Props = $props()
47
47
 
48
- const { scene, renderer, scheduler, renderStage } = useThrelte()
48
+ const { scene, renderer, renderStage } = useThrelte()
49
49
 
50
50
  const handedness = $derived<'left' | 'right'>(left ? 'left' : right ? 'right' : (hand ?? 'left'))
51
51
 
52
52
  $effect.pre(() => {
53
- handEvents[handedness] = {
53
+ const key = handedness
54
+ handEvents[key] = {
54
55
  onconnected,
55
56
  ondisconnected,
56
57
  onpinchend,
@@ -58,10 +59,12 @@
58
59
  }
59
60
 
60
61
  return () => {
61
- handEvents[handedness] = undefined
62
+ handEvents[key] = undefined
62
63
  }
63
64
  })
64
65
 
66
+ const stage = useStage(Symbol('xr-hand-stage'), { before: renderStage })
67
+
65
68
  const group = new Group()
66
69
 
67
70
  /**
@@ -90,7 +93,7 @@
90
93
  group.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w)
91
94
  },
92
95
  {
93
- stage: scheduler.createStage(Symbol('xr-hand-stage'), { before: renderStage }),
96
+ stage,
94
97
  running: () =>
95
98
  isHandTracking.current &&
96
99
  (wrist !== undefined || children !== undefined) &&
@@ -21,7 +21,7 @@ This should be placed within a Threlte `<Canvas />`.
21
21
  import type { EventListener, WebXRManager, Event as ThreeEvent } from 'three'
22
22
  import type { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js'
23
23
  import type { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js'
24
- import type { Snippet } from 'svelte'
24
+ import { untrack, type Snippet } from 'svelte'
25
25
  import { useThrelte } from '@threlte/core'
26
26
  import {
27
27
  isHandTracking,
@@ -78,6 +78,9 @@ This should be placed within a Threlte `<Canvas />`.
78
78
 
79
79
  /** Called when available inputsources change */
80
80
  oninputsourceschange?: (event: XRSessionEvent) => void
81
+
82
+ /** Called when the session frame rate changes. */
83
+ onframeratechange?: (event: XRSessionEvent) => void
81
84
  }
82
85
 
83
86
  let {
@@ -88,6 +91,7 @@ This should be placed within a Threlte `<Canvas />`.
88
91
  onsessionend,
89
92
  onvisibilitychange,
90
93
  oninputsourceschange,
94
+ onframeratechange,
91
95
  fallback,
92
96
  children,
93
97
  handFactory,
@@ -96,8 +100,6 @@ This should be placed within a Threlte `<Canvas />`.
96
100
 
97
101
  const { renderer, renderMode } = useThrelte()
98
102
 
99
- let originalRenderMode = $renderMode
100
-
101
103
  setupRaf()
102
104
  setupHeadset()
103
105
  setupControllers(controllerFactory)
@@ -105,12 +107,19 @@ This should be placed within a Threlte `<Canvas />`.
105
107
 
106
108
  const handleSessionStart: EventListener<object, 'sessionstart', WebXRManager> = (event) => {
107
109
  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
+ }
108
116
  onsessionstart?.(event)
109
117
  }
110
118
 
111
119
  const handleSessionEnd = (event: XRSessionEvent) => {
112
120
  onsessionend?.(event)
113
121
  isPresenting.current = false
122
+ isHandTracking.current = false
114
123
  session.current = undefined
115
124
  }
116
125
 
@@ -124,7 +133,7 @@ This should be placed within a Threlte `<Canvas />`.
124
133
  }
125
134
 
126
135
  const handleFramerateChange = (event: XRSessionEvent) => {
127
- onvisibilitychange?.(event)
136
+ onframeratechange?.(event)
128
137
  }
129
138
 
130
139
  $effect(() => {
@@ -148,11 +157,15 @@ This should be placed within a Threlte `<Canvas />`.
148
157
  })
149
158
 
150
159
  $effect.pre(() => {
151
- if (isPresenting.current) {
152
- originalRenderMode = renderMode.current
153
- renderMode.set('always')
154
- } else {
155
- renderMode.set(originalRenderMode)
160
+ if (!isPresenting.current) return
161
+
162
+ // Capture the mode from before we forced 'always' so it survives
163
+ // any manual renderMode changes made during the session.
164
+ const saved = untrack(() => renderMode.current)
165
+ renderMode.set('always')
166
+
167
+ return () => {
168
+ renderMode.set(saved)
156
169
  }
157
170
  })
158
171
 
@@ -1,7 +1,7 @@
1
1
  import type { WebXRManager, Event as ThreeEvent } from 'three';
2
2
  import type { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
3
3
  import type { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
4
- import type { Snippet } from 'svelte';
4
+ import { type Snippet } from 'svelte';
5
5
  interface Props {
6
6
  /**
7
7
  * Enables foveated rendering. Default is `1`, the three.js default.
@@ -36,6 +36,8 @@ interface Props {
36
36
  onvisibilitychange?: (event: XRSessionEvent) => void;
37
37
  /** Called when available inputsources change */
38
38
  oninputsourceschange?: (event: XRSessionEvent) => void;
39
+ /** Called when the session frame rate changes. */
40
+ onframeratechange?: (event: XRSessionEvent) => void;
39
41
  }
40
42
  /**
41
43
  * `<XR />` is a WebXR manager that configures your scene for XR rendering and interaction.
@@ -10,11 +10,18 @@ export const useHandJoint = (handedness, joint) => {
10
10
  let jointSpace = $state.raw();
11
11
  useTask(() => {
12
12
  const space = xrhand?.hand.joints[joint];
13
- // The joint radius is a good indicator that the joint is ready
13
+ // The joint radius is a good indicator that the joint is ready.
14
+ // Re-check each frame so we pick up reconnects and clear on disconnect.
14
15
  if (space?.jointRadius !== undefined) {
15
- jointSpace = space;
16
+ if (jointSpace !== space) {
17
+ jointSpace = space;
18
+ invalidate();
19
+ }
20
+ }
21
+ else if (jointSpace !== undefined) {
22
+ jointSpace = undefined;
16
23
  invalidate();
17
24
  }
18
- }, { running: () => jointSpace === undefined });
25
+ });
19
26
  return toCurrentReadable(() => jointSpace);
20
27
  };
@@ -42,7 +42,10 @@ export const setupHands = (factory) => {
42
42
  }
43
43
  const handleDisconnected = (event) => {
44
44
  dispatch(event);
45
- hands[event.data.handedness] = undefined;
45
+ const { handedness } = event.data;
46
+ if (handedness === 'left' || handedness === 'right') {
47
+ hands[handedness] = undefined;
48
+ }
46
49
  };
47
50
  for (const handSpace of handSpaces) {
48
51
  handSpace.addEventListener('connected', handleConnected);
@@ -1,11 +1,11 @@
1
1
  import { Group } from 'three';
2
- import { useThrelte, useTask } from '@threlte/core';
2
+ import { useThrelte, useTask, useStage } from '@threlte/core';
3
3
  import { isPresenting } from './state.svelte.js';
4
4
  export const headset = new Group();
5
5
  export const setupHeadset = () => {
6
- const { renderer, camera, scheduler, renderStage } = useThrelte();
6
+ const { renderer, camera, renderStage } = useThrelte();
7
+ const stage = useStage(Symbol('xr-headset-stage'), { before: renderStage });
7
8
  const { xr } = renderer;
8
- const stage = scheduler.createStage(Symbol('xr-headset-stage'), { before: renderStage });
9
9
  useTask(() => {
10
10
  const space = xr.getReferenceSpace();
11
11
  if (space === null)
@@ -15,6 +15,8 @@ export const usePointerControls = () => {
15
15
  };
16
16
  const removeInteractiveObject = (object) => {
17
17
  const index = context.interactiveObjects.indexOf(object);
18
+ if (index === -1)
19
+ return;
18
20
  context.interactiveObjects.splice(index, 1);
19
21
  dispatchers.delete(object);
20
22
  };
@@ -1,4 +1,4 @@
1
- import { injectPlugin, isInstanceOf, observe } from '@threlte/core';
1
+ import { injectPlugin, isInstanceOf } from '@threlte/core';
2
2
  import { usePointerControls } from './hook.js';
3
3
  import { events } from './types.js';
4
4
  export const injectPointerControlsPlugin = () => {
@@ -22,10 +22,6 @@ export const injectPointerControlsPlugin = () => {
22
22
  removeInteractiveObject(ref);
23
23
  };
24
24
  });
25
- observe.pre(() => [args.ref], ([ref]) => {
26
- addInteractiveObject(ref, args.props);
27
- return () => removeInteractiveObject(ref);
28
- });
29
25
  return {
30
26
  pluginProps: events
31
27
  };
@@ -27,6 +27,8 @@ export const createTeleportContext = (compute) => {
27
27
  };
28
28
  const removeSurface = (mesh) => {
29
29
  const index = context.interactiveObjects.indexOf(mesh);
30
+ if (index === -1)
31
+ return;
30
32
  context.interactiveObjects.splice(index, 1);
31
33
  context.surfaces.delete(mesh.uuid);
32
34
  context.dispatchers.delete(mesh);
@@ -41,6 +43,8 @@ export const createTeleportContext = (compute) => {
41
43
  };
42
44
  const removeBlocker = (mesh) => {
43
45
  const index = context.interactiveObjects.indexOf(mesh);
46
+ if (index === -1)
47
+ return;
44
48
  context.interactiveObjects.splice(index, 1);
45
49
  context.blockers.delete(mesh.uuid);
46
50
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threlte/xr",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "author": "Micheal Parks <michealparks1989@gmail.com> (https://parks.lol)",
5
5
  "license": "MIT",
6
6
  "description": "Tools to more easily create VR and AR experiences with Threlte",
@@ -28,7 +28,7 @@
28
28
  "typescript-eslint": "^8.32.0",
29
29
  "vite": "^7.1.4",
30
30
  "vite-plugin-mkcert": "^1.17.5",
31
- "@threlte/core": "8.5.8"
31
+ "@threlte/core": "8.5.9"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "svelte": ">=5",