@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.
Files changed (68) hide show
  1. package/dist/components/Controller.svelte +59 -60
  2. package/dist/components/Hand.svelte +21 -29
  3. package/dist/components/XR.svelte +146 -16
  4. package/dist/components/XR.svelte.d.ts +20 -0
  5. package/dist/components/XROrigin.svelte +82 -0
  6. package/dist/components/XROrigin.svelte.d.ts +33 -0
  7. package/dist/components/internal/TeleportRay.svelte +15 -3
  8. package/dist/hooks/currentReadable.svelte.d.ts +28 -1
  9. package/dist/hooks/currentReadable.svelte.js +36 -9
  10. package/dist/hooks/useController.svelte.d.ts +3 -3
  11. package/dist/hooks/useController.svelte.js +30 -7
  12. package/dist/hooks/useHand.svelte.d.ts +2 -2
  13. package/dist/hooks/useHand.svelte.js +26 -5
  14. package/dist/hooks/useHandJoint.svelte.js +6 -5
  15. package/dist/hooks/useHitTest.svelte.js +56 -12
  16. package/dist/hooks/useTeleport.d.ts +11 -9
  17. package/dist/hooks/useTeleport.js +62 -14
  18. package/dist/hooks/useXR.js +5 -5
  19. package/dist/hooks/useXROrigin.svelte.d.ts +10 -0
  20. package/dist/hooks/useXROrigin.svelte.js +11 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +3 -0
  23. package/dist/internal/inputSources.svelte.d.ts +84 -0
  24. package/dist/internal/inputSources.svelte.js +91 -0
  25. package/dist/internal/setupHeadset.svelte.js +18 -6
  26. package/dist/internal/setupInputSources.d.ts +4 -0
  27. package/dist/internal/setupInputSources.js +319 -0
  28. package/dist/internal/state.svelte.d.ts +10 -12
  29. package/dist/internal/state.svelte.js +9 -3
  30. package/dist/lib/getXRSessionOptions.d.ts +1 -1
  31. package/dist/lib/getXRSessionOptions.js +8 -7
  32. package/dist/lib/toggleXRSession.d.ts +1 -1
  33. package/dist/lib/toggleXRSession.js +20 -5
  34. package/dist/plugins/pointerControls/compute.js +14 -9
  35. package/dist/plugins/pointerControls/context.d.ts +3 -3
  36. package/dist/plugins/pointerControls/context.js +12 -6
  37. package/dist/plugins/pointerControls/index.d.ts +4 -3
  38. package/dist/plugins/pointerControls/index.js +63 -29
  39. package/dist/plugins/pointerControls/setup.svelte.js +64 -44
  40. package/dist/plugins/pointerControls/types.d.ts +14 -3
  41. package/dist/plugins/teleportControls/compute.d.ts +1 -1
  42. package/dist/plugins/teleportControls/compute.js +11 -8
  43. package/dist/plugins/teleportControls/context.d.ts +4 -4
  44. package/dist/plugins/teleportControls/context.js +1 -4
  45. package/dist/plugins/teleportControls/index.js +7 -4
  46. package/dist/plugins/teleportControls/setup.svelte.js +10 -9
  47. package/dist/plugins/touchControls/compute.d.ts +3 -0
  48. package/dist/plugins/touchControls/compute.js +13 -0
  49. package/dist/plugins/touchControls/context.d.ts +12 -0
  50. package/dist/plugins/touchControls/context.js +27 -0
  51. package/dist/plugins/touchControls/hook.d.ts +5 -0
  52. package/dist/plugins/touchControls/hook.js +26 -0
  53. package/dist/plugins/touchControls/index.d.ts +33 -0
  54. package/dist/plugins/touchControls/index.js +41 -0
  55. package/dist/plugins/touchControls/plugin.svelte.d.ts +1 -0
  56. package/dist/plugins/touchControls/plugin.svelte.js +24 -0
  57. package/dist/plugins/touchControls/setup.svelte.d.ts +2 -0
  58. package/dist/plugins/touchControls/setup.svelte.js +247 -0
  59. package/dist/plugins/touchControls/types.d.ts +62 -0
  60. package/dist/plugins/touchControls/types.js +11 -0
  61. package/dist/types.d.ts +1 -1
  62. package/package.json +3 -3
  63. package/dist/internal/setupControllers.d.ts +0 -2
  64. package/dist/internal/setupControllers.js +0 -73
  65. package/dist/internal/setupHands.d.ts +0 -2
  66. package/dist/internal/setupHands.js +0 -67
  67. package/dist/internal/useHandTrackingState.d.ts +0 -5
  68. 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,2 @@
1
+ import type { ControlsContext, HandContext } from './types.js';
2
+ export declare const setupTouchControls: (context: ControlsContext, handContext: HandContext, fixedStep?: number) => void;
@@ -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)[];
@@ -0,0 +1,11 @@
1
+ export const events = [
2
+ 'onclick',
3
+ 'onpointerup',
4
+ 'onpointerdown',
5
+ 'onpointerover',
6
+ 'onpointerout',
7
+ 'onpointerenter',
8
+ 'onpointerleave',
9
+ 'onpointermove',
10
+ 'onpointermissed'
11
+ ];
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: null;
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.5.5",
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/core": "8.5.9",
32
- "@threlte/extras": "9.14.9"
31
+ "@threlte/extras": "9.15.0",
32
+ "@threlte/core": "8.5.10"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "svelte": ">=5",
@@ -1,2 +0,0 @@
1
- import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
2
- export declare const setupControllers: (factory?: XRControllerModelFactory) => void;
@@ -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,2 +0,0 @@
1
- import { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
2
- export declare const setupHands: (factory?: XRHandModelFactory) => void;
@@ -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,5 +0,0 @@
1
- /**
2
- * There are some cases where we need to know if hand tracking is now active before an input source
3
- * connection or disconnection event. This is the way to do that.
4
- */
5
- export declare const useHandTrackingState: () => () => boolean;
@@ -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
- };