Skip to content

Commit 7f21494

Browse files
committed
Create separate example
Move some code
1 parent 54ff18e commit 7f21494

File tree

11 files changed

+194
-91
lines changed

11 files changed

+194
-91
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
22
dist
33
package-lock.json
4-
.DS_Store
4+
.DS_Store
5+
.idea
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Canvas, createPortal } from '@react-three/fiber'
2+
import {
3+
useHover,
4+
createXRStore,
5+
XR,
6+
XROrigin,
7+
TeleportTarget,
8+
XRControllerGamepadComponentId,
9+
XRControllerState,
10+
useLoadXRControllerLayout,
11+
useLoadXRControllerModel,
12+
getXRControllerComponentObject,
13+
} from '@react-three/xr'
14+
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react'
15+
import { Mesh, Object3D, Vector3 } from 'three'
16+
17+
const store = createXRStore({
18+
hand: { teleportPointer: true },
19+
controller: { teleportPointer: true },
20+
})
21+
22+
export function App() {
23+
const [position, setPosition] = useState(new Vector3())
24+
return (
25+
<>
26+
<button onClick={() => store.enterVR()}>Enter VR</button>
27+
<button onClick={() => store.enterAR()}>Enter AR</button>
28+
<Canvas style={{ width: '100%', flexGrow: 1 }}>
29+
<XR store={store}>
30+
<ambientLight />
31+
<XROrigin position={position} />
32+
<Cube />
33+
34+
<group scale={10} position={[0, 0, 4]} rotation={[Math.PI / 2, 0, -Math.PI / 4]}>
35+
<DemoController />
36+
</group>
37+
<TeleportTarget onTeleport={setPosition}>
38+
<mesh scale={[10, 1, 10]} position={[0, -0.5, 0]}>
39+
<boxGeometry />
40+
<meshBasicMaterial color="green" />
41+
</mesh>
42+
</TeleportTarget>
43+
</XR>
44+
</Canvas>
45+
</>
46+
)
47+
}
48+
49+
const unboundControllerContext = createContext<XRControllerState | undefined>(undefined)
50+
51+
export const UnboundController: FC<PropsWithChildren<{ profileIds: string[] }>> = ({ profileIds, children }) => {
52+
const layout = useLoadXRControllerLayout(['meta-quest-touch-plus'], 'right')
53+
const model = useLoadXRControllerModel(layout)
54+
55+
return model ? (
56+
<unboundControllerContext.Provider value={{ model, layout }}>
57+
<primitive object={model} />
58+
{children}
59+
</unboundControllerContext.Provider>
60+
) : null
61+
}
62+
63+
export const UnboundControllerComponent: FC<PropsWithChildren<{ id: XRControllerGamepadComponentId }>> = ({
64+
id,
65+
children,
66+
}) => {
67+
const [object, setObject] = useState<Object3D | undefined>(undefined)
68+
const { model, layout } = useContext(unboundControllerContext)
69+
70+
useEffect(() => {
71+
if (!model) {
72+
return
73+
}
74+
const component = getXRControllerComponentObject(model, layout, id)
75+
76+
setObject(component?.object)
77+
}, [model, layout, id])
78+
return object ? createPortal(children, object) : null
79+
}
80+
81+
function DemoController() {
82+
return (
83+
<UnboundController profileIds={['meta-quest-touch-plus']}>
84+
<UnboundControllerComponent id={'a-button'}>
85+
<mesh>
86+
<sphereGeometry args={[0.01]} />
87+
<meshBasicMaterial color={0xff0000} />
88+
</mesh>
89+
</UnboundControllerComponent>
90+
<UnboundControllerComponent id={'b-button'}>
91+
<mesh>
92+
<sphereGeometry args={[0.01]} />
93+
<meshBasicMaterial color={0xff0000} />
94+
</mesh>
95+
</UnboundControllerComponent>
96+
</UnboundController>
97+
)
98+
}
99+
100+
function Cube() {
101+
const ref = useRef<Mesh>(null)
102+
const hover = useHover(ref)
103+
return (
104+
<mesh
105+
onClick={() => store.setHand({ rayPointer: { cursorModel: { color: 'green' } } }, 'right')}
106+
position={[0, 2, 0]}
107+
pointerEventsType={{ deny: 'grab' }}
108+
ref={ref}
109+
>
110+
<boxGeometry />
111+
<meshBasicMaterial color={hover ? 'red' : 'blue'} />
112+
</mesh>
113+
)
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
<script async type="module" src="./index.tsx"></script>
8+
</head>
9+
<body style="touch-action: none; margin: 0; position: relative; width: 100dvw; height: 100dvh; overflow: hidden;">
10+
<div id="root" style="position: absolute; inset: 0; display: flex; flex-direction: column;"></div>
11+
</body>
12+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRoot } from 'react-dom/client'
2+
import { App } from './app.js'
3+
import { StrictMode } from 'react'
4+
5+
createRoot(document.getElementById('root')!).render(
6+
<StrictMode>
7+
<App />
8+
</StrictMode>,
9+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"dependencies": {
3+
"@react-three/xr": "workspace:^"
4+
},
5+
"scripts": {
6+
"dev": "vite --host"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig } from 'vite'
2+
import path from 'path'
3+
import react from '@vitejs/plugin-react'
4+
import basicSsl from '@vitejs/plugin-basic-ssl'
5+
6+
// https://vitejs.dev/config/
7+
export default defineConfig({
8+
plugins: [react(), basicSsl()],
9+
resolve: {
10+
alias: [{ find: '@react-three/xr', replacement: path.resolve(__dirname, '../../packages/react/xr/src/index.ts') }],
11+
dedupe: ['@react-three/fiber', 'three'],
12+
},
13+
})

examples/react-three-xr/app.tsx

-22
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ export function App() {
2727
<ambientLight />
2828
<XROrigin position={position} />
2929
<Cube />
30-
<group scale={10} position={[0, 0, 4]} rotation={[Math.PI / 2, 0, -Math.PI / 4]}>
31-
<DemoController />
32-
</group>
3330
<TeleportTarget onTeleport={setPosition}>
3431
<mesh scale={[10, 1, 10]} position={[0, -0.5, 0]}>
3532
<boxGeometry />
@@ -42,25 +39,6 @@ export function App() {
4239
)
4340
}
4441

45-
function DemoController() {
46-
return (
47-
<UnboundController profileIds={['meta-quest-touch-plus']}>
48-
<UnboundControllerComponent id={'a-button'}>
49-
<mesh>
50-
<sphereGeometry args={[0.01]} />
51-
<meshBasicMaterial color={0xff0000} />
52-
</mesh>
53-
</UnboundControllerComponent>
54-
<UnboundControllerComponent id={'b-button'}>
55-
<mesh>
56-
<sphereGeometry args={[0.01]} />
57-
<meshBasicMaterial color={0xff0000} />
58-
</mesh>
59-
</UnboundControllerComponent>
60-
</UnboundController>
61-
)
62-
}
63-
6442
function Cube() {
6543
const ref = useRef<Mesh>(null)
6644
const hover = useHover(ref)

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
"three": "^0.167.1",
2828
"typescript": "^5.5.4",
2929
"vite": "^5.2.11"
30-
}
30+
},
31+
"packageManager": "[email protected]+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
3132
}

