Skip to content

Commit 02b638b

Browse files
committed
fix: forward events while pointer is captured by using getClosestUV
1 parent a5c09c0 commit 02b638b

File tree

4 files changed

+202
-127
lines changed

4 files changed

+202
-127
lines changed

examples/uikit/app.tsx

+127-123
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Canvas, useThree } from '@react-three/fiber'
2-
import { useHover, createXRStore, XR } from '@react-three/xr'
2+
import { useHover, createXRStore, XR, XRLayer } from '@react-three/xr'
33
import { Environment } from '@react-three/drei'
44
import { Container, Text, Image, Root, setPreferredColorScheme, Fullscreen } from '@react-three/uikit'
55
import { Button, Slider } from '@react-three/uikit-default'
@@ -34,140 +34,144 @@ export function App() {
3434
<SwitchToXRPointerEvents />
3535
<XR store={store}>
3636
<Environment preset="city" />
37-
<group position={[0, 1.5, -0.5]}>
38-
<Root pointerEventsType={{ deny: 'grab' }} pixelSize={0.001}>
39-
<Container width="100%" display="flex" alignItems="center" justifyContent="center">
37+
<XRLayer
38+
dpr={4}
39+
pixelWidth={512}
40+
pixelHeight={512}
41+
pointerEventsType={{ deny: 'grab' }}
42+
position={[0, 1.5, -0.5]}
43+
>
44+
<Fullscreen>
45+
<Container
46+
dark={{ backgroundColor: 'rgb(31,41,55)' }}
47+
flexDirection="column"
48+
height="auto"
49+
width="100%"
50+
backgroundColor="rgb(255,255,255)"
51+
borderRadius={8}
52+
overflow="hidden"
53+
>
4054
<Container
41-
dark={{ backgroundColor: 'rgb(31,41,55)' }}
42-
width={384}
43-
flexDirection="column"
44-
height="auto"
45-
backgroundColor="rgb(255,255,255)"
46-
borderRadius={8}
47-
overflow="hidden"
55+
dark={{ backgroundColor: 'rgb(55,65,81)' }}
56+
display="flex"
57+
justifyContent="space-between"
58+
alignItems="center"
59+
borderTopLeftRadius={8}
60+
borderTopRightRadius={8}
61+
backgroundColor="rgb(243,244,246)"
62+
paddingLeft={16}
63+
paddingRight={16}
64+
paddingTop={8}
65+
paddingBottom={8}
4866
>
49-
<Container
50-
dark={{ backgroundColor: 'rgb(55,65,81)' }}
51-
display="flex"
52-
justifyContent="space-between"
53-
alignItems="center"
54-
borderTopLeftRadius={8}
55-
borderTopRightRadius={8}
56-
backgroundColor="rgb(243,244,246)"
57-
paddingLeft={16}
58-
paddingRight={16}
59-
paddingTop={8}
60-
paddingBottom={8}
67+
<Text
68+
fontSize={18}
69+
fontWeight={500}
70+
lineHeight={28}
71+
color="rgb(17,24,39)"
72+
dark={{ color: 'rgb(243,244,246)' }}
73+
flexDirection="column"
6174
>
62-
<Text
63-
fontSize={18}
64-
fontWeight={500}
65-
lineHeight={28}
66-
color="rgb(17,24,39)"
67-
dark={{ color: 'rgb(243,244,246)' }}
68-
flexDirection="column"
69-
>
70-
Music Player
71-
</Text>
72-
<Container display="flex" flexDirection="row" gapColumn={8}>
73-
<ExpandIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
74-
<ConstructionIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
75-
<MenuIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
75+
Music Player
76+
</Text>
77+
<Container display="flex" flexDirection="row" gapColumn={8}>
78+
<ExpandIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
79+
<ConstructionIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
80+
<MenuIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
81+
</Container>
82+
</Container>
83+
<Container display="flex" flexDirection="column" gapRow={16} padding={16}>
84+
<Container display="flex" alignItems="center" flexDirection="row" gapColumn={16}>
85+
<Image height={64} src="placeholder.svg" width={64} flexDirection="column"></Image>
86+
<Container flexGrow={1} flexShrink={1} flexBasis="0%" flexDirection="column" gapRow={4}>
87+
<Text
88+
fontSize={18}
89+
fontWeight={500}
90+
lineHeight={28}
91+
color="rgb(17,24,39)"
92+
dark={{ color: 'rgb(243,244,246)' }}
93+
flexDirection="column"
94+
>
95+
Blowin' in the Wind
96+
</Text>
97+
<Text
98+
fontSize={14}
99+
lineHeight={20}
100+
color="rgb(107,114,128)"
101+
dark={{ color: 'rgb(156,163,175)' }}
102+
flexDirection="column"
103+
>
104+
Bob Dylan {counter.toString()}
105+
</Text>
76106
</Container>
77107
</Container>
78-
<Container display="flex" flexDirection="column" gapRow={16} padding={16}>
79-
<Container display="flex" alignItems="center" flexDirection="row" gapColumn={16}>
80-
<Image height={64} src="placeholder.svg" width={64} flexDirection="column"></Image>
81-
<Container flexGrow={1} flexShrink={1} flexBasis="0%" flexDirection="column" gapRow={4}>
82-
<Text
83-
fontSize={18}
84-
fontWeight={500}
85-
lineHeight={28}
86-
color="rgb(17,24,39)"
87-
dark={{ color: 'rgb(243,244,246)' }}
88-
flexDirection="column"
89-
>
90-
Blowin' in the Wind
91-
</Text>
92-
<Text
93-
fontSize={14}
94-
lineHeight={20}
95-
color="rgb(107,114,128)"
96-
dark={{ color: 'rgb(156,163,175)' }}
97-
flexDirection="column"
98-
>
99-
Bob Dylan {counter.toString()}
100-
</Text>
101-
</Container>
108+
<Slider />
109+
<Container display="flex" alignItems="center" justifyContent="space-between">
110+
<Button size="icon" variant="ghost">
111+
<ArrowLeftIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
112+
</Button>
113+
<Button onClick={() => setCounter((c) => c + 1)} size="icon" variant="ghost" padding={8}>
114+
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
115+
</Button>
116+
<Button size="icon" variant="ghost">
117+
<ArrowRightIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
118+
</Button>
119+
</Container>
120+
</Container>
121+
<Container padding={16} flexDirection="column">
122+
<Text
123+
fontSize={18}
124+
fontWeight={500}
125+
lineHeight={28}
126+
color="rgb(17,24,39)"
127+
dark={{ color: 'rgb(243,244,246)' }}
128+
marginBottom={8}
129+
flexDirection="column"
130+
>
131+
Playlist
132+
</Text>
133+
<Container flexDirection="column" gapRow={8}>
134+
<Container display="flex" alignItems="center" justifyContent="space-between">
135+
<Text
136+
fontSize={14}
137+
lineHeight={20}
138+
color="rgb(17,24,39)"
139+
dark={{ color: 'rgb(243,244,246)' }}
140+
flexDirection="column"
141+
>
142+
Like a Rolling Stone
143+
</Text>
144+
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
102145
</Container>
103-
<Slider flexGrow={1} flexShrink={1} flexBasis="0%" />
104146
<Container display="flex" alignItems="center" justifyContent="space-between">
105-
<Button size="icon" variant="ghost">
106-
<ArrowLeftIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
107-
</Button>
108-
<Button onClick={() => setCounter((c) => c + 1)} size="icon" variant="ghost" padding={8}>
109-
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
110-
</Button>
111-
<Button size="icon" variant="ghost">
112-
<ArrowRightIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
113-
</Button>
147+
<Text
148+
fontSize={14}
149+
lineHeight={20}
150+
color="rgb(17,24,39)"
151+
dark={{ color: 'rgb(243,244,246)' }}
152+
flexDirection="column"
153+
>
154+
The Times They Are a-Changin'
155+
</Text>
156+
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
114157
</Container>
115-
</Container>
116-
<Container padding={16} flexDirection="column">
117-
<Text
118-
fontSize={18}
119-
fontWeight={500}
120-
lineHeight={28}
121-
color="rgb(17,24,39)"
122-
dark={{ color: 'rgb(243,244,246)' }}
123-
marginBottom={8}
124-
flexDirection="column"
125-
>
126-
Playlist
127-
</Text>
128-
<Container flexDirection="column" gapRow={8}>
129-
<Container display="flex" alignItems="center" justifyContent="space-between">
130-
<Text
131-
fontSize={14}
132-
lineHeight={20}
133-
color="rgb(17,24,39)"
134-
dark={{ color: 'rgb(243,244,246)' }}
135-
flexDirection="column"
136-
>
137-
Like a Rolling Stone
138-
</Text>
139-
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
140-
</Container>
141-
<Container display="flex" alignItems="center" justifyContent="space-between">
142-
<Text
143-
fontSize={14}
144-
lineHeight={20}
145-
color="rgb(17,24,39)"
146-
dark={{ color: 'rgb(243,244,246)' }}
147-
flexDirection="column"
148-
>
149-
The Times They Are a-Changin'
150-
</Text>
151-
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
152-
</Container>
153-
<Container display="flex" alignItems="center" justifyContent="space-between">
154-
<Text
155-
fontSize={14}
156-
lineHeight={20}
157-
color="rgb(17,24,39)"
158-
dark={{ color: 'rgb(243,244,246)' }}
159-
flexDirection="column"
160-
>
161-
Subterranean Homesick Blues
162-
</Text>
163-
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
164-
</Container>
158+
<Container display="flex" alignItems="center" justifyContent="space-between">
159+
<Text
160+
fontSize={14}
161+
lineHeight={20}
162+
color="rgb(17,24,39)"
163+
dark={{ color: 'rgb(243,244,246)' }}
164+
flexDirection="column"
165+
>
166+
Subterranean Homesick Blues
167+
</Text>
168+
<PlayIcon color="rgb(17,24,39)" dark={{ color: 'rgb(243,244,246)' }} />
165169
</Container>
166170
</Container>
167171
</Container>
168172
</Container>
169-
</Root>
170-
</group>
173+
</Fullscreen>
174+
</XRLayer>
171175
</XR>
172176
</Canvas>
173177
</>

packages/pointer-events/src/forward.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Camera, Object3D, Quaternion, Scene, Vector2, Vector3 } from 'three'
1+
import { Camera, Mesh, Object3D, Scene, Vector2 } from 'three'
22
import { Pointer, PointerOptions } from './pointer.js'
33
import { NativeEvent, NativeWheelEvent, PointerEvent } from './event.js'
44
import { CameraRayIntersector } from './intersections/ray.js'
55
import { generateUniquePointerId } from './pointer/index.js'
66
import { IntersectionOptions } from './intersections/index.js'
7+
import { getClosestUV } from './utils.js'
78

