@react-three/xr
Version:
VR/AR for react-three-fiber
88 lines (87 loc) • 4.29 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { configureXRControllerModel, createUpdateXRControllerVisuals, loadXRControllerModel, XRControllerLayoutLoader, } from '@pmndrs/xr/internals';
import { createPortal, useFrame } from '@react-three/fiber';
import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { suspend } from 'suspend-react';
import { useXRInputSourceStateContext } from './input.js';
import { XRSpace } from './space.js';
/**
* Component for placing content in the controller anchored at a specific component such as the Thumbstick
*
* @param props
* #### `id` - `XRControllerGamepadComponentId` Is the id of the component where content should be placed (e.g. `"a-button"`)
* #### `onPress?` - `Function` Is an optional callback to receive when the component is pressed
* #### `onRelease?` - `Function` Is an optional callback to receive when the component is released
* #### `children?` - `ReactNode` Children to be placed inside the componenent (e.g. visualizing a tooltip over the button...)
*
* @function
*/
export const XRControllerComponent = forwardRef(({ id, children, onPress, onRelease }, ref) => {
const state = useXRInputSourceStateContext('controller');
const [object, setObject] = useState(undefined);
useImperativeHandle(ref, () => object, [object]);
useXRControllerButtonEvent(state, id, (state) => (state === 'pressed' ? onPress?.() : onRelease?.()));
useFrame(() => setObject(state.gamepad[id]?.object));
if (object == null) {
return;
}
return createPortal(children, object);
});
/**
* Hook for subscribing to a button state change event on the controller
* @param controller the controller to listen for the event
* @param id of the button
* @param onChange callback that gets executed when the state of the button changes
*/
export function useXRControllerButtonEvent(controller, id, onChange) {
const state = useRef(undefined);
useFrame(() => {
const currentState = controller?.gamepad[id]?.state;
if (currentState != null && currentState != state.current) {
onChange(currentState);
}
state.current = currentState;
});
}
const LoadXRControllerModelSymbol = Symbol('loadXRControllerModel');
/**
* Component for rendering a 3D model for the XRController
* @param props
* #### `colorWrite` - Configures the colorWrite property of the model
* #### `renderOrder` - Configures the render order model
* @function
*/
export const XRControllerModel = forwardRef((options, ref) => {
const state = useXRInputSourceStateContext('controller');
const model = suspend(loadXRControllerModel, [state.layout, undefined, LoadXRControllerModelSymbol]);
configureXRControllerModel(model, options);
state.object = model;
useImperativeHandle(ref, () => model, [model]);
const update = useMemo(() => createUpdateXRControllerVisuals(model, state.layout, state.gamepad), [model, state.layout, state.gamepad]);
useFrame(update);
return (_jsx(XRSpace, { space: "grip-space", children: _jsx("primitive", { object: model }) }));
});
const LoadXRControllerLayoutSymbol = Symbol('loadXRControllerLayout');
/**
* For rendering a controller that is not included in WebXR. (e.g controller tutorials/demos)
*
* @param profileIds
* @param handedness
* @param XRControllerLayoutLoaderOptions
* @returns Promise<XRControllerLayout>
*/
export function useLoadXRControllerLayout(profileIds, handedness, { baseAssetPath, defaultControllerProfileId } = {}) {
const loader = useMemo(() => new XRControllerLayoutLoader({ baseAssetPath, defaultControllerProfileId }), [baseAssetPath, defaultControllerProfileId]);
return suspend(() => {
const result = loader.loadAsync(profileIds, handedness);
return result instanceof Promise ? result : Promise.resolve(result);
}, [LoadXRControllerLayoutSymbol, handedness, ...profileIds]);
}
/**
* Loads the controller model for the given layout. This is a suspendable function, so it can be used with React Suspense.
* @param layout: XRControllerLayout
* @returns Promise<THREE.Group>
*/
export function useLoadXRControllerModel(layout) {
return suspend(loadXRControllerModel, [layout, undefined, LoadXRControllerModelSymbol]);
}