packages/react/xr/src/controller.tsx

+7-61
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
1-
import {
2-
ReactNode,
3-
forwardRef,
4-
useImperativeHandle,
5-
useMemo,
6-
useRef,
7-
useState,
8-
useEffect,
9-
FC,
10-
createContext,
11-
useContext,
12-
} from 'react'
1+
import { ReactNode, forwardRef, useImperativeHandle, useMemo, useRef, useState, useEffect } from 'react'
132
import { suspend } from 'suspend-react'
143
import {
15-
XRControllerGamepadComponentId,
16-
XRControllerGamepadComponentState,
17-
XRControllerModelOptions,
18-
XRControllerState,
194
configureXRControllerModel,
205
createUpdateXRControllerVisuals,
216
loadXRControllerModel,
7+
XRControllerGamepadComponentId,
8+
XRControllerGamepadComponentState,
229
XRControllerLayout,
2310
XRControllerLayoutLoader,
2411
XRControllerLayoutLoaderOptions,
12+
XRControllerModelOptions,
13+
XRControllerState,
2514
} from '@pmndrs/xr/internals'
2615
import { createPortal, useFrame } from '@react-three/fiber'
2716
import { Object3D } from 'three'
28-
import { useXRInputSourceState, useXRInputSourceStateContext } from './input.js'
17+
import { useXRInputSourceStateContext } from './input.js'
2918

