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)