@threlte/xr 1.5.4 → 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 -57
- package/dist/components/Hand.svelte +24 -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/Cursor.svelte +5 -10
- package/dist/components/internal/PointerCursor.svelte +18 -4
- package/dist/components/internal/TeleportCursor.svelte +4 -1
- 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 +8 -6
- 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 +22 -7
- package/dist/plugins/pointerControls/compute.js +14 -5
- 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 -31
- package/dist/plugins/pointerControls/plugin.svelte.js +0 -5
- package/dist/plugins/pointerControls/setup.svelte.js +92 -78
- package/dist/plugins/pointerControls/types.d.ts +16 -3
- package/dist/plugins/pointerControls/types.js +2 -1
- package/dist/plugins/teleportControls/compute.d.ts +1 -1
- package/dist/plugins/teleportControls/compute.js +11 -4
- package/dist/plugins/teleportControls/context.d.ts +4 -4
- package/dist/plugins/teleportControls/context.js +1 -4
- package/dist/plugins/teleportControls/index.js +8 -8
- 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 -2
- package/dist/internal/setupControllers.d.ts +0 -2
- package/dist/internal/setupControllers.js +0 -68
- 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,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lastSessionRequest, referenceSpaceType, session, xr } from '../internal/state.svelte.js';
|
|
2
2
|
import { getXRSessionOptions } from './getXRSessionOptions.js';
|
|
3
|
+
let pending;
|
|
3
4
|
/**
|
|
4
5
|
* Starts / ends an XR session.
|
|
5
6
|
*
|
|
@@ -8,26 +9,40 @@ import { getXRSessionOptions } from './getXRSessionOptions.js';
|
|
|
8
9
|
* @param force Whether this button should only enter / exit an `XRSession`. Default is to toggle both ways
|
|
9
10
|
* @returns
|
|
10
11
|
*/
|
|
11
|
-
export const toggleXRSession =
|
|
12
|
+
export const toggleXRSession = (sessionMode, sessionInit, force) => {
|
|
13
|
+
if (pending !== undefined)
|
|
14
|
+
return pending;
|
|
15
|
+
pending = run(sessionMode, sessionInit, force).finally(() => {
|
|
16
|
+
pending = undefined;
|
|
17
|
+
});
|
|
18
|
+
return pending;
|
|
19
|
+
};
|
|
20
|
+
const run = async (sessionMode, sessionInit, force) => {
|
|
12
21
|
const currentSession = session.current;
|
|
13
22
|
const hasSession = currentSession !== undefined;
|
|
14
23
|
if (force === 'enter' && hasSession)
|
|
15
24
|
return currentSession;
|
|
16
25
|
if (force === 'exit' && !hasSession)
|
|
17
26
|
return;
|
|
18
|
-
// Exit a session if entered
|
|
27
|
+
// Exit a session if entered. `session.current` is cleared by XR.svelte's
|
|
28
|
+
// `handleSessionEnd` when the 'end' event fires — don't duplicate that here.
|
|
19
29
|
if (hasSession) {
|
|
20
30
|
await currentSession.end();
|
|
21
|
-
session.current = undefined;
|
|
22
31
|
return;
|
|
23
32
|
}
|
|
24
|
-
|
|
33
|
+
const manager = xr.current;
|
|
34
|
+
if (manager === undefined) {
|
|
25
35
|
throw new Error('An <XR> component was not created when attempting to toggle a session.');
|
|
26
36
|
}
|
|
27
37
|
// Otherwise enter a session
|
|
28
38
|
const options = getXRSessionOptions(referenceSpaceType.current, sessionInit);
|
|
29
|
-
const nextSession = await navigator.xr
|
|
30
|
-
|
|
39
|
+
const nextSession = await navigator.xr?.requestSession(sessionMode, options);
|
|
40
|
+
if (nextSession === undefined) {
|
|
41
|
+
throw new Error('A session was not able to be created.');
|
|
42
|
+
}
|
|
43
|
+
await manager.setSession(nextSession);
|
|
44
|
+
lastSessionRequest.mode = sessionMode;
|
|
45
|
+
lastSessionRequest.sessionInit = sessionInit;
|
|
31
46
|
session.current = nextSession;
|
|
32
47
|
return nextSession;
|
|
33
48
|
};
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { Vector3 } from 'three';
|
|
2
|
-
import {
|
|
2
|
+
import { getControllerState, getHandState } from '../../internal/inputSources.svelte.js';
|
|
3
|
+
const origin = new Vector3();
|
|
3
4
|
const forward = new Vector3();
|
|
4
|
-
export const defaultComputeFunction = (
|
|
5
|
-
const
|
|
5
|
+
export const defaultComputeFunction = (_context, handContext) => {
|
|
6
|
+
const state = handContext.sourceType === 'controller'
|
|
7
|
+
? getControllerState(handContext.hand)
|
|
8
|
+
: getHandState(handContext.hand);
|
|
9
|
+
const targetRay = state?.targetRay;
|
|
6
10
|
if (targetRay === undefined)
|
|
7
11
|
return;
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
// Read origin/direction from matrixWorld so the ray is in real world space,
|
|
13
|
+
// even when an ancestor (e.g. <XROrigin>) has a non-identity transform.
|
|
14
|
+
// Force an update because this runs before the frame's scene.updateMatrixWorld.
|
|
15
|
+
targetRay.updateWorldMatrix(true, false);
|
|
16
|
+
origin.setFromMatrixPosition(targetRay.matrixWorld);
|
|
17
|
+
forward.set(0, 0, -1).transformDirection(targetRay.matrixWorld);
|
|
18
|
+
handContext.raycaster.set(origin, forward);
|
|
10
19
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Object3D } from 'three';
|
|
2
|
-
import type { ControlsContext, HandContext } from './types.js';
|
|
3
|
-
export declare const getHandContext: (hand: "left" | "right") => HandContext;
|
|
4
|
-
export declare const setHandContext: (hand: "left" | "right", context: HandContext) => void;
|
|
2
|
+
import type { ControlsContext, HandContext, PointerSourceType } from './types.js';
|
|
3
|
+
export declare const getHandContext: (hand: "left" | "right", sourceType: PointerSourceType) => HandContext;
|
|
4
|
+
export declare const setHandContext: (hand: "left" | "right", sourceType: PointerSourceType, context: HandContext) => void;
|
|
5
5
|
export declare const getControlsContext: () => ControlsContext;
|
|
6
6
|
export declare const setControlsContext: (context: ControlsContext) => void;
|
|
7
7
|
interface InternalContext {
|
|
@@ -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,45 +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';
|
|
8
|
-
|
|
7
|
+
import { pointerIntersection, pointerState } from '../../internal/state.svelte.js';
|
|
8
|
+
const aggregateStates = new Map();
|
|
9
9
|
export const pointerControls = (handedness, options) => {
|
|
10
10
|
if (getControlsContext() === undefined) {
|
|
11
11
|
injectPointerControlsPlugin();
|
|
12
12
|
setInternalContext();
|
|
13
|
-
setControlsContext({
|
|
14
|
-
interactiveObjects: [],
|
|
15
|
-
raycaster: new Raycaster(),
|
|
16
|
-
compute: options?.compute ?? defaultComputeFunction,
|
|
17
|
-
filter: options?.filter
|
|
18
|
-
});
|
|
13
|
+
setControlsContext({ interactiveObjects: [] });
|
|
19
14
|
}
|
|
20
15
|
const context = getControlsContext();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
40
57
|
});
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
|
43
74
|
});
|
|
75
|
+
syncSharedState();
|
|
44
76
|
return {
|
|
45
|
-
enabled
|
|
46
|
-
hovered
|
|
77
|
+
enabled,
|
|
78
|
+
hovered
|
|
47
79
|
};
|
|
48
80
|
};
|
|
@@ -5,11 +5,6 @@ export const injectPointerControlsPlugin = () => {
|
|
|
5
5
|
injectPlugin('threlte-pointer-controls', (args) => {
|
|
6
6
|
if (!isInstanceOf(args.ref, 'Object3D'))
|
|
7
7
|
return;
|
|
8
|
-
const hasEventHandlers = Object.entries(args.props).some(([key, value]) => {
|
|
9
|
-
return value !== undefined && events.includes(key);
|
|
10
|
-
});
|
|
11
|
-
if (!hasEventHandlers)
|
|
12
|
-
return;
|
|
13
8
|
const { addInteractiveObject, removeInteractiveObject } = usePointerControls();
|
|
14
9
|
$effect.pre(() => {
|
|
15
10
|
const ref = args.ref;
|
|
@@ -1,21 +1,36 @@
|
|
|
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';
|
|
7
|
+
// Hover identity must match the dedup key used in `getHits`, otherwise the ID
|
|
8
|
+
// changes mid-hover (e.g. the hit's face index changes as the ray sweeps a
|
|
9
|
+
// plain mesh) and the object flickers between pointerout/pointerenter every
|
|
10
|
+
// frame.
|
|
8
11
|
const getIntersectionId = (intersection) => {
|
|
9
|
-
|
|
12
|
+
const target = intersection.eventObject ?? intersection.object;
|
|
13
|
+
if (intersection.instanceId !== undefined) {
|
|
14
|
+
return `${target.uuid}|${intersection.instanceId}`;
|
|
15
|
+
}
|
|
16
|
+
if (intersection.object.isPoints) {
|
|
17
|
+
return `${target.uuid}|${intersection.index}`;
|
|
18
|
+
}
|
|
19
|
+
return target.uuid;
|
|
10
20
|
};
|
|
11
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;
|
|
12
26
|
export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) => {
|
|
13
27
|
const handedness = handContext.hand;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
28
|
+
const pointerId = nextPointerId++;
|
|
29
|
+
const enabled = fromStore(handContext.enabled);
|
|
16
30
|
const { dispatchers } = getInternalContext();
|
|
17
31
|
let hits = [];
|
|
18
|
-
const
|
|
32
|
+
const lastRayOrigin = new Vector3();
|
|
33
|
+
const lastRayDirection = new Vector3();
|
|
19
34
|
const handlePointerDown = (event) => {
|
|
20
35
|
// Save initial coordinates on pointer-down
|
|
21
36
|
const [hit] = hits;
|
|
@@ -29,24 +44,18 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
29
44
|
handleEvent('onpointerup', event);
|
|
30
45
|
};
|
|
31
46
|
const handleClick = (event) => {
|
|
32
|
-
// If a click yields no results, pass it back to the user as a miss
|
|
33
|
-
// Missed events have to come first in order to establish user-land side-effect clean up
|
|
34
|
-
if (hits.length === 0) {
|
|
35
|
-
pointerMissed(context.interactiveObjects, event);
|
|
36
|
-
}
|
|
37
47
|
handleEvent('onclick', event);
|
|
38
48
|
};
|
|
39
49
|
function cancelPointer(intersections) {
|
|
40
50
|
if (handContext.hovered.size === 0)
|
|
41
51
|
return;
|
|
52
|
+
const currentIds = new Set();
|
|
53
|
+
for (const hit of intersections) {
|
|
54
|
+
currentIds.add(getIntersectionId(hit));
|
|
55
|
+
}
|
|
42
56
|
const toRemove = [];
|
|
43
57
|
for (const [id, hoveredObj] of handContext.hovered) {
|
|
44
|
-
|
|
45
|
-
// we call pointerout and delete the object from the hovered elements map
|
|
46
|
-
if (intersections.length === 0 ||
|
|
47
|
-
!intersections.some((hit) => hit.object === hoveredObj.object &&
|
|
48
|
-
hit.index === hoveredObj.index &&
|
|
49
|
-
hit.instanceId === hoveredObj.instanceId)) {
|
|
58
|
+
if (!currentIds.has(id)) {
|
|
50
59
|
toRemove.push([id, hoveredObj]);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
@@ -64,10 +73,11 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
64
73
|
if (handContext.hovered.size === 0) {
|
|
65
74
|
handContext.pointerOverTarget.set(false);
|
|
66
75
|
}
|
|
76
|
+
handContext.syncSharedState();
|
|
67
77
|
}
|
|
68
78
|
const getHits = () => {
|
|
69
79
|
const intersections = [];
|
|
70
|
-
const rawHits =
|
|
80
|
+
const rawHits = handContext.raycaster.intersectObjects(context.interactiveObjects, true);
|
|
71
81
|
const seen = new Set();
|
|
72
82
|
// Deduplicate hits by object. When recursive=true, intersectObjects searches
|
|
73
83
|
// each registered object's full subtree, so a child that is itself registered
|
|
@@ -87,8 +97,9 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
87
97
|
seen.add(key);
|
|
88
98
|
return true;
|
|
89
99
|
});
|
|
90
|
-
const filtered =
|
|
91
|
-
|
|
100
|
+
const filtered = handContext.filter === undefined ? hits : handContext.filter(hits, context, handContext);
|
|
101
|
+
handContext.currentIntersection = filtered[0];
|
|
102
|
+
handContext.syncSharedState();
|
|
92
103
|
// Bubble up the events, find the event source (eventObject)
|
|
93
104
|
for (const hit of filtered) {
|
|
94
105
|
let eventObject = hit.object;
|
|
@@ -104,17 +115,23 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
104
115
|
};
|
|
105
116
|
function pointerMissed(objects, event) {
|
|
106
117
|
for (const object of objects) {
|
|
107
|
-
dispatchers.get(object)?.
|
|
118
|
+
dispatchers.get(object)?.onpointermissed?.(event);
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
121
|
function processHits() {
|
|
111
|
-
|
|
122
|
+
handContext.compute(context, handContext);
|
|
112
123
|
return getHits();
|
|
113
124
|
}
|
|
114
125
|
const handleEvent = (name, event) => {
|
|
115
126
|
const isPointerMove = name === 'onpointermove';
|
|
116
127
|
const isClickEvent = name === 'onclick' || name === 'oncontextmenu';
|
|
117
|
-
//
|
|
128
|
+
// Fire pointermissed for objects that were not under the pointer at pointerdown.
|
|
129
|
+
// Must come before the dispatch loop so user-land cleanup runs first.
|
|
130
|
+
if (isClickEvent) {
|
|
131
|
+
pointerMissed(context.interactiveObjects.filter((object) => !handContext.initialHits.includes(object)), event);
|
|
132
|
+
}
|
|
133
|
+
// Update hover state before dispatch so that pointerout/pointerleave fire
|
|
134
|
+
// before pointerover/pointerenter on newly hit objects.
|
|
118
135
|
if (isPointerMove)
|
|
119
136
|
cancelPointer(hits);
|
|
120
137
|
let stopped = false;
|
|
@@ -127,6 +144,8 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
127
144
|
stopped,
|
|
128
145
|
...hit,
|
|
129
146
|
intersections: hits,
|
|
147
|
+
handedness,
|
|
148
|
+
pointerId,
|
|
130
149
|
stopPropagation() {
|
|
131
150
|
stopped = true;
|
|
132
151
|
intersectionEvent.stopped = true;
|
|
@@ -140,7 +159,7 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
140
159
|
delta: 0,
|
|
141
160
|
nativeEvent: event,
|
|
142
161
|
pointer: handContext.pointer.current,
|
|
143
|
-
ray:
|
|
162
|
+
ray: handContext.raycaster.ray
|
|
144
163
|
};
|
|
145
164
|
if (isPointerMove) {
|
|
146
165
|
// Move event ...
|
|
@@ -157,6 +176,7 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
157
176
|
events.onpointerover?.(intersectionEvent);
|
|
158
177
|
events.onpointerenter?.(intersectionEvent);
|
|
159
178
|
handContext.pointerOverTarget.set(true);
|
|
179
|
+
handContext.syncSharedState();
|
|
160
180
|
}
|
|
161
181
|
else if (hoveredItem.stopped) {
|
|
162
182
|
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
|
|
@@ -166,15 +186,11 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
166
186
|
// Call pointer move
|
|
167
187
|
events.onpointermove?.(intersectionEvent);
|
|
168
188
|
}
|
|
169
|
-
else if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
events[name]?.(intersectionEvent);
|
|
175
|
-
}
|
|
176
|
-
else if (isClickEvent && handContext.initialHits.includes(hit.eventObject)) {
|
|
177
|
-
pointerMissed(context.interactiveObjects.filter((object) => !handContext.initialHits.includes(object)), event);
|
|
189
|
+
else if (events[name] !== undefined) {
|
|
190
|
+
// All other events
|
|
191
|
+
if (!isClickEvent || handContext.initialHits.includes(hit.eventObject)) {
|
|
192
|
+
events[name]?.(intersectionEvent);
|
|
193
|
+
}
|
|
178
194
|
}
|
|
179
195
|
if (stopped)
|
|
180
196
|
break dispatchEvents;
|
|
@@ -182,61 +198,59 @@ export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) =
|
|
|
182
198
|
};
|
|
183
199
|
const { start, stop } = useFixed(() => {
|
|
184
200
|
hits = processHits();
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
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) {
|
|
189
204
|
handleEvent('onpointermove');
|
|
190
205
|
}
|
|
191
|
-
|
|
206
|
+
lastRayOrigin.copy(ray.origin);
|
|
207
|
+
lastRayDirection.copy(ray.direction);
|
|
192
208
|
}, {
|
|
193
209
|
fixedStep,
|
|
194
210
|
autoStart: false
|
|
195
211
|
});
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
const removeHandlers = () => {
|
|
200
|
-
controller.targetRay.removeEventListener('selectstart', handlePointerDown);
|
|
201
|
-
controller.targetRay.removeEventListener('selectend', handlePointerUp);
|
|
202
|
-
controller.targetRay.removeEventListener('select', handleClick);
|
|
203
|
-
};
|
|
204
|
-
if ($enabled) {
|
|
205
|
-
controller.targetRay.addEventListener('selectstart', handlePointerDown);
|
|
206
|
-
controller.targetRay.addEventListener('selectend', handlePointerUp);
|
|
207
|
-
controller.targetRay.addEventListener('select', handleClick);
|
|
208
|
-
return removeHandlers;
|
|
212
|
+
$effect.pre(() => {
|
|
213
|
+
if (isPresenting.current && enabled.current) {
|
|
214
|
+
start();
|
|
209
215
|
}
|
|
210
216
|
else {
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
stop();
|
|
218
|
+
hits = [];
|
|
219
|
+
handContext.currentIntersection = undefined;
|
|
220
|
+
cancelPointer([]);
|
|
221
|
+
handContext.syncSharedState();
|
|
213
222
|
}
|
|
214
223
|
});
|
|
215
|
-
|
|
216
|
-
if (
|
|
224
|
+
$effect.pre(() => {
|
|
225
|
+
if (handContext.sourceType !== 'controller')
|
|
217
226
|
return;
|
|
218
|
-
|
|
219
|
-
input.hand.removeEventListener('pinchstart', handlePointerDown);
|
|
220
|
-
input.hand.removeEventListener('pinchend', handlePointerUp);
|
|
221
|
-
input.hand.removeEventListener('pinchend', handleClick);
|
|
222
|
-
};
|
|
223
|
-
if (enabled) {
|
|
224
|
-
input.hand.addEventListener('pinchstart', handlePointerDown);
|
|
225
|
-
input.hand.addEventListener('pinchend', handlePointerUp);
|
|
226
|
-
input.hand.addEventListener('pinchend', handleClick);
|
|
227
|
-
return removeHandlers;
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
removeHandlers();
|
|
227
|
+
if (!enabled.current)
|
|
231
228
|
return;
|
|
232
|
-
|
|
229
|
+
return addSubscriber({
|
|
230
|
+
type: 'controller',
|
|
231
|
+
handedness,
|
|
232
|
+
callbacks: {
|
|
233
|
+
onselectstart: handlePointerDown,
|
|
234
|
+
onselectend: handlePointerUp,
|
|
235
|
+
onselect: handleClick
|
|
236
|
+
}
|
|
237
|
+
});
|
|
233
238
|
});
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
});
|
|
241
255
|
});
|
|
242
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]>;
|
|
@@ -13,6 +14,11 @@ export interface IntersectionEvent extends Intersection {
|
|
|
13
14
|
eventObject: Object3D;
|
|
14
15
|
/** An array of intersections */
|
|
15
16
|
intersections: Intersection[];
|
|
17
|
+
/** Which hand dispatched this event. Each controller/hand fires enter/leave/etc. independently. */
|
|
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;
|
|
16
22
|
/** Normalized event coordinates */
|
|
17
23
|
pointer: Vector3;
|
|
18
24
|
/** Delta between first click and this event */
|
|
@@ -29,12 +35,11 @@ export interface IntersectionEvent extends Intersection {
|
|
|
29
35
|
export type FilterFunction = (items: Intersection[], state: ControlsContext, handState: HandContext) => Intersection[];
|
|
30
36
|
export type ControlsContext = {
|
|
31
37
|
interactiveObjects: Object3D[];
|
|
32
|
-
raycaster: Raycaster;
|
|
33
|
-
compute: ComputeFunction;
|
|
34
|
-
filter?: FilterFunction | undefined;
|
|
35
38
|
};
|
|
36
39
|
export type HandContext = {
|
|
37
40
|
hand: 'left' | 'right';
|
|
41
|
+
/** Physical XR source this runtime tracks for the handedness. */
|
|
42
|
+
sourceType: PointerSourceType;
|
|
38
43
|
enabled: CurrentWritable<boolean>;
|
|
39
44
|
pointer: CurrentWritable<Vector3>;
|
|
40
45
|
pointerOverTarget: CurrentWritable<boolean>;
|
|
@@ -42,6 +47,14 @@ export type HandContext = {
|
|
|
42
47
|
initialClick: [x: number, y: number, z: number];
|
|
43
48
|
initialHits: Object3D[];
|
|
44
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;
|
|
45
58
|
};
|
|
46
59
|
export interface PointerCaptureTarget {
|
|
47
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,10 +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
|
+
// 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);
|
|
10
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;
|