@threlte/xr 1.5.5 → 1.6.1

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 (69) hide show
  1. package/dist/components/Controller.svelte +59 -60
  2. package/dist/components/Hand.svelte +21 -29
  3. package/dist/components/XR.svelte +146 -16
  4. package/dist/components/XR.svelte.d.ts +20 -0
  5. package/dist/components/XROrigin.svelte +82 -0
  6. package/dist/components/XROrigin.svelte.d.ts +33 -0
  7. package/dist/components/internal/TeleportRay.svelte +15 -3
  8. package/dist/hooks/currentReadable.svelte.d.ts +28 -1
  9. package/dist/hooks/currentReadable.svelte.js +36 -9
  10. package/dist/hooks/useController.svelte.d.ts +3 -3
  11. package/dist/hooks/useController.svelte.js +30 -7
  12. package/dist/hooks/useHand.svelte.d.ts +2 -2
  13. package/dist/hooks/useHand.svelte.js +26 -5
  14. package/dist/hooks/useHandJoint.svelte.js +6 -5
  15. package/dist/hooks/useHitTest.svelte.js +56 -12
  16. package/dist/hooks/useTeleport.d.ts +11 -9
  17. package/dist/hooks/useTeleport.js +62 -14
  18. package/dist/hooks/useXR.js +5 -5
  19. package/dist/hooks/useXROrigin.svelte.d.ts +10 -0
  20. package/dist/hooks/useXROrigin.svelte.js +11 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +3 -0
  23. package/dist/internal/inputSources.svelte.d.ts +84 -0
  24. package/dist/internal/inputSources.svelte.js +91 -0
  25. package/dist/internal/setupHeadset.svelte.js +18 -6
  26. package/dist/internal/setupInputSources.d.ts +4 -0
  27. package/dist/internal/setupInputSources.js +319 -0
  28. package/dist/internal/state.svelte.d.ts +10 -12
  29. package/dist/internal/state.svelte.js +9 -3
  30. package/dist/lib/getXRSessionOptions.d.ts +1 -1
  31. package/dist/lib/getXRSessionOptions.js +8 -7
  32. package/dist/lib/toggleXRSession.d.ts +1 -1
  33. package/dist/lib/toggleXRSession.js +20 -5
  34. package/dist/plugins/pointerControls/compute.js +14 -9
  35. package/dist/plugins/pointerControls/context.d.ts +3 -3
  36. package/dist/plugins/pointerControls/context.js +12 -6
  37. package/dist/plugins/pointerControls/index.d.ts +4 -3
  38. package/dist/plugins/pointerControls/index.js +63 -29
  39. package/dist/plugins/pointerControls/setup.svelte.js +64 -44
  40. package/dist/plugins/pointerControls/types.d.ts +14 -3
  41. package/dist/plugins/teleportControls/compute.d.ts +1 -1
  42. package/dist/plugins/teleportControls/compute.js +11 -8
  43. package/dist/plugins/teleportControls/context.d.ts +4 -4
  44. package/dist/plugins/teleportControls/context.js +1 -4
  45. package/dist/plugins/teleportControls/index.js +7 -4
  46. package/dist/plugins/teleportControls/plugin.svelte.js +12 -51
  47. package/dist/plugins/teleportControls/setup.svelte.js +10 -9
  48. package/dist/plugins/touchControls/compute.d.ts +3 -0
  49. package/dist/plugins/touchControls/compute.js +13 -0
  50. package/dist/plugins/touchControls/context.d.ts +12 -0
  51. package/dist/plugins/touchControls/context.js +27 -0
  52. package/dist/plugins/touchControls/hook.d.ts +5 -0
  53. package/dist/plugins/touchControls/hook.js +26 -0
  54. package/dist/plugins/touchControls/index.d.ts +33 -0
  55. package/dist/plugins/touchControls/index.js +41 -0
  56. package/dist/plugins/touchControls/plugin.svelte.d.ts +1 -0
  57. package/dist/plugins/touchControls/plugin.svelte.js +24 -0
  58. package/dist/plugins/touchControls/setup.svelte.d.ts +2 -0
  59. package/dist/plugins/touchControls/setup.svelte.js +247 -0
  60. package/dist/plugins/touchControls/types.d.ts +62 -0
  61. package/dist/plugins/touchControls/types.js +11 -0
  62. package/dist/types.d.ts +1 -1
  63. package/package.json +3 -3
  64. package/dist/internal/setupControllers.d.ts +0 -2
  65. package/dist/internal/setupControllers.js +0 -73
  66. package/dist/internal/setupHands.d.ts +0 -2
  67. package/dist/internal/setupHands.js +0 -67
  68. package/dist/internal/useHandTrackingState.d.ts +0 -5
  69. package/dist/internal/useHandTrackingState.js +0 -20