89
export type ForwardablePointerEvent = { pointerId?: number; pointerType?: string; pointerState?: any } & NativeEvent
910

@@ -56,10 +57,11 @@ function portalEventToCoords(e: unknown, target: Vector2): Vector2 {
5657
if (!(e instanceof PointerEvent)) {
5758
return target.set(0, 0)
5859
}
59-
if (e.uv == null) {
60+
if (!(e.object instanceof Mesh)) {
6061
return target.set(0, 0)
6162
}
62-
target.copy(e.uv).multiplyScalar(2).addScalar(-1)
63+
getClosestUV(target, e.point, e.object)
64+
target.multiplyScalar(2).addScalar(-1)
6365
return target
6466
}
6567

packages/pointer-events/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './intersections/index.js'
1313
export * from './forward.js'
1414
export * from './pointer/index.js'
1515
export * from './combine.js'
16+
export { getClosestUV } from './utils.js'

packages/pointer-events/src/utils.ts

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Object3D } from 'three'
1+
import { BufferAttribute, Matrix4, Mesh, Object3D, Triangle, Vector2, Vector3 } from 'three'
22
import { PointerEventsMap } from './event.js'
33

44
declare module 'three' {
@@ -53,3 +53,71 @@ const r3fEventToHandlerMap: Record<keyof PointerEventsMap, string> = {
5353
pointerup: 'onPointerUp',
5454
wheel: 'onWheel',
5555
}
56+
57+
const triangleHelper1 = new Triangle()
58+
const triangleHelper2 = new Triangle()
59+
const aVec2Helper = new Vector2()
60+
const bVec2Helper = new Vector2()
61+
const cVec2Helper = new Vector2()
62+
const pointHelper = new Vector3()
63+
const inverseMatrix = new Matrix4()
64+
const localPointHelper = new Vector3()
65+
66+
export function getClosestUV(target: Vector2, point: Vector3, mesh: Mesh): void {
67+
localPointHelper.copy(point).applyMatrix4(inverseMatrix.copy(mesh.matrixWorld).invert())
68+
const uv = mesh.geometry.attributes.uv
69+
if (uv == null || !(uv instanceof BufferAttribute)) {
70+
return void target.set(0, 0)
71+
}
72+
let clostestDistance: number | undefined
73+
loopThroughTriangles(mesh, (i1, i2, i3) => {
74+
mesh.getVertexPosition(i1, triangleHelper1.a)
75+
mesh.getVertexPosition(i2, triangleHelper1.b)
76+
mesh.getVertexPosition(i3, triangleHelper1.c)
77+
78+
const distance = triangleHelper1.closestPointToPoint(localPointHelper, pointHelper).distanceTo(localPointHelper)
79+
80+
if (clostestDistance != null && distance >= clostestDistance) {
81+
return void target.set(0, 0)
82+
}
83+
84+
clostestDistance = distance
85+
triangleHelper2.copy(triangleHelper1)
86+
aVec2Helper.fromBufferAttribute(uv, i1)
87+
bVec2Helper.fromBufferAttribute(uv, i2)
88+
cVec2Helper.fromBufferAttribute(uv, i3)
89+
})
90+
91+
if (clostestDistance == null) {
92+
return void target.set(0, 0)
93+
}
94+
95+
triangleHelper2.closestPointToPoint(localPointHelper, pointHelper)
96+
97+
triangleHelper2.getInterpolation(pointHelper, aVec2Helper, bVec2Helper, cVec2Helper, target)
98+
}
99+
100+
function loopThroughTriangles(mesh: Mesh, fn: (i1: number, i2: number, i3: number) => void) {
101+
const drawRange = mesh.geometry.drawRange
102+
if (mesh.geometry.index != null) {
103+
const index = mesh.geometry.index
104+
const start = Math.max(0, drawRange.start)
105+
const end = Math.min(index.count, drawRange.start + drawRange.count)
106+
for (let i = start; i < end; i += 3) {
107+
fn(index.getX(i), index.getX(i + 1), index.getX(i + 2))
108+
}
109+
return
110+
}
111+
const position = mesh.geometry.attributes.position
112+
113+
if (position == null) {
114+
return
115+
}
116+
117+
const start = Math.max(0, drawRange.start)
118+
const end = Math.min(position.count, drawRange.start + drawRange.count)
119+
120+
for (let i = start; i < end; i += 3) {
121+
fn(i, i + 1, i + 2)
122+
}
123+
}

0 commit comments

Comments
 (0)