3019
/**
3120
* component for placing content in the controller anchored at a specific component such as the Thumbstick
@@ -90,7 +79,7 @@ const LoadXRControllerModelSymbol = Symbol('loadXRControllerModel')
9079
*/
9180
export const XRControllerModel = forwardRef<Object3D, XRControllerModelOptions>((options, ref) => {
9281
const state = useXRInputSourceStateContext('controller')
93-
const model = useLoadXRControllerModel(state.layout)
82+
const model = suspend(loadXRControllerModel, [state.layout, undefined, LoadXRControllerModelSymbol])
9483
configureXRControllerModel(model, options)
9584
state.object = model
9685
useImperativeHandle(ref, () => model, [model])
@@ -127,46 +116,3 @@ export function useLoadXRControllerModel(layout: XRControllerLayout) {
127116
[layout, undefined, LoadXRControllerModelSymbol],
128117
)
129118
}
130-
131-
export function getXRControllerComponentObject(
132-
model: Object3D,
133-
layout: XRControllerLayout,
134-
componentId: XRControllerGamepadComponentId,
135-
) {
136-
const component = layout.components[componentId]
137-
// TODO: Add support for providing gamepad state
138-
const firstVisualResponse = component.visualResponses[Object.keys(component.visualResponses)[0]]
139-
if (!firstVisualResponse) return
140-
const valueNode = model.getObjectByName(firstVisualResponse.valueNodeName)
141-
142-
return { object: valueNode }
143-
}
144-
145-
const unboundControllerContext = createContext<XRControllerState | undefined>(undefined)
146-
147-
export const UnboundController: FC<{ profileIds: string[] }> = ({ profileIds, children }) => {
148-
const layout = useLoadXRControllerLayout(['meta-quest-touch-plus'], 'right')
149-
const model = useLoadXRControllerModel(layout)
150-
151-
return model ? (
152-
<unboundControllerContext.Provider value={{ model, layout }}>
153-
<primitive object={model} />
154-
{children}
155-
</unboundControllerContext.Provider>
156-
) : null
157-
}
158-
159-
export const UnboundControllerComponent: FC = ({ id, children }) => {
160-
const [object, setObject] = useState<Object3D | undefined>(undefined)
161-
const { model, layout } = useContext(unboundControllerContext)
162-
163-
useEffect(() => {
164-
if (!model) {
165-
return
166-
}
167-
const component = getXRControllerComponentObject(model, layout, id)
168-
169-
setObject(component?.object)
170-
}, [model, layout, id])
171-
return object ? createPortal(children, object) : null
172-
}

packages/react/xr/src/xr.tsx

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import {
2-
XRState as BaseXRState,
3-
XRStore as BaseXRStore,
4-
XRStoreOptions as BaseXRStoreOptions,
52
createXRStore as createXRStoreImpl,
6-
DefaultXRHandOptions,
73
DefaultXRControllerOptions,
8-
DefaultXRTransientPointerOptions,
94
DefaultXRGazeOptions,
5+
DefaultXRHandOptions,
106
DefaultXRScreenInputOptions,
7+
DefaultXRTransientPointerOptions,
8+
XRControllerGamepadComponentId,
9+
XRControllerLayout,
10+
XRState as BaseXRState,
11+
XRStore as BaseXRStore,
12+
XRStoreOptions as BaseXRStoreOptions,
1113
} from '@pmndrs/xr/internals'
12-
import { Camera, useFrame, useThree, useStore as useRootStore } from '@react-three/fiber'
14+
import { Camera, useFrame, useStore as useRootStore, useThree } from '@react-three/fiber'
1315
import { ComponentType, ReactNode, useContext, useEffect } from 'react'
1416
import { useStore } from 'zustand'
1517
import { xrContext } from './contexts.js'
1618
import { XRElements } from './elements.js'
19+
import { Object3D } from 'three'
1720

1821
type XRElementImplementation = {
1922
/**
@@ -123,3 +126,20 @@ export function useXR<T = XRState>(
123126
) {
124127
return useStore(useXRStore(), selector, equalityFn)
125128
}
129+
130+
/**
131+
* function for getting the object of a specific component from the xr controller model
132+
*/
133+
export function getXRControllerComponentObject(
134+
model: Object3D,
135+
layout: XRControllerLayout,
136+
componentId: XRControllerGamepadComponentId,
137+
) {
138+
const component = layout.components[componentId]
139+
// TODO: Add support for providing gamepad state
140+
const firstVisualResponse = component.visualResponses[Object.keys(component.visualResponses)[0]]
141+
if (!firstVisualResponse) return
142+
const valueNode = model.getObjectByName(firstVisualResponse.valueNodeName)
143+
144+
return { object: valueNode }
145+
}

0 commit comments

Comments
 (0)