@@ -0,0 +1,91 @@
1
+ class InputSourcesState {
2
+ current = $state.raw([]);
3
+ }
4
+ export const inputSources = new InputSourcesState();
5
+ const subscribers = new Set();
6
+ /**
7
+ * Registers callbacks with the module-level XR input-source dispatcher.
8
+ *
9
+ * This does not subscribe to a specific `XRInputSource`, `XRSession`, or
10
+ * Three.js object. Instead, the subscriber is stored in the internal
11
+ * `subscribers` set and receives events for whichever current input-source
12
+ * state matches its `type` and `handedness`.
13
+ *
14
+ * For example, a `{ type: 'controller', handedness: 'left' }` subscriber will
15
+ * receive forwarded events for the current left controller, even if the
16
+ * underlying `XRInputSource` instance disconnects and reconnects.
17
+ *
18
+ * Returns a cleanup function that removes the subscriber from the dispatcher.
19
+ */
20
+ export const addSubscriber = (sub) => {
21
+ subscribers.add(sub);
22
+ return () => {
23
+ subscribers.delete(sub);
24
+ };
25
+ };
26
+ export const dispatchEvent = (state, eventType, event) => {
27
+ const key = `on${eventType}`;
28
+ for (const sub of subscribers) {
29
+ if (sub.type !== state.type)
30
+ continue;
31
+ if (sub.handedness !== state.handedness)
32
+ continue;
33
+ const cb = sub.callbacks[key];
34
+ cb?.(event);
35
+ }
36
+ };
37
+ const dispatchSpaceEvent = (state, event) => {
38
+ state.targetRay.dispatchEvent(event);
39
+ if (state.type === 'controller') {
40
+ state.grip.dispatchEvent(event);
41
+ }
42
+ if (state.type === 'hand') {
43
+ state.hand.dispatchEvent(event);
44
+ }
45
+ };
46
+ export const createInputSourceEvent = (state, type, extra = {}) => {
47
+ return {
48
+ type,
49
+ data: state.inputSource,
50
+ inputSource: state.inputSource,
51
+ target: state.type === 'hand' ? state.hand : state.targetRay,
52
+ ...extra
53
+ };
54
+ };
55
+ export const dispatchInputSourceStateEvent = (state, eventType, event, options = {}) => {
56
+ if (options.dispatchSpaces !== false) {
57
+ dispatchSpaceEvent(state, event);
58
+ }
59
+ dispatchEvent(state, eventType, event);
60
+ };
61
+ const getPreferredState = (predicate, options) => {
62
+ if (options?.isPrimary !== undefined) {
63
+ return inputSources.current.find((state) => state.isPrimary === options.isPrimary && predicate(state));
64
+ }
65
+ return (inputSources.current.find((state) => state.isPrimary && predicate(state)) ??
66
+ inputSources.current.find(predicate));
67
+ };
68
+ export const getInputSourceState = (inputSource, options) => {
69
+ if (options?.isPrimary !== undefined) {
70
+ return inputSources.current.find((state) => state.inputSource === inputSource && state.isPrimary === options.isPrimary);
71
+ }
72
+ return inputSources.current.find((state) => state.inputSource === inputSource);
73
+ };
74
+ export const getControllerState = (handedness, options) => {
75
+ return getPreferredState((state) => state.type === 'controller' && state.handedness === handedness, options);
76
+ };
77
+ export const getHandState = (handedness, options) => {
78
+ return getPreferredState((state) => state.type === 'hand' && state.handedness === handedness, options);
79
+ };
80
+ export const dispatchInputSourceEvent = (inputSource, eventType, event) => {
81
+ const state = getInputSourceState(inputSource);
82
+ if (state === undefined)
83
+ return;
84
+ dispatchInputSourceStateEvent(state, eventType, event);
85
+ };
86
+ export const dispatchXRInputSourceEvent = (event) => {
87
+ const state = getInputSourceState(event.inputSource);
88
+ if (state === undefined)
89
+ return;
90
+ dispatchInputSourceStateEvent(state, event.type, createInputSourceEvent(state, event.type, { frame: event.frame }));
91
+ };
@@ -1,10 +1,14 @@
1
- import { Group } from 'three';
1
+ import { Group, Matrix4, Vector3 } from 'three';
2
2
  import { useThrelte, useTask, useStage } from '@threlte/core';
