Skip to content

Commit c9cd905

Browse files
committed
feat: react-three/xr: anchors, dom overlay, hit tes
1 parent 8005cb1 commit c9cd905

File tree

14 files changed

+369
-84
lines changed

14 files changed

+369
-84
lines changed

packages/react/xr/package.json

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,49 @@
11
{
2-
"name": "@react-three/xr",
3-
"description": "VR/AR for react-three-fiber",
4-
"author": "Bela Bohlender",
5-
"license": "SEE LICENSE IN LICENSE",
6-
"homepage": "https://github.com/pmndrs/xr",
7-
"type": "module",
8-
"keywords": [
9-
"r3f",
10-
"xr",
11-
"ar",
12-
"vr",
13-
"three.js",
14-
"react",
15-
"typescript"
16-
],
17-
"repository": {
18-
"type": "git",
19-
"url": "[email protected]:pmndrs/xr.git"
20-
},
21-
"files": [
22-
"dist"
23-
],
24-
"main": "dist/index.js",
25-
"dependencies": {
26-
"@pmndrs/xr": "workspace:^",
27-
"suspend-react": "^0.1.3",
28-
"zustand": "^4.5.2",
29-
"@pmndrs/pointer-events": "workspace:^"
30-
},
31-
"scripts": {
32-
"build": "tsc",
33-
"check:prettier": "prettier --check src",
34-
"check:eslint": "eslint 'src/**/*.ts'",
35-
"fix:prettier": "prettier --write src",
36-
"fix:eslint": "eslint 'src/**/*.ts' --fix"
37-
},
38-
"devDependencies": {
39-
"@vitejs/plugin-react": "^4.3.0",
40-
"vite": "^5.2.11"
41-
}
42-
}
2+
"name": "@react-three/xr",
3+
"description": "VR/AR for react-three-fiber",
4+
"author": "Bela Bohlender",
5+
"license": "SEE LICENSE IN LICENSE",
6+
"homepage": "https://github.com/pmndrs/xr",
7+
"type": "module",
8+
"keywords": [
9+
"r3f",
10+
"xr",
11+
"ar",
12+
"vr",
13+
"three.js",
14+
"react",
15+
"typescript"
16+
],
17+
"repository": {
18+
"type": "git",
19+
"url": "[email protected]:pmndrs/xr.git"
20+
},
21+
"files": [
22+
"dist"
23+
],
24+
"main": "dist/index.js",
25+
"peerDependencies": {
26+
"@react-three/fiber": ">=8",
27+
"react": ">=18",
28+
"react-dom": ">=18",
29+
"three": "*"
30+
},
31+
"dependencies": {
32+
"@pmndrs/pointer-events": "workspace:^",
33+
"@pmndrs/xr": "workspace:^",
34+
"suspend-react": "^0.1.3",
35+
"tunnel-rat": "^0.1.2",
36+
"zustand": "^4.5.2"
37+
},
38+
"scripts": {
39+
"build": "tsc",
40+
"check:prettier": "prettier --check src",
41+
"check:eslint": "eslint 'src/**/*.ts'",
42+
"fix:prettier": "prettier --write src",
43+
"fix:eslint": "eslint 'src/**/*.ts' --fix"
44+
},
45+
"devDependencies": {
46+
"@vitejs/plugin-react": "^4.3.0",
47+
"vite": "^5.2.11"
48+
}
49+
}

