@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
@@ -0,0 +1,84 @@
1
+ import type { XRGripSpace, XRHandSpace, XRTargetRaySpace } from 'three';
2
+ import type { XRControllerModel } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
3
+ import type { XRHandModel } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
4
+ import type { XRControllerEvents, XRHandEvents } from '../types.js';
5
+ export type XRHandInputSource = XRInputSource & {
6
+ hand: XRHand;
7
+ };
8
+ export type XRInputSourceStateBase = {
9
+ id: string;
10
+ inputSource: XRInputSource;
11
+ handedness: XRHandedness;
12
+ isPrimary: boolean;
13
+ targetRay: XRTargetRaySpace;
14
+ };
15
+ export type XRControllerSourceState = XRInputSourceStateBase & {
16
+ type: 'controller';
17
+ grip: XRGripSpace;
18
+ model: XRControllerModel;
19
+ };
20
+ export type XRHandSourceState = Omit<XRInputSourceStateBase, 'inputSource'> & {
21
+ type: 'hand';
22
+ inputSource: XRHandInputSource;
23
+ hand: XRHandSpace;
24
+ model: XRHandModel;
25
+ };
26
+ export type XRGazeSourceState = XRInputSourceStateBase & {
27
+ type: 'gaze';
28
+ };
29
+ export type XRTransientPointerSourceState = XRInputSourceStateBase & {
30
+ type: 'transientPointer';
31
+ };
32
+ export type XRScreenInputSourceState = XRInputSourceStateBase & {
33
+ type: 'screenInput';
34
+ };
35
+ export type XRInputSourceState = XRControllerSourceState | XRHandSourceState | XRGazeSourceState | XRTransientPointerSourceState | XRScreenInputSourceState;
36
+ declare class InputSourcesState {
37
+ current: readonly XRInputSourceState[];
38
+ }
39
+ export declare const inputSources: InputSourcesState;
40
+ export type ControllerSubscriber = {
41
+ type: 'controller';
42
+ handedness: XRHandedness;
43
+ callbacks: XRControllerEvents;
44
+ };
45
+ export type HandSubscriber = {
46
+ type: 'hand';
47
+ handedness: 'left' | 'right';
48
+ callbacks: XRHandEvents;
49
+ };
50
+ export type Subscriber = ControllerSubscriber | HandSubscriber;
51
+ /**
52
+ * Registers callbacks with the module-level XR input-source dispatcher.
53
+ *
54
+ * This does not subscribe to a specific `XRInputSource`, `XRSession`, or
55
+ * Three.js object. Instead, the subscriber is stored in the internal
56
+ * `subscribers` set and receives events for whichever current input-source
57
+ * state matches its `type` and `handedness`.
58
+ *
59
+ * For example, a `{ type: 'controller', handedness: 'left' }` subscriber will
60
+ * receive forwarded events for the current left controller, even if the
61
+ * underlying `XRInputSource` instance disconnects and reconnects.
62
+ *
63
+ * Returns a cleanup function that removes the subscriber from the dispatcher.
64
+ */
65
+ export declare const addSubscriber: (sub: Subscriber) => () => void;
66
+ export declare const dispatchEvent: (state: XRInputSourceState, eventType: string, event: unknown) => void;
67
+ export declare const createInputSourceEvent: (state: XRInputSourceState, type: string, extra?: Record<string, unknown>) => {
68
+ type: string;
69
+ data: XRInputSource | XRHandInputSource;
70
+ inputSource: XRInputSource | XRHandInputSource;
71
+ target: XRTargetRaySpace | XRHandSpace;
72
+ };
73
+ export declare const dispatchInputSourceStateEvent: (state: XRInputSourceState, eventType: string, event: unknown, options?: {
74
+ dispatchSpaces?: boolean;
75
+ }) => void;
76
+ type ResolveOptions = {
77
+ isPrimary?: boolean;
78
+ };
79
+ export declare const getInputSourceState: (inputSource: XRInputSource, options?: ResolveOptions) => XRInputSourceState | undefined;
80
+ export declare const getControllerState: (handedness: XRHandedness, options?: ResolveOptions) => XRControllerSourceState | undefined;
81
+ export declare const getHandState: (handedness: "left" | "right", options?: ResolveOptions) => XRHandSourceState | undefined;
82
+ export declare const dispatchInputSourceEvent: (inputSource: XRInputSource, eventType: string, event: unknown) => void;
83
+ export declare const dispatchXRInputSourceEvent: (event: XRInputSourceEvent) => void;
84
+ export {};
@@ -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>;