@threlte/xr 0.0.11 → 0.0.12
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 +62 -13
- package/dist/components/Controller.svelte.d.ts +4 -0
- package/dist/components/Hand.svelte +11 -7
- package/dist/components/Headset.svelte +4 -7
- package/dist/components/internal/Cursor.svelte +51 -0
- package/dist/components/internal/Cursor.svelte.d.ts +19 -0
- package/dist/components/internal/PointerCursor.svelte +42 -0
- package/dist/components/internal/PointerCursor.svelte.d.ts +18 -0
- package/dist/components/internal/ScenePortal.svelte +10 -0
- package/dist/components/internal/ScenePortal.svelte.d.ts +16 -0
- package/dist/components/internal/ShortRay.svelte +45 -0
- package/dist/components/{ShortRay.svelte.d.ts → internal/ShortRay.svelte.d.ts} +4 -2
- package/dist/components/internal/TeleportCursor.svelte +47 -0
- package/dist/components/internal/TeleportCursor.svelte.d.ts +18 -0
- package/dist/components/internal/TeleportRay.svelte +71 -0
- package/dist/components/internal/TeleportRay.svelte.d.ts +20 -0
- package/dist/hooks/useController.d.ts +0 -4
- package/dist/hooks/useController.js +0 -11
- package/dist/index.d.ts +10 -3
- package/dist/index.js +13 -2
- package/dist/internal/headset.js +1 -1
- package/dist/internal/stores.d.ts +28 -2
- package/dist/internal/stores.js +28 -2
- package/dist/internal/useFixed.d.ts +11 -0
- package/dist/internal/useFixed.js +17 -0
- package/dist/plugins/pointerControls/compute.d.ts +3 -0
- package/dist/plugins/pointerControls/compute.js +14 -0
- package/dist/plugins/pointerControls/context.d.ts +12 -0
- package/dist/plugins/pointerControls/context.js +27 -0
- package/dist/plugins/pointerControls/hook.d.ts +5 -0
- package/dist/plugins/pointerControls/hook.js +24 -0
- package/dist/plugins/pointerControls/index.d.ts +27 -0
- package/dist/plugins/pointerControls/index.js +54 -0
- package/dist/plugins/pointerControls/plugin.d.ts +1 -0
- package/dist/plugins/pointerControls/plugin.js +28 -0
- package/dist/plugins/pointerControls/setup.d.ts +2 -0
- package/dist/plugins/pointerControls/setup.js +203 -0
- package/dist/plugins/pointerControls/types.d.ts +61 -0
- package/dist/plugins/pointerControls/types.js +11 -0
- package/dist/plugins/pointerControls/useComponentEventHandlers.d.ts +4 -0
- package/dist/plugins/pointerControls/useComponentEventHandlers.js +15 -0
- package/dist/plugins/teleportControls/compute.d.ts +3 -0
- package/dist/plugins/teleportControls/compute.js +14 -0
- package/dist/plugins/teleportControls/context.d.ts +20 -0
- package/dist/plugins/teleportControls/context.js +18 -0
- package/dist/plugins/teleportControls/hook.d.ts +6 -0
- package/dist/plugins/teleportControls/hook.js +40 -0
- package/dist/plugins/teleportControls/index.d.ts +19 -0
- package/dist/plugins/teleportControls/index.js +54 -0
- package/dist/plugins/teleportControls/plugin.d.ts +4 -0
- package/dist/plugins/teleportControls/plugin.js +54 -0
- package/dist/plugins/teleportControls/setup.d.ts +2 -0
- package/dist/plugins/teleportControls/setup.js +62 -0
- package/package.json +2 -2
- package/dist/components/Ray.svelte +0 -23
- package/dist/components/Ray.svelte.d.ts +0 -18
- package/dist/components/ShortRay.svelte +0 -32
- package/dist/components/TeleportControls.svelte +0 -136
- package/dist/components/TeleportControls.svelte.d.ts +0 -54
- package/dist/hooks/index.d.ts +0 -7
- package/dist/hooks/index.js +0 -7
- package/dist/plugins/teleportPlugin.d.ts +0 -1
- package/dist/plugins/teleportPlugin.js +0 -41
|
@@ -5,6 +5,32 @@ export declare const isPresenting: import("@threlte/core").CurrentWritable<boole
|
|
|
5
5
|
export declare const isHandTracking: import("@threlte/core").CurrentWritable<boolean>;
|
|
6
6
|
export declare const session: import("@threlte/core").CurrentWritable<XRSession | undefined>;
|
|
7
7
|
export declare const referenceSpaceType: import("@threlte/core").CurrentWritable<XRReferenceSpaceType | undefined>;
|
|
8
|
-
export declare const activeTeleportController: import("@threlte/core").CurrentWritable<import("three").XRTargetRaySpace | undefined>;
|
|
9
|
-
export declare const pendingTeleportDestination: import("@threlte/core").CurrentWritable<import("three").Vector3 | undefined>;
|
|
10
8
|
export declare const xr: import("@threlte/core").CurrentWritable<import("three").WebXRManager | undefined>;
|
|
9
|
+
export declare const teleportState: import("@threlte/core").CurrentWritable<{
|
|
10
|
+
left: {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
hovering: boolean;
|
|
13
|
+
};
|
|
14
|
+
right: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
hovering: boolean;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
export declare const teleportIntersection: {
|
|
20
|
+
left: import("@threlte/core").CurrentWritable<import("three").Intersection<import("three").Object3D<import("three").Event>> | undefined>;
|
|
21
|
+
right: import("@threlte/core").CurrentWritable<import("three").Intersection<import("three").Object3D<import("three").Event>> | undefined>;
|
|
22
|
+
};
|
|
23
|
+
export declare const pointerState: import("@threlte/core").CurrentWritable<{
|
|
24
|
+
left: {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
hovering: boolean;
|
|
27
|
+
};
|
|
28
|
+
right: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
hovering: boolean;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
export declare const pointerIntersection: {
|
|
34
|
+
left: import("@threlte/core").CurrentWritable<import("three").Intersection<import("three").Object3D<import("three").Event>> | undefined>;
|
|
35
|
+
right: import("@threlte/core").CurrentWritable<import("three").Intersection<import("three").Object3D<import("three").Event>> | undefined>;
|
|
36
|
+
};
|
package/dist/internal/stores.js
CHANGED
|
@@ -5,6 +5,32 @@ export const isPresenting = currentWritable(false);
|
|
|
5
5
|
export const isHandTracking = currentWritable(false);
|
|
6
6
|
export const session = currentWritable(undefined);
|
|
7
7
|
export const referenceSpaceType = currentWritable(undefined);
|
|
8
|
-
export const activeTeleportController = currentWritable(undefined);
|
|
9
|
-
export const pendingTeleportDestination = currentWritable(undefined);
|
|
10
8
|
export const xr = currentWritable(undefined);
|
|
9
|
+
export const teleportState = currentWritable({
|
|
10
|
+
left: {
|
|
11
|
+
enabled: false,
|
|
12
|
+
hovering: false,
|
|
13
|
+
},
|
|
14
|
+
right: {
|
|
15
|
+
enabled: false,
|
|
16
|
+
hovering: false,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
export const teleportIntersection = {
|
|
20
|
+
left: currentWritable(undefined),
|
|
21
|
+
right: currentWritable(undefined),
|
|
22
|
+
};
|
|
23
|
+
export const pointerState = currentWritable({
|
|
24
|
+
left: {
|
|
25
|
+
enabled: false,
|
|
26
|
+
hovering: false,
|
|
27
|
+
},
|
|
28
|
+
right: {
|
|
29
|
+
enabled: false,
|
|
30
|
+
hovering: false,
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
export const pointerIntersection = {
|
|
34
|
+
left: currentWritable(undefined),
|
|
35
|
+
right: currentWritable(undefined),
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ThrelteUseFrameOptions, type ThrelteContext } from '@threlte/core';
|
|
2
|
+
type UseFixedOptions = ThrelteUseFrameOptions & {
|
|
3
|
+
fixedStep?: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* A fixed useFrame, based on https://github.com/threlte/threlte/pull/654
|
|
7
|
+
*
|
|
8
|
+
* @Todo Can be removed if this or a similar feature is merged.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useFixed: (fn: (ctx: ThrelteContext, delta: number) => void, options: UseFixedOptions) => import("@threlte/core/dist/hooks/useFrame").ThrelteUseFrame;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useFrame } from '@threlte/core';
|
|
2
|
+
/**
|
|
3
|
+
* A fixed useFrame, based on https://github.com/threlte/threlte/pull/654
|
|
4
|
+
*
|
|
5
|
+
* @Todo Can be removed if this or a similar feature is merged.
|
|
6
|
+
*/
|
|
7
|
+
export const useFixed = (fn, options) => {
|
|
8
|
+
let fixedStepTimeAccumulator = 0;
|
|
9
|
+
let fixedStep = options.fixedStep ?? 1 / 60;
|
|
10
|
+
return useFrame((ctx, delta) => {
|
|
11
|
+
fixedStepTimeAccumulator += delta;
|
|
12
|
+
while (fixedStepTimeAccumulator >= fixedStep) {
|
|
13
|
+
fixedStepTimeAccumulator -= fixedStep;
|
|
14
|
+
fn(ctx, fixedStep);
|
|
15
|
+
}
|
|
16
|
+
}, options);
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Vector3 } from 'three';
|
|
2
|
+
import { useController } from '../../hooks/useController';
|
|
3
|
+
const controllers = {
|
|
4
|
+
left: useController('left'),
|
|
5
|
+
right: useController('right')
|
|
6
|
+
};
|
|
7
|
+
const forward = new Vector3();
|
|
8
|
+
export const defaultComputeFunction = (context, handContext) => {
|
|
9
|
+
const targetRay = controllers[handContext.hand].current?.targetRay;
|
|
10
|
+
if (targetRay === undefined)
|
|
11
|
+
return;
|
|
12
|
+
forward.set(0, 0, -1).applyQuaternion(targetRay.quaternion);
|
|
13
|
+
context.raycaster.set(targetRay.position, forward);
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { createRawEventDispatcher } from '@threlte/core';
|
|
2
|
+
import type { ControlsContext, HandContext, ThrelteXREvents } from './types';
|
|
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<THREE.Object3D, ReturnType<typeof createRawEventDispatcher<ThrelteXREvents>>>;
|
|
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('pointer-controls-context-left'),
|
|
4
|
+
right: Symbol('pointer-controls-context-right')
|
|
5
|
+
};
|
|
6
|
+
const contextKey = Symbol('pointer-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('pointer-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,24 @@
|
|
|
1
|
+
import { createRawEventDispatcher } from '@threlte/core';
|
|
2
|
+
import { getControlsContext, getInternalContext } from './context';
|
|
3
|
+
export const usePointerControls = () => {
|
|
4
|
+
const { dispatchers } = getInternalContext();
|
|
5
|
+
const context = getControlsContext();
|
|
6
|
+
const eventDispatcher = createRawEventDispatcher();
|
|
7
|
+
const addInteractiveObject = (object) => {
|
|
8
|
+
// check if the object is already in the list
|
|
9
|
+
if (context.interactiveObjects.indexOf(object) > -1) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
dispatchers.set(object, eventDispatcher);
|
|
13
|
+
context.interactiveObjects.push(object);
|
|
14
|
+
};
|
|
15
|
+
const removeInteractiveObject = (object) => {
|
|
16
|
+
const index = context.interactiveObjects.indexOf(object);
|
|
17
|
+
context.interactiveObjects.splice(index, 1);
|
|
18
|
+
dispatchers.delete(object);
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
addInteractiveObject,
|
|
22
|
+
removeInteractiveObject
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ComputeFunction } from './compute';
|
|
2
|
+
import type { FilterFunction } from './types';
|
|
3
|
+
export type PointerControlsOptions = {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The compute function is responsible for updating the state of the pointerControls plugin.
|
|
7
|
+
* It needs to set up the raycaster and the pointer vector. If no compute function is provided,
|
|
8
|
+
* the plugin will use the default compute function.
|
|
9
|
+
*/
|
|
10
|
+
compute?: ComputeFunction;
|
|
11
|
+
/**
|
|
12
|
+
* The filter function is responsible for filtering and sorting the
|
|
13
|
+
* intersections. By default, the intersections are sorted by distance. If no
|
|
14
|
+
* filter function is provided, the plugin will use the default filter function.
|
|
15
|
+
*/
|
|
16
|
+
filter?: FilterFunction;
|
|
17
|
+
/**
|
|
18
|
+
* Sets the interval at which raycasting occurs.
|
|
19
|
+
*
|
|
20
|
+
* @default 1 / 40
|
|
21
|
+
*/
|
|
22
|
+
fixedStep?: number;
|
|
23
|
+
};
|
|
24
|
+
export declare const pointerControls: (handedness: 'left' | 'right', options?: PointerControlsOptions) => {
|
|
25
|
+
enabled: import("@threlte/core").CurrentWritable<boolean>;
|
|
26
|
+
hovered: Map<string, import("./types").IntersectionEvent>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Raycaster, Vector3 } from 'three';
|
|
2
|
+
import { currentWritable, watch } from '@threlte/core';
|
|
3
|
+
import { defaultComputeFunction } from './compute';
|
|
4
|
+
import { injectPointerControlsPlugin } from './plugin';
|
|
5
|
+
import { setupPointerControls } from './setup';
|
|
6
|
+
import { getControlsContext, getHandContext, setControlsContext, setHandContext, setInternalContext } from './context';
|
|
7
|
+
import { pointerState } from '../../internal/stores';
|
|
8
|
+
let controlsCounter = 0;
|
|
9
|
+
export const pointerControls = (handedness, options) => {
|
|
10
|
+
if (getControlsContext() === undefined) {
|
|
11
|
+
injectPointerControlsPlugin();
|
|
12
|
+
setInternalContext();
|
|
13
|
+
setControlsContext({
|
|
14
|
+
interactiveObjects: [],
|
|
15
|
+
raycaster: new Raycaster(),
|
|
16
|
+
compute: options?.compute ?? defaultComputeFunction,
|
|
17
|
+
filter: options?.filter,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const context = getControlsContext();
|
|
21
|
+
if (getHandContext(handedness) === undefined) {
|
|
22
|
+
const enabled = options?.enabled ?? true;
|
|
23
|
+
const ctx = {
|
|
24
|
+
hand: handedness,
|
|
25
|
+
enabled: currentWritable(enabled),
|
|
26
|
+
pointer: currentWritable(new Vector3()),
|
|
27
|
+
pointerOverTarget: currentWritable(false),
|
|
28
|
+
lastEvent: undefined,
|
|
29
|
+
initialClick: [0, 0, 0],
|
|
30
|
+
initialHits: [],
|
|
31
|
+
hovered: new Map(),
|
|
32
|
+
};
|
|
33
|
+
setHandContext(handedness, ctx);
|
|
34
|
+
setupPointerControls(context, ctx, options?.fixedStep);
|
|
35
|
+
}
|
|
36
|
+
const handContext = getHandContext(handedness);
|
|
37
|
+
watch(handContext.enabled, (enabled) => {
|
|
38
|
+
controlsCounter += (enabled ? 1 : -1);
|
|
39
|
+
pointerState.update((value) => {
|
|
40
|
+
value[handedness].enabled = controlsCounter > 0;
|
|
41
|
+
return value;
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
watch(handContext.pointerOverTarget, (hovering) => {
|
|
45
|
+
pointerState.update((value) => {
|
|
46
|
+
value[handedness].hovering = hovering;
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
enabled: handContext.enabled,
|
|
52
|
+
hovered: handContext.hovered,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const injectPointerControlsPlugin: () => void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { injectPlugin, watch } from '@threlte/core';
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
import { usePointerControls } from './hook';
|
|
4
|
+
import { useComponentHasEventHandlers } from './useComponentEventHandlers';
|
|
5
|
+
export const injectPointerControlsPlugin = () => {
|
|
6
|
+
injectPlugin('threlte-pointer-controls', ({ ref }) => {
|
|
7
|
+
if (ref.isMesh !== true)
|
|
8
|
+
return;
|
|
9
|
+
const { addInteractiveObject, removeInteractiveObject } = usePointerControls();
|
|
10
|
+
const refStore = writable(ref);
|
|
11
|
+
const { hasEventHandlers } = useComponentHasEventHandlers();
|
|
12
|
+
watch([hasEventHandlers, refStore], ([hasEventHandlers, ref]) => {
|
|
13
|
+
// Because hasEventHandlers will only be set from false to true in the
|
|
14
|
+
// lifecycle of the component, we can safely assume that we do not need to
|
|
15
|
+
// remove the object from the list of interactive objects when
|
|
16
|
+
// hasEventHandlers is false.
|
|
17
|
+
if (!hasEventHandlers)
|
|
18
|
+
return;
|
|
19
|
+
addInteractiveObject(ref);
|
|
20
|
+
return () => removeInteractiveObject(ref);
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
onRefChange(ref) {
|
|
24
|
+
refStore.set(ref);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Vector3 } from 'three';
|
|
2
|
+
import { watch } from '@threlte/core';
|
|
3
|
+
import { getInternalContext } from './context';
|
|
4
|
+
import { useController } from '../../hooks/useController';
|
|
5
|
+
import { useHand } from '../../hooks/useHand';
|
|
6
|
+
import { useXR } from '../../hooks/useXR';
|
|
7
|
+
import { useFixed } from '../../internal/useFixed';
|
|
8
|
+
import { pointerIntersection } from '../../internal/stores';
|
|
9
|
+
const getIntersectionId = (intersection) => {
|
|
10
|
+
return `${(intersection.eventObject || intersection.object).uuid}/${intersection.index}${intersection.instanceId ?? ''}`;
|
|
11
|
+
};
|
|
12
|
+
const EPSILON = 0.0001;
|
|
13
|
+
export const setupPointerControls = (context, handContext, fixedStep = 1 / 40) => {
|
|
14
|
+
const handedness = handContext.hand;
|
|
15
|
+
const controller = useController(handedness);
|
|
16
|
+
const hand = useHand(handedness);
|
|
17
|
+
const { dispatchers } = getInternalContext();
|
|
18
|
+
let hits = [];
|
|
19
|
+
let lastPosition = new Vector3();
|
|
20
|
+
const handlePointerDown = (event) => {
|
|
21
|
+
// Save initial coordinates on pointer-down
|
|
22
|
+
const [hit] = hits;
|
|
23
|
+
if (!hit)
|
|
24
|
+
return;
|
|
25
|
+
handContext.initialClick = [hit.point.x, hit.point.y, hit.point.z];
|
|
26
|
+
handContext.initialHits = hits.map((hit) => hit.eventObject);
|
|
27
|
+
handleEvent('pointerdown', event);
|
|
28
|
+
};
|
|
29
|
+
const handlePointerUp = (event) => {
|
|
30
|
+
handleEvent('pointerup', event);
|
|
31
|
+
};
|
|
32
|
+
const handleClick = (event) => {
|
|
33
|
+
// If a click yields no results, pass it back to the user as a miss
|
|
34
|
+
// Missed events have to come first in order to establish user-land side-effect clean up
|
|
35
|
+
if (hits.length === 0) {
|
|
36
|
+
pointerMissed(context.interactiveObjects, event);
|
|
37
|
+
}
|
|
38
|
+
handleEvent('click', event);
|
|
39
|
+
};
|
|
40
|
+
function cancelPointer(intersections) {
|
|
41
|
+
for (const [, hoveredObj] of handContext.hovered) {
|
|
42
|
+
// When no objects were hit or the the hovered object wasn't found underneath the cursor
|
|
43
|
+
// we call pointerout and delete the object from the hovered elements map
|
|
44
|
+
if (intersections.length === 0 ||
|
|
45
|
+
!intersections.some((hit) => hit.object === hoveredObj.object &&
|
|
46
|
+
hit.index === hoveredObj.index &&
|
|
47
|
+
hit.instanceId === hoveredObj.instanceId)) {
|
|
48
|
+
const { eventObject } = hoveredObj;
|
|
49
|
+
handContext.hovered.delete(getIntersectionId(hoveredObj));
|
|
50
|
+
const eventDispatcher = dispatchers.get(eventObject);
|
|
51
|
+
if (eventDispatcher) {
|
|
52
|
+
// Clear out intersects, they are outdated by now
|
|
53
|
+
const data = { ...hoveredObj, intersections };
|
|
54
|
+
eventDispatcher('pointerout', data);
|
|
55
|
+
eventDispatcher('pointerleave', data);
|
|
56
|
+
// Deal with cancelation
|
|
57
|
+
handContext.pointerOverTarget.set(false);
|
|
58
|
+
cancelPointer([]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const getHits = () => {
|
|
64
|
+
const intersections = [];
|
|
65
|
+
const hits = context.raycaster.intersectObjects(context.interactiveObjects, true);
|
|
66
|
+
const filtered = context.filter === undefined ? hits : context.filter(hits, context, handContext);
|
|
67
|
+
pointerIntersection[handedness].set(filtered[0]);
|
|
68
|
+
// Bubble up the events, find the event source (eventObject)
|
|
69
|
+
for (const hit of filtered) {
|
|
70
|
+
let eventObject = hit.object;
|
|
71
|
+
// Bubble event up
|
|
72
|
+
while (eventObject) {
|
|
73
|
+
if (dispatchers.has(eventObject)) {
|
|
74
|
+
intersections.push({ ...hit, eventObject });
|
|
75
|
+
}
|
|
76
|
+
eventObject = eventObject.parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return intersections;
|
|
80
|
+
};
|
|
81
|
+
function pointerMissed(objects, event) {
|
|
82
|
+
for (const object of objects) {
|
|
83
|
+
dispatchers.get(object)?.('pointermissed', event);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function processHits() {
|
|
87
|
+
context.compute(context, handContext);
|
|
88
|
+
return getHits();
|
|
89
|
+
}
|
|
90
|
+
const handleEvent = (name, event) => {
|
|
91
|
+
const isPointerMove = name === 'pointermove';
|
|
92
|
+
const isClickEvent = name === 'click' || name === 'contextmenu';
|
|
93
|
+
// Take care of unhover
|
|
94
|
+
if (isPointerMove)
|
|
95
|
+
cancelPointer(hits);
|
|
96
|
+
let stopped = false;
|
|
97
|
+
// loop through all hits and dispatch events
|
|
98
|
+
dispatchEvents: for (const hit of hits) {
|
|
99
|
+
const intersectionEvent = {
|
|
100
|
+
stopped,
|
|
101
|
+
...hit,
|
|
102
|
+
intersections: hits,
|
|
103
|
+
stopPropagation() {
|
|
104
|
+
stopped = true;
|
|
105
|
+
intersectionEvent.stopped = true;
|
|
106
|
+
if (handContext.hovered.size > 0 &&
|
|
107
|
+
Array.from(handContext.hovered.values()).some((i) => i.eventObject === hit.eventObject)) {
|
|
108
|
+
// Objects cannot flush out higher up objects that have already caught the event
|
|
109
|
+
const higher = hits.slice(0, hits.indexOf(hit));
|
|
110
|
+
cancelPointer([...higher, hit]);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
delta: 0,
|
|
114
|
+
nativeEvent: event,
|
|
115
|
+
pointer: handContext.pointer.current,
|
|
116
|
+
ray: context.raycaster.ray
|
|
117
|
+
};
|
|
118
|
+
const eventDispatcher = dispatchers.get(hit.eventObject);
|
|
119
|
+
if (eventDispatcher === undefined)
|
|
120
|
+
return;
|
|
121
|
+
if (isPointerMove) {
|
|
122
|
+
// Move event ...
|
|
123
|
+
handContext.pointer.update((value) => value.copy(intersectionEvent.point));
|
|
124
|
+
if (eventDispatcher.hasEventListener('pointerover') ||
|
|
125
|
+
eventDispatcher.hasEventListener('pointerenter') ||
|
|
126
|
+
eventDispatcher.hasEventListener('pointerout') ||
|
|
127
|
+
eventDispatcher.hasEventListener('pointerleave')) {
|
|
128
|
+
const id = getIntersectionId(intersectionEvent);
|
|
129
|
+
const hoveredItem = handContext.hovered.get(id);
|
|
130
|
+
if (hoveredItem === undefined) {
|
|
131
|
+
// If the object wasn't previously hovered, book it and call its handler
|
|
132
|
+
handContext.hovered.set(id, intersectionEvent);
|
|
133
|
+
eventDispatcher('pointerover', intersectionEvent);
|
|
134
|
+
eventDispatcher('pointerenter', intersectionEvent);
|
|
135
|
+
handContext.pointerOverTarget.set(true);
|
|
136
|
+
}
|
|
137
|
+
else if (hoveredItem.stopped) {
|
|
138
|
+
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
|
|
139
|
+
intersectionEvent.stopPropagation();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Call pointer move
|
|
143
|
+
eventDispatcher('pointermove', intersectionEvent);
|
|
144
|
+
}
|
|
145
|
+
else if ((!isClickEvent || handContext.initialHits.includes(hit.eventObject)) &&
|
|
146
|
+
eventDispatcher.hasEventListener(name)) {
|
|
147
|
+
// Missed events have to come first
|
|
148
|
+
pointerMissed(context.interactiveObjects.filter((object) => !handContext.initialHits.includes(object)), event);
|
|
149
|
+
// Call the event
|
|
150
|
+
eventDispatcher(name, intersectionEvent);
|
|
151
|
+
}
|
|
152
|
+
else if (isClickEvent && handContext.initialHits.includes(hit.eventObject)) {
|
|
153
|
+
pointerMissed(context.interactiveObjects.filter((object) => !handContext.initialHits.includes(object)), event);
|
|
154
|
+
}
|
|
155
|
+
if (stopped)
|
|
156
|
+
break dispatchEvents;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const { start, stop } = useFixed(() => {
|
|
160
|
+
hits = processHits();
|
|
161
|
+
const targetRay = controller.current?.targetRay;
|
|
162
|
+
if (targetRay === undefined)
|
|
163
|
+
return;
|
|
164
|
+
if (targetRay.position.distanceTo(lastPosition) > EPSILON) {
|
|
165
|
+
handleEvent('pointermove');
|
|
166
|
+
}
|
|
167
|
+
lastPosition.copy(targetRay.position);
|
|
168
|
+
}, {
|
|
169
|
+
fixedStep,
|
|
170
|
+
autostart: false
|
|
171
|
+
});
|
|
172
|
+
watch(controller, (input) => {
|
|
173
|
+
if (input === undefined)
|
|
174
|
+
return;
|
|
175
|
+
input.targetRay.addEventListener('selectstart', handlePointerDown);
|
|
176
|
+
input.targetRay.addEventListener('selectend', handlePointerUp);
|
|
177
|
+
input.targetRay.addEventListener('select', handleClick);
|
|
178
|
+
return () => {
|
|
179
|
+
input.targetRay.removeEventListener('selectstart', handlePointerDown);
|
|
180
|
+
input.targetRay.removeEventListener('selectend', handlePointerUp);
|
|
181
|
+
input.targetRay.removeEventListener('select', handleClick);
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
// watch(hand, (input) => {
|
|
185
|
+
// if (input === undefined) return
|
|
186
|
+
// input.hand.addEventListener('pinchstart', handlePointerDown)
|
|
187
|
+
// input.hand.addEventListener('pinchend', handlePointerUp)
|
|
188
|
+
// input.hand.addEventListener('pinchend', handleClick)
|
|
189
|
+
// return () => {
|
|
190
|
+
// input.hand.removeEventListener('pinchstart', handlePointerDown)
|
|
191
|
+
// input.hand.removeEventListener('pinchend', handlePointerUp)
|
|
192
|
+
// input.hand.removeEventListener('pinchend', handleClick)
|
|
193
|
+
// }
|
|
194
|
+
// })
|
|
195
|
+
watch([useXR().isPresenting, handContext.enabled], ([isPresenting, enabled]) => {
|
|
196
|
+
if (isPresenting && enabled) {
|
|
197
|
+
start();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
stop();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CurrentWritable } from '@threlte/core';
|
|
2
|
+
import type { ComputeFunction } from './compute';
|
|
3
|
+
export type Properties<T> = Pick<T, {
|
|
4
|
+
[K in keyof T]: T[K] extends (_: any) => any ? never : K;
|
|
5
|
+
}[keyof T]>;
|
|
6
|
+
export interface Intersection extends THREE.Intersection {
|
|
7
|
+
/** The event source (the object which registered the handler) */
|
|
8
|
+
eventObject: THREE.Object3D;
|
|
9
|
+
}
|
|
10
|
+
export interface IntersectionEvent extends Intersection {
|
|
11
|
+
/** The event source (the object which registered the handler) */
|
|
12
|
+
eventObject: THREE.Object3D;
|
|
13
|
+
/** An array of intersections */
|
|
14
|
+
intersections: Intersection[];
|
|
15
|
+
/** Normalized event coordinates */
|
|
16
|
+
pointer: THREE.Vector3;
|
|
17
|
+
/** Delta between first click and this event */
|
|
18
|
+
delta: number;
|
|
19
|
+
/** The ray that pierced it */
|
|
20
|
+
ray: THREE.Ray;
|
|
21
|
+
/** stopPropagation will stop underlying handlers from firing */
|
|
22
|
+
stopPropagation: () => void;
|
|
23
|
+
/** The original host event */
|
|
24
|
+
nativeEvent: THREE.Event | undefined;
|
|
25
|
+
/** If the event was stopped by calling stopPropagation */
|
|
26
|
+
stopped: boolean;
|
|
27
|
+
}
|
|
28
|
+
export type FilterFunction = (items: THREE.Intersection[], state: ControlsContext, handState: HandContext) => THREE.Intersection[];
|
|
29
|
+
export type ControlsContext = {
|
|
30
|
+
interactiveObjects: THREE.Object3D[];
|
|
31
|
+
raycaster: THREE.Raycaster;
|
|
32
|
+
compute: ComputeFunction;
|
|
33
|
+
filter?: FilterFunction | undefined;
|
|
34
|
+
};
|
|
35
|
+
export type HandContext = {
|
|
36
|
+
hand: 'left' | 'right';
|
|
37
|
+
enabled: CurrentWritable<boolean>;
|
|
38
|
+
pointer: CurrentWritable<THREE.Vector3>;
|
|
39
|
+
pointerOverTarget: CurrentWritable<boolean>;
|
|
40
|
+
lastEvent: THREE.Event | undefined;
|
|
41
|
+
initialClick: [x: number, y: number, z: number];
|
|
42
|
+
initialHits: THREE.Object3D[];
|
|
43
|
+
hovered: Map<string, IntersectionEvent>;
|
|
44
|
+
};
|
|
45
|
+
export interface PointerCaptureTarget {
|
|
46
|
+
intersection: Intersection;
|
|
47
|
+
target: Element;
|
|
48
|
+
}
|
|
49
|
+
export type ThrelteXREvents = {
|
|
50
|
+
click: IntersectionEvent;
|
|
51
|
+
contextmenu: IntersectionEvent;
|
|
52
|
+
pointerup: IntersectionEvent;
|
|
53
|
+
pointerdown: IntersectionEvent;
|
|
54
|
+
pointerover: IntersectionEvent;
|
|
55
|
+
pointerout: IntersectionEvent;
|
|
56
|
+
pointerenter: IntersectionEvent;
|
|
57
|
+
pointerleave: IntersectionEvent;
|
|
58
|
+
pointermove: IntersectionEvent;
|
|
59
|
+
pointermissed: IntersectionEvent;
|
|
60
|
+
};
|
|
61
|
+
export declare const events: (keyof ThrelteXREvents)[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { onMount } from 'svelte';
|
|
2
|
+
import { get_current_component } from 'svelte/internal';
|
|
3
|
+
import { writable } from 'svelte/store';
|
|
4
|
+
import { events } from './types';
|
|
5
|
+
export const useComponentHasEventHandlers = () => {
|
|
6
|
+
const component = get_current_component();
|
|
7
|
+
const hasEventHandlers = writable(false);
|
|
8
|
+
onMount(() => {
|
|
9
|
+
const match = Object.keys(component.$$.callbacks).some((callback) => events.includes(callback));
|
|
10
|
+
hasEventHandlers.set(match);
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
hasEventHandlers
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Vector3 } from 'three';
|
|
2
|
+
import { useController } from '../../hooks/useController';
|
|
3
|
+
const controllers = {
|
|
4
|
+
left: useController('left'),
|
|
5
|
+
right: useController('right')
|
|
6
|
+
};
|
|
7
|
+
const forward = new Vector3();
|
|
8
|
+
export const defaultComputeFunction = (context, handContext) => {
|
|
9
|
+
const targetRay = controllers[handContext.hand].current?.targetRay;
|
|
10
|
+
if (targetRay === undefined)
|
|
11
|
+
return;
|
|
12
|
+
forward.set(0, 0, -1).applyQuaternion(targetRay.quaternion);
|
|
13
|
+
context.raycaster.set(targetRay.position, forward);
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CurrentWritable, createRawEventDispatcher } from '@threlte/core';
|
|
2
|
+
export type ComputeFunction = (context: Context, handContext: HandContext) => void;
|
|
3
|
+
export interface Context {
|
|
4
|
+
interactiveObjects: THREE.Mesh[];
|
|
5
|
+
surfaces: Map<string, THREE.Mesh>;
|
|
6
|
+
blockers: Map<string, THREE.Mesh>;
|
|
7
|
+
dispatchers: WeakMap<THREE.Mesh, ReturnType<typeof createRawEventDispatcher>>;
|
|
8
|
+
raycaster: THREE.Raycaster;
|
|
9
|
+
compute: ComputeFunction;
|
|
10
|
+
}
|
|
11
|
+
export interface HandContext {
|
|
12
|
+
hand: 'left' | 'right';
|
|
13
|
+
enabled: CurrentWritable<boolean>;
|
|
14
|
+
active: CurrentWritable<boolean>;
|
|
15
|
+
hovered: CurrentWritable<THREE.Intersection | undefined>;
|
|
16
|
+
}
|
|
17
|
+
export declare const getHandContext: (hand: 'left' | 'right') => HandContext;
|
|
18
|
+
export declare const setHandContext: (hand: 'left' | 'right', context: HandContext) => void;
|
|
19
|
+
export declare const getTeleportContext: () => Context;
|
|
20
|
+
export declare const setTeleportContext: (context: Context) => void;
|