@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getControlsContext, getInternalContext } from './context.js';
|
|
2
|
+
export const useTouchControls = () => {
|
|
3
|
+
const context = getControlsContext();
|
|
4
|
+
const { dispatchers } = getInternalContext();
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('No touch controls context found. Did you forget to implement touchControls()?');
|
|
7
|
+
}
|
|
8
|
+
const addInteractiveObject = (object, events) => {
|
|
9
|
+
if (context.interactiveObjects.indexOf(object) > -1) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
dispatchers.set(object, events);
|
|
13
|
+
context.interactiveObjects.push(object);
|
|
14
|
+
};
|
|
15
|
+
const removeInteractiveObject = (object) => {
|
|
16
|
+
const index = context.interactiveObjects.indexOf(object);
|
|
17
|
+
if (index === -1)
|
|
18
|
+
return;
|
|
19
|
+
context.interactiveObjects.splice(index, 1);
|
|
20
|
+
dispatchers.delete(object);
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
addInteractiveObject,
|
|
24
|
+
removeInteractiveObject
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ComputeFunction } from './compute.js';
|
|
2
|
+
import type { FilterFunction } from './types.js';
|
|
3
|
+
import type { HandJoints } from '../../lib/handJoints.js';
|
|
4
|
+
export type TouchControlsOptions = {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Which hand joint to track. Defaults to the index fingertip.
|
|
8
|
+
* @default 'index-finger-tip'
|
|
9
|
+
*/
|
|
10
|
+
joint?: HandJoints;
|
|
11
|
+
/**
|
|
12
|
+
* Distance at which an object starts receiving hover events.
|
|
13
|
+
* @default 0.03
|
|
14
|
+
*/
|
|
15
|
+
hoverRadius?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Distance below which a hover transitions to `pointerdown` (and crossing back up fires
|
|
18
|
+
* `pointerup` + `click`).
|
|
19
|
+
* @default 0.01
|
|
20
|
+
*/
|
|
21
|
+
downRadius?: number;
|
|
22
|
+
compute?: ComputeFunction;
|
|
23
|
+
filter?: FilterFunction;
|
|
24
|
+
/**
|
|
25
|
+
* Interval at which joint positions are polled and intersections are recomputed.
|
|
26
|
+
* @default 1 / 40
|
|
27
|
+
*/
|
|
28
|
+
fixedStep?: number;
|
|
29
|
+
};
|
|
30
|
+
export declare const touchControls: (handedness: "left" | "right", options?: TouchControlsOptions) => {
|
|
31
|
+
enabled: import("@threlte/core").CurrentWritable<boolean>;
|
|
32
|
+
hovered: Map<string, import("./types.js").IntersectionEvent>;
|
|
33
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Vector3 } from 'three';
|
|
2
|
+
import { currentWritable } from '@threlte/core';
|
|
3
|
+
import { defaultComputeFunction } from './compute.js';
|
|
4
|
+
import { injectTouchControlsPlugin } from './plugin.svelte.js';
|
|
5
|
+
import { setupTouchControls } from './setup.svelte.js';
|
|
6
|
+
import { getControlsContext, getHandContext, setControlsContext, setHandContext, setInternalContext } from './context.js';
|
|
7
|
+
export const touchControls = (handedness, options) => {
|
|
8
|
+
if (getControlsContext() === undefined) {
|
|
9
|
+
injectTouchControlsPlugin();
|
|
10
|
+
setInternalContext();
|
|
11
|
+
setControlsContext({ interactiveObjects: [] });
|
|
12
|
+
}
|
|
13
|
+
const context = getControlsContext();
|
|
14
|
+
if (getHandContext(handedness) === undefined) {
|
|
15
|
+
const ctx = {
|
|
16
|
+
hand: handedness,
|
|
17
|
+
enabled: currentWritable(options?.enabled ?? true),
|
|
18
|
+
pointer: currentWritable(new Vector3()),
|
|
19
|
+
pointerOverTarget: currentWritable(false),
|
|
20
|
+
origin: new Vector3(),
|
|
21
|
+
originValid: false,
|
|
22
|
+
lastEvent: undefined,
|
|
23
|
+
initialClick: [0, 0, 0],
|
|
24
|
+
initialHits: [],
|
|
25
|
+
hovered: new Map(),
|
|
26
|
+
down: false,
|
|
27
|
+
joint: options?.joint ?? 'index-finger-tip',
|
|
28
|
+
hoverRadius: options?.hoverRadius ?? 0.03,
|
|
29
|
+
downRadius: options?.downRadius ?? 0.01,
|
|
30
|
+
compute: options?.compute ?? defaultComputeFunction,
|
|
31
|
+
filter: options?.filter
|
|
32
|
+
};
|
|
33
|
+
setHandContext(handedness, ctx);
|
|
34
|
+
setupTouchControls(context, ctx, options?.fixedStep);
|
|
35
|
+
}
|
|
36
|
+
const handContext = getHandContext(handedness);
|
|
37
|
+
return {
|
|
38
|
+
enabled: handContext.enabled,
|
|
39
|
+
hovered: handContext.hovered
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const injectTouchControlsPlugin: () => void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { injectPlugin, isInstanceOf } from '@threlte/core';
|
|
2
|
+
import { useTouchControls } from './hook.js';
|
|
3
|
+
import { events } from './types.js';
|
|
4
|
+
export const injectTouchControlsPlugin = () => {
|
|
5
|
+
injectPlugin('threlte-touch-controls', (args) => {
|
|
6
|
+
if (!isInstanceOf(args.ref, 'Object3D'))
|
|
7
|
+
return;
|
|
8
|
+
const { addInteractiveObject, removeInteractiveObject } = useTouchControls();
|
|
9
|
+
$effect.pre(() => {
|
|
10
|
+
const ref = args.ref;
|
|
11
|
+
const props = args.props;
|
|
12
|
+
const hasEventHandlers = events.some((eventName) => typeof props[eventName] === 'function');
|
|
13
|
+
if (!hasEventHandlers)
|
|
14
|
+
return;
|
|
15
|
+
addInteractiveObject(ref, props);
|
|
16
|
+
return () => {
|
|
17
|
+
removeInteractiveObject(ref);
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
pluginProps: events
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Matrix4, Mesh, Ray, Sphere, Vector3 } from 'three';
|
|
2
|
+
import { observe } from '@threlte/core';
|
|
3
|
+
import { fromStore } from 'svelte/store';
|
|
4
|
+
import { getInternalContext } from './context.js';
|
|
5
|
+
import { useFixed } from '../../internal/useFixed.js';
|
|
6
|
+
import { isPresenting } from '../../internal/state.svelte.js';
|
|
7
|
+
const getIntersectionId = (intersection) => {
|
|
8
|
+
const target = intersection.eventObject ?? intersection.object;
|
|
9
|
+
if (intersection.instanceId !== undefined) {
|
|
10
|
+
return `${target.uuid}|${intersection.instanceId}`;
|
|
11
|
+
}
|
|
12
|
+
if (intersection.object.isPoints) {
|
|
13
|
+
return `${target.uuid}|${intersection.index}`;
|
|
14
|
+
}
|
|
15
|
+
return target.uuid;
|
|
16
|
+
};
|
|
17
|
+
let nextPointerId = 2001;
|
|
18
|
+
const worldSphere = new Sphere();
|
|
19
|
+
const invMatrix = new Matrix4();
|
|
20
|
+
const localOrigin = new Vector3();
|
|
21
|
+
const localClamped = new Vector3();
|
|
22
|
+
const surfacePoint = new Vector3();
|
|
23
|
+
// The IntersectionEvent shape still carries `ray`, which isn't meaningful for
|
|
24
|
+
// touch. A fixed dummy ray keeps the type happy and matches the shape that the
|
|
25
|
+
// ray-based plugins provide.
|
|
26
|
+
const dummyRay = new Ray();
|
|
27
|
+
export const setupTouchControls = (context, handContext, fixedStep = 1 / 40) => {
|
|
28
|
+
const handedness = handContext.hand;
|
|
29
|
+
const pointerId = nextPointerId++;
|
|
30
|
+
const enabled = fromStore(handContext.enabled);
|
|
31
|
+
const { dispatchers } = getInternalContext();
|
|
32
|
+
let hits = [];
|
|
33
|
+
const pushHit = (raw, origin, reachSquared, object) => {
|
|
34
|
+
const mesh = object;
|
|
35
|
+
const geometry = mesh.geometry;
|
|
36
|
+
if (geometry === undefined)
|
|
37
|
+
return;
|
|
38
|
+
if (geometry.boundingSphere === null)
|
|
39
|
+
geometry.computeBoundingSphere();
|
|
40
|
+
if (geometry.boundingBox === null)
|
|
41
|
+
geometry.computeBoundingBox();
|
|
42
|
+
if (geometry.boundingSphere === null || geometry.boundingBox === null)
|
|
43
|
+
return;
|
|
44
|
+
mesh.updateWorldMatrix(true, false);
|
|
45
|
+
// Broad-phase: world-space bounding sphere reject.
|
|
46
|
+
worldSphere.copy(geometry.boundingSphere).applyMatrix4(mesh.matrixWorld);
|
|
47
|
+
const broad = handContext.hoverRadius + worldSphere.radius;
|
|
48
|
+
if (origin.distanceToSquared(worldSphere.center) > broad * broad)
|
|
49
|
+
return;
|
|
50
|
+
// Narrow-phase: closest point on the local-space AABB (so rotation /
|
|
51
|
+
// scale are handled exactly), projected back to world.
|
|
52
|
+
invMatrix.copy(mesh.matrixWorld).invert();
|
|
53
|
+
localOrigin.copy(origin).applyMatrix4(invMatrix);
|
|
54
|
+
geometry.boundingBox.clampPoint(localOrigin, localClamped);
|
|
55
|
+
surfacePoint.copy(localClamped).applyMatrix4(mesh.matrixWorld);
|
|
56
|
+
const distSq = origin.distanceToSquared(surfacePoint);
|
|
57
|
+
if (distSq > reachSquared)
|
|
58
|
+
return;
|
|
59
|
+
raw.push({
|
|
60
|
+
distance: Math.sqrt(distSq),
|
|
61
|
+
point: surfacePoint.clone(),
|
|
62
|
+
object: mesh,
|
|
63
|
+
eventObject: mesh,
|
|
64
|
+
face: null
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const collectHits = (raw, origin, reachSquared, object, seen) => {
|
|
68
|
+
if (seen.has(object.uuid))
|
|
69
|
+
return;
|
|
70
|
+
seen.add(object.uuid);
|
|
71
|
+
pushHit(raw, origin, reachSquared, object);
|
|
72
|
+
for (const child of object.children) {
|
|
73
|
+
collectHits(raw, origin, reachSquared, child, seen);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function cancelPointer(intersections) {
|
|
77
|
+
if (handContext.hovered.size === 0)
|
|
78
|
+
return;
|
|
79
|
+
const currentIds = new Set();
|
|
80
|
+
for (const hit of intersections) {
|
|
81
|
+
currentIds.add(getIntersectionId(hit));
|
|
82
|
+
}
|
|
83
|
+
const toRemove = [];
|
|
84
|
+
for (const [id, hoveredObj] of handContext.hovered) {
|
|
85
|
+
if (!currentIds.has(id)) {
|
|
86
|
+
toRemove.push([id, hoveredObj]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const [id, hoveredObj] of toRemove) {
|
|
90
|
+
const { eventObject } = hoveredObj;
|
|
91
|
+
handContext.hovered.delete(id);
|
|
92
|
+
const events = dispatchers.get(eventObject);
|
|
93
|
+
if (events !== undefined) {
|
|
94
|
+
const data = { ...hoveredObj, intersections };
|
|
95
|
+
events.onpointerout?.(data);
|
|
96
|
+
events.onpointerleave?.(data);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (handContext.hovered.size === 0) {
|
|
100
|
+
handContext.pointerOverTarget.set(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Unlike `pointerControls`, this plugin doesn't publish a per-hand
|
|
104
|
+
// `pointerIntersection` global. There's no on-screen cursor for touch — the
|
|
105
|
+
// tracked joint is the cursor — so nothing internal needs to subscribe to
|
|
106
|
+
// the closest hit. Consumers that want hover state from outside event
|
|
107
|
+
// handlers can read the returned `hovered` Map.
|
|
108
|
+
const getHits = () => {
|
|
109
|
+
if (!handContext.originValid)
|
|
110
|
+
return [];
|
|
111
|
+
const origin = handContext.origin;
|
|
112
|
+
const reach = handContext.hoverRadius;
|
|
113
|
+
const reachSquared = reach * reach;
|
|
114
|
+
const raw = [];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const obj of context.interactiveObjects) {
|
|
117
|
+
collectHits(raw, origin, reachSquared, obj, seen);
|
|
118
|
+
}
|
|
119
|
+
raw.sort((a, b) => a.distance - b.distance);
|
|
120
|
+
const filtered = handContext.filter === undefined ? raw : handContext.filter(raw, context, handContext);
|
|
121
|
+
const intersections = [];
|
|
122
|
+
for (const hit of filtered) {
|
|
123
|
+
let eventObject = hit.object;
|
|
124
|
+
while (eventObject) {
|
|
125
|
+
if (dispatchers.has(eventObject)) {
|
|
126
|
+
intersections.push({ ...hit, eventObject });
|
|
127
|
+
}
|
|
128
|
+
eventObject = eventObject.parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return intersections;
|
|
132
|
+
};
|
|
133
|
+
function pointerMissed(objects, event) {
|
|
134
|
+
for (const object of objects) {
|
|
135
|
+
dispatchers.get(object)?.onpointermissed?.(event);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const handleEvent = (name, event) => {
|
|
139
|
+
const isPointerMove = name === 'onpointermove';
|
|
140
|
+
const isClickEvent = name === 'onclick';
|
|
141
|
+
if (isClickEvent) {
|
|
142
|
+
pointerMissed(context.interactiveObjects.filter((object) => !handContext.initialHits.includes(object)), event);
|
|
143
|
+
}
|
|
144
|
+
if (isPointerMove)
|
|
145
|
+
cancelPointer(hits);
|
|
146
|
+
let stopped = false;
|
|
147
|
+
dispatchEvents: for (const hit of hits) {
|
|
148
|
+
const events = dispatchers.get(hit.eventObject);
|
|
149
|
+
if (events === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
const intersectionEvent = {
|
|
152
|
+
stopped,
|
|
153
|
+
...hit,
|
|
154
|
+
intersections: hits,
|
|
155
|
+
handedness,
|
|
156
|
+
pointerId,
|
|
157
|
+
stopPropagation() {
|
|
158
|
+
stopped = true;
|
|
159
|
+
intersectionEvent.stopped = true;
|
|
160
|
+
if (handContext.hovered.size > 0 &&
|
|
161
|
+
Array.from(handContext.hovered.values()).some((i) => i.eventObject === hit.eventObject)) {
|
|
162
|
+
const higher = hits.slice(0, hits.indexOf(hit));
|
|
163
|
+
cancelPointer([...higher, hit]);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
delta: 0,
|
|
167
|
+
nativeEvent: event,
|
|
168
|
+
pointer: handContext.pointer.current,
|
|
169
|
+
ray: dummyRay
|
|
170
|
+
};
|
|
171
|
+
if (isPointerMove) {
|
|
172
|
+
handContext.pointer.update((value) => value.copy(intersectionEvent.point));
|
|
173
|
+
if (events.onpointerover ||
|
|
174
|
+
events.onpointerenter ||
|
|
175
|
+
events.onpointerout ||
|
|
176
|
+
events.onpointerleave) {
|
|
177
|
+
const id = getIntersectionId(intersectionEvent);
|
|
178
|
+
const hoveredItem = handContext.hovered.get(id);
|
|
179
|
+
if (hoveredItem === undefined) {
|
|
180
|
+
handContext.hovered.set(id, intersectionEvent);
|
|
181
|
+
events.onpointerover?.(intersectionEvent);
|
|
182
|
+
events.onpointerenter?.(intersectionEvent);
|
|
183
|
+
handContext.pointerOverTarget.set(true);
|
|
184
|
+
}
|
|
185
|
+
else if (hoveredItem.stopped) {
|
|
186
|
+
intersectionEvent.stopPropagation();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
events.onpointermove?.(intersectionEvent);
|
|
190
|
+
}
|
|
191
|
+
else if (events[name] !== undefined) {
|
|
192
|
+
if (!isClickEvent || handContext.initialHits.includes(hit.eventObject)) {
|
|
193
|
+
events[name]?.(intersectionEvent);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (stopped)
|
|
197
|
+
break dispatchEvents;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
// Release-phase dispatch uses hits synthesized from `initialHits` (the
|
|
201
|
+
// objects that received pointerdown), so pointerup/click fire even if the
|
|
202
|
+
// finger has moved past the object — mirrors DOM pointer capture.
|
|
203
|
+
const buildCapturedHits = () => {
|
|
204
|
+
const [x, y, z] = handContext.initialClick;
|
|
205
|
+
return handContext.initialHits.map((object) => ({
|
|
206
|
+
distance: 0,
|
|
207
|
+
point: new Vector3(x, y, z),
|
|
208
|
+
object,
|
|
209
|
+
eventObject: object,
|
|
210
|
+
face: null
|
|
211
|
+
}));
|
|
212
|
+
};
|
|
213
|
+
const { start, stop } = useFixed(() => {
|
|
214
|
+
handContext.compute(context, handContext);
|
|
215
|
+
hits = getHits();
|
|
216
|
+
// Hover / move every tick — the joint moves continuously, so there is
|
|
217
|
+
// no "still pointer" optimization to make here.
|
|
218
|
+
handleEvent('onpointermove');
|
|
219
|
+
const closest = hits[0];
|
|
220
|
+
const shouldBeDown = closest !== undefined && closest.distance < handContext.downRadius;
|
|
221
|
+
if (shouldBeDown && !handContext.down) {
|
|
222
|
+
handContext.down = true;
|
|
223
|
+
handContext.initialClick = [closest.point.x, closest.point.y, closest.point.z];
|
|
224
|
+
handContext.initialHits = hits.map((h) => h.eventObject);
|
|
225
|
+
handleEvent('onpointerdown');
|
|
226
|
+
}
|
|
227
|
+
else if (!shouldBeDown && handContext.down) {
|
|
228
|
+
handContext.down = false;
|
|
229
|
+
const liveHits = hits;
|
|
230
|
+
hits = buildCapturedHits();
|
|
231
|
+
handleEvent('onpointerup');
|
|
232
|
+
handleEvent('onclick');
|
|
233
|
+
hits = liveHits;
|
|
234
|
+
}
|
|
235
|
+
}, {
|
|
236
|
+
fixedStep,
|
|
237
|
+
autoStart: false
|
|
238
|
+
});
|
|
239
|
+
observe.pre(() => [isPresenting.current, enabled.current], ([presenting, active]) => {
|
|
240
|
+
if (presenting && active) {
|
|
241
|
+
start();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
stop();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Intersection as ThreeIntersection, Object3D, Vector3, Ray, Event } from 'three';
|
|
2
|
+
import type { CurrentWritable } from '@threlte/core';
|
|
3
|
+
import type { ComputeFunction } from './compute.js';
|
|
4
|
+
import type { HandJoints } from '../../lib/handJoints.js';
|
|
5
|
+
export type Properties<T> = Pick<T, {
|
|
6
|
+
[K in keyof T]: T[K] extends (_: any) => any ? never : K;
|
|
7
|
+
}[keyof T]>;
|
|
8
|
+
export interface Intersection<T extends Object3D = Object3D> extends ThreeIntersection<T> {
|
|
9
|
+
eventObject: Object3D;
|
|
10
|
+
}
|
|
11
|
+
export interface IntersectionEvent extends Intersection {
|
|
12
|
+
eventObject: Object3D;
|
|
13
|
+
intersections: Intersection[];
|
|
14
|
+
handedness: 'left' | 'right';
|
|
15
|
+
pointerId: number;
|
|
16
|
+
pointer: Vector3;
|
|
17
|
+
delta: number;
|
|
18
|
+
ray: Ray;
|
|
19
|
+
stopPropagation: () => void;
|
|
20
|
+
nativeEvent: Event | undefined;
|
|
21
|
+
stopped: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type FilterFunction = (items: Intersection[], state: ControlsContext, handState: HandContext) => Intersection[];
|
|
24
|
+
export type ControlsContext = {
|
|
25
|
+
interactiveObjects: Object3D[];
|
|
26
|
+
};
|
|
27
|
+
export type HandContext = {
|
|
28
|
+
hand: 'left' | 'right';
|
|
29
|
+
enabled: CurrentWritable<boolean>;
|
|
30
|
+
pointer: CurrentWritable<Vector3>;
|
|
31
|
+
pointerOverTarget: CurrentWritable<boolean>;
|
|
32
|
+
/** World-space position of this hand's tracked joint, updated by `compute` each tick. */
|
|
33
|
+
origin: Vector3;
|
|
34
|
+
/** `false` when the joint isn't currently tracked. Skips intersection work and hides debug. */
|
|
35
|
+
originValid: boolean;
|
|
36
|
+
lastEvent: Event | undefined;
|
|
37
|
+
initialClick: [x: number, y: number, z: number];
|
|
38
|
+
initialHits: Object3D[];
|
|
39
|
+
hovered: Map<string, IntersectionEvent>;
|
|
40
|
+
/** Whether the joint is currently past the downRadius threshold. */
|
|
41
|
+
down: boolean;
|
|
42
|
+
/** Which joint to track for this hand. */
|
|
43
|
+
joint: HandJoints;
|
|
44
|
+
/** Distance at which an object starts receiving hover events for this hand. */
|
|
45
|
+
hoverRadius: number;
|
|
46
|
+
/** Distance at which hover transitions to pointerdown for this hand. */
|
|
47
|
+
downRadius: number;
|
|
48
|
+
compute: ComputeFunction;
|
|
49
|
+
filter?: FilterFunction | undefined;
|
|
50
|
+
};
|
|
51
|
+
export type ThrelteXREvents = {
|
|
52
|
+
onclick: IntersectionEvent;
|
|
53
|
+
onpointerup: IntersectionEvent;
|
|
54
|
+
onpointerdown: IntersectionEvent;
|
|
55
|
+
onpointerover: IntersectionEvent;
|
|
56
|
+
onpointerout: IntersectionEvent;
|
|
57
|
+
onpointerenter: IntersectionEvent;
|
|
58
|
+
onpointerleave: IntersectionEvent;
|
|
59
|
+
onpointermove: IntersectionEvent;
|
|
60
|
+
onpointermissed: IntersectionEvent;
|
|
61
|
+
};
|
|
62
|
+
export declare const events: (keyof ThrelteXREvents)[];
|
package/dist/types.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type XRHandEvent<Type = XRHandEventType> = Type extends 'connected' | 'di
|
|
|
37
37
|
} : Type extends 'pinchstart' | 'pinchend' ? {
|
|
38
38
|
type: Type;
|
|
39
39
|
handedness: 'left' | 'right';
|
|
40
|
-
target:
|
|
40
|
+
target: XRHandSpace;
|
|
41
41
|
} : never;
|
|
42
42
|
export type XRHandEventCallback<Type> = (event: XRHandEvent<Type>) => void;
|
|
43
43
|
export interface XRHandEvents {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threlte/xr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"author": "Micheal Parks <michealparks1989@gmail.com> (https://parks.lol)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Tools to more easily create VR and AR experiences with Threlte",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"typescript-eslint": "^8.32.0",
|
|
29
29
|
"vite": "^7.1.4",
|
|
30
30
|
"vite-plugin-mkcert": "^1.17.5",
|
|
31
|
-
"@threlte/
|
|
32
|
-
"@threlte/
|
|
31
|
+
"@threlte/extras": "9.15.0",
|
|
32
|
+
"@threlte/core": "8.5.10"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"svelte": ">=5",
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
|
|
2
|
-
import { useThrelte } from '@threlte/core';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
4
|
-
import { useHandTrackingState } from './useHandTrackingState.js';
|
|
5
|
-
import { controllers } from '../hooks/useController.svelte.js';
|
|
6
|
-
import { controllerEvents } from './state.svelte.js';
|
|
7
|
-
export const setupControllers = (factory) => {
|
|
8
|
-
const { xr } = useThrelte().renderer;
|
|
9
|
-
const hasHands = useHandTrackingState();
|
|
10
|
-
const targetRaySpaces = [xr.getController(0), xr.getController(1)];
|
|
11
|
-
const indexMap = new Map();
|
|
12
|
-
const modelFactory = factory ?? new XRControllerModelFactory();
|
|
13
|
-
targetRaySpaces.forEach((targetRay, index) => {
|
|
14
|
-
indexMap.set(targetRay, {
|
|
15
|
-
targetRay,
|
|
16
|
-
grip: xr.getControllerGrip(index),
|
|
17
|
-
model: modelFactory.createControllerModel(targetRay)
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
onMount(() => {
|
|
21
|
-
const dispatch = (event) => {
|
|
22
|
-
if (hasHands())
|
|
23
|
-
return;
|
|
24
|
-
const { data } = event;
|
|
25
|
-
controllerEvents[data.handedness]?.[`on${event.type}`]?.(event);
|
|
26
|
-
};
|
|
27
|
-
function handleConnected(event) {
|
|
28
|
-
const { data: inputSource } = event;
|
|
29
|
-
// The targetRaySpace 'connected' event fires for both controller and
|
|
30
|
-
// hand-tracking input sources. The controllers slot represents a physical
|
|
31
|
-
// controller — setupHands handles the hand-tracking side.
|
|
32
|
-
if (inputSource.hand)
|
|
33
|
-
return;
|
|
34
|
-
const { model, targetRay, grip } = indexMap.get(this);
|
|
35
|
-
controllers[event.data.handedness] = {
|
|
36
|
-
inputSource,
|
|
37
|
-
targetRay,
|
|
38
|
-
grip,
|
|
39
|
-
model
|
|
40
|
-
};
|
|
41
|
-
dispatch(event);
|
|
42
|
-
}
|
|
43
|
-
const handleDisconnected = (event) => {
|
|
44
|
-
dispatch(event);
|
|
45
|
-
controllers[event.data.handedness] = undefined;
|
|
46
|
-
};
|
|
47
|
-
for (const targetRay of targetRaySpaces) {
|
|
48
|
-
targetRay.addEventListener('connected', handleConnected);
|
|
49
|
-
targetRay.addEventListener('disconnected', handleDisconnected);
|
|
50
|
-
targetRay.addEventListener('select', dispatch);
|
|
51
|
-
targetRay.addEventListener('selectstart', dispatch);
|
|
52
|
-
targetRay.addEventListener('selectend', dispatch);
|
|
53
|
-
targetRay.addEventListener('squeeze', dispatch);
|
|
54
|
-
targetRay.addEventListener('squeezestart', dispatch);
|
|
55
|
-
targetRay.addEventListener('squeezeend', dispatch);
|
|
56
|
-
}
|
|
57
|
-
return () => {
|
|
58
|
-
for (const targetRay of targetRaySpaces) {
|
|
59
|
-
targetRay.removeEventListener('connected', handleConnected);
|
|
60
|
-
targetRay.removeEventListener('disconnected', handleDisconnected);
|
|
61
|
-
targetRay.removeEventListener('select', dispatch);
|
|
62
|
-
targetRay.removeEventListener('selectstart', dispatch);
|
|
63
|
-
targetRay.removeEventListener('selectend', dispatch);
|
|
64
|
-
targetRay.removeEventListener('squeeze', dispatch);
|
|
65
|
-
targetRay.removeEventListener('squeezestart', dispatch);
|
|
66
|
-
targetRay.removeEventListener('squeezeend', dispatch);
|
|
67
|
-
}
|
|
68
|
-
controllers.left = undefined;
|
|
69
|
-
controllers.right = undefined;
|
|
70
|
-
controllers.none = undefined;
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
|
|
2
|
-
import { useThrelte } from '@threlte/core';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
4
|
-
import { hands } from '../hooks/useHand.svelte.js';
|
|
5
|
-
import { useHandTrackingState } from './useHandTrackingState.js';
|
|
6
|
-
import { handEvents } from './state.svelte.js';
|
|
7
|
-
export const setupHands = (factory) => {
|
|
8
|
-
const { xr } = useThrelte().renderer;
|
|
9
|
-
const hasHands = useHandTrackingState();
|
|
10
|
-
const handSpaces = [xr.getHand(0), xr.getHand(1)];
|
|
11
|
-
const map = new Map();
|
|
12
|
-
const modelFactory = factory ?? new XRHandModelFactory();
|
|
13
|
-
handSpaces.forEach((handSpace, index) => {
|
|
14
|
-
map.set(handSpace, {
|
|
15
|
-
hand: handSpace,
|
|
16
|
-
targetRay: xr.getController(index),
|
|
17
|
-
model: modelFactory.createHandModel(handSpace, 'mesh')
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
onMount(() => {
|
|
21
|
-
const dispatch = (event) => {
|
|
22
|
-
if (!hasHands())
|
|
23
|
-
return;
|
|
24
|
-
const handEvent = event;
|
|
25
|
-
const handedness = 'handedness' in handEvent ? handEvent.handedness : handEvent.data.handedness;
|
|
26
|
-
handEvents[handedness]?.[`on${event.type}`]?.(event);
|
|
27
|
-
};
|
|
28
|
-
function handleConnected(event) {
|
|
29
|
-
const { model, targetRay } = map.get(this);
|
|
30
|
-
const { data } = event;
|
|
31
|
-
const { handedness, hand: inputSource } = data;
|
|
32
|
-
if (handedness === 'none' || inputSource === undefined) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
hands[handedness] = {
|
|
36
|
-
hand: this,
|
|
37
|
-
model,
|
|
38
|
-
inputSource,
|
|
39
|
-
targetRay
|
|
40
|
-
};
|
|
41
|
-
dispatch(event);
|
|
42
|
-
}
|
|
43
|
-
const handleDisconnected = (event) => {
|
|
44
|
-
dispatch(event);
|
|
45
|
-
const { handedness } = event.data;
|
|
46
|
-
if (handedness === 'left' || handedness === 'right') {
|
|
47
|
-
hands[handedness] = undefined;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
for (const handSpace of handSpaces) {
|
|
51
|
-
handSpace.addEventListener('connected', handleConnected);
|
|
52
|
-
handSpace.addEventListener('disconnected', handleDisconnected);
|
|
53
|
-
handSpace.addEventListener('pinchstart', dispatch);
|
|
54
|
-
handSpace.addEventListener('pinchend', dispatch);
|
|
55
|
-
}
|
|
56
|
-
return () => {
|
|
57
|
-
for (const handSpace of handSpaces) {
|
|
58
|
-
handSpace.removeEventListener('connected', handleConnected);
|
|
59
|
-
handSpace.removeEventListener('disconnected', handleDisconnected);
|
|
60
|
-
handSpace.removeEventListener('pinchstart', dispatch);
|
|
61
|
-
handSpace.removeEventListener('pinchend', dispatch);
|
|
62
|
-
}
|
|
63
|
-
hands.left = undefined;
|
|
64
|
-
hands.right = undefined;
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useThrelte } from '@threlte/core';
|
|
2
|
-
/**
|
|
3
|
-
* There are some cases where we need to know if hand tracking is now active before an input source
|
|
4
|
-
* connection or disconnection event. This is the way to do that.
|
|
5
|
-
*/
|
|
6
|
-
export const useHandTrackingState = () => {
|
|
7
|
-
const { renderer } = useThrelte();
|
|
8
|
-
return () => {
|
|
9
|
-
const sources = renderer.xr.getSession()?.inputSources;
|
|
10
|
-
if (sources === undefined) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
for (const source of sources) {
|
|
14
|
-
if (source.hand !== undefined) {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
};
|
|
20
|
-
};
|