diff --git a/examples/auth/src/App.tsx b/examples/auth/src/App.tsx
index 587a6097..a3245a86 100644
--- a/examples/auth/src/App.tsx
+++ b/examples/auth/src/App.tsx
@@ -12,6 +12,7 @@ import { Defaults, colors } from '@/theme.js'
import { Button } from '@/button.js'
import { UserAuthForm } from './components/user-auth-form.js'
import { Perf } from 'r3f-perf'
+import { noEvents, PointerEvents } from '@react-three/xr'
setPreferredColorScheme('light')
@@ -23,8 +24,10 @@ export default function App() {
camera={{ position: [0, 0, 18], fov: 35 }}
style={{ height: '100dvh', touchAction: 'none' }}
gl={{ localClippingEnabled: true }}
+ events={noEvents}
{...canvasInputProps}
>
+
{/*
diff --git a/package.json b/package.json
index 50860cc5..b16cbc9d 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,6 @@
"typescript-json-schema": "^0.63.0",
"vite": "^5.0.12",
"vercel": "^34.1.8",
- "@react-three/xr": "6.4.1"
+ "@react-three/xr": "6.4.3"
}
}
diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts
index 284395e1..753bf868 100644
--- a/packages/uikit/src/components/image.ts
+++ b/packages/uikit/src/components/image.ts
@@ -289,7 +289,7 @@ function createImageMesh(
if (rootObjectMatrixWorld != null) {
mesh.raycast = makeClippedCast(
mesh,
- makePanelRaycast(rootObjectMatrixWorld, boundingSphere, globalMatrix, mesh),
+ makePanelRaycast(mesh.raycast.bind(mesh), rootObjectMatrixWorld, boundingSphere, globalMatrix, mesh),
root.object,
parentContext.clippingRect,
orderInfo,
diff --git a/packages/uikit/src/components/input.ts b/packages/uikit/src/components/input.ts
index 5b0d88f7..0d287c93 100644
--- a/packages/uikit/src/components/input.ts
+++ b/packages/uikit/src/components/input.ts
@@ -36,7 +36,7 @@ import { Listeners, setupLayoutListeners, setupClippedListeners } from '../liste
import { Object3DRef, ParentContext } from '../context.js'
import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js'
import { createInteractionPanel } from '../panel/instanced-panel-mesh.js'
-import { EventHandlers } from '../events.js'
+import { EventHandlers, ThreeEvent } from '../events.js'
import { Vector2Tuple, Vector2, Vector3Tuple } from 'three'
import { CaretProperties, createCaret } from '../caret.js'
import { SelectionBoxes, SelectionProperties, createSelection } from '../selection.js'
@@ -342,35 +342,44 @@ export function computedSelectionHandlers(
if (disabled.value) {
return undefined
}
- let startCharIndex: number | undefined
+ let dragState: { startCharIndex: number; pointerId: number } | undefined
+ const onPointerFinish = (e: ThreeEvent) => {
+ if (dragState == null || dragState.pointerId != e.pointerId) {
+ return
+ }
+ e.stopImmediatePropagation?.()
+ dragState = undefined
+ }
return {
onPointerDown: (e) => {
- if (e.defaultPrevented || e.uv == null || instancedTextRef.current == null) {
+ if (dragState != null || e.defaultPrevented || e.uv == null || instancedTextRef.current == null) {
return
}
cancelBlur(e.nativeEvent)
- e.stopPropagation?.()
- const charIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current)
- startCharIndex = charIndex
-
- setTimeout(() => focus(charIndex, charIndex))
- },
- onPointerUp: (e) => {
- startCharIndex = undefined
- },
- onPointerLeave: (e) => {
- startCharIndex = undefined
+ e.stopImmediatePropagation?.()
+ if ('setPointerCapture' in e.object && typeof e.object.setPointerCapture === 'function') {
+ e.object.setPointerCapture(e.pointerId)
+ }
+ const startCharIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current)
+ dragState = {
+ pointerId: e.pointerId,
+ startCharIndex,
+ }
+ setTimeout(() => focus(startCharIndex, startCharIndex))
},
+ onPointerUp: onPointerFinish,
+ onPointerLeave: onPointerFinish,
+ onPointerCancel: onPointerFinish,
onPointerMove: (e) => {
- if (startCharIndex == null || e.uv == null || instancedTextRef.current == null) {
+ if (dragState?.pointerId != e.pointerId || e.uv == null || instancedTextRef.current == null) {
return
}
- e.stopPropagation?.()
+ e.stopImmediatePropagation?.()
const charIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current)
- const start = Math.min(startCharIndex, charIndex)
- const end = Math.max(startCharIndex, charIndex)
- const direction = startCharIndex < charIndex ? 'forward' : 'backward'
+ const start = Math.min(dragState.startCharIndex, charIndex)
+ const end = Math.max(dragState.startCharIndex, charIndex)
+ const direction = dragState.startCharIndex < charIndex ? 'forward' : 'backward'
setTimeout(() => focus(start, end, direction))
},
@@ -480,6 +489,6 @@ function uvToCharIndex(
const [bTop, , , bLeft] = borderInset
const [pTop, , , pLeft] = paddingInset
const x = uv.x * width - bLeft - pLeft
- const y = -uv.y * height + bTop + pTop
+ const y = (uv.y - 1) * height + bTop + pTop
return instancedText.getCharIndex(x, y)
}
diff --git a/packages/uikit/src/events.ts b/packages/uikit/src/events.ts
index fa527810..59238759 100644
--- a/packages/uikit/src/events.ts
+++ b/packages/uikit/src/events.ts
@@ -5,6 +5,7 @@ export type ThreeEvent = Intersection & {
defaultPrevented?: boolean
stopped?: boolean
stopPropagation?: () => void
+ stopImmediatePropagation?: () => void
} & (TSourceEvent extends { pointerId: number } ? { pointerId: number } : {})
export type KeyToEvent = Parameters[K]>[0]
diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts
index 8f4a4694..77b1a8c2 100644
--- a/packages/uikit/src/panel/instanced-panel-mesh.ts
+++ b/packages/uikit/src/panel/instanced-panel-mesh.ts
@@ -30,7 +30,7 @@ export function createInteractionPanel(
if (rootObject != null) {
panel.raycast = makeClippedCast(
panel,
- makePanelRaycast(rootObject.matrixWorld, boundingSphere, globalMatrix, panel),
+ makePanelRaycast(panel.raycast.bind(panel), rootObject.matrixWorld, boundingSphere, globalMatrix, panel),
rootContext.object,
parentClippingRect,
orderInfo,
diff --git a/packages/uikit/src/panel/interaction-panel-mesh.ts b/packages/uikit/src/panel/interaction-panel-mesh.ts
index 432fb9e7..736c8112 100644
--- a/packages/uikit/src/panel/interaction-panel-mesh.ts
+++ b/packages/uikit/src/panel/interaction-panel-mesh.ts
@@ -1,26 +1,14 @@
-import { Intersection, Matrix4, Mesh, Object3D, Plane, Ray, Sphere, Vector2, Vector2Tuple, Vector3 } from 'three'
+import { Intersection, Matrix4, Mesh, Object3D, Plane, Sphere, Vector2, Vector2Tuple, Vector3 } from 'three'
import { ClippingRect } from '../clipping.js'
import { effect, Signal } from '@preact/signals-core'
import { OrderInfo } from '../order.js'
-import { Object3DRef, RootContext } from '../context.js'
+import { Object3DRef } from '../context.js'
import { computeMatrixWorld, Initializers } from '../internals.js'
+import { clamp } from 'three/src/math/MathUtils.js'
const planeHelper = new Plane()
const vectorHelper = new Vector3()
-const sides: Array = [
- //left
- new Plane().setFromNormalAndCoplanarPoint(new Vector3(1, 0, 0), new Vector3(-0.5, 0, 0)),
- //right
- new Plane().setFromNormalAndCoplanarPoint(new Vector3(-1, 0, 0), new Vector3(0.5, 0, 0)),
- //bottom
- new Plane().setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), new Vector3(0, -0.5, 0)),
- //top
- new Plane().setFromNormalAndCoplanarPoint(new Vector3(0, -1, 0), new Vector3(0, 0.5, 0)),
-]
-
-const distancesHelper = [0, 0, 0, 0]
-
export type AllowedPointerEventsType =
| 'all'
| ((poinerId: number, pointerType: string, pointerState: unknown) => boolean)
@@ -42,15 +30,8 @@ export type PointerEventsProperties = {
pointerEventsOrder?: number
}
-const scaleHelper = new Vector3()
-const matrixHelper = new Matrix4()
-
-function isSingularMatrix(matrix: Matrix4) {
- scaleHelper.setFromMatrixScale(matrix)
- return scaleHelper.x === 0 || scaleHelper.y === 0 || scaleHelper.z === 0
-}
-
const sphereHelper = new Sphere()
+const matrixHelper = new Matrix4()
export function makePanelSpherecast(
rootObjectMatrixWorld: Matrix4,
@@ -60,41 +41,20 @@ export function makePanelSpherecast(
): Exclude {
return (sphere, intersects) => {
sphereHelper.copy(globalSphereWithLocalScale).applyMatrix4(rootObjectMatrixWorld)
- if (!sphereHelper.intersectsSphere(sphere)) {
- return
- }
if (
- isSingularMatrix(matrixHelper) ||
- !computeMatrixWorld(matrixHelper, object.matrix, rootObjectMatrixWorld, globalMatrixSignal)
+ !sphereHelper.intersectsSphere(sphere) ||
+ !computeMatrixWorld(object.matrixWorld, object.matrix, rootObjectMatrixWorld, globalMatrixSignal)
) {
return
}
- planeHelper.constant = 0
- planeHelper.normal.set(0, 0, 1)
- planeHelper.applyMatrix4(matrixHelper)
+ vectorHelper.copy(sphere.center).applyMatrix4(matrixHelper.copy(object.matrixWorld).invert())
+ vectorHelper.x = clamp(vectorHelper.x, -0.5, 0.5)
+ vectorHelper.x = clamp(vectorHelper.y, -0.5, 0.5)
+ vectorHelper.z = 0
- planeHelper.projectPoint(sphere.center, vectorHelper)
-
- if (vectorHelper.distanceToSquared(sphere.center) > sphere.radius * sphere.radius) {
- return
- }
-
- for (let i = 0; i < 4; i++) {
- const side = sides[i]
- planeHelper.copy(side).applyMatrix4(matrixHelper)
-
- let distance = planeHelper.distanceToPoint(vectorHelper)
- if (distance < 0) {
- if (Math.abs(distance) > sphere.radius) {
- return
- }
- //clamp point
- planeHelper.projectPoint(vectorHelper, vectorHelper)
- distance = 0
- }
- distancesHelper[i] = distance
- }
+ const uv = new Vector2(vectorHelper.x, vectorHelper.y)
+ vectorHelper.applyMatrix4(object.matrixWorld)
const distance = sphere.center.distanceTo(vectorHelper)
if (distance > sphere.radius) {
@@ -105,15 +65,36 @@ export function makePanelSpherecast(
distance,
object,
point: vectorHelper.clone(),
- uv: new Vector2(
- distancesHelper[0] / (distancesHelper[0] + distancesHelper[1]),
- distancesHelper[3] / (distancesHelper[2] + distancesHelper[3]),
- ),
+ uv,
normal: new Vector3(0, 0, 1),
})
}
}
+export function makePanelRaycast(
+ raycast: Mesh['raycast'],
+ rootObjectMatrixWorld: Matrix4,
+ globalSphereWithLocalScale: Sphere,
+ globalMatrixSignal: Signal,
+ object: Object3D,
+): Mesh['raycast'] {
+ return (raycaster, intersects) => {
+ sphereHelper.copy(globalSphereWithLocalScale).applyMatrix4(rootObjectMatrixWorld)
+ if (
+ !raycaster.ray.intersectsSphere(sphereHelper) ||
+ !computeMatrixWorld(object.matrixWorld, object.matrix, rootObjectMatrixWorld, globalMatrixSignal)
+ ) {
+ return
+ }
+
+ raycast(raycaster, intersects)
+ }
+}
+
+export function isInteractionPanel(object: Object3D) {
+ return 'isInteractionPanel' in object
+}
+
export function computedBoundingSphere(
pixelSize: Signal,
globalMatrixSignal: Signal,
@@ -129,63 +110,15 @@ export function computedBoundingSphere(
return
}
sphere.center.set(0, 0, 0)
- sphere.radius = 0.5
+ const [w, h] = sizeValue
+ const maxDiameter = Math.sqrt(w * w + h * h)
+ sphere.radius = maxDiameter * 0.5 * pixelSize.value
sphere.applyMatrix4(globalMatrix)
- sphere.radius *= Math.max(...sizeValue) * pixelSize.value
}),
)
return sphere
}
-export function makePanelRaycast(
- rootObjectMatrixWorld: Matrix4,
- globalSphereWithLocalScale: Sphere,
- globalMatrixSignal: Signal,
- object: Object3D,
-): Mesh['raycast'] {
- return (raycaster, intersects) => {
- sphereHelper.copy(globalSphereWithLocalScale).applyMatrix4(rootObjectMatrixWorld)
- if (!raycaster.ray.intersectsSphere(sphereHelper)) {
- return
- }
- if (
- !computeMatrixWorld(matrixHelper, object.matrix, rootObjectMatrixWorld, globalMatrixSignal) ||
- isSingularMatrix(matrixHelper)
- ) {
- return
- }
- planeHelper.constant = 0
- planeHelper.normal.set(0, 0, 1)
- planeHelper.applyMatrix4(matrixHelper)
- if (raycaster.ray.intersectPlane(planeHelper, vectorHelper) == null) {
- return
- }
-
- for (let i = 0; i < 4; i++) {
- const side = sides[i]
- planeHelper.copy(side).applyMatrix4(matrixHelper)
- if ((distancesHelper[i] = planeHelper.distanceToPoint(vectorHelper)) < 0) {
- return
- }
- }
-
- intersects.push({
- distance: vectorHelper.distanceTo(raycaster.ray.origin),
- object,
- point: vectorHelper.clone(),
- uv: new Vector2(
- distancesHelper[0] / (distancesHelper[0] + distancesHelper[1]),
- distancesHelper[3] / (distancesHelper[2] + distancesHelper[3]),
- ),
- normal: new Vector3(0, 0, 1),
- })
- }
-}
-
-export function isInteractionPanel(object: Object3D) {
- return 'isInteractionPanel' in object
-}
-
/**
* clips the sphere / raycast
* also marks the mesh as a interaction panel
diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts
index 534462ef..3792e034 100644
--- a/packages/uikit/src/scroll.ts
+++ b/packages/uikit/src/scroll.ts
@@ -186,57 +186,56 @@ export function computedScrollHandlers(
if (!isScrollable.value) {
return undefined
}
- const onPointerFinish = ({ pointerId, object }: ThreeEvent) => {
+ const onPointerFinish = (event: ThreeEvent) => {
if ('releasePointerCapture' in object && typeof object.releasePointerCapture === 'function') {
- object.releasePointerCapture(pointerId)
+ object.releasePointerCapture(event.pointerId)
}
- if (!downPointerMap.delete(pointerId) || downPointerMap.size > 0 || scrollPosition.value == null) {
+ if (!downPointerMap.delete(event.pointerId) || scrollPosition.value == null) {
+ return
+ }
+ event.stopImmediatePropagation?.()
+ if (downPointerMap.size > 0) {
return
}
//only request a render if the last pointer that was dragging stopped dragging and this panel is actually scrollable
root.requestRender()
}
return {
- onPointerDown: ({ pointerId, point, nativeEvent, object: eventObject }) => {
- const isMouseInteraction = nativeEvent.pointerType === 'mouse'
- const localPoint = object.current!.worldToLocal(point.clone())
-
- const scrollbarAxisIndex = !isMouseInteraction
- ? undefined
- : getIntersectedScrollbarIndex(
- localPoint,
- root.pixelSize.peek(),
- scrollbarWidth.peek(),
- nodeState.size.peek(),
- nodeState.maxScrollPosition.peek(),
- nodeState.borderInset.peek(),
- scrollPosition.peek(),
- )
- if (scrollbarAxisIndex != null) {
- if ('setPointerCapture' in eventObject && typeof eventObject.setPointerCapture === 'function') {
- eventObject.setPointerCapture(pointerId)
- }
- downPointerMap.set(pointerId, {
- type: 'scroll-bar',
- localPoint,
- axisIndex: scrollbarAxisIndex,
- })
- return
- }
-
- if (isMouseInteraction) {
+ onPointerDown: (event) => {
+ if (event.nativeEvent.pointerType === 'mouse') {
return
}
+ event.stopImmediatePropagation?.()
+ const localPoint = object.current!.worldToLocal(event.point.clone())
- if ('setPointerCapture' in eventObject && typeof eventObject.setPointerCapture === 'function') {
- eventObject.setPointerCapture(pointerId)
+ const scrollbarAxisIndex = getIntersectedScrollbarIndex(
+ localPoint,
+ root.pixelSize.peek(),
+ scrollbarWidth.peek(),
+ nodeState.size.peek(),
+ nodeState.maxScrollPosition.peek(),
+ nodeState.borderInset.peek(),
+ scrollPosition.peek(),
+ )
+
+ if ('setPointerCapture' in event.object && typeof event.object.setPointerCapture === 'function') {
+ event.object.setPointerCapture(event.pointerId)
}
- downPointerMap.set(pointerId, {
- type: 'scroll-panel',
- timestamp: performance.now(),
- localPoint,
- })
+ downPointerMap.set(
+ event.pointerId,
+ scrollbarAxisIndex != null
+ ? {
+ type: 'scroll-bar',
+ localPoint,
+ axisIndex: scrollbarAxisIndex,
+ }
+ : {
+ type: 'scroll-panel',
+ timestamp: performance.now(),
+ localPoint,
+ },
+ )
},
onPointerUp: onPointerFinish,
onPointerLeave: onPointerFinish,
@@ -249,6 +248,7 @@ export function computedScrollHandlers(
if (prevInteraction == null) {
return
}
+ event.stopImmediatePropagation?.()
object.current!.worldToLocal(localPointHelper.copy(event.point))
distanceHelper.copy(localPointHelper).sub(prevInteraction.localPoint)
distanceHelper.divideScalar(root.pixelSize.peek())
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c4b6080b..efc1d7af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,8 +15,8 @@ importers:
specifier: ^8.16.2
version: 8.16.2(react-dom@18.3.1)(react@18.3.1)(three@0.161.0)
'@react-three/xr':
- specifier: 6.4.1
- version: 6.4.1(@react-three/fiber@8.16.2)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(three@0.161.0)
+ specifier: 6.4.3
+ version: 6.4.3(@react-three/fiber@8.16.2)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(three@0.161.0)
'@types/chai':
specifier: ^4.3.10
version: 4.3.11
@@ -2753,17 +2753,17 @@ packages:
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
dev: true
- /@pmndrs/pointer-events@6.4.1:
- resolution: {integrity: sha512-oTjaxFjq06CZUGbX4Z32egcrLUy7ZaFN2jihID1LLjmlGi9Ev3BpiFYpJmkvdVCpkttGrwuHj/YEhbOsT1UYiQ==}
+ /@pmndrs/pointer-events@6.4.3:
+ resolution: {integrity: sha512-NRn3K6CvYurA084n4S7ua6/xz9EG+OwFMCIXRRCi2cW7PLv7xgS+1lBlTUzeCFeG1tITTRRYDYZatK1v26cNxQ==}
dev: true
- /@pmndrs/xr@6.4.1(@types/react@18.3.1)(react@18.3.1)(three@0.161.0):
- resolution: {integrity: sha512-17tqwL9V2QMLa1SBt4vYd5Be4l2W+21ZjknbKpTZsXZTLLh7T++F30tUdS9DpZ1ka5GCymqg2zcuQEP8hAt5xw==}
+ /@pmndrs/xr@6.4.3(@types/react@18.3.1)(react@18.3.1)(three@0.161.0):
+ resolution: {integrity: sha512-deTmIzx3g6IJVS4jCwzFY5orXSVpntlN3bpg49xBfWxASe6jOAmqryLfKGUmCD5bMRxFsxktVH7HuYm56JT3/g==}
peerDependencies:
three: '*'
dependencies:
'@iwer/devui': 0.2.1(iwer@1.0.4)
- '@pmndrs/pointer-events': 6.4.1
+ '@pmndrs/pointer-events': 6.4.3
iwer: 1.0.4
meshline: 3.3.1(three@0.161.0)
three: 0.161.0
@@ -3923,16 +3923,16 @@ packages:
use-asset: 1.0.4(react@18.3.1)
dev: false
- /@react-three/xr@6.4.1(@react-three/fiber@8.16.2)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(three@0.161.0):
- resolution: {integrity: sha512-ZfsP6sC8dA/ZlZtdY6YDqwfAmUsNbBNQDmEVIipPJRFKh50vvHrKsXD2nZGlQWW623u8gNglOFROGVAG9AWedg==}
+ /@react-three/xr@6.4.3(@react-three/fiber@8.16.2)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(three@0.161.0):
+ resolution: {integrity: sha512-TKoKxDPNFIsCZqAD8qBZ2DRhAJUm4iPiWGeQeagb5Ole7nssRb7Mse5Hkc9XZ2/G+hodYfIHLiMJGP5wHGgPig==}
peerDependencies:
'@react-three/fiber': '>=8'
react: '>=18'
react-dom: '>=18'
three: '*'
dependencies:
- '@pmndrs/pointer-events': 6.4.1
- '@pmndrs/xr': 6.4.1(@types/react@18.3.1)(react@18.3.1)(three@0.161.0)
+ '@pmndrs/pointer-events': 6.4.3
+ '@pmndrs/xr': 6.4.3(@types/react@18.3.1)(react@18.3.1)(three@0.161.0)
'@react-three/fiber': 8.16.2(react-dom@18.3.1)(react@18.3.1)(three@0.161.0)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)