@threlte/xr 1.5.5 → 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.
- 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/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
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { getContext, setContext } from 'svelte';
|
|
2
2
|
const handContextKeys = {
|
|
3
|
-
left:
|
|
4
|
-
|
|
3
|
+
left: {
|
|
4
|
+
controller: Symbol('pointer-controls-context-left-controller'),
|
|
5
|
+
hand: Symbol('pointer-controls-context-left-hand')
|
|
6
|
+
},
|
|
7
|
+
right: {
|
|
8
|
+
controller: Symbol('pointer-controls-context-right-controller'),
|
|
9
|
+
hand: Symbol('pointer-controls-context-right-hand')
|
|
10
|
+
}
|
|
5
11
|
};
|
|
6
12
|
const contextKey = Symbol('pointer-controls-context');
|
|
7
|
-
export const getHandContext = (hand) => {
|
|
8
|
-
return getContext(handContextKeys[hand]);
|
|
13
|
+
export const getHandContext = (hand, sourceType) => {
|
|
14
|
+
return getContext(handContextKeys[hand][sourceType]);
|
|
9
15
|
};
|
|
10
|
-
export const setHandContext = (hand, context) => {
|
|
11
|
-
setContext(handContextKeys[hand], context);
|
|
16
|
+
export const setHandContext = (hand, sourceType, context) => {
|
|
17
|
+
setContext(handContextKeys[hand][sourceType], context);
|
|
12
18
|
};
|
|
13
19
|
export const getControlsContext = () => {
|
|
14
20
|
return getContext(contextKey);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type CurrentWritable } from '@threlte/core';
|
|
1
2
|
import { type ComputeFunction } from './compute.js';
|
|
2
|
-
import type { FilterFunction } from './types.js';
|
|
3
|
+
import type { FilterFunction, IntersectionEvent } from './types.js';
|
|
3
4
|
export type PointerControlsOptions = {
|
|
4
5
|
enabled?: boolean;
|
|
5
6
|
/**
|
|
@@ -22,6 +23,6 @@ export type PointerControlsOptions = {
|
|
|
22
23
|
fixedStep?: number;
|
|
23
24
|
};
|
|
24
25
|
export declare const pointerControls: (handedness: "left" | "right", options?: PointerControlsOptions) => {
|
|
25
|
-
enabled:
|
|
26
|
-
hovered: Map<string,
|
|
26
|
+
enabled: CurrentWritable<boolean>;
|
|
27
|
+
hovered: Map<string, IntersectionEvent>;
|
|
27
28
|
};
|
|
@@ -4,43 +4,77 @@ import { defaultComputeFunction } from './compute.js';
|
|
|
4
4
|
import { injectPointerControlsPlugin } from './plugin.svelte.js';
|
|
5
5
|
import { setupPointerControls } from './setup.svelte.js';
|
|
6
6
|
import { getControlsContext, getHandContext, setControlsContext, setHandContext, setInternalContext } from './context.js';
|
|
7
|
-
import { pointerState } from '../../internal/state.svelte.js';
|
|
7
|
+
import { pointerIntersection, pointerState } from '../../internal/state.svelte.js';
|
|
8
|
+
const aggregateStates = new Map();
|
|
8
9
|
export const pointerControls = (handedness, options) => {
|
|
9
10
|
if (getControlsContext() === undefined) {
|
|
10
11
|
injectPointerControlsPlugin();
|
|
11
12
|
setInternalContext();
|
|
12
|
-
setControlsContext({
|
|
13
|
-
interactiveObjects: [],
|
|
14
|
-
raycaster: new Raycaster(),
|
|
15
|
-
compute: options?.compute ?? defaultComputeFunction,
|
|
16
|
-
filter: options?.filter
|
|
17
|
-
});
|
|
13
|
+
setControlsContext({ interactiveObjects: [] });
|
|
18
14
|
}
|
|
19
15
|
const context = getControlsContext();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
16
|
+
const aggregateState = aggregateStates.get(handedness) ??
|
|
17
|
+
(() => {
|
|
18
|
+
const state = {
|
|
19
|
+
enabled: currentWritable(options?.enabled ?? true),
|
|
20
|
+
hovered: new Map()
|
|
21
|
+
};
|
|
22
|
+
aggregateStates.set(handedness, state);
|
|
23
|
+
return state;
|
|
24
|
+
})();
|
|
25
|
+
const { enabled, hovered } = aggregateState;
|
|
26
|
+
let controllerContext = getHandContext(handedness, 'controller');
|
|
27
|
+
let handContext = getHandContext(handedness, 'hand');
|
|
28
|
+
const syncSharedState = () => {
|
|
29
|
+
hovered.clear();
|
|
30
|
+
for (const [id, event] of controllerContext.hovered) {
|
|
31
|
+
hovered.set(`controller:${id}`, event);
|
|
32
|
+
}
|
|
33
|
+
for (const [id, event] of handContext.hovered) {
|
|
34
|
+
hovered.set(`hand:${id}`, event);
|
|
35
|
+
}
|
|
36
|
+
// Shared handedness-level pointer visuals are currently controller-only:
|
|
37
|
+
// <Controller /> renders the cursor/ray from these globals, while hand
|
|
38
|
+
// pointer events are dispatched independently without a matching visual.
|
|
39
|
+
pointerState[handedness].hovering = controllerContext.pointerOverTarget.current;
|
|
40
|
+
pointerIntersection[handedness] = controllerContext.currentIntersection;
|
|
41
|
+
};
|
|
42
|
+
const createContext = (sourceType) => ({
|
|
43
|
+
hand: handedness,
|
|
44
|
+
sourceType,
|
|
45
|
+
enabled,
|
|
46
|
+
pointer: currentWritable(new Vector3()),
|
|
47
|
+
pointerOverTarget: currentWritable(false),
|
|
48
|
+
lastEvent: undefined,
|
|
49
|
+
initialClick: [0, 0, 0],
|
|
50
|
+
initialHits: [],
|
|
51
|
+
hovered: new Map(),
|
|
52
|
+
currentIntersection: undefined,
|
|
53
|
+
raycaster: new Raycaster(),
|
|
54
|
+
syncSharedState,
|
|
55
|
+
compute: options?.compute ?? defaultComputeFunction,
|
|
56
|
+
filter: options?.filter
|
|
38
57
|
});
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
const setupContexts = [];
|
|
59
|
+
if (controllerContext === undefined) {
|
|
60
|
+
controllerContext = createContext('controller');
|
|
61
|
+
setHandContext(handedness, 'controller', controllerContext);
|
|
62
|
+
setupContexts.push(controllerContext);
|
|
63
|
+
}
|
|
64
|
+
if (handContext === undefined) {
|
|
65
|
+
handContext = createContext('hand');
|
|
66
|
+
setHandContext(handedness, 'hand', handContext);
|
|
67
|
+
setupContexts.push(handContext);
|
|
68
|
+
}
|
|
69
|
+
for (const setupContext of setupContexts) {
|
|
70
|
+
setupPointerControls(context, setupContext, options?.fixedStep);
|
|
71
|
+
}
|
|
72
|
+
observe.pre(() => [enabled], ([nextEnabled]) => {
|
|
73
|
+
pointerState[handedness].enabled = nextEnabled;
|
|
41
74
|
});
|
|
75
|
+
syncSharedState();
|
|
42
76
|
return {
|
|
43
|
-
enabled
|
|
44
|
-
hovered
|
|
77
|
+
enabled,
|
|
78
|
+
hovered
|
|
45
79
|
};
|
|
46
80
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Vector3 } from 'three';
|
|
2
|
-
import {
|
|
2
|
+
import { fromStore } from 'svelte/store';
|
|
3
3
|
import { getInternalContext } from './context.js';
|
|
4
|
-
import {
|
|
5
|
-
import { useHand } from '../../hooks/useHand.svelte.js';
|
|
4
|
+
import { addSubscriber } from '../../internal/inputSources.svelte.js';
|
|
6
5
|
import { useFixed } from '../../internal/useFixed.js';
|
|
7
|
-
import { isPresenting
|
|
6
|
+
import { isPresenting } from '../../internal/state.svelte.js';
|
|
8
7
|
// Hover identity must match the dedup key used in `getHits`, otherwise the ID
|
|
9
8
|
// changes mid-hover (e.g. the hit's face index changes as the ray sweeps a
|
|
10
9
|
// plain mesh) and the object flickers between pointerout/pointerenter every
|
|
@@ -20,13 +19,18 @@ const getIntersectionId = (intersection) => {
|
|
|
20
19
|
return target.uuid;
|
|
21
20
|
};
|
|
22
21
|
const EPSILON = 0.0001;
|
|
22
|
+
// Starts high enough to stay clear of browser-assigned DOM pointerIds in the
|
|
23
|
+
// same session. Incremented per setupPointerControls call so each hand — and
|
|
24
|
+
// each reconnect — gets a distinct id.
|
|
25
|
+
let nextPointerId = 1001;
|
|
23
26
|
export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) => {
|
|
24
27
|
const handedness = handContext.hand;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
28
|
+
const pointerId = nextPointerId++;
|
|
29
|
+
const enabled = fromStore(handContext.enabled);
|
|
27
30
|
const { dispatchers } = getInternalContext();
|
|
28
31
|
let hits = [];
|
|
29
|
-
const
|
|
32
|
+
const lastRayOrigin = new Vector3();
|
|
33
|
+
const lastRayDirection = new Vector3();
|
|
30
34
|
const handlePointerDown = (event) => {
|
|
31
35
|
// Save initial coordinates on pointer-down
|
|
32
36
|
const [hit] = hits;
|
|
@@ -69,10 +73,11 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
69
73
|
if (handContext.hovered.size === 0) {
|
|
70
74
|
handContext.pointerOverTarget.set(false);
|
|
71
75
|
}
|
|
76
|
+
handContext.syncSharedState();
|
|
72
77
|
}
|
|
73
78
|
const getHits = () => {
|
|
74
79
|
const intersections = [];
|
|
75
|
-
const rawHits =
|
|
80
|
+
const rawHits = handContext.raycaster.intersectObjects(context.interactiveObjects, true);
|
|
76
81
|
const seen = new Set();
|
|
77
82
|
// Deduplicate hits by object. When recursive=true, intersectObjects searches
|
|
78
83
|
// each registered object's full subtree, so a child that is itself registered
|
|
@@ -92,8 +97,9 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
92
97
|
seen.add(key);
|
|
93
98
|
return true;
|
|
94
99
|
});
|
|
95
|
-
const filtered =
|
|
96
|
-
|
|
100
|
+
const filtered = handContext.filter === undefined ? hits : handContext.filter(hits, context, handContext);
|
|
101
|
+
handContext.currentIntersection = filtered[0];
|
|
102
|
+
handContext.syncSharedState();
|
|
97
103
|
// Bubble up the events, find the event source (eventObject)
|
|
98
104
|
for (const hit of filtered) {
|
|
99
105
|
let eventObject = hit.object;
|
|
@@ -109,11 +115,11 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
109
115
|
};
|
|
110
116
|
function pointerMissed(objects, event) {
|
|
111
117
|
for (const object of objects) {
|
|
112
|
-
dispatchers.get(object)?.
|
|
118
|
+
dispatchers.get(object)?.onpointermissed?.(event);
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
function processHits() {
|
|
116
|
-
|
|
122
|
+
handContext.compute(context, handContext);
|
|
117
123
|
return getHits();
|
|
118
124
|
}
|
|
119
125
|
const handleEvent = (name, event) => {
|
|
@@ -139,6 +145,7 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
139
145
|
...hit,
|
|
140
146
|
intersections: hits,
|
|
141
147
|
handedness,
|
|
148
|
+
pointerId,
|
|
142
149
|
stopPropagation() {
|
|
143
150
|
stopped = true;
|
|
144
151
|
intersectionEvent.stopped = true;
|
|
@@ -152,7 +159,7 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
152
159
|
delta: 0,
|
|
153
160
|
nativeEvent: event,
|
|
154
161
|
pointer: handContext.pointer.current,
|
|
155
|
-
ray:
|
|
162
|
+
ray: handContext.raycaster.ray
|
|
156
163
|
};
|
|
157
164
|
if (isPointerMove) {
|
|
158
165
|
// Move event ...
|
|
@@ -169,6 +176,7 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
169
176
|
events.onpointerover?.(intersectionEvent);
|
|
170
177
|
events.onpointerenter?.(intersectionEvent);
|
|
171
178
|
handContext.pointerOverTarget.set(true);
|
|
179
|
+
handContext.syncSharedState();
|
|
172
180
|
}
|
|
173
181
|
else if (hoveredItem.stopped) {
|
|
174
182
|
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
|
|
@@ -190,47 +198,59 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
190
198
|
};
|
|
191
199
|
const { start, stop } = useFixed(() => {
|
|
192
200
|
hits = processHits();
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
if (targetRay.position.distanceTo(lastPosition) > EPSILON) {
|
|
201
|
+
const ray = handContext.raycaster.ray;
|
|
202
|
+
if (ray.origin.distanceToSquared(lastRayOrigin) > EPSILON * EPSILON ||
|
|
203
|
+
1 - ray.direction.dot(lastRayDirection) > EPSILON) {
|
|
197
204
|
handleEvent('onpointermove');
|
|
198
205
|
}
|
|
199
|
-
|
|
206
|
+
lastRayOrigin.copy(ray.origin);
|
|
207
|
+
lastRayDirection.copy(ray.direction);
|
|
200
208
|
}, {
|
|
201
209
|
fixedStep,
|
|
202
210
|
autoStart: false
|
|
203
211
|
});
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
return;
|
|
207
|
-
controller.targetRay.addEventListener('selectstart', handlePointerDown);
|
|
208
|
-
controller.targetRay.addEventListener('selectend', handlePointerUp);
|
|
209
|
-
controller.targetRay.addEventListener('select', handleClick);
|
|
210
|
-
return () => {
|
|
211
|
-
controller.targetRay.removeEventListener('selectstart', handlePointerDown);
|
|
212
|
-
controller.targetRay.removeEventListener('selectend', handlePointerUp);
|
|
213
|
-
controller.targetRay.removeEventListener('select', handleClick);
|
|
214
|
-
};
|
|
215
|
-
});
|
|
216
|
-
observe.pre(() => [hand, handContext.enabled], ([input, enabled]) => {
|
|
217
|
-
if (input === undefined || !enabled)
|
|
218
|
-
return;
|
|
219
|
-
input.hand.addEventListener('pinchstart', handlePointerDown);
|
|
220
|
-
input.hand.addEventListener('pinchend', handlePointerUp);
|
|
221
|
-
input.hand.addEventListener('pinchend', handleClick);
|
|
222
|
-
return () => {
|
|
223
|
-
input.hand.removeEventListener('pinchstart', handlePointerDown);
|
|
224
|
-
input.hand.removeEventListener('pinchend', handlePointerUp);
|
|
225
|
-
input.hand.removeEventListener('pinchend', handleClick);
|
|
226
|
-
};
|
|
227
|
-
});
|
|
228
|
-
observe.pre(() => [isPresenting.current, handContext.enabled], ([isPresenting, $enabled]) => {
|
|
229
|
-
if (isPresenting && $enabled) {
|
|
212
|
+
$effect.pre(() => {
|
|
213
|
+
if (isPresenting.current && enabled.current) {
|
|
230
214
|
start();
|
|
231
215
|
}
|
|
232
216
|
else {
|
|
233
217
|
stop();
|
|
218
|
+
hits = [];
|
|
219
|
+
handContext.currentIntersection = undefined;
|
|
220
|
+
cancelPointer([]);
|
|
221
|
+
handContext.syncSharedState();
|
|
234
222
|
}
|
|
235
223
|
});
|
|
224
|
+
$effect.pre(() => {
|
|
225
|
+
if (handContext.sourceType !== 'controller')
|
|
226
|
+
return;
|
|
227
|
+
if (!enabled.current)
|
|
228
|
+
return;
|
|
229
|
+
return addSubscriber({
|
|
230
|
+
type: 'controller',
|
|
231
|
+
handedness,
|
|
232
|
+
callbacks: {
|
|
233
|
+
onselectstart: handlePointerDown,
|
|
234
|
+
onselectend: handlePointerUp,
|
|
235
|
+
onselect: handleClick
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
$effect.pre(() => {
|
|
240
|
+
if (handContext.sourceType !== 'hand')
|
|
241
|
+
return;
|
|
242
|
+
if (!enabled.current)
|
|
243
|
+
return;
|
|
244
|
+
return addSubscriber({
|
|
245
|
+
type: 'hand',
|
|
246
|
+
handedness,
|
|
247
|
+
callbacks: {
|
|
248
|
+
onpinchstart: handlePointerDown,
|
|
249
|
+
onpinchend: ((event) => {
|
|
250
|
+
handlePointerUp(event);
|
|
251
|
+
handleClick(event);
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
236
256
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Intersection as ThreeIntersection, Object3D, Vector3, Ray, Raycaster, Event } from 'three';
|
|
2
2
|
import type { CurrentWritable } from '@threlte/core';
|
|
3
3
|
import type { ComputeFunction } from './compute.js';
|
|
4
|
+
export type PointerSourceType = 'controller' | 'hand';
|
|
4
5
|
export type Properties<T> = Pick<T, {
|
|
5
6
|
[K in keyof T]: T[K] extends (_: any) => any ? never : K;
|
|
6
7
|
}[keyof T]>;
|
|
@@ -15,6 +16,9 @@ export interface IntersectionEvent extends Intersection {
|
|
|
15
16
|
intersections: Intersection[];
|
|
16
17
|
/** Which hand dispatched this event. Each controller/hand fires enter/leave/etc. independently. */
|
|
17
18
|
handedness: 'left' | 'right';
|
|
19
|
+
/** Stable identifier for this pointer source. Mirrors DOM PointerEvent.pointerId so downstream
|
|
20
|
+
* consumers that key per-pointer state by id can distinguish hands / reconnects. */
|
|
21
|
+
pointerId: number;
|
|
18
22
|
/** Normalized event coordinates */
|
|
19
23
|
pointer: Vector3;
|
|
20
24
|
/** Delta between first click and this event */
|
|
@@ -31,12 +35,11 @@ export interface IntersectionEvent extends Intersection {
|
|
|
31
35
|
export type FilterFunction = (items: Intersection[], state: ControlsContext, handState: HandContext) => Intersection[];
|
|
32
36
|
export type ControlsContext = {
|
|
33
37
|
interactiveObjects: Object3D[];
|
|
34
|
-
raycaster: Raycaster;
|
|
35
|
-
compute: ComputeFunction;
|
|
36
|
-
filter?: FilterFunction | undefined;
|
|
37
38
|
};
|
|
38
39
|
export type HandContext = {
|
|
39
40
|
hand: 'left' | 'right';
|
|
41
|
+
/** Physical XR source this runtime tracks for the handedness. */
|
|
42
|
+
sourceType: PointerSourceType;
|
|
40
43
|
enabled: CurrentWritable<boolean>;
|
|
41
44
|
pointer: CurrentWritable<Vector3>;
|
|
42
45
|
pointerOverTarget: CurrentWritable<boolean>;
|
|
@@ -44,6 +47,14 @@ export type HandContext = {
|
|
|
44
47
|
initialClick: [x: number, y: number, z: number];
|
|
45
48
|
initialHits: Object3D[];
|
|
46
49
|
hovered: Map<string, IntersectionEvent>;
|
|
50
|
+
currentIntersection: Intersection | undefined;
|
|
51
|
+
/** Per-hand raycaster — keeps `intersectionEvent.ray` consistent across the
|
|
52
|
+
* tick even when the other hand also raycasts. */
|
|
53
|
+
raycaster: Raycaster;
|
|
54
|
+
/** Syncs aggregate handedness-level state after this source mutates hover or hit state. */
|
|
55
|
+
syncSharedState: () => void;
|
|
56
|
+
compute: ComputeFunction;
|
|
57
|
+
filter?: FilterFunction | undefined;
|
|
47
58
|
};
|
|
48
59
|
export interface PointerCaptureTarget {
|
|
49
60
|
intersection: Intersection;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Context, HandContext } from './context.js';
|
|
2
2
|
export type ComputeFunction = (context: Context, handContext: HandContext) => void;
|
|
3
|
-
export declare const defaultComputeFunction: (
|
|
3
|
+
export declare const defaultComputeFunction: (_context: Context, handContext: HandContext) => void;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { Vector3 } from 'three';
|
|
2
2
|
import { controllers } from '../../hooks/useController.svelte.js';
|
|
3
|
+
import { hands } from '../../hooks/useHand.svelte.js';
|
|
4
|
+
const origin = new Vector3();
|
|
3
5
|
const forward = new Vector3();
|
|
4
|
-
export const defaultComputeFunction = (
|
|
5
|
-
const targetRay = controllers[handContext.hand]?.targetRay;
|
|
6
|
+
export const defaultComputeFunction = (_context, handContext) => {
|
|
7
|
+
const targetRay = controllers[handContext.hand]?.targetRay ?? hands[handContext.hand]?.targetRay;
|
|
6
8
|
if (targetRay === undefined)
|
|
7
9
|
return;
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Read origin/direction from matrixWorld so the ray is in real world space,
|
|
11
|
+
// even when an ancestor (e.g. <XROrigin>) has a non-identity transform.
|
|
12
|
+
// Force an update because this runs before the frame's scene.updateMatrixWorld.
|
|
13
|
+
targetRay.updateWorldMatrix(true, false);
|
|
14
|
+
origin.setFromMatrixPosition(targetRay.matrixWorld);
|
|
15
|
+
forward.set(0, 0, -1).transformDirection(targetRay.matrixWorld);
|
|
16
|
+
handContext.raycaster.set(origin, forward);
|
|
14
17
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type Mesh, Raycaster, type Intersection } from 'three';
|
|
2
2
|
import type { CurrentWritable } from '@threlte/core';
|
|
3
|
-
import type { TeleportControlsOptions } from './index.js';
|
|
4
3
|
export type ComputeFunction = (context: Context, handContext: HandContext) => void;
|
|
5
4
|
export type TeleportEvents = Record<string, (arg: unknown) => void>;
|
|
6
5
|
export interface Context {
|
|
@@ -8,8 +7,6 @@ export interface Context {
|
|
|
8
7
|
surfaces: Map<string, Mesh>;
|
|
9
8
|
blockers: Map<string, Mesh>;
|
|
10
9
|
dispatchers: WeakMap<Mesh, Record<string, (arg: unknown) => void>>;
|
|
11
|
-
raycaster: Raycaster;
|
|
12
|
-
compute: ComputeFunction;
|
|
13
10
|
addBlocker: (mesh: Mesh) => void;
|
|
14
11
|
removeBlocker: (mesh: Mesh) => void;
|
|
15
12
|
addSurface: (mesh: Mesh, events: TeleportEvents) => void;
|
|
@@ -20,8 +17,11 @@ export interface HandContext {
|
|
|
20
17
|
enabled: CurrentWritable<boolean>;
|
|
21
18
|
active: CurrentWritable<boolean>;
|
|
22
19
|
hovered: CurrentWritable<Intersection | undefined>;
|
|
20
|
+
/** Per-hand raycaster — keeps intersection state isolated between hands. */
|
|
21
|
+
raycaster: Raycaster;
|
|
22
|
+
compute: ComputeFunction;
|
|
23
23
|
}
|
|
24
24
|
export declare const getHandContext: (hand: "left" | "right") => HandContext;
|
|
25
25
|
export declare const setHandContext: (hand: "left" | "right", context: HandContext) => void;
|
|
26
26
|
export declare const useTeleportControls: () => Context;
|
|
27
|
-
export declare const createTeleportContext: (
|
|
27
|
+
export declare const createTeleportContext: () => Context;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Raycaster } from 'three';
|
|
2
2
|
import { getContext, setContext } from 'svelte';
|
|
3
|
-
import { defaultComputeFunction } from './compute.js';
|
|
4
3
|
const handContextKeys = {
|
|
5
4
|
left: Symbol('teleport-controls-context-left-hand'),
|
|
6
5
|
right: Symbol('teleport-controls-context-right-hand')
|
|
@@ -15,7 +14,7 @@ export const setHandContext = (hand, context) => {
|
|
|
15
14
|
export const useTeleportControls = () => {
|
|
16
15
|
return getContext(contextKey);
|
|
17
16
|
};
|
|
18
|
-
export const createTeleportContext = (
|
|
17
|
+
export const createTeleportContext = () => {
|
|
19
18
|
const addSurface = (mesh, events) => {
|
|
20
19
|
// check if the object is already in the list
|
|
21
20
|
if (context.interactiveObjects.indexOf(mesh) > -1) {
|
|
@@ -53,8 +52,6 @@ export const createTeleportContext = (compute) => {
|
|
|
53
52
|
surfaces: new Map(),
|
|
54
53
|
blockers: new Map(),
|
|
55
54
|
dispatchers: new WeakMap(),
|
|
56
|
-
raycaster: new Raycaster(),
|
|
57
|
-
compute: compute ?? defaultComputeFunction,
|
|
58
55
|
addBlocker,
|
|
59
56
|
removeBlocker,
|
|
60
57
|
addSurface,
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
+
import { Raycaster } from 'three';
|
|
1
2
|
import { currentWritable, observe } from '@threlte/core';
|
|
2
3
|
import { createTeleportContext, useTeleportControls, getHandContext } from './context.js';
|
|
3
4
|
import { injectTeleportControlsPlugin } from './plugin.svelte.js';
|
|
4
5
|
import { setHandContext } from './context.js';
|
|
5
6
|
import { setupTeleportControls } from './setup.svelte.js';
|
|
7
|
+
import { defaultComputeFunction } from './compute.js';
|
|
6
8
|
import { teleportState } from '../../internal/state.svelte.js';
|
|
7
9
|
export const teleportControls = (handedness, options) => {
|
|
8
10
|
if (useTeleportControls() === undefined) {
|
|
9
11
|
injectTeleportControlsPlugin();
|
|
10
|
-
createTeleportContext(
|
|
12
|
+
createTeleportContext();
|
|
11
13
|
}
|
|
12
14
|
const context = useTeleportControls();
|
|
13
15
|
if (getHandContext(handedness) === undefined) {
|
|
14
|
-
const enabled = options?.enabled ?? true;
|
|
15
16
|
const ctx = {
|
|
16
17
|
hand: handedness,
|
|
17
18
|
active: currentWritable(false),
|
|
18
|
-
enabled: currentWritable(enabled),
|
|
19
|
-
hovered: currentWritable(undefined)
|
|
19
|
+
enabled: currentWritable(options?.enabled ?? true),
|
|
20
|
+
hovered: currentWritable(undefined),
|
|
21
|
+
raycaster: new Raycaster(),
|
|
22
|
+
compute: options?.compute ?? defaultComputeFunction
|
|
20
23
|
};
|
|
21
24
|
setHandContext(handedness, ctx);
|
|
22
25
|
setupTeleportControls(context, ctx, options?.fixedStep);
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { controllers } from '../../hooks/useController.svelte.js';
|
|
1
|
+
import { useController } from '../../hooks/useController.svelte.js';
|
|
3
2
|
import { useTeleport } from '../../hooks/useTeleport.js';
|
|
4
3
|
import { useFixed } from '../../internal/useFixed.js';
|
|
5
4
|
import { isPresenting, teleportIntersection } from '../../internal/state.svelte.js';
|
|
5
|
+
import { fromStore } from 'svelte/store';
|
|
6
6
|
export const setupTeleportControls = (context, handContext, fixedStep = 1 / 40) => {
|
|
7
7
|
const handedness = handContext.hand;
|
|
8
|
-
const
|
|
8
|
+
const enabled = fromStore(handContext.enabled);
|
|
9
|
+
const controller = fromStore(useController(handedness));
|
|
9
10
|
const teleport = useTeleport();
|
|
10
11
|
const handleHoverEnd = () => {
|
|
11
12
|
handContext.hovered.set(undefined);
|
|
12
13
|
teleportIntersection[handedness] = undefined;
|
|
13
14
|
};
|
|
14
15
|
const { start, stop } = useFixed(() => {
|
|
15
|
-
const gamepad = controller?.inputSource.gamepad;
|
|
16
|
+
const gamepad = controller.current?.inputSource.gamepad;
|
|
16
17
|
if (gamepad === undefined) {
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
@@ -30,15 +31,15 @@ export const setupTeleportControls = (context, handContext, fixedStep = 1 / 40)
|
|
|
30
31
|
}
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
const [intersect] =
|
|
34
|
+
handContext.compute(context, handContext);
|
|
35
|
+
const [intersect] = handContext.raycaster.intersectObjects(context.interactiveObjects, true);
|
|
35
36
|
if (intersect === undefined) {
|
|
36
37
|
if (handContext.hovered.current !== undefined) {
|
|
37
38
|
handleHoverEnd();
|
|
38
39
|
}
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
if (
|
|
42
|
+
if (context.blockers.has(intersect.object.uuid)) {
|
|
42
43
|
if (handContext.hovered.current !== undefined) {
|
|
43
44
|
handleHoverEnd();
|
|
44
45
|
}
|
|
@@ -50,8 +51,8 @@ export const setupTeleportControls = (context, handContext, fixedStep = 1 / 40)
|
|
|
50
51
|
fixedStep,
|
|
51
52
|
autoStart: false
|
|
52
53
|
});
|
|
53
|
-
|
|
54
|
-
if (isPresenting &&
|
|
54
|
+
$effect.pre(() => {
|
|
55
|
+
if (isPresenting.current && enabled.current) {
|
|
55
56
|
start();
|
|
56
57
|
}
|
|
57
58
|
else {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { hands } from '../../hooks/useHand.svelte.js';
|
|
2
|
+
export const defaultComputeFunction = (_context, handContext) => {
|
|
3
|
+
handContext.originValid = false;
|
|
4
|
+
const xrhand = hands[handContext.hand];
|
|
5
|
+
if (xrhand === undefined)
|
|
6
|
+
return;
|
|
7
|
+
const jointSpace = xrhand.hand.joints[handContext.joint];
|
|
8
|
+
if (jointSpace === undefined || jointSpace.jointRadius === undefined)
|
|
9
|
+
return;
|
|
10
|
+
jointSpace.updateWorldMatrix(true, false);
|
|
11
|
+
handContext.origin.setFromMatrixPosition(jointSpace.matrixWorld);
|
|
12
|
+
handContext.originValid = true;
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
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;
|
|
5
|
+
export declare const getControlsContext: () => ControlsContext;
|
|
6
|
+
export declare const setControlsContext: (context: ControlsContext) => void;
|
|
7
|
+
interface InternalContext {
|
|
8
|
+
dispatchers: WeakMap<Object3D, Record<string, (arg: unknown) => void>>;
|
|
9
|
+
}
|
|
10
|
+
export declare const getInternalContext: () => InternalContext;
|
|
11
|
+
export declare const setInternalContext: () => void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const handContextKeys = {
|
|
3
|
+
left: Symbol('touch-controls-context-left'),
|
|
4
|
+
right: Symbol('touch-controls-context-right')
|
|
5
|
+
};
|
|
6
|
+
const contextKey = Symbol('touch-controls-context');
|
|
7
|
+
export const getHandContext = (hand) => {
|
|
8
|
+
return getContext(handContextKeys[hand]);
|
|
9
|
+
};
|
|
10
|
+
export const setHandContext = (hand, context) => {
|
|
11
|
+
setContext(handContextKeys[hand], context);
|
|
12
|
+
};
|
|
13
|
+
export const getControlsContext = () => {
|
|
14
|
+
return getContext(contextKey);
|
|
15
|
+
};
|
|
16
|
+
export const setControlsContext = (context) => {
|
|
17
|
+
setContext(contextKey, context);
|
|
18
|
+
};
|
|
19
|
+
const internalContextKey = Symbol('touch-controls-internal-context');
|
|
20
|
+
export const getInternalContext = () => {
|
|
21
|
+
return getContext(internalContextKey);
|
|
22
|
+
};
|
|
23
|
+
export const setInternalContext = () => {
|
|
24
|
+
setContext(internalContextKey, {
|
|
25
|
+
dispatchers: new WeakMap()
|
|
26
|
+
});
|
|
27
|
+
};
|