diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2d62eff2..a5a7ed10 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -132,6 +132,7 @@ jobs: mkdir -p public/examples/hit-testing mkdir -p public/examples/uikit mkdir -p public/examples/portal + mkdir -p public/examples/guards cp -r ./examples/minecraft/dist/* ./public/examples/minecraft cp -r ./examples/pingpong/dist/* ./public/examples/pingpong cp -r ./examples/rag-doll/dist/* ./public/examples/rag-doll @@ -144,6 +145,7 @@ jobs: cp -r ./examples/hit-testing/dist/* ./public/examples/hit-testing cp -r ./examples/uikit/dist/* ./public/examples/uikit cp -r ./examples/portal/dist/* ./public/examples/portal + cp -r ./examples/guards/dist/* ./public/examples/guards - name: Upload Artifact uses: actions/upload-artifact@v4 diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index ab7c0f75..1112c329 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -4,26 +4,478 @@ description: Render and show parts of your application conditionally using guard nav: 20 --- -Guards allow to conditionally display or include content. For instance, the `IfInSessionMode` guard allows only displaying a background when the session is not an AR session. The `IfInSessionMode` can receive either a list of `allow` session modes or a list of `deny` session modes. +One of the coolest features of web development is the amount of devices that you can reach with your application. Everything from desktop browsers to mobile phones, and even some watches are able to visit webpages. While a large number of devices can access your application, not all of them are able to provide the same experiences. It's important to remember this and plan accordingly when building your application. This tutorial will show you how to conditionally enable or disable parts of your application based on the client's current device using the various "guard" components provided by `@react-three/xr`. +### Setup +As always, we need a new project to work with. Create a new React vite project and install the following dependencies: +`npm i three @react-three/fiber @react-three/xr @react-three/drei @react-three/uikit; npm i -D @types/three` + +In `App.tsx`, set up a basic scene, and in `styles.css` add some basic styling:: + +**App.tsx:** ```tsx +import { OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' -import { IfInSessionMode, XR, createXRStore } from '@react-three/xr' +import * as THREE from 'three' +import { createXRStore, XR } from '@react-three/xr' +import './styles.css' + +const store = createXRStore({ offerSession: false, emulate: false }) -const store = createXRStore() +const axisColor = new THREE.Color('#9d3d4a') +const gridColor = new THREE.Color('#4f4f4f') export function App() { return ( - <> - - +
+ + + - - - + + + + + + +
+ ) +} +``` + +**styles.css:** +```css +html { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; +} + +.App { + font-family: sans-serif; + text-align: center; + width: 100vw; + height: 100vh; +} + +button { + position: absolute; + background: black; + border-radius: 0.5rem; + border: none; + font-weight: bold; + color: white; + padding: 1rem 2rem; + cursor: pointer; + font-size: 1.5rem; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); +} + +.enterVRButton { + bottom: 1rem; + transform: translate(-50%, 0); + left: 40%; +} + +.enterARButton { + bottom: 1rem; + left: 60%; + transform: translate(-50%, 0); +} +``` + +### Our First Guard +Our simple demo here already has something worth putting a guard on. We have an XR scene ready to launch from the enter VR button, but what if the user is on a device that doesn't support VR? We can use the `IfSessionModeSupported` guard to only show the enter VR button when the user's device supports immersive VR sessions. `IfSessionModeSupported` takes a `mode` prop which can be set to `immersive-vr`, `immersive-ar`, or `inline`. Let's wrap our enter VR and enter AR buttons with the `IfSessionModeSupported` guard. + +**App.tsx:** +```tsx +import { IfSessionModeSupported } from '@react-three/xr' + +//... Previous code + + + + + + +//... Previous code +``` + +### ShowIfSessionModeSupported +If you look in the API you might notice that there is also a `ShowIfSessionModeSupported` guard. There are 2 main differences between `ShowIfSessionModeSupported` and `IfSessionModeSupported`. The first difference is that `ShowIfSessionModeSupported` only works within the react-three/fiber canvas. The second difference is that `IfSessionModeSupported` will not **render** its children at all if mode doesn't match the session, while `ShowIfSessionModeSupported` will render its children, but set their **visibility** to false. This means that with `ShowIfSessionModeSupported` the components will still exist in the scene, but they will not be visible. We can demonstrate this by making a simple message component that we will only show when VR sessions are supported. Add a new file called `Message.tsx` with the following code: + +**Message.tsx:** +```tsx +import { Container, Text } from '@react-three/uikit' + +interface MessageProps { + message: string +} + +export const Message = ({ message }: MessageProps) => { + console.log('But I am still rendered no matter what!') + return ( + + + {message} + + + ) +} +``` + +Now add the `` component into our scene wrapped with the `ShowIfSessionModeSupported` guard: + +**App.tsx:** +```tsx +//... Previous code +import { createXRStore, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' +import { Message } from './Message.js' +//... Previous code + + + + + + + {/* Show message when VR is supported */} + + + +//... Previous code +``` + +Notice that when you run the application on your desktop web browser, you will see the message in the console from the `Message` component, but you won't see the `UIKit` message rendered in the scene. The next components that we are going to cover also have both show and conditional render versions, but for simplicity, we are only going to cover the versions that optionally render going forward. + +### IfFacingCamera +This guard allows us to only render children when they are seen by the camera from a specific direction. This can be helpful for optimizing performance by not showing things that the user can't see. To demonstrate this guard, let's create a simple spinning box that will only render if it is viewed from the negative z axis. First, create a new file called `SpinningBox.tsx` with the following code: + +**SpinningBox.tsx:** +```tsx +import { Box } from '@react-three/drei' +import { useFrame } from '@react-three/fiber' +import { IfFacingCamera } from '@react-three/xr' +import { useRef } from 'react' +import * as THREE from 'three' + +interface SpinningBoxProps { + position?: [number, number, number] +} + +const cameraDirectionHelper = new THREE.Vector3(0, 0, -1) + +export const SpinningBox = ({ position }: SpinningBoxProps) => { + const boxRef = useRef(null) + + useFrame((state, delta) => { + const box = boxRef.current + if (box) { + box.rotation.y += delta + } + }) + + return ( + <> + + + + + - ) + ) +} +``` + +Now import and add the `SpinningBox` component into our scene: + +**App.tsx:** +```tsx +//... Previous code +import { SpinningBox } from './SpinningBox.js' +//... Previous code + + + + + {/* Add the SpinningBox component */} + + + +//... Previous code +``` +If you look at the scene now, you will not be able to see the spinning box, but if you rotate it around to view from the negative z axis, the box will appear and start spinning. + +### IfInSessionMode +While experimenting with the previous guards, we have been using the `` component to move the camera around. In desktop react-three/fiber applications the `` component is a very handy way to move the camera around and explore your scene. Unfortunately, the `` component does not play nicely with XR sessions. Luckily, we have a guard that can help us in this situation. The `IfInSessionMode` guard allows us to conditionally render components based on the current XR session mode. It accepts two props, a deny, and an allow list. In our case we want to **deny** our content when we are in an "immersive-ar", or an "immersive-vr" session. In the `App.tsx` file, wrap the `` component with the `IfInSessionMode` guard, and set it to deny both "immersive-ar" and "immersive-vr" modes: + +**App.tsx:** +```tsx +//... Previous code +import { createXRStore, IfInSessionMode, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' +//... Previous code + // Wrap OrbitControls with IfInSessionMode + + + +//... Previous code +``` + +### IfSessionVisible +We are down to our last component guard! The `IfSessionVisible` guard allows us to conditionally render content based on whether the XR session is visible or not. This can be useful for pausing certain effects or animations when the session is not visible to the user (e.g. when they press the menu button on a VR controller). To demonstrate this guard, let's create a simple box that hides whenever the session is paused. Create a new file called `ShyBox.tsx` with the following code: + +**ShyBox.tsx:** +```tsx +import { Box } from '@react-three/drei' +import { IfSessionVisible } from '@react-three/xr' +import { useRef } from 'react' +import * as THREE from 'three' + +interface ShyBoxProps { + position?: [number, number, number] +} + +export const ShyBox = ({ position }: ShyBoxProps) => { + const boxRef = useRef(null) + + return ( + + + + + + ) } ``` + +Now import and add the `ShyBox` component into our scene: + +**App.tsx:** +```tsx +//... Previous code +import { ShyBox } from './ShyBox.js' +//... Previous code + + + + + {/* Add the ShyBox component */} + + + + + + +//... Previous code +``` + +The best way to test this guard is to run the application in a VR session, then press the menu button on your VR controller to bring up the system menu. You should see the `ShyBox` disappear when the system menu is open, and reappear when you close the menu. + +### Hooks +We've made it through all of the component guards 🥳. All that's left now is to explore the hooks that are provided by `@react-three/xr`. Many of the components that we've covered so far use these hooks under the hood, and they can be useful for implementing logic based off of the state of the session rather than just hiding and displaying things. + +### useXRSessionFeatureEnabled +`useXRSessionFeatureEnabled` lets you check if a feature is enabled in the current XR session. For example, you can check if `hand-tracking` is available on your device. The full list of features can be found here at [MDN](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/requestSession#session_features). For our little demo app here, we're going to create a little panel that shows which features are supported by our device. Create a new file called `SupportedFeaturesPanel.tsx` with the following code: + +**SupportedFeaturesPanel.tsx:** +```tsx +import { Container, Text } from '@react-three/uikit' +import { useXRSessionFeatureEnabled } from '@react-three/xr' + +interface SupportedFeaturesPanelProps { + position?: [number, number, number] +} + +export const SupportedFeaturesPanel = ({ position }: SupportedFeaturesPanelProps) => { + const canUseAnchors = useXRSessionFeatureEnabled('anchors') + const canUseBoundedFloor = useXRSessionFeatureEnabled('bounded-floor') + const canUseDepthSensing = useXRSessionFeatureEnabled('depth-sensing') + const canUseDomOverlay = useXRSessionFeatureEnabled('dom-overlay') + const canUseHandTracking = useXRSessionFeatureEnabled('hand-tracking') + const canUseHitTest = useXRSessionFeatureEnabled('hit-test') + const canUseLayers = useXRSessionFeatureEnabled('layers') + const canUseLightEstimation = useXRSessionFeatureEnabled('light-estimation') + const canUseLocal = useXRSessionFeatureEnabled('local') + const canUseLocalFloor = useXRSessionFeatureEnabled('local-floor') + const canUseSecondaryViews = useXRSessionFeatureEnabled('secondary-views') + const canUseUnbounded = useXRSessionFeatureEnabled('unbounded') + const canUseViewer = useXRSessionFeatureEnabled('viewer') + + const getTextColor = (enabled: boolean) => (enabled ? 'lightgreen' : 'red') + + return ( + + + {'Supported Features:'} + {`Anchors: ${canUseAnchors}`} + {`Bounded Floor: ${canUseBoundedFloor}`} + {`Depth Sensing: ${canUseDepthSensing}`} + {`DOM Overlay: ${canUseDomOverlay}`} + {`Hand Tracking: ${canUseHandTracking}`} + {`Hit Test: ${canUseHitTest}`} + {`Layers: ${canUseLayers}`} + {`Light Estimation: ${canUseLightEstimation}`} + {`Local: ${canUseLocal}`} + {`Local Floor: ${canUseLocalFloor}`} + {`Secondary Views: ${canUseSecondaryViews}`} + {`Unbounded: ${canUseUnbounded}`} + {`Viewer: ${canUseViewer}`} + + + ) +} +``` + +Now add the `SupportedFeaturesPanel` component into our scene, and we have a nice view of what features are supported by our device. + +**App.tsx:** +```tsx +//... Previous code +import { SupportedFeaturesPanel } from './SupportedFeaturesPanel.js' +//... Previous code + + + + + + {/* Add the SupportedFeaturesPanel component */} + + {/* Previous code */} +``` + +*Note:* Just for clarity, in practice, you only need to check for the features that you actually plan on using in your application. + +### useXRSessionVisibilityState +The `useXRSessionVisibilityState` hook allows you to get the current visibility state of the XR session. The visibility state can be one of three values: `visible`, `hidden`, or `visible-blurred`. This can be useful for hiding elements, pausing or resuming animations, or changing effects based on whether the session is currently visible to the user. In our demo here, we're going to create a box that changes color based on the visibility state of the session. Create a new file called `ColorChangingBox.tsx` with the following code: + +**ColorChangingBox.tsx:** +```tsx +import { Box } from '@react-three/drei' +import { useXRSessionVisibilityState } from '@react-three/xr' +import { useEffect, useState } from 'react' +import * as THREE from 'three' + +interface ColorChangingBoxProps { + position?: [number, number, number] +} + +export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { + const [color, setColor] = useState(new THREE.Color('blue')) + const visState = useXRSessionVisibilityState() + + useEffect(() => { + if (visState !== 'visible') { + setColor(new THREE.Color(Math.random() * 0xffffff)) + } + }, [visState]) + + return ( + + + + ) +} +``` + +Add the box to our scene next to the other boxes: + +**App.tsx:** +```tsx +//... Previous code +import { ColorChangingBox } from './ColorChangingBox.js' +//... Previous code + + + + + + + {/* Add the ColorChangingBox component */} + +//... Previous code +``` + +Now when wearing a VR headset, if you pause the application by bringing up the system menu, the box will change to a random color. + +### useXRSessionModeSupported +We've finally made it to the last hook! The `useXRSessionModeSupported` hook allows you to check if a specific session mode is supported by the current device. This can be useful for conditionally enabling or disabling features based on the capabilities of the device. For example, you might want to check if `immersive-ar` is supported before showing an AR-specific feature in your application. In our case, we're going to keep things nice and simple and show another panel that displays which session modes are supported by our current device. Create a new file called `SupportedSessionModesPanel.tsx` with the following code: + +**SupportedSessionModesPanel.tsx:** +```tsx +import { Container, Text } from '@react-three/uikit' +import { useXRSessionModeSupported } from '@react-three/xr' + +interface SupportedSessionModesPanelProps { + position?: [number, number, number] +} + +export const SupportedSessionModesPanel = ({ position }: SupportedSessionModesPanelProps) => { + const supportsImmersiveVR = useXRSessionModeSupported('immersive-vr') + const supportsImmersiveAR = useXRSessionModeSupported('immersive-ar') + const supportsInline = useXRSessionModeSupported('inline') + + const getTextColor = (supported?: boolean) => (supported ? 'lightgreen' : 'red') + + return ( + + + {'Supported Session Modes:'} + {`Immersive VR: ${supportsImmersiveVR}`} + {`Immersive AR: ${supportsImmersiveAR}`} + {`Inline: ${supportsInline}`} + + + ) +} +``` + +By now you should be well familiar with the drill. Add the `SupportedSessionModesPanel` component into our scene next to the other panel: + +**App.tsx:** +```tsx +//... Previous code +import { SupportedSessionModesPanel } from './SupportedSessionModesPanel.js' +//... Previous code + + + + {/* Add the SupportedSessionModesPanel component */} + + +//... Previous code +``` + +### Conclusion +With that, we've covered all of the guards and hooks that allow you to implement conditional rendering and logic into your XR applications. Best of luck using these techniques and happy coding! + +- *The example project can be found here: [Guards Example](https://pmndrs.github.io/xr/examples/guards/)* +- *Full source code for this tutorial can be found here: [Guards Example Source](https://github.com/pmndrs/xr/tree/main/examples/guards)* diff --git a/examples/guards/.gitignore b/examples/guards/.gitignore new file mode 100644 index 00000000..53c37a16 --- /dev/null +++ b/examples/guards/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/examples/guards/ColorChangingBox.tsx b/examples/guards/ColorChangingBox.tsx new file mode 100644 index 00000000..a8331a32 --- /dev/null +++ b/examples/guards/ColorChangingBox.tsx @@ -0,0 +1,25 @@ +import { Box } from '@react-three/drei' +import { useXRSessionVisibilityState } from '@react-three/xr' +import { useEffect, useState } from 'react' +import * as THREE from 'three' + +interface ColorChangingBoxProps { + position?: [number, number, number] +} + +export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { + const [color, setColor] = useState(new THREE.Color('blue')) + const visState = useXRSessionVisibilityState() + + useEffect(() => { + if (visState !== 'visible') { + setColor(new THREE.Color(Math.random() * 0xffffff)) + } + }, [visState]) + + return ( + + + + ) +} diff --git a/examples/guards/Message.tsx b/examples/guards/Message.tsx new file mode 100644 index 00000000..4be6fe4d --- /dev/null +++ b/examples/guards/Message.tsx @@ -0,0 +1,16 @@ +import { Container, Text } from '@react-three/uikit' + +interface MessageProps { + message: string +} + +export const Message = ({ message }: MessageProps) => { + console.log('But I am still rendered no matter what!') + return ( + + + {message} + + + ) +} diff --git a/examples/guards/ShyBox.tsx b/examples/guards/ShyBox.tsx new file mode 100644 index 00000000..4711a8e8 --- /dev/null +++ b/examples/guards/ShyBox.tsx @@ -0,0 +1,20 @@ +import { Box } from '@react-three/drei' +import { IfSessionVisible } from '@react-three/xr' +import { useRef } from 'react' +import * as THREE from 'three' + +interface ShyBoxProps { + position?: [number, number, number] +} + +export const ShyBox = ({ position }: ShyBoxProps) => { + const boxRef = useRef(null) + + return ( + + + + + + ) +} diff --git a/examples/guards/SpinningBox.tsx b/examples/guards/SpinningBox.tsx new file mode 100644 index 00000000..ee2dbc5e --- /dev/null +++ b/examples/guards/SpinningBox.tsx @@ -0,0 +1,32 @@ +import { Box } from '@react-three/drei' +import { useFrame } from '@react-three/fiber' +import { IfFacingCamera } from '@react-three/xr' +import { useRef } from 'react' +import * as THREE from 'three' + +interface SpinningBoxProps { + position?: [number, number, number] +} + +const cameraDirectionHelper = new THREE.Vector3(0, 0, -1) + +export const SpinningBox = ({ position }: SpinningBoxProps) => { + const boxRef = useRef(null) + + useFrame((state, delta) => { + const box = boxRef.current + if (box) { + box.rotation.y += delta + } + }) + + return ( + <> + + + + + + + ) +} diff --git a/examples/guards/SupportedFeaturesPanel.tsx b/examples/guards/SupportedFeaturesPanel.tsx new file mode 100644 index 00000000..b867d52d --- /dev/null +++ b/examples/guards/SupportedFeaturesPanel.tsx @@ -0,0 +1,53 @@ +import { Container, Text } from '@react-three/uikit' +import { useXRSessionFeatureEnabled } from '@react-three/xr' + +interface SupportedFeaturesPanelProps { + position?: [number, number, number] +} + +export const SupportedFeaturesPanel = ({ position }: SupportedFeaturesPanelProps) => { + const canUseAnchors = useXRSessionFeatureEnabled('anchors') + const canUseBoundedFloor = useXRSessionFeatureEnabled('bounded-floor') + const canUseDepthSensing = useXRSessionFeatureEnabled('depth-sensing') + const canUseDomOverlay = useXRSessionFeatureEnabled('dom-overlay') + const canUseHandTracking = useXRSessionFeatureEnabled('hand-tracking') + const canUseHitTest = useXRSessionFeatureEnabled('hit-test') + const canUseLayers = useXRSessionFeatureEnabled('layers') + const canUseLightEstimation = useXRSessionFeatureEnabled('light-estimation') + const canUseLocal = useXRSessionFeatureEnabled('local') + const canUseLocalFloor = useXRSessionFeatureEnabled('local-floor') + const canUseSecondaryViews = useXRSessionFeatureEnabled('secondary-views') + const canUseUnbounded = useXRSessionFeatureEnabled('unbounded') + const canUseViewer = useXRSessionFeatureEnabled('viewer') + + const getTextColor = (enabled: boolean) => (enabled ? 'lightgreen' : 'red') + + return ( + + + {'Supported Features:'} + {`Anchors: ${canUseAnchors}`} + {`Bounded Floor: ${canUseBoundedFloor}`} + {`Depth Sensing: ${canUseDepthSensing}`} + {`DOM Overlay: ${canUseDomOverlay}`} + {`Hand Tracking: ${canUseHandTracking}`} + {`Hit Test: ${canUseHitTest}`} + {`Layers: ${canUseLayers}`} + {`Light Estimation: ${canUseLightEstimation}`} + {`Local: ${canUseLocal}`} + {`Local Floor: ${canUseLocalFloor}`} + {`Secondary Views: ${canUseSecondaryViews}`} + {`Unbounded: ${canUseUnbounded}`} + {`Viewer: ${canUseViewer}`} + + + ) +} diff --git a/examples/guards/SupportedSessionModesPanel.tsx b/examples/guards/SupportedSessionModesPanel.tsx new file mode 100644 index 00000000..f8f40df7 --- /dev/null +++ b/examples/guards/SupportedSessionModesPanel.tsx @@ -0,0 +1,33 @@ +import { Container, Text } from '@react-three/uikit' +import { useXRSessionModeSupported } from '@react-three/xr' + +interface SupportedSessionModesPanelProps { + position?: [number, number, number] +} + +export const SupportedSessionModesPanel = ({ position }: SupportedSessionModesPanelProps) => { + const supportsImmersiveVR = useXRSessionModeSupported('immersive-vr') + const supportsImmersiveAR = useXRSessionModeSupported('immersive-ar') + const supportsInline = useXRSessionModeSupported('inline') + + const getTextColor = (supported?: boolean) => (supported ? 'lightgreen' : 'red') + + return ( + + + {'Supported Session Modes:'} + {`Immersive VR: ${supportsImmersiveVR}`} + {`Immersive AR: ${supportsImmersiveAR}`} + {`Inline: ${supportsInline}`} + + + ) +} diff --git a/examples/guards/app.tsx b/examples/guards/app.tsx new file mode 100644 index 00000000..fefe813c --- /dev/null +++ b/examples/guards/app.tsx @@ -0,0 +1,53 @@ +import { OrbitControls, Plane } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' +import { createXRStore, IfInSessionMode, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' +import * as THREE from 'three' +import { ColorChangingBox } from './ColorChangingBox.js' +import { Message } from './Message.js' +import { ShyBox } from './ShyBox.js' +import { SpinningBox } from './SpinningBox.js' +import './styles.css' +import { SupportedFeaturesPanel } from './SupportedFeaturesPanel.js' +import { SupportedSessionModesPanel } from './SupportedSessionModesPanel.js' + +const store = createXRStore({ offerSession: false, emulate: false }) + +const axisColor = new THREE.Color('#9d3d4a') +const gridColor = new THREE.Color('#4f4f4f') + +export function App() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/examples/guards/index.html b/examples/guards/index.html new file mode 100644 index 00000000..4b84000e --- /dev/null +++ b/examples/guards/index.html @@ -0,0 +1,12 @@ + + + + + + Guards + + + +
+ + \ No newline at end of file diff --git a/examples/guards/index.tsx b/examples/guards/index.tsx new file mode 100644 index 00000000..c9fcb72a --- /dev/null +++ b/examples/guards/index.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './App.js' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/examples/guards/package.json b/examples/guards/package.json new file mode 100644 index 00000000..cd8a68ff --- /dev/null +++ b/examples/guards/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "@react-three/uikit": "^1.0.56", + "@react-three/xr": "workspace:~" + }, + "scripts": { + "dev": "vite --host", + "build": "vite build", + "check:eslint": "eslint \"*.{ts,tsx}\"", + "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" + } +} diff --git a/examples/guards/styles.css b/examples/guards/styles.css new file mode 100644 index 00000000..324cb3c2 --- /dev/null +++ b/examples/guards/styles.css @@ -0,0 +1,57 @@ +html { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; +} + +.App { + font-family: sans-serif; + text-align: center; + width: 100vw; + height: 100vh; +} + +.desktopMessage { + color: white; + background-color: black; + width: 30rem; + height: 5rem; + border-radius: 1rem; + position: absolute; + display: flex; + top: 2rem; + left: 2rem; + font-size: 2rem; + font-weight: bold; + padding-left: 1rem; + padding-top: 1rem; +} + +button { + position: absolute; + background: black; + border-radius: 0.5rem; + border: none; + font-weight: bold; + color: white; + padding: 1rem 2rem; + cursor: pointer; + font-size: 1.5rem; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); +} + +.enterVRButton { + bottom: 1rem; + transform: translate(-50%, 0); + left: 40%; +} + +.enterARButton { + bottom: 1rem; + left: 60%; + transform: translate(-50%, 0); +} \ No newline at end of file diff --git a/examples/guards/vite.config.ts b/examples/guards/vite.config.ts new file mode 100644 index 00000000..521b5dd8 --- /dev/null +++ b/examples/guards/vite.config.ts @@ -0,0 +1,12 @@ +import basicSsl from '@vitejs/plugin-basic-ssl' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: '/xr/examples/guards/', + plugins: [react(), basicSsl()], + resolve: { + dedupe: ['@react-three/fiber', 'three'], + }, +}) diff --git a/packages/react/xr/src/guard/facing-camera.tsx b/packages/react/xr/src/guard/facing-camera.tsx index cb2ee36e..c3bd096a 100644 --- a/packages/react/xr/src/guard/facing-camera.tsx +++ b/packages/react/xr/src/guard/facing-camera.tsx @@ -23,14 +23,24 @@ interface FacingCameraProps { direction: Vector3 angle?: number } + /** - * Guard that only **shows** its children by toggling their visibility if the camera is facing the object. + * Guard that only **shows** its children by toggling their visibility if the camera is facing the object from the specified direction within the specified angle. * Calculation is based on the provided angle and direction. * * @param props * #### `children` - `ReactNode` The ReactNode elements to conditionally show. * #### `direction` - [Vector3](https://threejs.org/docs/#api/en/math/Vector3) Direction vector to check against the camera's facing direction. * #### `angle` - `number` The angle in radians to determine visibility. Defaults to `Math.PI / 2` (90 degrees). + * @example + * ```tsx + * // Negative Z direction with 180 degree angle + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function ShowIfFacingCamera({ children, direction, angle = Math.PI / 2 }: FacingCameraProps) { const ref = useRef(null) @@ -49,13 +59,22 @@ export function ShowIfFacingCamera({ children, direction, angle = Math.PI / 2 }: } /** - * Guard that only **renders** its children into the scene if the camera is facing the object. + * Guard that only **renders** its children into the scene if the camera is facing the object from the specified direction within the specified angle. * Calculation is based on the provided angle and direction. * * @param props * #### `children` - `ReactNode` The ReactNode elements to conditionally render. * #### `direction` - [Vector3](https://threejs.org/docs/#api/en/math/Vector3) Direction vector to check against the camera's facing direction. * #### `angle` - `number` The angle in radians to determine visibility. Defaults to `Math.PI / 2` (90 degrees). + * @example + * ```tsx + * // Negative Z direction with 180 degree angle + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function IfFacingCamera({ children, direction, angle = Math.PI / 2 }: FacingCameraProps) { const ref = useRef(null) diff --git a/packages/react/xr/src/guard/focus.tsx b/packages/react/xr/src/guard/focus.tsx index e1542901..8fde59a1 100644 --- a/packages/react/xr/src/guard/focus.tsx +++ b/packages/react/xr/src/guard/focus.tsx @@ -11,6 +11,14 @@ interface SessionVisibleProps { * * @param props ‎ * #### `children?` - `ReactNode` The ReactNode elements to conditionally show. + * @example + * ```tsx + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function ShowIfSessionVisible({ children }: SessionVisibleProps) { const state = useXRSessionVisibilityState() @@ -23,6 +31,14 @@ export function ShowIfSessionVisible({ children }: SessionVisibleProps) { * * @param props ‎ * #### `children?` - `ReactNode` The ReactNode elements to conditionally show. + * @example + * ```tsx + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function IfSessionVisible({ children }: SessionVisibleProps) { const state = useXRSessionVisibilityState() diff --git a/packages/react/xr/src/guard/session-mode.tsx b/packages/react/xr/src/guard/session-mode.tsx index 1873179e..63410bac 100644 --- a/packages/react/xr/src/guard/session-mode.tsx +++ b/packages/react/xr/src/guard/session-mode.tsx @@ -29,6 +29,18 @@ interface InSessionModeProps { * #### `children?` - `ReactNode` The ReactNode elements to conditionally show. * #### `allow?` - `XRSessionMode | ReadonlyArray` The session mode(s) where the children will be shown. If not provided, the children will be shown in all modes except the ones in `deny`. * #### `deny?` - `XRSessionMode | ReadonlyArray` The session mode(s) where the children will be hidden. + * @example + * ```tsx + * + * + * + * + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function ShowIfInSessionMode({ children, allow, deny }: InSessionModeProps) { const visible = useIsInSessionMode(allow, deny) @@ -43,6 +55,18 @@ export function ShowIfInSessionMode({ children, allow, deny }: InSessionModeProp * #### `children?` - `ReactNode` The ReactNode elements to conditionally render. * #### `allow?` - `XRSessionMode | ReadonlyArray` The session mode(s) where the children will be rendered. If not provided, the children will be rendered in all modes except the ones in `deny`. * #### `deny?` - `XRSessionMode | ReadonlyArray` The session mode(s) where the children will not be rendered. + * @example + * ```tsx + * + * + * + * + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function IfInSessionMode({ children, allow, deny }: InSessionModeProps) { const visible = useIsInSessionMode(allow, deny) diff --git a/packages/react/xr/src/guard/session-supported.tsx b/packages/react/xr/src/guard/session-supported.tsx index c866ff52..3ce1e117 100644 --- a/packages/react/xr/src/guard/session-supported.tsx +++ b/packages/react/xr/src/guard/session-supported.tsx @@ -12,6 +12,14 @@ interface SessionModeSupportedProps { * @param props * #### `children?` - `ReactNode` The ReactNode elements to conditionally show. * #### `mode` - `XRSessionMode` The session mode used to determine if the children will be shown. + * @example + * ```tsx + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function ShowIfSessionModeSupported({ children, mode }: SessionModeSupportedProps) { const supported = useXRSessionModeSupported(mode) @@ -24,6 +32,14 @@ export function ShowIfSessionModeSupported({ children, mode }: SessionModeSuppor * @param props * #### `children?` - `ReactNode` The ReactNode elements to conditionally render. * #### `mode` - `XRSessionMode` The session mode used to determine if the children will be rendered. + * @example + * ```tsx + * + * + * + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function IfSessionModeSupported({ children, mode }: SessionModeSupportedProps) { const supported = useXRSessionModeSupported(mode) diff --git a/packages/react/xr/src/hooks.ts b/packages/react/xr/src/hooks.ts index a25ace46..2765e733 100644 --- a/packages/react/xr/src/hooks.ts +++ b/packages/react/xr/src/hooks.ts @@ -58,7 +58,13 @@ export function useHover( /** * Gets the visibility state of the XR session. * - * @returns The visibility state of the XR session. + * @returns The current visibility state of the XR session. + * @example + * ```tsx + * const visibilityState = useXRSessionVisibilityState() + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function useXRSessionVisibilityState() { return useXR((xr) => xr.visibilityState) @@ -78,6 +84,12 @@ export function useInitRoomCapture() { * * @param {XRSessionMode} mode - The `XRSessionMode` to check against. * @param {(error: any) => void} [onError] - Callback executed when an error occurs. + * @example + * ```tsx + * const isImmersiveVrSupported = useXRSessionModeSupported('immersive-vr') + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function useXRSessionModeSupported(mode: XRSessionMode, onError?: (error: any) => void) { const onErrorRef = useRef(onError) @@ -123,8 +135,14 @@ export const useSessionModeSupported = useXRSessionModeSupported /** * Checks if a specific XR session feature is enabled. * - * @param {string} feature - The XR session feature to check against. + * @param {string} feature - The XR session [feature](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/requestSession#session_features) to check against. * @returns {boolean} Whether the feature is enabled. + * @example + * ```tsx + * const isAnchorsEnabled = useXRSessionFeatureEnabled('anchors') + * ``` + * @see [Guards Example](https://pmndrs.github.io/xr/examples/guards/) + * @see [Guards Tutorial](https://pmndrs.github.io/xr/docs/tutorials/guards) */ export function useXRSessionFeatureEnabled(feature: string) { return useXR(({ session }) => session?.enabledFeatures?.includes(feature) ?? false)