UNPKG

@react-three/xr

Version:

VR/AR for react-three-fiber

214 lines (213 loc) 8.18 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { createXRHitTestSource, requestXRHitTest } from '@pmndrs/xr'; import { useFrame } from '@react-three/fiber'; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { useStore } from 'zustand'; import { useXRStore } from './xr.js'; export { createXRHitTestSource, requestXRHitTest } from '@pmndrs/xr'; /** * Hook for creating a hit test source originating from the provided object or XRSpace. The provided object must be statically positioned in the XRSpace. * * @param relativeTo - The XRSpace, XRReferenceSpace, or Object3D to perform hit-tests from * @param trackableType - A string, or array of strings that specify the types of surfaces to hit test against ('point', 'plane', 'mesh') * * @example * function ManualHitTest() { * const meshRef = useRef<Mesh>(null) * const hitTestSource = useXRHitTestSource('viewer') * const [someCondition, setSomeCondition] = useState(false) * const [hitResults, setHitResults] = useState<XRHitTestResult[]>([]) * * useFrame((_, __, frame: XRFrame | undefined) => { * // Only perform hit testing when certain conditions are met * if (frame && hitTestSource && someCondition) { * const results = frame.getHitTestResults(hitTestSource.source) * setHitResults(results) * } * }) * return ( * <IfInSessionMode allow={'immersive-ar'}> * <XRDomOverlay> * <button onClick={() => setSomeCondition(true)}>Turn on hit testing</button> * </XRDomOverlay> * </IfInSessionMode> * ) * } * * @see [Hit Test Tutorial](https://pmndrs.github.io/xr/docs/tutorials/hit-test) * @see [Hit Test Example](https://pmndrs.github.io/xr/examples/hit-testing/) */ export function useXRHitTestSource(relativeTo, trackableType) { const [source, setState] = useState(); useCreateXRHitTestSource(relativeTo, trackableType, setState); return source; } /** * Hook for setting up a continous hit test originating from the provided object or XRSpace. The provided object must be statically positioned in the XRSpace. * * @param fn - Callback function that contains the results of the hit test, and a function to retrieve the world matrix * @param relativeTo - The XRSpace, XRReferenceSpace, or Object3D to perform hit-tests from * @param trackableType - A string, or array of strings that specify the types of surfaces to hit test against ('point', 'plane', 'mesh') * * @example * const matrixHelper = new Matrix4() * const hitTestPosition = new Vector3() * * function ContinuousHitTest() { * const previewRef = useRef<Mesh>(null) * * useXRHitTest( * (results, getWorldMatrix) => { * if (results.length === 0) return * * getWorldMatrix(matrixHelper, results[0]) * hitTestPosition.setFromMatrixPosition(matrixHelper) * }, * 'viewer' * ) * * useFrame(() => { * if (hitTestPosition && previewRef.current) { * previewRef.current.position.copy(hitTestPosition) * } * }) * * return ( * <mesh ref={previewRef} position={hitPosition}> * <sphereGeometry args={[0.05]} /> * <meshBasicMaterial color="red" /> * </mesh> * ) * } * * @see [Hit Test Tutorial](https://pmndrs.github.io/xr/docs/tutorials/hit-test) * @see [Hit Test Example](https://pmndrs.github.io/xr/examples/hit-testing/) */ export function useXRHitTest(fn, relativeTo, trackableType) { const sourceRef = useRef(undefined); useCreateXRHitTestSource(relativeTo, trackableType, useCallback((source) => (sourceRef.current = source), [])); useFrame((_s, _d, frame) => { if (fn == null || frame == null || sourceRef.current == null) { return; } fn(frame.getHitTestResults(sourceRef.current.source), sourceRef.current.getWorldMatrix); }); } function useCreateXRHitTestSource(relativeTo, trackableType, onLoad) { const store = useXRStore(); const session = useStore(store, (s) => s.session); useEffect(() => { if (session == null) { return; } let storedResult; let cancelled = false; const relativeToResolved = relativeTo instanceof XRSpace || typeof relativeTo === 'string' ? relativeTo : relativeTo?.current; if (relativeToResolved == null) { return; } createXRHitTestSource(store, session, relativeToResolved, trackableType).then((result) => { if (cancelled) { return; } storedResult = result; onLoad(result); }); return () => { onLoad(undefined); cancelled = true; storedResult?.source.cancel(); }; }, [session, store, relativeTo, trackableType, onLoad]); } /** * Hook that returns a function to request a single hit test. Cannot be called in the useFrame hook. * * @example * const matrixHelper = new Matrix4() * function EventDrivenHitTest() { * const requestHitTest = useXRRequestHitTest() * const [placedObjects, setPlacedObjects] = useState<Vector3[]>([]) * * const handleTap = async () => { * const hitTestResult = await requestHitTest('viewer', ['plane', 'mesh']) * const { results, getWorldMatrix } = hitTestResult * if (results?.length > 0) { * getWorldMatrix(matrixHelper, results[0]) * const position = new Vector3().setFromMatrixPosition(matrixHelper) * setPlacedObjects((prev) => [...prev, position]) * } * } * * return ( * <> * <IfInSessionMode allow={'immersive-ar'}> * <XRDomOverlay> * <button onClick={handleTap}>Place Object</button> * </XRDomOverlay> * </IfInSessionMode> * * {placedObjects.map((position, index) => ( * <mesh key={index} position={position}> * <sphereGeometry args={[0.1]} /> * <meshBasicMaterial color="blue" /> * </mesh> * ))} * </> * ) * } * * @see [Hit Test Tutorial](https://pmndrs.github.io/xr/docs/tutorials/hit-test) * @see [Hit Test Example](https://pmndrs.github.io/xr/examples/hit-testing/) */ export function useXRRequestHitTest() { const store = useXRStore(); return useCallback((relativeTo, trackableType) => { const relativeToResolved = relativeTo instanceof XRSpace || typeof relativeTo === 'string' ? relativeTo : relativeTo.current; if (relativeToResolved == null) { return; } return requestXRHitTest(store, relativeToResolved, trackableType); }, [store]); } /** * A convenience wrapper component for the useXRHitTest hook. Used to setup hit testing in the scene. * * @param props * #### `space` - [XRSpaceType](https://developer.mozilla.org/en-US/docs/Web/API/XRSpace) | [XRReferenceSpaceType](https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpace#reference_space_types) * #### `onResults` - Callback function that is called with the results of the hit test * * @example * const matrixHelper = new Matrix4() * const hitTestPosition = new Vector3() * * const store = createXRStore({ * hand: () => { * const inputSourceState = useXRInputSourceStateContext() * * return ( * <> * <DefaultXRHand /> * <XRHitTest * space={inputSourceState.inputSource.targetRaySpace} * onResults={(results, getWorldMatrix) => { * if (results.length === 0) return * getWorldMatrix(matrixHelper, results[0]) * hitTestPosition.setFromMatrixPosition(matrixHelper) * }} * /> * </> * ) * }, * }) * * @see [Hit Test Tutorial](https://pmndrs.github.io/xr/docs/tutorials/hit-test) * @see [Hit Test Example](https://pmndrs.github.io/xr/examples/hit-testing/) * @function */ export const XRHitTest = forwardRef(({ trackableType, onResults, space, ...rest }, ref) => { const internalRef = useRef(null); useImperativeHandle(ref, () => internalRef.current); useXRHitTest(onResults, space ?? internalRef, trackableType); return _jsx("group", { ...rest, ref: internalRef }); });