@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
@@ -1,14 +1,20 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  const handContextKeys = {
3
- left: Symbol('pointer-controls-context-left'),
4
- right: Symbol('pointer-controls-context-right')
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: import("@threlte/core").CurrentWritable<boolean>;
26
- hovered: Map<string, import("./types.js").IntersectionEvent>;
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
- if (getHandContext(handedness) === undefined) {
21
- const enabled = options?.enabled ?? true;
22
- const ctx = {
23
- hand: handedness,
24
- enabled: currentWritable(enabled),
25
- pointer: currentWritable(new Vector3()),
26
- pointerOverTarget: currentWritable(false),
27
- lastEvent: undefined,
28
- initialClick: [0, 0, 0],
29
- initialHits: [],
30
- hovered: new Map()
31
- };
32
- setHandContext(handedness, ctx);
33
- setupPointerControls(context, ctx, options?.fixedStep);
34
- }
35
- const handContext = getHandContext(handedness);
36
- observe.pre(() => [handContext.enabled], ([enabled]) => {
37
- pointerState[handedness].enabled = enabled;
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
- observe.pre(() => [handContext.pointerOverTarget], ([hovering]) => {
40
- pointerState[handedness].hovering = hovering;
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: handContext.enabled,
44
- hovered: handContext.hovered
77
+ enabled,
78
+ hovered
45
79
  };
46
80
  };
@@ -1,10 +1,9 @@
1
1
  import { Vector3 } from 'three';
2
- import { observe } from '@threlte/core';
2
+ import { fromStore } from 'svelte/store';
3
3
  import { getInternalContext } from './context.js';
4
- import { controllers } from '../../hooks/useController.svelte.js';
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, pointerIntersection } from '../../internal/state.svelte.js';
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 controller = $derived(controllers[handedness]);
26
- const hand = useHand(handedness);
28
+ const pointerId = nextPointerId++;
29
+ const enabled = fromStore(handContext.enabled);
27
30
  const { dispatchers } = getInternalContext();
28
31
  let hits = [];
29
- const lastPosition = new Vector3();
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 = context.raycaster.intersectObjects(context.interactiveObjects, true);
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 = context.filter === undefined ? hits : context.filter(hits, context, handContext);
96
- pointerIntersection[handedness] = filtered[0];
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)?.pointermissed?.(event);
118
+ dispatchers.get(object)?.onpointermissed?.(event);
113
119
  }
114
120
  }
115
121
  function processHits() {
116
- context.compute(context, handContext);
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: context.raycaster.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 targetRay = controller?.targetRay;
194
- if (targetRay === undefined)
195
- return;
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
- lastPosition.copy(targetRay.position);
206
+ lastRayOrigin.copy(ray.origin);
207
+ lastRayDirection.copy(ray.direction);
200
208
  }, {
201
209
  fixedStep,
202
210
  autoStart: false
203
211
  });
204
- observe.pre(() => [controller, handContext.enabled], ([controller, $enabled]) => {
205
- if (controller === undefined || !$enabled)
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: (context: Context, handContext: HandContext) => void;
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 = (context, handContext) => {
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
- // `<Controller>` attaches targetRay to the scene root so local === world;
9
- // we can read `.position`/`.quaternion` directly without a matrixWorld
10
- // roundtrip (which would force-recompose the matrix three.js writes from
11
- // the XR pose and introduce a drift against the visible render).
12
- forward.set(0, 0, -1).applyQuaternion(targetRay.quaternion);
13
- context.raycaster.set(targetRay.position, forward);
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: (compute: TeleportControlsOptions["compute"]) => Context;
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 = (compute) => {
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(options?.compute);
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 { observe } from '@threlte/core';
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 controller = $derived(controllers[handedness]);
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
- context.compute(context, handContext);
34
- const [intersect] = context.raycaster.intersectObjects(context.interactiveObjects, true);
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 (intersect !== undefined && context.blockers.has(intersect.object.uuid)) {
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
- observe.pre(() => [isPresenting.current, handContext.enabled], ([isPresenting, $enabled]) => {
54
- if (isPresenting && $enabled) {
54
+ $effect.pre(() => {
55
+ if (isPresenting.current && enabled.current) {
55
56
  start();
56
57
  }
57
58
  else {
@@ -0,0 +1,3 @@
1
+ import type { ControlsContext, HandContext } from './types.js';
2
+ export type ComputeFunction = (context: ControlsContext, handContext: HandContext) => void;
3
+ export declare const defaultComputeFunction: ComputeFunction;
@@ -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
+ };
@@ -0,0 +1,5 @@
1
+ import type { Object3D } from 'three';
2
+ export declare const useTouchControls: () => {
3
+ addInteractiveObject: (object: Object3D, events: Record<string, (arg: unknown) => void>) => void;
4
+ removeInteractiveObject: (object: Object3D) => void;
5
+ };