All files / src/hooks useXr.ts

0% Statements 0/40
0% Branches 0/15
0% Functions 0/15
0% Lines 0/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87                                                                                                                                                                             
import { useCallback, useEffect, useMemo, useState } from 'react';
 
import { useApp } from '@playcanvas/react/hooks';
import { CameraComponent, XRSPACE_LOCALFLOOR, XRTYPE_AR, XRTYPE_VR } from 'playcanvas';
 
export type XrType = 'VR' | 'AR';
 
interface UseXrOptions {
  onError?: (error: Error) => void;
}
 
const XR_TYPES: Record<XrType, string> = {
  VR: XRTYPE_VR,
  AR: XRTYPE_AR,
};
 
export const useXr = ({ onError }: UseXrOptions = {}) => {
  const app = useApp();
  const [available, setAvailable] = useState<Record<XrType, boolean>>({ VR: false, AR: false });
  const [isCurrentlyInXr, setIsCurrentlyInXr] = useState(app.xr?.active ?? false);
 
  useEffect(() => {
    // We subscribe to the manager directly because `app` doesn't update when XR's availability changes.
    const handleAvailable = (type: string, isAvailable: boolean) => {
      if (type === XRTYPE_VR) {
        setAvailable((prev) => ({ ...prev, VR: isAvailable }));
      } else if (type === XRTYPE_AR) {
        setAvailable((prev) => ({ ...prev, AR: isAvailable }));
      }
    };
 
    setAvailable({
      VR: app.xr?.isAvailable(XRTYPE_VR) ?? false,
      AR: app.xr?.isAvailable(XRTYPE_AR) ?? false,
    });
 
    app.xr?.on('available', handleAvailable);
 
    return () => {
      app.xr?.off('available', handleAvailable);
    };
  }, [app]);
 
  useEffect(() => {
    const handleStart = () => setIsCurrentlyInXr(true);
    const handleEnd = () => setIsCurrentlyInXr(false);
 
    setIsCurrentlyInXr(app.xr?.active ?? false);
 
    app.xr?.on('start', handleStart);
    app.xr?.on('end', handleEnd);
 
    return () => {
      app.xr?.off('start', handleStart);
      app.xr?.off('end', handleEnd);
    };
  }, [app]);
 
  const isXrAvailable = useCallback((type: XrType) => available[type], [available]);
 
  const startXr = useCallback(
    (type: XrType) => {
      const camera = app.root.findComponent('camera') as CameraComponent;
      app.xr?.start(camera, XR_TYPES[type], XRSPACE_LOCALFLOOR, {
        callback: (err: Error | null) => {
          if (err) {
            onError?.(err);
            app.xr?.end();
          }
        },
      });
    },
    [app, onError]
  );
 
  const endXr = useCallback(() => {
    app.xr?.end();
  }, [app]);
 
  return useMemo(
    () => ({ isXrAvailable, startXr, endXr, isCurrentlyInXr }),
    [isXrAvailable, startXr, endXr, isCurrentlyInXr]
  );
};
 
export default useXr;