3
3
  import { isPresenting } from './state.svelte.js';
4
+ import { useXROrigin } from '../hooks/useXROrigin.svelte.js';
4
5
  export const headset = new Group();
6
+ const poseMatrix = new Matrix4();
7
+ const tempScale = new Vector3();
5
8
  export const setupHeadset = () => {
6
9
  const { renderer, camera, renderStage } = useThrelte();
7
10
  const stage = useStage(Symbol('xr-headset-stage'), { before: renderStage });
11
+ const xrOrigin = useXROrigin();
8
12
  const { xr } = renderer;
9
13
  useTask(() => {
10
14
  const space = xr.getReferenceSpace();
@@ -15,17 +19,25 @@ export const setupHeadset = () => {
15
19
  // It can be null on android chrome when using phone AR.
16
20
  if (pose === undefined || pose === null)
17
21
  return;
18
- const { position, orientation } = pose.transform;
19
- headset.position.copy(position);
20
- headset.quaternion.copy(orientation);
22
+ const origin = xrOrigin.current;
23
+ if (origin === undefined) {
24
+ const { position, orientation } = pose.transform;
25
+ headset.position.copy(position);
26
+ headset.quaternion.copy(orientation);
27
+ }
28
+ else {
29
+ origin.updateWorldMatrix(true, false);
30
+ poseMatrix.fromArray(pose.transform.matrix).premultiply(origin.matrixWorld);
31
+ poseMatrix.decompose(headset.position, headset.quaternion, tempScale);
32
+ }
21
33
  }, {
22
34
  autoInvalidate: false,
23
35
  stage,
24
36
  running: () => isPresenting.current
25
37
  });
26
38
  useTask(() => {
27
- headset.position.copy(camera.current.position);
28
- headset.quaternion.copy(camera.current.quaternion);
39
+ camera.current.getWorldPosition(headset.position);
40
+ camera.current.getWorldQuaternion(headset.quaternion);
29
41
  }, {
30
42
  autoInvalidate: false,
31
43
  stage,
@@ -0,0 +1,4 @@
1
+ import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
2
+ import { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
3
+ export type BindInputSourcesToSession = (session: XRSession | undefined) => void;
4
+ export declare const setupInputSources: (controllerFactory?: XRControllerModelFactory, handFactory?: XRHandModelFactory) => BindInputSourcesToSession;
@@ -0,0 +1,319 @@
1
+ import { Group, Vector3 } from 'three';
2
+ import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
3
+ import { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
4
+ import { useTask, useThrelte } from '@threlte/core';
5
+ import { createInputSourceEvent, dispatchInputSourceStateEvent, inputSources } from './inputSources.svelte.js';
6
+ const PINCH_DISTANCE = 0.02;
7
+ const PINCH_THRESHOLD = 0.005;
8
+ const makeId = (inputSource) => `${inputSource.handedness}-${inputSource.hand ? 'hand' : 'nohand'}-${inputSource.targetRayMode}-${inputSource.profiles.join(',')}`;
9
+ const createSpaceWithVelocity = () => {
10
+ const group = new Group();
11
+ group.matrixAutoUpdate = false;
12
+ group.visible = false;
13
+ group.hasLinearVelocity = false;
14
+ group.linearVelocity = new Vector3();
15
+ group.hasAngularVelocity = false;
16
+ group.angularVelocity = new Vector3();
17
+ return group;
18
+ };
19
+ const createTargetRaySpace = () => createSpaceWithVelocity();
20
+ const createGripSpace = () => createSpaceWithVelocity();
21
+ const createHandSpace = () => {
22
+ const hand = new Group();
23
+ hand.matrixAutoUpdate = false;
24
+ hand.visible = false;
25
+ hand.joints = {};
26
+ hand.inputState = { pinching: false };
27
+ return hand;
28
+ };
29
+ const hideState = (state) => {
30
+ state.targetRay.visible = false;
31
+ if (state.type === 'controller') {
32
+ state.grip.visible = false;
33
+ }
34
+ if (state.type === 'hand') {
35
+ state.hand.visible = false;
36
+ for (const joint of Object.values(state.hand.joints ?? {})) {
37
+ joint.visible = false;
38
+ }
39
+ state.hand.inputState.pinching = false;
40
+ }
41
+ };
42
+ const ensureHandJoint = (hand, inputJoint) => {
43
+ const existing = hand.joints[inputJoint.jointName];
44
+ if (existing !== undefined) {
45
+ return existing;
46
+ }
47
+ const joint = new Group();
48
+ joint.matrixAutoUpdate = false;
49
+ joint.visible = false;
50
+ hand.joints[inputJoint.jointName] = joint;
51
+ hand.add(joint);
52
+ return joint;
53
+ };
54
+ const ensureHandJoints = (state) => {
55
+ const hand = state.hand;
56
+ for (const inputJoint of state.inputSource.hand.values()) {
57
+ ensureHandJoint(hand, inputJoint);
58
+ }
59
+ };
60
+ const updateSpacePose = (space, pose) => {
61
+ if (pose === undefined || pose === null) {
62
+ space.visible = false;
63
+ space.hasLinearVelocity = false;
64
+ space.hasAngularVelocity = false;
65
+ return;
66
+ }
67
+ space.matrix.fromArray(pose.transform.matrix);
68
+ space.matrix.decompose(space.position, space.quaternion, space.scale);
69
+ space.matrixWorldNeedsUpdate = true;
70
+ space.visible = true;
71
+ if ('linearVelocity' in pose &&
72
+ pose.linearVelocity !== null &&
73
+ pose.linearVelocity !== undefined) {
74
+ ;
75
+ space.hasLinearVelocity = true;
76
+ space.linearVelocity.copy(pose.linearVelocity);
77
+ }
78
+ else {
79
+ ;
80
+ space.hasLinearVelocity = false;
81
+ }
82
+ if ('angularVelocity' in pose &&
83
+ pose.angularVelocity !== null &&
84
+ pose.angularVelocity !== undefined) {
85
+ ;
86
+ space.hasAngularVelocity = true;
87
+ space.angularVelocity.copy(pose.angularVelocity);
88
+ }
89
+ else {
90
+ ;
91
+ space.hasAngularVelocity = false;
92
+ }
93
+ };
94
+ const updatePinchState = (state) => {
95
+ const hand = state.hand;
96
+ const inputState = hand.inputState;
97
+ const indexTip = hand.joints['index-finger-tip'];
98
+ const thumbTip = hand.joints['thumb-tip'];
99
+ if (indexTip === undefined || thumbTip === undefined || !indexTip.visible || !thumbTip.visible) {
100
+ if (inputState.pinching) {
101
+ inputState.pinching = false;
102
+ const event = createInputSourceEvent(state, 'pinchend', {
103
+ handedness: state.handedness,
104
+ target: hand
105
+ });
106
+ dispatchInputSourceStateEvent(state, 'pinchend', event);
107
+ }
108
+ return;
109
+ }
110
+ const distance = indexTip.position.distanceTo(thumbTip.position);
111
+ if (inputState.pinching && distance > PINCH_DISTANCE + PINCH_THRESHOLD) {
112
+ inputState.pinching = false;
113
+ const event = createInputSourceEvent(state, 'pinchend', {
114
+ handedness: state.handedness,
115
+ target: hand
116
+ });
117
+ dispatchInputSourceStateEvent(state, 'pinchend', event);
118
+ }
119
+ else if (!inputState.pinching && distance <= PINCH_DISTANCE - PINCH_THRESHOLD) {
120
+ inputState.pinching = true;
121
+ const event = createInputSourceEvent(state, 'pinchstart', {
122
+ handedness: state.handedness,
123
+ target: hand
124
+ });
125
+ dispatchInputSourceStateEvent(state, 'pinchstart', event);
126
+ }
127
+ };
128
+ const updateXRControllerState = (state, frame, referenceSpace) => {
129
+ let gripPose;
130
+ if (state.inputSource.gripSpace !== undefined) {
131
+ gripPose = frame.getPose(state.inputSource.gripSpace, referenceSpace);
132
+ updateSpacePose(state.grip, gripPose);
133
+ }
134
+ let targetRayPose = frame.getPose(state.inputSource.targetRaySpace, referenceSpace);
135
+ if (targetRayPose === null && gripPose !== undefined && gripPose !== null) {
136
+ targetRayPose = gripPose;
137
+ }
138
+ updateSpacePose(state.targetRay, targetRayPose);
139
+ if (state.inputSource.gripSpace === undefined || gripPose === undefined || gripPose === null) {
140
+ updateSpacePose(state.grip, targetRayPose);
141
+ }
142
+ };
143
+ const updateXRHandState = (state, frame, referenceSpace) => {
144
+ updateSpacePose(state.targetRay, frame.getPose(state.inputSource.targetRaySpace, referenceSpace));
145
+ const hand = state.hand;
146
+ ensureHandJoints(state);
147
+ let visible = false;
148
+ for (const inputJoint of state.inputSource.hand.values()) {
149
+ const pose = frame.getJointPose?.(inputJoint, referenceSpace);
150
+ const joint = ensureHandJoint(hand, inputJoint);
151
+ if (pose !== undefined && pose !== null) {
152
+ joint.matrix.fromArray(pose.transform.matrix);
153
+ joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
154
+ joint.matrixWorldNeedsUpdate = true;
155
+ joint.jointRadius = pose.radius;
156
+ visible = true;
157
+ }
158
+ joint.visible = pose !== undefined && pose !== null;
159
+ }
160
+ hand.visible = visible;
161
+ updatePinchState(state);
162
+ };
163
+ const updateXRInputSourceState = (state, frame, referenceSpace) => {
164
+ if (frame.session.visibilityState === 'visible-blurred' ||
165
+ frame.session.visibilityState === 'hidden') {
166
+ hideState(state);
167
+ return;
168
+ }
169
+ switch (state.type) {
170
+ case 'controller':
171
+ updateXRControllerState(state, frame, referenceSpace);
172
+ break;
173
+ case 'hand':
174
+ updateXRHandState(state, frame, referenceSpace);
175
+ break;
176
+ default:
177
+ updateSpacePose(state.targetRay, frame.getPose(state.inputSource.targetRaySpace, referenceSpace));
178
+ break;
179
+ }
180
+ };
181
+ let idCounter = 0;
182
+ const createXRInputSourceState = (id, inputSource, isPrimary, controllerModelFactory, handModelFactory) => {
183
+ const base = {
184
+ id,
185
+ inputSource,
186
+ handedness: inputSource.handedness,
187
+ isPrimary,
188
+ targetRay: createTargetRaySpace()
189
+ };
190
+ if (inputSource.hand != null) {
191
+ if (inputSource.handedness === 'none')
192
+ return undefined;
193
+ const hand = createHandSpace();
194
+ const state = {
195
+ ...base,
196
+ type: 'hand',
197
+ inputSource: inputSource,
198
+ hand,
199
+ model: handModelFactory.createHandModel(hand, 'mesh')
200
+ };
201
+ ensureHandJoints(state);
202
+ return state;
203
+ }
204
+ switch (inputSource.targetRayMode) {
205
+ case 'gaze':
206
+ return { ...base, type: 'gaze' };
207
+ case 'screen':
208
+ return { ...base, type: 'screenInput' };
209
+ case 'transient-pointer':
210
+ return { ...base, type: 'transientPointer' };
211
+ case 'tracked-pointer':
212
+ default: {
213
+ const grip = createGripSpace();
214
+ return {
215
+ ...base,
216
+ type: 'controller',
217
+ grip,
218
+ model: controllerModelFactory.createControllerModel(grip)
219
+ };
220
+ }
221
+ }
222
+ };
223
+ const createSyncXRInputSourceStates = (controllerModelFactory, handModelFactory) => {
224
+ const idMap = new Map();
225
+ return (_session, current, changes) => {
226
+ if (changes === 'remove-all') {
227
+ for (const state of current) {
228
+ const event = createInputSourceEvent(state, 'disconnected');
229
+ dispatchInputSourceStateEvent(state, 'disconnected', event);
230
+ hideState(state);
231
+ }
232
+ return [];
233
+ }
234
+ const target = [...current];
235
+ for (const { added, isPrimary, removed } of changes) {
236
+ if (removed != null) {
237
+ for (const inputSource of removed) {
238
+ const index = target.findIndex((state) => state.isPrimary === isPrimary && state.inputSource === inputSource);
239
+ if (index === -1)
240
+ continue;
241
+ const [state] = target.splice(index, 1);
242
+ const event = createInputSourceEvent(state, 'disconnected');
243
+ dispatchInputSourceStateEvent(state, 'disconnected', event);
244
+ hideState(state);
245
+ }
246
+ }
247
+ if (added == null)
248
+ continue;
249
+ for (const inputSource of added) {
250
+ if (target.some((state) => state.isPrimary === isPrimary && state.inputSource === inputSource)) {
251
+ continue;
252
+ }
253
+ const key = makeId(inputSource);
254
+ let id = idMap.get(key);
255
+ if (id == null) {
256
+ id = `${idCounter++}`;
257
+ idMap.set(key, id);
258
+ }
259
+ const state = createXRInputSourceState(id, inputSource, isPrimary, controllerModelFactory, handModelFactory);
260
+ if (state === undefined) {
261
+ continue;
262
+ }
263
+ target.push(state);
264
+ const event = createInputSourceEvent(state, 'connected');
265
+ dispatchInputSourceStateEvent(state, 'connected', event);
266
+ }
267
+ }
268
+ return target;
269
+ };
270
+ };
271
+ const createBindToSession = (syncXRInputSourceStates) => {
272
+ let cleanupSession;
273
+ return (session) => {
274
+ cleanupSession?.();
275
+ cleanupSession = undefined;
276
+ if (session == null) {
277
+ inputSources.current = [];
278
+ return;
279
+ }
280
+ const inputSourceChangesList = [];
281
+ const applySourcesChange = () => {
282
+ inputSources.current = syncXRInputSourceStates(session, inputSources.current, inputSourceChangesList);
283
+ inputSourceChangesList.length = 0;
284
+ };
285
+ const onInputSourcesChange = (event) => {
286
+ inputSourceChangesList.push({
287
+ isPrimary: true,
288
+ added: event.added,
289
+ removed: event.removed
290
+ });
291
+ applySourcesChange();
292
+ };
293
+ session.addEventListener('inputsourceschange', onInputSourcesChange);
294
+ inputSources.current = syncXRInputSourceStates(session, [], [{ isPrimary: true, added: session.inputSources }]);
295
+ cleanupSession = () => {
296
+ inputSources.current = syncXRInputSourceStates(session, inputSources.current, 'remove-all');
297
+ session.removeEventListener('inputsourceschange', onInputSourcesChange);
298
+ };
299
+ };
300
+ };
301
+ export const setupInputSources = (controllerFactory, handFactory) => {
302
+ const { xr } = useThrelte().renderer;
303
+ const controllerModelFactory = controllerFactory ?? new XRControllerModelFactory();
304
+ const handModelFactory = handFactory ?? new XRHandModelFactory();
305
+ const syncXRInputSourceStates = createSyncXRInputSourceStates(controllerModelFactory, handModelFactory);
306
+ const bindToSession = createBindToSession(syncXRInputSourceStates);
307
+ useTask(() => {
308
+ const frame = xr.getFrame();
309
+ const referenceSpace = xr.getReferenceSpace();
310
+ if (frame === null || referenceSpace === null)
311
+ return;
312
+ for (const state of inputSources.current) {
313
+ updateXRInputSourceState(state, frame, referenceSpace);
314
+ }
315
+ }, {
316
+ running: () => inputSources.current.length > 0
317
+ });
318
+ return bindToSession;
319
+ };
@@ -1,18 +1,9 @@
1
1
  import type { WebXRManager, Intersection } from 'three';
2
- import type { XRControllerEvents, XRHandEvents } from '../types.js';
3
- interface ControllerEvents {
4
- left?: XRControllerEvents;
5
- right?: XRControllerEvents;
6
- }
7
- interface HandEvents {
8
- left?: XRHandEvents;
9
- right?: XRHandEvents;
10
- }
11
2
  declare class Presenting {
12
3
  current: boolean;
13
4
  }
14
5
  declare class IsHandTracking {
15
- current: boolean;
6
+ get current(): boolean;
16
7
  }
17
8
  declare class Session {
18
9
  current: XRSession | undefined;
@@ -23,6 +14,14 @@ declare class ReferenceSpaceType {
23
14
  declare class XR {
24
15
  current: WebXRManager | undefined;
25
16
  }
17
+ declare class LastSessionRequest {
18
+ mode: XRSessionMode | undefined;
19
+ sessionInit: (XRSessionInit & {
20
+ domOverlay?: {
21
+ root: Element;
22
+ };
23
+ }) | undefined;
24
+ }
26
25
  declare class PointerState {
27
26
  enabled: boolean;
28
27
  hovering: boolean;
@@ -36,8 +35,7 @@ export declare const isHandTracking: IsHandTracking;
36
35
  export declare const session: Session;
37
36
  export declare const referenceSpaceType: ReferenceSpaceType;
38
37
  export declare const xr: XR;
39
- export declare const controllerEvents: ControllerEvents;
40
- export declare const handEvents: HandEvents;
38
+ export declare const lastSessionRequest: LastSessionRequest;
41
39
  export declare const teleportState: {
42
40
  left: PointerState;
43
41
  right: PointerState;
@@ -1,8 +1,11 @@
1
+ import { inputSources } from './inputSources.svelte.js';
1
2
  class Presenting {
2
3
  current = $state(false);
3
4
  }
4
5
  class IsHandTracking {
5
- current = $state(false);
6
+ get current() {
7
+ return inputSources.current.some((s) => s.type === 'hand');
8
+ }
6
9
  }
7
10
  class Session {
8
11
  current = $state.raw();
@@ -13,6 +16,10 @@ class ReferenceSpaceType {
13
16
  class XR {
14
17
  current = $state.raw();
15
18
  }
19
+ class LastSessionRequest {
20
+ mode = $state.raw();
21
+ sessionInit = $state.raw();
22
+ }
16
23
  class PointerState {
17
24
  enabled = $state(false);
18
25
  hovering = $state(false);
@@ -26,8 +33,7 @@ export const isHandTracking = new IsHandTracking();
26
33
  export const session = new Session();
27
34
  export const referenceSpaceType = new ReferenceSpaceType();
28
35
  export const xr = new XR();
29
- export const controllerEvents = {};
30
- export const handEvents = {};
36
+ export const lastSessionRequest = new LastSessionRequest();
31
37
  export const teleportState = {
32
38
  left: new PointerState(),
33
39
  right: new PointerState()
@@ -1 +1 @@
1
- export declare const getXRSessionOptions: (referenceSpaceType?: XRReferenceSpaceType, sessionInit?: XRSessionInit) => XRSessionInit | undefined;
1
+ export declare const getXRSessionOptions: (referenceSpaceType?: XRReferenceSpaceType, sessionInit?: XRSessionInit, fallbackSessionInit?: XRSessionInit) => XRSessionInit | undefined;
@@ -1,15 +1,16 @@
1
- export const getXRSessionOptions = (referenceSpaceType, sessionInit) => {
2
- if (referenceSpaceType === undefined && sessionInit === undefined) {
1
+ export const getXRSessionOptions = (referenceSpaceType, sessionInit, fallbackSessionInit) => {
2
+ const init = sessionInit ?? fallbackSessionInit;
3
+ if (referenceSpaceType === undefined && init === undefined) {
3
4
  return undefined;
4
5
  }
5
- if (referenceSpaceType && sessionInit === undefined) {
6
+ if (referenceSpaceType && init === undefined) {
6
7
  return { optionalFeatures: [referenceSpaceType] };
7
8
  }
8
- if (referenceSpaceType && sessionInit) {
9
+ if (referenceSpaceType && init) {
9
10
  return {
10
- ...sessionInit,
11
- optionalFeatures: [...new Set([...(sessionInit.optionalFeatures ?? []), referenceSpaceType])]
11
+ ...init,
12
+ optionalFeatures: [...new Set([...(init.optionalFeatures ?? []), referenceSpaceType])]
12
13
  };
13
14
  }
14
- return sessionInit;
15
+ return init;
15
16
  };
@@ -8,6 +8,6 @@
8
8
  */
9
9
  export declare const toggleXRSession: (sessionMode: XRSessionMode, sessionInit?: XRSessionInit & {
10
10
  domOverlay?: {
11
- root: HTMLElement;
11
+ root: Element;
12
12
  };
13
13
  }, force?: "enter" | "exit") => Promise<XRSession | undefined>;
@@ -1,5 +1,6 @@
1
- import { session, referenceSpaceType, xr } from '../internal/state.svelte.js';
1
+ import { lastSessionRequest, referenceSpaceType, session, xr } from '../internal/state.svelte.js';
2
2
  import { getXRSessionOptions } from './getXRSessionOptions.js';
3
+ let pending;
3
4
  /**
4
5
  * Starts / ends an XR session.
5
6
  *
@@ -8,7 +9,15 @@ import { getXRSessionOptions } from './getXRSessionOptions.js';
8
9
  * @param force Whether this button should only enter / exit an `XRSession`. Default is to toggle both ways
9
10
  * @returns
10
11
  */
11
- export const toggleXRSession = async (sessionMode, sessionInit, force) => {
12
+ export const toggleXRSession = (sessionMode, sessionInit, force) => {
13
+ if (pending !== undefined)
14
+ return pending;
15
+ pending = run(sessionMode, sessionInit, force).finally(() => {
16
+ pending = undefined;
17
+ });
18
+ return pending;
19
+ };
20
+ const run = async (sessionMode, sessionInit, force) => {
12
21
  const currentSession = session.current;
13
22
  const hasSession = currentSession !== undefined;
14
23
  if (force === 'enter' && hasSession)
@@ -21,13 +30,19 @@ export const toggleXRSession = async (sessionMode, sessionInit, force) => {
21
30
  await currentSession.end();
22
31
  return;
23
32
  }
24
- if (xr.current === undefined) {
33
+ const manager = xr.current;
34
+ if (manager === undefined) {
25
35
  throw new Error('An <XR> component was not created when attempting to toggle a session.');
26
36
  }
27
37
  // Otherwise enter a session
28
38
  const options = getXRSessionOptions(referenceSpaceType.current, sessionInit);
29
- const nextSession = await navigator.xr.requestSession(sessionMode, options);
30
- await xr.current.setSession(nextSession);
39
+ const nextSession = await navigator.xr?.requestSession(sessionMode, options);
40
+ if (nextSession === undefined) {
41
+ throw new Error('A session was not able to be created.');
42
+ }
43
+ await manager.setSession(nextSession);
44
+ lastSessionRequest.mode = sessionMode;
45
+ lastSessionRequest.sessionInit = sessionInit;
31
46
  session.current = nextSession;
32
47
  return nextSession;
33
48
  };
@@ -1,14 +1,19 @@
1
1
  import { Vector3 } from 'three';
2
- import { controllers } from '../../hooks/useController.svelte.js';
2
+ import { getControllerState, getHandState } from '../../internal/inputSources.svelte.js';
3
+ const origin = new Vector3();
3
4
  const forward = new Vector3();
4
- export const defaultComputeFunction = (context, handContext) => {
5
- const targetRay = controllers[handContext.hand]?.targetRay;
5
+ export const defaultComputeFunction = (_context, handContext) => {
6
+ const state = handContext.sourceType === 'controller'
7
+ ? getControllerState(handContext.hand)
8
+ : getHandState(handContext.hand);
9
+ const targetRay = state?.targetRay;
6
10
  if (targetRay === undefined)
7
11
  return;
8
- // `<Controller>` attaches targetRay to the scene root so local === world;
9
- // we can read `.position`/`.quaternion` directly without a matrixWorld
10
- // roundtrip (which would force-recompose the matrix three.js writes from
11
- // the XR pose and introduce a drift against the visible render).
12
- forward.set(0, 0, -1).applyQuaternion(targetRay.quaternion);
13
- context.raycaster.set(targetRay.position, forward);
12
+ // Read origin/direction from matrixWorld so the ray is in real world space,
13
+ // even when an ancestor (e.g. <XROrigin>) has a non-identity transform.
14
+ // Force an update because this runs before the frame's scene.updateMatrixWorld.
15
+ targetRay.updateWorldMatrix(true, false);
16
+ origin.setFromMatrixPosition(targetRay.matrixWorld);
17
+ forward.set(0, 0, -1).transformDirection(targetRay.matrixWorld);
18
+ handContext.raycaster.set(origin, forward);
14
19
  };
@@ -1,7 +1,7 @@
1
1
  import type { Object3D } from 'three';
2
- import type { ControlsContext, HandContext } from './types.js';
3
- export declare const getHandContext: (hand: "left" | "right") => HandContext;
4
- export declare const setHandContext: (hand: "left" | "right", context: HandContext) => void;
2
+ import type { ControlsContext, HandContext, PointerSourceType } from './types.js';
3
+ export declare const getHandContext: (hand: "left" | "right", sourceType: PointerSourceType) => HandContext;
4
+ export declare const setHandContext: (hand: "left" | "right", sourceType: PointerSourceType, context: HandContext) => void;
5
5
  export declare const getControlsContext: () => ControlsContext;
6
6
  export declare const setControlsContext: (context: ControlsContext) => void;
7
7
  interface InternalContext {