Skip to content

Commit 1109050

Browse files
committed
feat: update foward events only once per frame per pointer
1 parent f4b5f2d commit 1109050

File tree

14 files changed

+109
-82
lines changed

14 files changed

+109
-82
lines changed

examples/hit-test-anchor/app.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useXRAnchor,
1313
useXRInputSourceStateContext,
1414
} from '@react-three/xr'
15+
import { noEvents } from '@react-three/xr/src'
1516
import { useEffect, useRef } from 'react'
1617
import { Matrix4, Mesh, Vector3 } from 'three'
1718
import { create } from 'zustand'
@@ -78,7 +79,7 @@ export function App() {
7879
return (
7980
<>
8081
<button onClick={() => store.enterAR()}>Enter AR</button>
81-
<Canvas style={{ width: '100%', flexGrow: 1 }}>
82+
<Canvas events={noEvents} style={{ width: '100%', flexGrow: 1 }}>
8283
<XR store={store}>
8384
<ambientLight />
8485
{new Array(leftLength).fill(undefined).map((_, i) => (

examples/layers/app.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Canvas, useThree } from '@react-three/fiber'
2-
import { createXRStore, useHover, XR, XRLayer, XROrigin } from '@react-three/xr'
1+
import { Canvas } from '@react-three/fiber'
2+
import { PointerEvents, noEvents, createXRStore, XR, XRLayer, XROrigin } from '@react-three/xr'
33
import { Text } from '@react-three/drei'
4-
import { useEffect, useMemo, useRef } from 'react'
5-
import { Mesh, SRGBColorSpace, VideoTexture } from 'three'
6-
import { forwardHtmlEvents } from '@pmndrs/pointer-events'
4+
import { useMemo } from 'react'
5+
import { SRGBColorSpace, VideoTexture } from 'three'
76

87
const store = createXRStore({
98
foveation: 0,
@@ -25,11 +24,11 @@ export function App() {
2524
<>
2625
<button onClick={() => store.enterAR()}>Enter AR</button>
2726
<Canvas
28-
events={() => ({ enabled: false, priority: 0 })}
27+
events={noEvents}
2928
style={{ width: '100%', flexGrow: 1 }}
3029
camera={{ position: [0, 0, 0], rotation: [0, 0, 0] }}
3130
>
32-
<SwitchToXRPointerEvents />
31+
<PointerEvents />
3332
<XR store={store}>
3433
<Text scale={0.03} color="black" position={[-0.6, 0.28, -0.5]}>
3534
32x32 XRLayer with DPR=32
@@ -67,11 +66,3 @@ export function App() {
6766
</>
6867
)
6968
}
70-
71-
export function SwitchToXRPointerEvents() {
72-
const domElement = useThree((s) => s.gl.domElement)
73-
const camera = useThree((s) => s.camera)
74-
const scene = useThree((s) => s.scene)
75-
useEffect(() => forwardHtmlEvents(domElement, () => camera, scene), [domElement, camera, scene])
76-
return null
77-
}

examples/pointer-events/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ getVoidObject(scene).addEventListener('click', () => console.log('click outer'))
5050

5151
const canvas = document.getElementById('root') as HTMLCanvasElement
5252

53-
forwardHtmlEvents(canvas, () => camera, scene)
54-
forwardObjectEvents(plane, () => innerCamera, innerScene)
53+
const { update: updateForwardHtmlEvents } = forwardHtmlEvents(canvas, () => camera, scene)
54+
const { update: updateForwardObjectEvents } = forwardObjectEvents(plane, () => innerCamera, innerScene)
5555

5656
const renderer = new WebGLRenderer({ antialias: true, canvas })
5757

5858
renderer.setAnimationLoop((time) => {
59+
updateForwardHtmlEvents()
60+
updateForwardObjectEvents()
5961
box.rotation.y = time * 0.0001
6062
plane.rotation.y = Math.sin(time * 0.001) * 0.3
6163
renderer.setRenderTarget(frambuffer)

examples/rag-doll/src/App.jsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { Canvas, useThree } from '@react-three/fiber'
1+
import { Canvas } from '@react-three/fiber'
22
import { OrbitControls } from '@react-three/drei'
33
import { Physics, usePlane } from '@react-three/cannon'
44
import { Cursor } from './helpers/Drag.js'
55
import { Guy } from './components/Guy.jsx'
66
import { Mug, Chair, Table, Lamp } from './components/Furniture.jsx'
7-
import { createXRStore, useControllerLocomotion, XR, XROrigin } from '@react-three/xr'
8-
import { useRef, Suspense, useEffect } from 'react'
9-
import { Group } from 'three'
10-
import { forwardHtmlEvents } from '@pmndrs/pointer-events'
7+
import { createXRStore, noEvents, useControllerLocomotion, XR, XROrigin, PointerEvents } from '@react-three/xr'
8+
import { useRef, Suspense } from 'react'
119

1210
const store = createXRStore({
1311
hand: { touchPointer: false },
@@ -42,10 +40,10 @@ export function App() {
4240
onPointerMissed={() => console.log('missed')}
4341
dpr={[1, 2]}
4442
shadows
45-
events={() => ({ enabled: false, priority: 0 })}
43+
events={noEvents}
4644
camera={{ position: [-40, 40, 40], fov: 25 }}
4745
>
48-
<SwitchToXRPointerEvents />
46+
<PointerEvents />
4947
<OrbitControls />
5048
<XR store={store}>
5149
<color attach="background" args={['#171720']} />
@@ -97,11 +95,3 @@ function Floor(props) {
9795
</mesh>
9896
)
9997
}
100-
101-
export function SwitchToXRPointerEvents() {
102-
const domElement = useThree((s) => s.gl.domElement)
103-
const camera = useThree((s) => s.camera)
104-
const scene = useThree((s) => s.scene)
105-
useEffect(() => forwardHtmlEvents(domElement, () => camera, scene), [domElement, camera, scene])
106-
return null
107-
}

examples/uikit/app.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Canvas, useThree } from '@react-three/fiber'
2-
import { createXRStore, XR, XRLayer, XROrigin } from '@react-three/xr'
1+
import { Canvas } from '@react-three/fiber'
2+
import { createXRStore, noEvents, PointerEvents, XR, XROrigin } from '@react-three/xr'
33
import { Environment } from '@react-three/drei'
4-
import { Container, Text, Image, Root, setPreferredColorScheme, Fullscreen } from '@react-three/uikit'
4+
import { Container, Text, Image, Root, setPreferredColorScheme } from '@react-three/uikit'
55
import { Button, Slider } from '@react-three/uikit-default'
66
import {
77
ArrowLeftIcon,
@@ -11,8 +11,7 @@ import {
1111
MenuIcon,
1212
PlayIcon,
1313
} from '@react-three/uikit-lucide'
14-
import { useEffect, useState } from 'react'
15-
import { forwardHtmlEvents } from '@pmndrs/pointer-events'
14+
import { useState } from 'react'
1615
import { useControls } from 'leva'
1716

1817
const store = createXRStore({
@@ -32,8 +31,8 @@ export function App() {
3231
return (
3332
<>
3433
<button onClick={() => store.enterAR()}>Enter AR</button>
35-
<Canvas events={() => ({ enabled: false, priority: 0 })} style={{ width: '100%', flexGrow: 1 }}>
36-
<SwitchToXRPointerEvents />
34+
<Canvas events={noEvents} style={{ width: '100%', flexGrow: 1 }}>
35+
<PointerEvents />
3736
<XR store={store}>
3837
<XROrigin visible={visible} />
3938
<Environment preset="city" />
@@ -176,11 +175,3 @@ export function App() {
176175
</>
177176
)
178177
}
179-
180-
export function SwitchToXRPointerEvents() {
181-
const domElement = useThree((s) => s.gl.domElement)
182-
const camera = useThree((s) => s.camera)
183-
const scene = useThree((s) => s.scene)
184-
useEffect(() => forwardHtmlEvents(domElement, () => camera, scene), [domElement, camera, scene])
185-
return null
186-
}

examples/use-gesture/app.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { Canvas, useThree } from '@react-three/fiber'
2-
import { useEffect } from 'react'
3-
import { forwardHtmlEvents } from '@pmndrs/pointer-events'
1+
import { Canvas } from '@react-three/fiber'
42
import { useDrag } from '@use-gesture/react'
3+
import { noEvents, PointerEvents } from '@react-three/xr'
54

65
export function App() {
76
return (
8-
<Canvas style={{ width: '100%', flexGrow: 1 }} events={() => ({ enabled: false, priority: 0 })}>
7+
<Canvas style={{ width: '100%', flexGrow: 1 }} events={noEvents}>
98
<DragCube />
10-
<SwitchToXRPointerEvents />
9+
<PointerEvents />
1110
</Canvas>
1211
)
1312
}
@@ -21,11 +20,3 @@ function DragCube() {
2120
</mesh>
2221
)
2322
}
24-
25-
export function SwitchToXRPointerEvents() {
26-
const domElement = useThree((s) => s.gl.domElement)
27-
const camera = useThree((s) => s.camera)
28-
const scene = useThree((s) => s.scene)
29-
useEffect(() => forwardHtmlEvents(domElement, () => camera, scene), [domElement, camera, scene])
30-
return null
31-
}

packages/pointer-events/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const canvas = document.getElementById('canvas')
1414
const scene = new THREE.Scene()
1515
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10)
1616
camera.position.z = 1
17-
forwardHtmlEvents(canvas, () => camera, scene)
17+
const { update } = forwardHtmlEvents(canvas, () => camera, scene)
1818

1919
const width = window.innerWidth,
2020
height = window.innerHeight
@@ -29,7 +29,10 @@ mesh.addEventListener('pointerout', () => material.color.set('red'))
2929

3030
const renderer = new THREE.WebGLRenderer({ antialias: true })
3131
renderer.setSize(width, height)
32-
renderer.setAnimationLoop(() => renderer.render(scene, camera))
32+
renderer.setAnimationLoop(() => {
33+
update()
34+
renderer.render(scene, camera)
35+
})
3336
```
3437

3538
## Filtering
@@ -79,4 +82,4 @@ rightPointer.move()
7982

8083
## Pitfalls
8184

82-
The `pointerEvents` attribute of any Mesh/Object3D/... will not be cloned when cloning the object.
85+
The `pointerEvents` attribute of any Mesh/Object3D/... will not be cloned when cloning the object.

packages/pointer-events/src/forward.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getClosestUV } from './utils.js'
99
export type ForwardablePointerEvent = { pointerId?: number; pointerType?: string; pointerState?: any } & NativeEvent
1010

1111
export type ForwardEventsOptions = {
12+
alwaysUpdate?: boolean
1213
/**
1314
* @default true
1415
*/
@@ -97,7 +98,7 @@ function forwardEvents(
9798
setPointerCapture: (pointerId: number) => void,
9899
releasePointerCapture: (ponterId: number) => void,
99100
options: ForwardEventsOptions = {},
100-
): () => void {
101+
): { destroy: () => void; update: () => void } {
101102
const forwardPointerCapture = options?.forwardPointerCapture ?? true
102103
const pointerMap = new Map<number, Pointer>()
103104
const pointerTypePrefix = options.pointerTypePrefix ?? 'forward-'
@@ -125,27 +126,49 @@ function forwardEvents(
125126
)
126127
return innerPointer
127128
}
128-
const pointerMoveListener = (e: ForwardablePointerEvent) => getInnerPointer(e).move(scene, e)
129+
130+
const lastMoveEventMap: Map<Pointer, ForwardablePointerEvent | undefined> = new Map()
131+
132+
const pointerMoveListener = (e: ForwardablePointerEvent) => lastMoveEventMap.set(getInnerPointer(e), e)
129133
const pointerCancelListener = (e: ForwardablePointerEvent) => getInnerPointer(e).cancel(e)
130134
const pointerDownListener = (e: ForwardablePointerEvent) => void (hasButton(e) && getInnerPointer(e).down(e))
131135
const pointerUpListener = (e: ForwardablePointerEvent) => void (hasButton(e) && getInnerPointer(e).up(e))
132-
const pointerLeaveListener = (e: ForwardablePointerEvent) => getInnerPointer(e).exit(e)
133136
const wheelListener = (e: ForwardablePointerEvent & NativeWheelEvent) => getInnerPointer(e).wheel(scene, e, false)
137+
const pointerLeaveListener = (e: ForwardablePointerEvent) => {
138+
const pointer = getInnerPointer(e)
139+
pointer.exit(e)
140+
lastMoveEventMap.delete(pointer)
141+
}
134142

135143
from.addEventListener('pointermove', pointerMoveListener)
136144
from.addEventListener('pointercancel', pointerCancelListener)
137145
from.addEventListener('pointerdown', pointerDownListener)
138146
from.addEventListener('pointerup', pointerUpListener)
139-
from.addEventListener('pointerleave', pointerLeaveListener)
140147
from.addEventListener('wheel', wheelListener)
148+
from.addEventListener('pointerleave', pointerLeaveListener)
141149

142-
return () => {
143-
from.removeEventListener('pointermove', pointerMoveListener)
144-
from.removeEventListener('pointercancel', pointerCancelListener)
145-
from.removeEventListener('pointerdown', pointerDownListener)
146-
from.removeEventListener('pointerup', pointerUpListener)
147-
from.removeEventListener('pointerleave', pointerLeaveListener)
148-
from.removeEventListener('wheel', wheelListener)
150+
return {
151+
destroy() {
152+
from.removeEventListener('pointermove', pointerMoveListener)
153+
from.removeEventListener('pointercancel', pointerCancelListener)
154+
from.removeEventListener('pointerdown', pointerDownListener)
155+
from.removeEventListener('pointerup', pointerUpListener)
156+
from.removeEventListener('wheel', wheelListener)
157+
from.removeEventListener('pointerleave', pointerLeaveListener)
158+
lastMoveEventMap.clear()
159+
},
160+
update() {
161+
for (const [pointer, lastMoveEvent] of lastMoveEventMap.entries()) {
162+
if (lastMoveEvent == null) {
163+
continue
164+
}
165+
pointer.move(scene, lastMoveEvent)
166+
if (options.alwaysUpdate === true) {
167+
continue
168+
}
169+
lastMoveEventMap.set(pointer, undefined)
170+
}
171+
},
149172
}
150173
}
151174

packages/pointer-events/src/intersections/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ export function intersectPointerEventTargets(
6666

6767
const isAllowed = isPointerEventsAllowed(hasListener, pointerEventsOrDefault, pointerEventsType)
6868
const length = pointers.length
69-
if (isAllowed === true) {
69+
if (length === 1) {
70+
if (isAllowed === true || (typeof isAllowed === 'function' && isAllowed(pointers[0]))) {
71+
filterAndInteresct(pointers[0], object, pointerEventsOrDefault, pointerEventsType, pointerEventsOrder)
72+
}
73+
} else if (isAllowed === true) {
7074
for (let i = 0; i < length; i++) {
7175
filterAndInteresct(pointers[i], object, pointerEventsOrDefault, pointerEventsType, pointerEventsOrder)
7276
}
@@ -80,7 +84,7 @@ export function intersectPointerEventTargets(
8084
}
8185
}
8286

83-
if (object.intersectChildren === false) {
87+
if (object.children.length === 0 || object.intersectChildren === false) {
8488
return
8589
}
8690

packages/pointer-events/tests/browser/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ const canvas = document.getElementById('canvas') as HTMLCanvasElement
1919
const camera = new OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0, 200)
2020
camera.position.z = 100
2121
//setup threejs event forwarding
22-
forwardHtmlEvents(document.body, () => camera, scene, {
22+
const { update } = forwardHtmlEvents(document.body, () => camera, scene, {
2323
forwardPointerCapture: false,
2424
clickThesholdMs: 1000 /*increasing threshold for slow testing machines*/,
2525
})
2626

2727
const renderer = new WebGLRenderer({ antialias: true, canvas })
2828
renderer.setSize(window.innerWidth, window.innerHeight)
29-
renderer.setAnimationLoop(() => renderer.render(scene, camera))
29+
renderer.setAnimationLoop(() => {
30+
update()
31+
renderer.render(scene, camera)
32+
})
3033

3134
const elements = e as Array<ElementInfo>
3235

packages/react/xr/src/events.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { forwardHtmlEvents } from '@pmndrs/pointer-events'
2+
import { addEffect, EventManager, useThree } from '@react-three/fiber'
3+
import { useEffect } from 'react'
4+
5+
export function PointerEvents() {
6+
const domElement = useThree((s) => s.gl.domElement)
7+
const camera = useThree((s) => s.camera)
8+
const scene = useThree((s) => s.scene)
9+
useEffect(() => {
10+
const { destroy, update } = forwardHtmlEvents(domElement, () => camera, scene)
11+
const cleanupUpdate = addEffect(update)
12+
return () => {
13+
cleanupUpdate()
14+
destroy()
15+
}
16+
}, [domElement, camera, scene])
17+
return null
18+
}
19+
20+
export const noEvents = (): EventManager<HTMLElement> => ({ enabled: false, priority: 0 })

packages/react/xr/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './anchor.js'
1616
export * from './dom-overlay.js'
1717
export * from './layer.js'
1818
export * from './controller-locomotion.js'
19+
export * from './events.js'
1920

2021
//react-three/xr v5 compatibility layer
2122
export * from './deprecated/index.js'

packages/react/xr/src/layer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
createXRLayerRenderTarget,
1515
} from '@pmndrs/xr'
1616
import {
17+
addEffect,
1718
context,
1819
InjectState,
1920
MeshProps,
@@ -330,7 +331,12 @@ function useForwardEvents(store: UseBoundStore<StoreApi<RootState>>, ref: RefObj
330331
return
331332
}
332333
cleanup?.()
333-
cleanup = forwardObjectEvents(current, () => state.camera, state.scene)
334+
const { destroy, update } = forwardObjectEvents(current, () => state.camera, state.scene)
335+
const cleanupUpdate = addEffect(update)
336+
cleanup = () => {
337+
destroy()
338+
cleanupUpdate()
339+
}
334340
}
335341
update(store.getState())
336342
const unsubscribe = store.subscribe(update)

0 commit comments

Comments
 (0)