packages/react/xr/src/anchor.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { requestXRAnchor, XRAnchorOptions } from '@pmndrs/xr'
2+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
3+
import { useXR, useXRStore } from './xr.js'
4+
5+
export {
6+
requestXRAnchor,
7+
type XRAnchorOptions,
8+
//requestXRPersistentAnchor,
9+
//loadXRPersistentAnchor,
10+
//deleteXRPersistentAnchor,
11+
} from '@pmndrs/xr'
12+
13+
/**
14+
* hook that returns a function that allows to request a xr anchor
15+
*/
16+
export function useRequestXRAnchor() {
17+
const store = useXRStore()
18+
return useMemo(() => requestXRAnchor.bind(null, store), [store])
19+
}
20+
21+
/**
22+
* hook that returns a function that allows to request a xr persistent anchor
23+
*
24+
export function useRequestXRPersistentAnchor() {
25+
const store = useXRStore()
26+
return useMemo(() => requestXRPersistentAnchor.bind(null, store), [store])
27+
}*/
28+
29+
/**
30+
* hook that returns a function that allows to load a xr persistent anchor
31+
*
32+
export function useLoadXRPersistentAnchor() {
33+
const session = useXR((xr) => xr.session)
34+
return useMemo(() => (session != null ? loadXRPersistentAnchor.bind(null, session) : undefined), [session])
35+
}*/
36+
37+
/**
38+
* hook that returns a function that allows to delete a xr persistent anchor
39+
*
40+
export function useDeleteXRPersistentAnchor() {
41+
const store = useXRStore()
42+
return useMemo(() => deleteXRPersistentAnchor.bind(null, store), [store])
43+
}*/
44+
45+
/*
46+
export function useXRPersistentAnchor(
47+
id: string,
48+
): [anchor: XRAnchor | undefined, createAnchor: (options: XRAnchorOptions) => Promise<XRAnchor | undefined>] {
49+
const cleanup = useRef<(() => void) | undefined>(() => {})
50+
const store = useXRStore()
51+
const session = useXR((xr) => xr.session)
52+
const [anchor, setAnchor] = useState<XRAnchor | undefined>(undefined)
53+
useEffect(() => {
54+
if (session == null) {
55+
return
56+
}
57+
cleanup.current?.()
58+
cleanup.current = undefined
59+
let cancelled = false
60+
cleanup.current = () => (cancelled = true)
61+
loadXRPersistentAnchor(session, id).then((anchor) => {
62+
if (cancelled) {
63+
anchor?.delete()
64+
return
65+
}
66+
cleanup.current = () => anchor?.delete()
67+
setAnchor(anchor)
68+
})
69+
return () => {
70+
cleanup.current?.()
71+
cleanup.current = undefined
72+
}
73+
}, [session, id])
74+
const create = useCallback(
75+
async (options: XRAnchorOptions) => {
76+
await deleteXRPersistentAnchor(store, id)
77+
cleanup.current?.()
78+
cleanup.current = undefined
79+
const abortRef = { current: false }
80+
cleanup.current = () => (abortRef.current = true)
81+
const anchor = await requestXRPersistentAnchor(store, id, options, abortRef)
82+
if (abortRef.current) {
83+
anchor?.delete()
84+
return undefined
85+
}
86+
cleanup.current = () => anchor?.delete()
87+
setAnchor(anchor)
88+
return anchor
89+
},
90+
[id, store],
91+
)
92+
return [anchor, create]
93+
}*/
94+
95+
/**
96+
* hook for requesting and storing a single xr anchor
97+
*/
98+
export function useXRAnchor(): [
99+
anchor: XRAnchor | undefined,
100+
createAnchor: (options: XRAnchorOptions) => Promise<XRAnchor | undefined>,
101+
] {
102+
const [anchor, setAnchor] = useState<XRAnchor | undefined>(undefined)
103+
const cleanup = useRef<(() => void) | undefined>(() => {})
104+
const store = useXRStore()
105+
const create = useCallback(
106+
async (options: XRAnchorOptions) => {
107+
cleanup.current?.()
108+
cleanup.current = undefined
109+
let cancelled = false
110+
cleanup.current = () => (cancelled = true)
111+
const anchor = await requestXRAnchor(store, options)
112+
if (cancelled) {
113+
anchor?.delete()
114+
return undefined
115+
}
116+
cleanup.current = () => anchor?.delete()
117+
setAnchor(anchor)
118+
return anchor
119+
},
120+
[store],
121+
)
122+
useEffect(() => () => void cleanup.current?.(), [])
123+
return [anchor, create]
124+
}

