@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.
- package/dist/components/Controller.svelte +59 -60
- package/dist/components/Hand.svelte +21 -29
- package/dist/components/XR.svelte +146 -16
- package/dist/components/XR.svelte.d.ts +20 -0
- package/dist/components/XROrigin.svelte +82 -0
- package/dist/components/XROrigin.svelte.d.ts +33 -0
- package/dist/components/internal/TeleportRay.svelte +15 -3
- package/dist/hooks/currentReadable.svelte.d.ts +28 -1
- package/dist/hooks/currentReadable.svelte.js +36 -9
- package/dist/hooks/useController.svelte.d.ts +3 -3
- package/dist/hooks/useController.svelte.js +30 -7
- package/dist/hooks/useHand.svelte.d.ts +2 -2
- package/dist/hooks/useHand.svelte.js +26 -5
- package/dist/hooks/useHandJoint.svelte.js +6 -5
- package/dist/hooks/useHitTest.svelte.js +56 -12
- package/dist/hooks/useTeleport.d.ts +11 -9
- package/dist/hooks/useTeleport.js +62 -14
- package/dist/hooks/useXR.js +5 -5
- package/dist/hooks/useXROrigin.svelte.d.ts +10 -0
- package/dist/hooks/useXROrigin.svelte.js +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/internal/inputSources.svelte.d.ts +84 -0
- package/dist/internal/inputSources.svelte.js +91 -0
- package/dist/internal/setupHeadset.svelte.js +18 -6
- package/dist/internal/setupInputSources.d.ts +4 -0
- package/dist/internal/setupInputSources.js +319 -0
- package/dist/internal/state.svelte.d.ts +10 -12
- package/dist/internal/state.svelte.js +9 -3
- package/dist/lib/getXRSessionOptions.d.ts +1 -1
- package/dist/lib/getXRSessionOptions.js +8 -7
- package/dist/lib/toggleXRSession.d.ts +1 -1
- package/dist/lib/toggleXRSession.js +20 -5
- package/dist/plugins/pointerControls/compute.js +14 -9
- package/dist/plugins/pointerControls/context.d.ts +3 -3
- package/dist/plugins/pointerControls/context.js +12 -6
- package/dist/plugins/pointerControls/index.d.ts +4 -3
- package/dist/plugins/pointerControls/index.js +63 -29
- package/dist/plugins/pointerControls/setup.svelte.js +64 -44
- package/dist/plugins/pointerControls/types.d.ts +14 -3
- package/dist/plugins/teleportControls/compute.d.ts +1 -1
- package/dist/plugins/teleportControls/compute.js +11 -8
- package/dist/plugins/teleportControls/context.d.ts +4 -4
- package/dist/plugins/teleportControls/context.js +1 -4
- package/dist/plugins/teleportControls/index.js +7 -4
- package/dist/plugins/teleportControls/plugin.svelte.js +12 -51
- package/dist/plugins/teleportControls/setup.svelte.js +10 -9
- package/dist/plugins/touchControls/compute.d.ts +3 -0
- package/dist/plugins/touchControls/compute.js +13 -0
- package/dist/plugins/touchControls/context.d.ts +12 -0
- package/dist/plugins/touchControls/context.js +27 -0
- package/dist/plugins/touchControls/hook.d.ts +5 -0
- package/dist/plugins/touchControls/hook.js +26 -0
- package/dist/plugins/touchControls/index.d.ts +33 -0
- package/dist/plugins/touchControls/index.js +41 -0
- package/dist/plugins/touchControls/plugin.svelte.d.ts +1 -0
- package/dist/plugins/touchControls/plugin.svelte.js +24 -0
- package/dist/plugins/touchControls/setup.svelte.d.ts +2 -0
- package/dist/plugins/touchControls/setup.svelte.js +247 -0
- package/dist/plugins/touchControls/types.d.ts +62 -0
- package/dist/plugins/touchControls/types.js +11 -0
- package/dist/types.d.ts +1 -1
- package/package.json +3 -3
- package/dist/internal/setupControllers.d.ts +0 -2
- package/dist/internal/setupControllers.js +0 -73
- package/dist/internal/setupHands.d.ts +0 -2
- package/dist/internal/setupHands.js +0 -67
- package/dist/internal/useHandTrackingState.d.ts +0 -5
- package/dist/internal/useHandTrackingState.js +0 -20
|
@@ -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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
6
|
+
if (referenceSpaceType && init === undefined) {
|
|
6
7
|
return { optionalFeatures: [referenceSpaceType] };
|
|
7
8
|
}
|
|
8
|
-
if (referenceSpaceType &&
|
|
9
|
+
if (referenceSpaceType && init) {
|
|
9
10
|
return {
|
|
10
|
-
...
|
|
11
|
-
optionalFeatures: [...new Set([...(
|
|
11
|
+
...init,
|
|
12
|
+
optionalFeatures: [...new Set([...(init.optionalFeatures ?? []), referenceSpaceType])]
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
|
-
return
|
|
15
|
+
return init;
|
|
15
16
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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 {
|
|
2
|
+
import { getControllerState, getHandState } from '../../internal/inputSources.svelte.js';
|
|
3
|
+
const origin = new Vector3();
|
|
3
4
|
const forward = new Vector3();
|
|
4
|
-
export const defaultComputeFunction = (
|
|
5
|
-
const
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|