From c42ffdf933eed2d85b77ff06b8dba9c1440ac4a7 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Tue, 28 Oct 2025 12:11:48 +0100 Subject: [PATCH 01/18] docs: Got started on the tutorial update --- docs/tutorials/guards.md | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index ab7c0f75..2ce0566f 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -4,6 +4,108 @@ description: Render and show parts of your application conditionally using guard nav: 20 --- +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 guards 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 `App.css` add some basic styling:: + +**App.tsx:** +```tsx +import { Box } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' +import { createXRStore, XR } from '@react-three/xr' +import './styles.css' + +const store = createXRStore() + +const axisColor = new THREE.Color('#9d3d4a') +const gridColor = new THREE.Color('#4f4f4f') + +export function App() { + return ( +
+ + + + + + + + + + +
+ ) +} +``` + +**App.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; + bottom: 1rem; + left: 50%; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); + transform: translate(-50%, 0); +} +``` + +# Our First Guard +Already in our application we have something worth putting a guard on. We have an XR scene already to go with an enter VR button, but what if the user is on a device that doesn't support VR? We can use the `IfInSessionMode` guard to only show the enter VR button when the user's device supports immersive VR sessions. `IfInSessionMode` accepts two props: `allow` and `deny`. The `allow` prop allows you to specify which in which sessions you content should be shown. `deny` does the opposite, specifying which sessions your content should not be shown in. In our case, we want to use the `allow` prop and only show the button when the session mode is `immersive-vr`. + +```tsx +import { IfInSessionMode } from '@react-three/xr' + +//... Previous code + + + +//... Previous code +``` + +IfFacingCamera ⛔ +ShowIfFacingCamera ⛔ +IfSessionVisible ⛔ +ShowIfSessionVisible ⛔ +IfInSessionMode ☑️ : Needs usage snippet, and links to tutorial and example in jsdoc +ShowIfInSessionMode ⛔ +IfSessionModeSupported ⛔ +ShowIfSessionModeSupported ⛔ +useXRSessionFeatureEnabled ⛔ +useXRSessionModeSupported ⛔ +useXRSessionVisibilityState ⛔ 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. ```tsx From d7c8dd8a413d12bbc50b73f45797533c6ae118b5 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 1 Nov 2025 20:59:25 +0100 Subject: [PATCH 02/18] feat: Added an example project for working with guards --- docs/tutorials/guards.md | 14 ++++-- examples/guards/.gitignore | 1 + examples/guards/app.tsx | 36 +++++++++++++++ examples/guards/index.html | 12 +++++ examples/guards/index.tsx | 9 ++++ examples/guards/package.json | 12 +++++ examples/guards/styles.css | 48 ++++++++++++++++++++ examples/guards/vite.config.ts | 12 +++++ pnpm-lock.yaml | 82 +++++++++++++++++++++++++++++++++- 9 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 examples/guards/.gitignore create mode 100644 examples/guards/app.tsx create mode 100644 examples/guards/index.html create mode 100644 examples/guards/index.tsx create mode 100644 examples/guards/package.json create mode 100644 examples/guards/styles.css create mode 100644 examples/guards/vite.config.ts diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 2ce0566f..4a48007c 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -100,12 +100,18 @@ ShowIfFacingCamera ⛔ IfSessionVisible ⛔ ShowIfSessionVisible ⛔ IfInSessionMode ☑️ : Needs usage snippet, and links to tutorial and example in jsdoc -ShowIfInSessionMode ⛔ -IfSessionModeSupported ⛔ -ShowIfSessionModeSupported ⛔ -useXRSessionFeatureEnabled ⛔ +ShowIfInSessionMode ⛔ - Checks visibilty only, not rendering +IfSessionModeSupported ⛔ - Doesn't render if toggled off +ShowIfSessionModeSupported ⛔ - Checks visibilty only, not rendering +useXRSessionFeatureEnabled ⛔ - Check for if MeshDetection is enabled useXRSessionModeSupported ⛔ useXRSessionVisibilityState ⛔ + + + + + + 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. ```tsx 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/app.tsx b/examples/guards/app.tsx new file mode 100644 index 00000000..48d66143 --- /dev/null +++ b/examples/guards/app.tsx @@ -0,0 +1,36 @@ +import { OrbitControls, Plane } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' +import { createXRStore, IfSessionModeSupported, XR } from '@react-three/xr' +import * as THREE from 'three' +import './styles.css' + +const store = createXRStore() + +const axisColor = new THREE.Color('#9d3d4a') +const gridColor = new THREE.Color('#4f4f4f') + +export function App() { + return ( +
+ + + + + + + + + + + + + + + + + + {"You're on desktop lols"} + +
+ ) +} \ No newline at end of file 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..94a7343b --- /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..99ae8455 --- /dev/null +++ b/examples/guards/styles.css @@ -0,0 +1,48 @@ +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; + bottom: 1rem; + left: 50%; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); + transform: translate(-50%, 0); +} 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/pnpm-lock.yaml b/pnpm-lock.yaml index eeb3ade2..a0821e72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,15 @@ importers: specifier: ^4.5.2 version: 4.5.6(@types/react@19.0.8)(react@19.0.0) + examples/guards: + dependencies: + '@react-three/uikit': + specifier: ^1.0.56 + version: 1.0.56(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(use-sync-external-store@1.4.0(react@19.0.0)) + '@react-three/xr': + specifier: workspace:~ + version: link:../../packages/react/xr + examples/handheld-ar: dependencies: '@react-three/xr': @@ -850,12 +859,18 @@ packages: '@pmndrs/msdfonts@0.8.7': resolution: {integrity: sha512-O+2xzQQeIV249SfuSJVsAIun2/9SnA9V2P90NRDxzRYVzI8smXk6na1iA7E/RsCvfpWFhyX3g9zaKdiTotavoA==} + '@pmndrs/msdfonts@1.0.56': + resolution: {integrity: sha512-E7dwr8+DbVe9LLR2vKycQgecX13NNNXu9E1brlhWfusGVwKBJ2njbko6PUvgObrHuYGApQYotXROW76sWSjhYA==} + '@pmndrs/pointer-events@6.6.1': resolution: {integrity: sha512-O7CMUtoCyEqVT5X0syjsRKazX+7Q7//zBKj5mjIfIP+nlcwvBYCvdRTgHf6cGqB7jRgXLCd/3LSEgdoAXys/3Q==} '@pmndrs/uikit-lucide@0.4.4': resolution: {integrity: sha512-us9lzwtG7gByDmYfqZ9KXeSnbMRWTuHXBdLWsCtmuu1mYk91UZlqsBdvLo0GsC8SDHz3S7Y49Hlgtsw4OcRZ/Q==} + '@pmndrs/uikit-pub-sub@1.0.56': + resolution: {integrity: sha512-K7SrjXEFec30NF1Nxg9dwRN90xqUSPSMgOyaoep77/PvKy2g+MyZhkQLFbhdBtwGOj1JhFK5zncvtZXltgrcoA==} + '@pmndrs/uikit@0.4.4': resolution: {integrity: sha512-AT4B0zp4KP96lqh6oam44VO/H49cOZjEm09+0XiVXIgzA721heUHXgI4FhWGb1qlcI4T/2FHMBaw5sXh1o1npw==} peerDependencies: @@ -876,6 +891,11 @@ packages: peerDependencies: three: '>=0.160' + '@pmndrs/uikit@1.0.56': + resolution: {integrity: sha512-Fh4uY4oP/vAMx6GTM5ZpXd484220ldBlZAuItgmWT4KeNML8YBBiZR0P/pOFt7AFgAt3yNWSkp/9Ya6Rui6a8w==} + peerDependencies: + three: '>=0.162' + '@preact/signals-core@1.8.0': resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} @@ -974,6 +994,12 @@ packages: '@react-three/fiber': '>=8' react: '>=18' + '@react-three/uikit@1.0.56': + resolution: {integrity: sha512-SrIWWPN1bMZe1YuEsefhfbdCU6/Ec9FgnVghbbx0VDS1DZjuh0NP+rMxs2qh+fxNVplEeeqwb8x5GryySJvRkA==} + peerDependencies: + '@react-three/fiber': '>=8' + react: '>=18' + '@rollup/rollup-android-arm-eabi@4.34.0': resolution: {integrity: sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==} cpu: [arm] @@ -3365,6 +3391,24 @@ packages: use-sync-external-store: optional: true + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3726,6 +3770,8 @@ snapshots: '@pmndrs/msdfonts@0.8.7': {} + '@pmndrs/msdfonts@1.0.56': {} + '@pmndrs/pointer-events@6.6.1': {} '@pmndrs/uikit-lucide@0.4.4(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': @@ -3735,6 +3781,10 @@ snapshots: - three - ts-node + '@pmndrs/uikit-pub-sub@1.0.56': + dependencies: + '@preact/signals-core': 1.8.0 + '@pmndrs/uikit@0.4.4(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: '@preact/signals-core': 1.8.0 @@ -3781,6 +3831,14 @@ snapshots: transitivePeerDependencies: - ts-node + '@pmndrs/uikit@1.0.56(three@0.173.0)': + dependencies: + '@pmndrs/msdfonts': 1.0.56 + '@pmndrs/uikit-pub-sub': 1.0.56 + '@preact/signals-core': 1.8.0 + three: 0.173.0 + yoga-layout: 3.2.1 + '@preact/signals-core@1.8.0': {} '@preact/signals@2.0.1(preact@10.25.4)': @@ -3872,7 +3930,7 @@ snapshots: '@react-three/uikit-default@0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: - '@react-three/uikit': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@react-three/uikit': 0.8.21(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) '@react-three/uikit-lucide': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) tunnel-rat: 0.1.2(@types/react@19.0.8)(react@19.0.0) transitivePeerDependencies: @@ -3896,7 +3954,7 @@ snapshots: '@react-three/uikit-lucide@0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: - '@react-three/uikit': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@react-three/uikit': 0.8.21(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) transitivePeerDependencies: - '@react-three/fiber' - '@types/react' @@ -3982,6 +4040,20 @@ snapshots: - three - ts-node + '@react-three/uikit@1.0.56(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(use-sync-external-store@1.4.0(react@19.0.0))': + dependencies: + '@pmndrs/uikit': 1.0.56(three@0.173.0) + '@preact/signals-core': 1.8.0 + '@react-three/fiber': 9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0) + react: 19.0.0 + suspend-react: 0.1.3(react@19.0.0) + zustand: 5.0.8(@types/react@19.0.8)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)) + transitivePeerDependencies: + - '@types/react' + - immer + - three + - use-sync-external-store + '@rollup/rollup-android-arm-eabi@4.34.0': optional: true @@ -6620,3 +6692,9 @@ snapshots: '@types/react': 19.0.8 react: 19.0.0 use-sync-external-store: 1.4.0(react@19.0.0) + + zustand@5.0.8(@types/react@19.0.8)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)): + optionalDependencies: + '@types/react': 19.0.8 + react: 19.0.0 + use-sync-external-store: 1.4.0(react@19.0.0) From d5c50de150ff88d15031fdfd03ebbb720b77d773 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Mon, 3 Nov 2025 11:34:34 +0100 Subject: [PATCH 03/18] chore: continued work on the guards tutorial --- docs/tutorials/guards.md | 21 ++++++++++++++++++--- examples/guards/Message.tsx | 16 ++++++++++++++++ examples/guards/app.tsx | 11 ++++++----- examples/guards/index.tsx | 2 +- packages/react/xr/src/hooks.ts | 1 + 5 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 examples/guards/Message.tsx diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 4a48007c..dc269abb 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -39,6 +39,9 @@ export function App() { + ) } @@ -81,20 +84,32 @@ button { ``` # Our First Guard -Already in our application we have something worth putting a guard on. We have an XR scene already to go with an enter VR button, but what if the user is on a device that doesn't support VR? We can use the `IfInSessionMode` guard to only show the enter VR button when the user's device supports immersive VR sessions. `IfInSessionMode` accepts two props: `allow` and `deny`. The `allow` prop allows you to specify which in which sessions you content should be shown. `deny` does the opposite, specifying which sessions your content should not be shown in. In our case, we want to use the `allow` prop and only show the button when the session mode is `immersive-vr`. +Already in our application we have something worth putting a guard on. We have an XR scene already to go with an 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. ```tsx import { IfInSessionMode } from '@react-three/xr' //... Previous code - + - + + + + //... Previous code ``` +# ShowIfInSessionMode +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: + +```tsx +``` + + IfFacingCamera ⛔ ShowIfFacingCamera ⛔ IfSessionVisible ⛔ diff --git a/examples/guards/Message.tsx b/examples/guards/Message.tsx new file mode 100644 index 00000000..d397c18f --- /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/app.tsx b/examples/guards/app.tsx index 48d66143..d110519d 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -1,7 +1,8 @@ import { OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' -import { createXRStore, IfSessionModeSupported, XR } from '@react-three/xr' +import { createXRStore, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' import * as THREE from 'three' +import { Message } from './Message.js' import './styles.css' const store = createXRStore() @@ -21,6 +22,9 @@ export function App() { + + + @@ -28,9 +32,6 @@ export function App() { - - {"You're on desktop lols"} - ) -} \ No newline at end of file +} diff --git a/examples/guards/index.tsx b/examples/guards/index.tsx index 94a7343b..c9fcb72a 100644 --- a/examples/guards/index.tsx +++ b/examples/guards/index.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { App } from './app.js' +import { App } from './App.js' createRoot(document.getElementById('root')!).render( diff --git a/packages/react/xr/src/hooks.ts b/packages/react/xr/src/hooks.ts index a25ace46..4da901eb 100644 --- a/packages/react/xr/src/hooks.ts +++ b/packages/react/xr/src/hooks.ts @@ -96,6 +96,7 @@ export function useXRSessionModeSupported(mode: XRSessionMode, onError?: (error: .isSessionSupported(mode) .then((isSupported) => { sessionSupported = isSupported + console.log('Session mode supported:', mode, isSupported) if (canceled) { return } From cd45e1a4c93fdb6902c060be626f0945d4f608db Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Tue, 4 Nov 2025 13:55:23 +0100 Subject: [PATCH 04/18] chore: Saving today's progress --- docs/tutorials/guards.md | 59 ++++++++++++++++++++++++++++++++++++++-- examples/guards/app.tsx | 2 +- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index dc269abb..68dc2ec2 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -19,7 +19,7 @@ import { Canvas } from '@react-three/fiber' import { createXRStore, XR } from '@react-three/xr' import './styles.css' -const store = createXRStore() +const store = createXRStore({ offerSession: false }) const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') @@ -103,12 +103,64 @@ import { IfInSessionMode } from '@react-three/xr' //... Previous code ``` -# ShowIfInSessionMode -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: +# 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: +Messages.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: + +```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 + +# IfSessionVisible + +# IfInSessionMode + +# Hooks + +### useXRSessionFeatureEnabled + +### useXRSessionModeSupported + +### useXRSessionVisibilityState IfFacingCamera ⛔ ShowIfFacingCamera ⛔ @@ -118,6 +170,7 @@ IfInSessionMode ☑️ : Needs usage snippet, and links to tutorial and example ShowIfInSessionMode ⛔ - Checks visibilty only, not rendering IfSessionModeSupported ⛔ - Doesn't render if toggled off ShowIfSessionModeSupported ⛔ - Checks visibilty only, not rendering + useXRSessionFeatureEnabled ⛔ - Check for if MeshDetection is enabled useXRSessionModeSupported ⛔ useXRSessionVisibilityState ⛔ diff --git a/examples/guards/app.tsx b/examples/guards/app.tsx index d110519d..71e2196b 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -5,7 +5,7 @@ import * as THREE from 'three' import { Message } from './Message.js' import './styles.css' -const store = createXRStore() +const store = createXRStore({ offerSession: false }) const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') From 46f9bed63419e4a33df38881e9cdd104d6be483c Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Thu, 6 Nov 2025 15:24:55 +0100 Subject: [PATCH 05/18] chore: Added the beginning of a section for `IfFacingCamera` --- docs/tutorials/guards.md | 6 ++++++ examples/guards/SpinningBox.tsx | 32 ++++++++++++++++++++++++++++++++ examples/guards/app.tsx | 4 +++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 examples/guards/SpinningBox.tsx diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 68dc2ec2..2efced32 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -149,6 +149,12 @@ import { Message } from './Message.js' 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 facing the camera. This can be helpful for optimizing performance by not showing things that the user can't see. To show off this guard, let's create a simple spinning box that will only render when it is facing the camera. First, create a new file called `SpinningBox.tsx` with the following code: + +SpinningBox.tsx: +```tsx + +``` # IfSessionVisible diff --git a/examples/guards/SpinningBox.tsx b/examples/guards/SpinningBox.tsx new file mode 100644 index 00000000..457b17c8 --- /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/app.tsx b/examples/guards/app.tsx index 71e2196b..e9df5acf 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -3,9 +3,10 @@ import { Canvas } from '@react-three/fiber' import { createXRStore, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' import * as THREE from 'three' import { Message } from './Message.js' +import { SpinningBox } from './SpinningBox.js' import './styles.css' -const store = createXRStore({ offerSession: false }) +const store = createXRStore({ offerSession: false, emulate: false }) const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') @@ -20,6 +21,7 @@ export function App() { + From ae0a7fb7aa3c09efe6115fb5bb434cb159596a63 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Thu, 6 Nov 2025 21:23:44 +0100 Subject: [PATCH 06/18] chore: Just chipping away at progress a little more at a time --- docs/tutorials/guards.md | 53 ++++++++++++++++++++++++++++++++++++++-- examples/guards/app.tsx | 2 +- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 2efced32..670bc938 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -149,17 +149,66 @@ import { Message } from './Message.js' 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 facing the camera. This can be helpful for optimizing performance by not showing things that the user can't see. To show off this guard, let's create a simple spinning box that will only render when it is facing the camera. First, create a new file called `SpinningBox.tsx` with the following code: +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 show off this guard, let's create a simple spinning box that will only render viewed from the camera from the -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 ( + <> + + + + + + + ) +} ``` -# IfSessionVisible +Now import and add the `SpinningBox` component into our scene: + +App.tsx: +```tsx +//... Previous code +import { SpinningBox } from './SpinningBox.js' +//... Previous code + + + + + + + +//... 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 -z axis, the box will appear and start spinning. # IfInSessionMode +# IfSessionVisible + # Hooks ### useXRSessionFeatureEnabled diff --git a/examples/guards/app.tsx b/examples/guards/app.tsx index e9df5acf..2d98514c 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -21,7 +21,7 @@ export function App() { - + From e3abfb83ad806b7ffc1f83a638a534830c5ea2d4 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 7 Nov 2025 13:46:25 +0100 Subject: [PATCH 07/18] docs: Documented another hook --- docs/tutorials/guards.md | 64 +++++++++++++++++++ examples/guards/ColorChangingBox.tsx | 29 +++++++++ examples/guards/Message.tsx | 2 +- examples/guards/ShyBox.tsx | 20 ++++++ examples/guards/SpinningBox.tsx | 2 +- examples/guards/app.tsx | 10 ++- packages/react/xr/src/guard/facing-camera.tsx | 2 +- 7 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 examples/guards/ColorChangingBox.tsx create mode 100644 examples/guards/ShyBox.tsx diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 670bc938..1b57e174 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -198,6 +198,7 @@ import { SpinningBox } from './SpinningBox.js' + {/* Add the SpinningBox component */} @@ -206,10 +207,73 @@ import { SpinningBox } from './SpinningBox.js' 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 -z axis, the box will appear and start spinning. # IfInSessionMode +While expiramenting with the previous guards, you were likely using the `` component to move the camera around. In desktop react-three/fiber applications, the orbit controls are 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, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' +//... Previous code +// Wrap OrbitControls with IfInSessionMode + + + +//... Previous code +``` # IfSessionVisible +We are down to our last component guard! This guard is more of a special use 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. For example, if the user switches to another tab or minimizes the browser window, we might want to pause certain animations to save resources. 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. ### useXRSessionFeatureEnabled diff --git a/examples/guards/ColorChangingBox.tsx b/examples/guards/ColorChangingBox.tsx new file mode 100644 index 00000000..864b7e2c --- /dev/null +++ b/examples/guards/ColorChangingBox.tsx @@ -0,0 +1,29 @@ +import { Box } from '@react-three/drei' +import { useFrame } from '@react-three/fiber' +import { IfSessionVisible } from '@react-three/xr' +import { useRef, useState } from 'react' +import * as THREE from 'three' + +interface ColorChangingBoxProps { + position?: [number, number, number] +} + +export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { + const boxRef = useRef(null) + const [color, setColor] = useState(new THREE.Color('blue')) + + useFrame(() => { + const box = boxRef.current + if (box) { + // box.material.color = color + } + }) + + return ( + + setColor(new THREE.Color(Math.random() * 0xffffff))}> + + + + ) +} diff --git a/examples/guards/Message.tsx b/examples/guards/Message.tsx index d397c18f..351e4986 100644 --- a/examples/guards/Message.tsx +++ b/examples/guards/Message.tsx @@ -7,7 +7,7 @@ interface MessageProps { 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 index 457b17c8..ee2dbc5e 100644 --- a/examples/guards/SpinningBox.tsx +++ b/examples/guards/SpinningBox.tsx @@ -23,7 +23,7 @@ export const SpinningBox = ({ position }: SpinningBoxProps) => { return ( <> - + diff --git a/examples/guards/app.tsx b/examples/guards/app.tsx index 2d98514c..01564034 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -1,8 +1,9 @@ import { OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' -import { createXRStore, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' +import { createXRStore, IfInSessionMode, IfSessionModeSupported, ShowIfSessionModeSupported, XR } from '@react-three/xr' import * as THREE from 'three' import { Message } from './Message.js' +import { ShyBox } from './ShyBox.js' import { SpinningBox } from './SpinningBox.js' import './styles.css' @@ -21,8 +22,11 @@ export function App() { - - + + + + + diff --git a/packages/react/xr/src/guard/facing-camera.tsx b/packages/react/xr/src/guard/facing-camera.tsx index e2d1ae78..cb2ee36e 100644 --- a/packages/react/xr/src/guard/facing-camera.tsx +++ b/packages/react/xr/src/guard/facing-camera.tsx @@ -61,5 +61,5 @@ export function IfFacingCamera({ children, direction, angle = Math.PI / 2 }: Fac const ref = useRef(null) const [show, setShow] = useState(false) useIsFacingCamera(ref, setShow, direction, angle) - return show ? <>{children} : null + return {show ? children : null} } From b9f8aae0c922564bda25f70f1fbcfd82f76b7e25 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Thu, 13 Nov 2025 11:14:12 +0100 Subject: [PATCH 08/18] feat: Added a new section in the tutorial for useXRSessionFeatureEnabled --- docs/tutorials/guards.md | 97 ++++++++++++++++++---- examples/guards/SupportedFeaturesPanel.tsx | 53 ++++++++++++ examples/guards/app.tsx | 2 + 3 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 examples/guards/SupportedFeaturesPanel.tsx diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 1b57e174..9311493c 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -103,7 +103,7 @@ import { IfInSessionMode } from '@react-three/xr' //... Previous code ``` -# ShowIfSessionModeSupported +### 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: Messages.tsx: @@ -148,7 +148,7 @@ import { Message } from './Message.js' 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 +### 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 show off this guard, let's create a simple spinning box that will only render viewed from the camera from the -z axis. First, create a new file called `SpinningBox.tsx` with the following code: SpinningBox.tsx: @@ -206,7 +206,7 @@ import { SpinningBox } from './SpinningBox.js' ``` 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 -z axis, the box will appear and start spinning. -# IfInSessionMode +### IfInSessionMode While expiramenting with the previous guards, you were likely using the `` component to move the camera around. In desktop react-three/fiber applications, the orbit controls are 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: @@ -221,7 +221,7 @@ import { createXRStore, IfSessionModeSupported, ShowIfSessionModeSupported, XR } //... Previous code ``` -# IfSessionVisible +### IfSessionVisible We are down to our last component guard! This guard is more of a special use 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. For example, if the user switches to another tab or minimizes the browser window, we might want to pause certain animations to save resources. 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: @@ -273,22 +273,91 @@ The best way to test this guard is to run the application in a VR session, then # 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. +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: -### useXRSessionModeSupported +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 */} +``` ### useXRSessionVisibilityState -IfFacingCamera ⛔ -ShowIfFacingCamera ⛔ -IfSessionVisible ⛔ -ShowIfSessionVisible ⛔ -IfInSessionMode ☑️ : Needs usage snippet, and links to tutorial and example in jsdoc -ShowIfInSessionMode ⛔ - Checks visibilty only, not rendering -IfSessionModeSupported ⛔ - Doesn't render if toggled off -ShowIfSessionModeSupported ⛔ - Checks visibilty only, not rendering +### useXRSessionModeSupported + + useXRSessionFeatureEnabled ⛔ - Check for if MeshDetection is enabled useXRSessionModeSupported ⛔ 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/app.tsx b/examples/guards/app.tsx index 01564034..e88ef784 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -6,6 +6,7 @@ import { Message } from './Message.js' import { ShyBox } from './ShyBox.js' import { SpinningBox } from './SpinningBox.js' import './styles.css' +import { SupportedFeaturesPanel } from './SupportedFeaturesPanel.js' const store = createXRStore({ offerSession: false, emulate: false }) @@ -24,6 +25,7 @@ export function App() { + From 682a1a71ba67c7b8875d22e34ade9ecebf982b2d Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Thu, 13 Nov 2025 23:29:33 +0100 Subject: [PATCH 09/18] docs: Finished the first draft of the new tutorial --- docs/tutorials/guards.md | 131 ++++++++++++++---- examples/guards/ColorChangingBox.tsx | 24 ++-- .../guards/SupportedSessionModesPanel.tsx | 33 +++++ examples/guards/app.tsx | 6 +- 4 files changed, 149 insertions(+), 45 deletions(-) create mode 100644 examples/guards/SupportedSessionModesPanel.tsx diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 9311493c..687e4013 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -342,52 +342,123 @@ App.tsx: //... Previous code import { SupportedFeaturesPanel } from './SupportedFeaturesPanel.js' //... Previous code - - - - - - - {/* Add the SupportedFeaturesPanel component */} - - {/* Previous code */} + + + + + + {/* Add the SupportedFeaturesPanel component */} + + {/* Previous code */} ``` ### 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 pausing or resuming animations or 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: -### useXRSessionModeSupported - +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] +} -useXRSessionFeatureEnabled ⛔ - Check for if MeshDetection is enabled -useXRSessionModeSupported ⛔ -useXRSessionVisibilityState ⛔ +export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { + const [color, setColor] = useState(new THREE.Color('blue')) + const visState = useXRSessionVisibilityState() + useEffect(() => { + if (visState === 'hidden') { + 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. -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. +### 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 { Canvas } from '@react-three/fiber' -import { IfInSessionMode, XR, createXRStore } from '@react-three/xr' +import { Container, Text } from '@react-three/uikit' +import { useXRSessionModeSupported } from '@react-three/xr' -const store = createXRStore() +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') -export function App() { 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! diff --git a/examples/guards/ColorChangingBox.tsx b/examples/guards/ColorChangingBox.tsx index 864b7e2c..fabb413d 100644 --- a/examples/guards/ColorChangingBox.tsx +++ b/examples/guards/ColorChangingBox.tsx @@ -1,7 +1,6 @@ import { Box } from '@react-three/drei' -import { useFrame } from '@react-three/fiber' -import { IfSessionVisible } from '@react-three/xr' -import { useRef, useState } from 'react' +import { useXRSessionVisibilityState } from '@react-three/xr' +import { useEffect, useState } from 'react' import * as THREE from 'three' interface ColorChangingBoxProps { @@ -9,21 +8,18 @@ interface ColorChangingBoxProps { } export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { - const boxRef = useRef(null) const [color, setColor] = useState(new THREE.Color('blue')) + const visState = useXRSessionVisibilityState() - useFrame(() => { - const box = boxRef.current - if (box) { - // box.material.color = color + useEffect(() => { + if (visState === 'hidden') { + setColor(new THREE.Color(Math.random() * 0xffffff)) } - }) + }, [visState]) return ( - - setColor(new THREE.Color(Math.random() * 0xffffff))}> - - - + + + ) } 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 index e88ef784..12959f25 100644 --- a/examples/guards/app.tsx +++ b/examples/guards/app.tsx @@ -2,11 +2,13 @@ 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 }) @@ -25,7 +27,9 @@ export function App() { - + + + From fa43754ac02b0f43e5871596c2f52361cf566d7c Mon Sep 17 00:00:00 2001 From: Kailean O'Keefe Date: Fri, 14 Nov 2025 04:57:17 -0700 Subject: [PATCH 10/18] docs: Final touch ups and optimizations before submitting a PR --- docs/tutorials/guards.md | 101 ++++++++++++++++++++++--------------- examples/guards/app.tsx | 8 ++- examples/guards/styles.css | 13 ++++- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 687e4013..59eab927 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -4,22 +4,23 @@ description: Render and show parts of your application conditionally using guard nav: 20 --- -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 guards provided by `@react-three/xr`. +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 `App.css` add some basic styling:: +In `App.tsx`, set up a basic scene, and in `styles.css` add some basic styling:: **App.tsx:** ```tsx -import { Box } from '@react-three/drei' +import { OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' +import * as THREE from 'three' import { createXRStore, XR } from '@react-three/xr' import './styles.css' -const store = createXRStore({ offerSession: false }) +const store = createXRStore({ offerSession: false, emulate: false }) const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') @@ -34,6 +35,7 @@ export function App() { + + - + ) diff --git a/examples/guards/styles.css b/examples/guards/styles.css index 99ae8455..324cb3c2 100644 --- a/examples/guards/styles.css +++ b/examples/guards/styles.css @@ -41,8 +41,17 @@ button { padding: 1rem 2rem; cursor: pointer; font-size: 1.5rem; - bottom: 1rem; - left: 50%; 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 From b5a9626d00493e4098d53ac5e3a52be24631b6f7 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 14 Nov 2025 13:51:53 +0100 Subject: [PATCH 11/18] docs: Fixed a final formatting issue with the tutorial --- docs/tutorials/guards.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index 59eab927..1371df41 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -6,7 +6,7 @@ nav: 20 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 +### 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` @@ -94,7 +94,7 @@ button { } ``` -# Our First Guard +### Our First Guard Already in our application we have something worth putting a guard on. We have an XR scene already to go with an 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:** @@ -288,7 +288,7 @@ The best way to test this guard is to run the application in a VR session, then > Some VR devices may count bringing up the system menu as "blurring" the session rather than hiding it. If this happens to you, you can get the same effect by using the `useXRSessionVisibilityState` hook which we will cover in the next section, and checking for 'visible-blurred' instead of 'hidden'👍 -# Hooks +### 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 @@ -481,5 +481,5 @@ import { SupportedSessionModesPanel } from './SupportedSessionModesPanel.js' //... Previous code ``` -# Conclusion +### 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! From ae2fdb201dcfaa1845459433a75890d51de87ac7 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 14 Nov 2025 13:55:30 +0100 Subject: [PATCH 12/18] chore: PR feedback cleanup. Didn't mean to commit these --- packages/react/xr/src/guard/facing-camera.tsx | 2 +- packages/react/xr/src/hooks.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/xr/src/guard/facing-camera.tsx b/packages/react/xr/src/guard/facing-camera.tsx index cb2ee36e..e2d1ae78 100644 --- a/packages/react/xr/src/guard/facing-camera.tsx +++ b/packages/react/xr/src/guard/facing-camera.tsx @@ -61,5 +61,5 @@ export function IfFacingCamera({ children, direction, angle = Math.PI / 2 }: Fac const ref = useRef(null) const [show, setShow] = useState(false) useIsFacingCamera(ref, setShow, direction, angle) - return {show ? children : null} + return show ? <>{children} : null } diff --git a/packages/react/xr/src/hooks.ts b/packages/react/xr/src/hooks.ts index 4da901eb..a25ace46 100644 --- a/packages/react/xr/src/hooks.ts +++ b/packages/react/xr/src/hooks.ts @@ -96,7 +96,6 @@ export function useXRSessionModeSupported(mode: XRSessionMode, onError?: (error: .isSessionSupported(mode) .then((isSupported) => { sessionSupported = isSupported - console.log('Session mode supported:', mode, isSupported) if (canceled) { return } From 6462290c4d05e0024b8f595762802a93fc5be724 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 14 Nov 2025 14:16:37 +0100 Subject: [PATCH 13/18] chore: Added the example to the workflow --- .github/workflows/static.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2d62eff2..155a9f25 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/guard 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/guard/dist/* ./public/examples/guard - name: Upload Artifact uses: actions/upload-artifact@v4 From cd1556f02267ce6d27710a2104d21e85a1dcb982 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 14 Nov 2025 14:25:11 +0100 Subject: [PATCH 14/18] chore: Adjusted path, added helpful links --- .github/workflows/static.yml | 4 ++-- docs/tutorials/guards.md | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 155a9f25..a5a7ed10 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -132,7 +132,7 @@ jobs: mkdir -p public/examples/hit-testing mkdir -p public/examples/uikit mkdir -p public/examples/portal - mkdir -p public/examples/guard + 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 @@ -145,7 +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/guard/dist/* ./public/examples/guard + 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 1371df41..cc883918 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -483,3 +483,6 @@ import { SupportedSessionModesPanel } from './SupportedSessionModesPanel.js' ### 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: [Guard Example](https://pmndrs.github.io/xr/examples/guards/)* +- *Full source code for this tutorial can be found here: [Guard Example Source](https://github.com/pmndrs/xr/tree/main/examples/guards)* From 494320899693d10671343b7a0f78fc7ab242a7ba Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 14 Nov 2025 14:31:54 +0100 Subject: [PATCH 15/18] chore: Restoring pnpm-lock --- pnpm-lock.yaml | 84 ++------------------------------------------------ 1 file changed, 3 insertions(+), 81 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0821e72..7084e7bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,15 +142,6 @@ importers: specifier: ^4.5.2 version: 4.5.6(@types/react@19.0.8)(react@19.0.0) - examples/guards: - dependencies: - '@react-three/uikit': - specifier: ^1.0.56 - version: 1.0.56(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(use-sync-external-store@1.4.0(react@19.0.0)) - '@react-three/xr': - specifier: workspace:~ - version: link:../../packages/react/xr - examples/handheld-ar: dependencies: '@react-three/xr': @@ -859,18 +850,12 @@ packages: '@pmndrs/msdfonts@0.8.7': resolution: {integrity: sha512-O+2xzQQeIV249SfuSJVsAIun2/9SnA9V2P90NRDxzRYVzI8smXk6na1iA7E/RsCvfpWFhyX3g9zaKdiTotavoA==} - '@pmndrs/msdfonts@1.0.56': - resolution: {integrity: sha512-E7dwr8+DbVe9LLR2vKycQgecX13NNNXu9E1brlhWfusGVwKBJ2njbko6PUvgObrHuYGApQYotXROW76sWSjhYA==} - '@pmndrs/pointer-events@6.6.1': resolution: {integrity: sha512-O7CMUtoCyEqVT5X0syjsRKazX+7Q7//zBKj5mjIfIP+nlcwvBYCvdRTgHf6cGqB7jRgXLCd/3LSEgdoAXys/3Q==} '@pmndrs/uikit-lucide@0.4.4': resolution: {integrity: sha512-us9lzwtG7gByDmYfqZ9KXeSnbMRWTuHXBdLWsCtmuu1mYk91UZlqsBdvLo0GsC8SDHz3S7Y49Hlgtsw4OcRZ/Q==} - '@pmndrs/uikit-pub-sub@1.0.56': - resolution: {integrity: sha512-K7SrjXEFec30NF1Nxg9dwRN90xqUSPSMgOyaoep77/PvKy2g+MyZhkQLFbhdBtwGOj1JhFK5zncvtZXltgrcoA==} - '@pmndrs/uikit@0.4.4': resolution: {integrity: sha512-AT4B0zp4KP96lqh6oam44VO/H49cOZjEm09+0XiVXIgzA721heUHXgI4FhWGb1qlcI4T/2FHMBaw5sXh1o1npw==} peerDependencies: @@ -891,11 +876,6 @@ packages: peerDependencies: three: '>=0.160' - '@pmndrs/uikit@1.0.56': - resolution: {integrity: sha512-Fh4uY4oP/vAMx6GTM5ZpXd484220ldBlZAuItgmWT4KeNML8YBBiZR0P/pOFt7AFgAt3yNWSkp/9Ya6Rui6a8w==} - peerDependencies: - three: '>=0.162' - '@preact/signals-core@1.8.0': resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} @@ -994,12 +974,6 @@ packages: '@react-three/fiber': '>=8' react: '>=18' - '@react-three/uikit@1.0.56': - resolution: {integrity: sha512-SrIWWPN1bMZe1YuEsefhfbdCU6/Ec9FgnVghbbx0VDS1DZjuh0NP+rMxs2qh+fxNVplEeeqwb8x5GryySJvRkA==} - peerDependencies: - '@react-three/fiber': '>=8' - react: '>=18' - '@rollup/rollup-android-arm-eabi@4.34.0': resolution: {integrity: sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==} cpu: [arm] @@ -3391,24 +3365,6 @@ packages: use-sync-external-store: optional: true - zustand@5.0.8: - resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3770,8 +3726,6 @@ snapshots: '@pmndrs/msdfonts@0.8.7': {} - '@pmndrs/msdfonts@1.0.56': {} - '@pmndrs/pointer-events@6.6.1': {} '@pmndrs/uikit-lucide@0.4.4(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': @@ -3781,10 +3735,6 @@ snapshots: - three - ts-node - '@pmndrs/uikit-pub-sub@1.0.56': - dependencies: - '@preact/signals-core': 1.8.0 - '@pmndrs/uikit@0.4.4(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: '@preact/signals-core': 1.8.0 @@ -3831,14 +3781,6 @@ snapshots: transitivePeerDependencies: - ts-node - '@pmndrs/uikit@1.0.56(three@0.173.0)': - dependencies: - '@pmndrs/msdfonts': 1.0.56 - '@pmndrs/uikit-pub-sub': 1.0.56 - '@preact/signals-core': 1.8.0 - three: 0.173.0 - yoga-layout: 3.2.1 - '@preact/signals-core@1.8.0': {} '@preact/signals@2.0.1(preact@10.25.4)': @@ -3930,7 +3872,7 @@ snapshots: '@react-three/uikit-default@0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: - '@react-three/uikit': 0.8.21(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@react-three/uikit': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) '@react-three/uikit-lucide': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) tunnel-rat: 0.1.2(@types/react@19.0.8)(react@19.0.0) transitivePeerDependencies: @@ -3954,7 +3896,7 @@ snapshots: '@react-three/uikit-lucide@0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: - '@react-three/uikit': 0.8.21(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@react-three/uikit': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) transitivePeerDependencies: - '@react-three/fiber' - '@types/react' @@ -4040,20 +3982,6 @@ snapshots: - three - ts-node - '@react-three/uikit@1.0.56(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(use-sync-external-store@1.4.0(react@19.0.0))': - dependencies: - '@pmndrs/uikit': 1.0.56(three@0.173.0) - '@preact/signals-core': 1.8.0 - '@react-three/fiber': 9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0) - react: 19.0.0 - suspend-react: 0.1.3(react@19.0.0) - zustand: 5.0.8(@types/react@19.0.8)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)) - transitivePeerDependencies: - - '@types/react' - - immer - - three - - use-sync-external-store - '@rollup/rollup-android-arm-eabi@4.34.0': optional: true @@ -6691,10 +6619,4 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 react: 19.0.0 - use-sync-external-store: 1.4.0(react@19.0.0) - - zustand@5.0.8(@types/react@19.0.8)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)): - optionalDependencies: - '@types/react': 19.0.8 - react: 19.0.0 - use-sync-external-store: 1.4.0(react@19.0.0) + use-sync-external-store: 1.4.0(react@19.0.0) \ No newline at end of file From da015784759a4afa406c079486b311d3cd982649 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 14 Nov 2025 14:32:45 +0100 Subject: [PATCH 16/18] chore: ugh --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7084e7bd..eeb3ade2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6619,4 +6619,4 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 react: 19.0.0 - use-sync-external-store: 1.4.0(react@19.0.0) \ No newline at end of file + use-sync-external-store: 1.4.0(react@19.0.0) From bfe8abafa4aa5fd441fd0e698b4c5d8219df4829 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 15 Nov 2025 00:23:50 +0100 Subject: [PATCH 17/18] docs: Updating the type doc and some minor wording tweaks --- docs/tutorials/guards.md | 6 ++--- packages/react/xr/src/guard/facing-camera.tsx | 23 ++++++++++++++++-- packages/react/xr/src/guard/focus.tsx | 16 +++++++++++++ packages/react/xr/src/guard/session-mode.tsx | 24 +++++++++++++++++++ .../react/xr/src/guard/session-supported.tsx | 16 +++++++++++++ packages/react/xr/src/hooks.ts | 22 +++++++++++++++-- 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index cc883918..f13925d1 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -95,7 +95,7 @@ button { ``` ### Our First Guard -Already in our application we have something worth putting a guard on. We have an XR scene already to go with an 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. +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 @@ -484,5 +484,5 @@ import { SupportedSessionModesPanel } from './SupportedSessionModesPanel.js' ### 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: [Guard Example](https://pmndrs.github.io/xr/examples/guards/)* -- *Full source code for this tutorial can be found here: [Guard Example Source](https://github.com/pmndrs/xr/tree/main/examples/guards)* +- *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/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) From f620bc62574adaf63dfacd9b090f653ac8387095 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 15 Nov 2025 00:46:20 +0100 Subject: [PATCH 18/18] docs: Fixed a bug in the example and made the very last final tweaks --- docs/tutorials/guards.md | 11 ++--------- examples/guards/ColorChangingBox.tsx | 2 +- examples/guards/Message.tsx | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/tutorials/guards.md b/docs/tutorials/guards.md index f13925d1..1112c329 100644 --- a/docs/tutorials/guards.md +++ b/docs/tutorials/guards.md @@ -129,7 +129,7 @@ interface MessageProps { export const Message = ({ message }: MessageProps) => { console.log('But I am still rendered no matter what!') return ( - + {message} @@ -284,10 +284,6 @@ import { ShyBox } from './ShyBox.js' 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. -> [!NOTE] -> Some VR devices may count bringing up the system menu as "blurring" the session rather than hiding it. If this happens to you, you can get the same effect by using the `useXRSessionVisibilityState` hook which we will cover in the next section, and checking for 'visible-blurred' instead of 'hidden'👍 - - ### 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. @@ -389,7 +385,7 @@ export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { const visState = useXRSessionVisibilityState() useEffect(() => { - if (visState === 'hidden') { + if (visState !== 'visible') { setColor(new THREE.Color(Math.random() * 0xffffff)) } }, [visState]) @@ -422,9 +418,6 @@ import { ColorChangingBox } from './ColorChangingBox.js' 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. -> [!NOTE] -> As mentioned earlier, some VR devices may count bringing up the system menu as "blurring" the session rather than hiding it. If you don't see the box change color, try checking for 'visible-blurred' in the `useEffect` instead of 'hidden'👍 - ### 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: diff --git a/examples/guards/ColorChangingBox.tsx b/examples/guards/ColorChangingBox.tsx index fabb413d..a8331a32 100644 --- a/examples/guards/ColorChangingBox.tsx +++ b/examples/guards/ColorChangingBox.tsx @@ -12,7 +12,7 @@ export const ColorChangingBox = ({ position }: ColorChangingBoxProps) => { const visState = useXRSessionVisibilityState() useEffect(() => { - if (visState === 'hidden') { + if (visState !== 'visible') { setColor(new THREE.Color(Math.random() * 0xffffff)) } }, [visState]) diff --git a/examples/guards/Message.tsx b/examples/guards/Message.tsx index 351e4986..4be6fe4d 100644 --- a/examples/guards/Message.tsx +++ b/examples/guards/Message.tsx @@ -7,7 +7,7 @@ interface MessageProps { export const Message = ({ message }: MessageProps) => { console.log('But I am still rendered no matter what!') return ( - + {message}