packages/react/xr/src/contexts.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GetXRSpace, XRInputSourceState } from '@pmndrs/xr/internals'
1+
import { XRInputSourceState } from '@pmndrs/xr/internals'
22
import { createContext } from 'react'
33
import { XRStore } from './xr.js'
44
import { CombinedPointer } from '@pmndrs/pointer-events'
@@ -7,5 +7,5 @@ export const xrContext = createContext<XRStore | undefined>(undefined)
77
export const xrMeshContext = createContext<XRMesh | undefined>(undefined)
88
export const xrPlaneContext = createContext<XRPlane | undefined>(undefined)
99
export const xrInputSourceStateContext = createContext<XRInputSourceState | undefined>(undefined)
10-
export const xrReferenceSpaceContext = createContext<GetXRSpace | undefined>(undefined)
10+
export const xrReferenceSpaceContext = createContext<XRSpace | undefined>(undefined)
1111
export const combinedPointerContext = createContext<CombinedPointer | undefined>(undefined)

packages/react/xr/src/default.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
GetXRSpace,
32
defaultGrabPointerOpacity,
43
defaultRayPointerOpacity,
54
defaultTouchPointerOpacity,
@@ -25,11 +24,11 @@ import {
2524
PointerCursorModel,
2625
PointerRayModel,
2726
useGrabPointer,
28-
usePointerXRSessionEvent,
27+
usePointerXRInputSourceEvents,
2928
useRayPointer,
3029
useTouchPointer,
3130
} from './pointer.js'
32-
import { XRSpace } from './space.js'
31+
import { XRSpace as XRSpaceImpl } from './space.js'
3332
import { xrInputSourceStateContext } from './contexts.js'
3433
import { TeleportPointerRayModel } from './teleport.js'
3534
import { createPortal, useFrame, useThree } from '@react-three/fiber'
@@ -50,7 +49,7 @@ export {
5049

5150
function DefaultXRInputSourceGrabPointer(
5251
event: 'select' | 'squeeze',
53-
getSpace: (source: XRInputSource) => GetXRSpace,
52+
getSpace: (source: XRInputSource) => XRSpace,
5453
options: DefaultXRInputSourceGrabPointerOptions,
5554
) {
5655
const state = useContext(xrInputSourceStateContext)
@@ -59,14 +58,14 @@ function DefaultXRInputSourceGrabPointer(
5958
}
6059
const ref = useRef<Object3D>(null)
6160
const pointer = useGrabPointer(ref, state, options)
62-
usePointerXRSessionEvent(pointer, state.inputSource, event)
61+
usePointerXRInputSourceEvents(pointer, state.inputSource, event, state.events)
6362
const cursorModelOptions = options.cursorModel
6463
return (
65-
<XRSpace ref={ref} space={getSpace(state.inputSource)}>
64+
<XRSpaceImpl ref={ref} space={getSpace(state.inputSource)}>
6665
{cursorModelOptions !== false && (
6766
<PointerCursorModel pointer={pointer} opacity={defaultGrabPointerOpacity} {...spreadable(cursorModelOptions)} />
6867
)}
69-
</XRSpace>
68+
</XRSpaceImpl>
7069
)
7170
}
7271

@@ -84,7 +83,7 @@ function DefaultXRInputSourceGrabPointer(
8483
export const DefaultXRHandGrabPointer = DefaultXRInputSourceGrabPointer.bind(
8584
null,
8685
'select',
87-
(inputSource) => () => inputSource.hand!.get('index-finger-tip'),
86+
(inputSource) => inputSource.hand!.get('index-finger-tip')!,
8887
)
8988

9089
/**
@@ -126,18 +125,18 @@ export function DefaultXRInputSourceRayPointer(options: DefaultXRInputSourceRayP
126125
}
127126
const ref = useRef<Object3D>(null)
128127
const pointer = useRayPointer(ref, state, options)
129-
usePointerXRSessionEvent(pointer, state.inputSource, 'select')
128+
usePointerXRInputSourceEvents(pointer, state.inputSource, 'select', state.events)
130129
const rayModelOptions = options.rayModel
131130
const cursorModelOptions = options.cursorModel
132131
return (
133-
<XRSpace ref={ref} space={state.inputSource.targetRaySpace}>
132+
<XRSpaceImpl ref={ref} space={state.inputSource.targetRaySpace}>
134133
{rayModelOptions !== false && (
135134
<PointerRayModel pointer={pointer} opacity={defaultRayPointerOpacity} {...spreadable(rayModelOptions)} />
136135
)}
137136
{cursorModelOptions !== false && (
138137
<PointerCursorModel pointer={pointer} opacity={defaultRayPointerOpacity} {...spreadable(cursorModelOptions)} />
139138
)}
140-
</XRSpace>
139+
</XRSpaceImpl>
141140
)
142141
}
143142

@@ -160,15 +159,15 @@ export function DefaultXRHandTouchPointer(options: DefaultXRHandTouchPointerOpti
160159
const pointer = useTouchPointer(ref, state, options)
161160
const cursorModelOptions = options.cursorModel
162161
return (
163-
<XRSpace ref={ref} space={() => state.inputSource.hand.get('index-finger-tip')}>
162+
<XRSpaceImpl ref={ref} space={state.inputSource.hand.get('index-finger-tip')!}>
164163
{cursorModelOptions !== false && (
165164
<PointerCursorModel
166165
pointer={pointer}
167166
opacity={defaultTouchPointerOpacity}
168167
{...spreadable(cursorModelOptions)}
169168
/>
170169
)}
171-
</XRSpace>
170+
</XRSpaceImpl>
172171
)
173172
}
174173

@@ -329,7 +328,7 @@ export function DefaultXRInputSourceTeleportPointer(options: DefaultXRInputSourc
329328
},
330329
'teleport',
331330
)
332-
usePointerXRSessionEvent(pointer, state.inputSource, 'select')
331+
usePointerXRInputSourceEvents(pointer, state.inputSource, 'select', state.events)
333332
const rayModelOptions = options.rayModel
334333
const cursorModelOptions = options.cursorModel
335334
const scene = useThree((state) => state.scene)
@@ -347,7 +346,7 @@ export function DefaultXRInputSourceTeleportPointer(options: DefaultXRInputSourc
347346
})
348347
return (
349348
<>
350-
<XRSpace ref={ref} space={state.inputSource.targetRaySpace} />
349+
<XRSpaceImpl ref={ref} space={state.inputSource.targetRaySpace} />
351350
{createPortal(
352351
<group ref={groupRef}>
353352
{rayModelOptions !== false && (

packages/react/xr/src/dom-overlay.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useXR } from './xr.js'
2+
import { forwardRef, HTMLProps, useEffect, useMemo } from 'react'
3+
import { createRoot } from 'react-dom/client'
4+
import tunnel from 'tunnel-rat'
5+
6+
/**
7+
* component to render html elements as overlay for handheld AR experiences
8+
*/
9+
export const XRDomOverlay = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>((props, ref) => {
10+
const domOverlayRoot = useXR((xr) => xr.domOverlayRoot)
11+
const { In, Out } = useMemo(tunnel, [])
12+
useEffect(() => {
13+
const root = createRoot(domOverlayRoot)
14+
root.render(<Out />)
15+
return () => root.unmount()
16+
}, [domOverlayRoot, Out])
17+
return (
18+
<In>
19+
<div {...props} ref={ref} />
20+
</In>
21+
)
22+
})

0 commit comments

Comments
 (0)