Created
July 18, 2022 18:07
-
-
Save bbohlender/d7531b86a1e0899d879ed287b13171b7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useMemo } from "react" | |
import { forwardRef, MutableRefObject, PropsWithChildren, useCallback, useRef } from "react" | |
import { useXREvent, XRController, XREvent } from "@react-three/xr" | |
import { useState } from "react" | |
import { Box3, Group, Matrix4, Object3D, Quaternion, Vector3, XRHandedness } from "three" | |
import { useTouchedEvent } from "./touchable" | |
import mergeRefs from "react-merge-refs" | |
import { useFrame } from "@react-three/fiber" | |
export type GrabbaleOnUpdate = (group: Group, grabbedBy: Array<Grabbed>) => void | |
export type Grabbed = { | |
controller: XRController | |
controllerToObjectOffset: Matrix4 | |
drop: () => void | |
} | |
export const Grabbable = forwardRef< | |
Group | undefined, | |
PropsWithChildren<{ | |
onChange?: (grabbedBy: Array<Grabbed>, startedGrabbing: Array<Grabbed>, stoppedGrabbing: Array<Grabbed>) => void | |
boundingBox: Box3 | |
controllerRadius: number | |
controllerOptions?: { handedness?: XRHandedness } | |
onUpdate?: GrabbaleOnUpdate | |
}> | |
>(({ onUpdate, children, controllerRadius, onChange, boundingBox, controllerOptions }, passedRef) => { | |
const ref = useRef<Group | undefined>() | |
const grabbedByRef = useRef<Array<Grabbed>>([]) | |
useGrabbedEvent( | |
ref, | |
boundingBox, | |
controllerRadius, | |
controllerOptions, | |
(grabbedBy, startedGrabbing, stoppedGrabbing) => { | |
grabbedByRef.current = grabbedBy | |
if (onChange) { | |
onChange(grabbedBy, startedGrabbing, stoppedGrabbing) | |
} | |
} | |
) | |
useFrame(() => { | |
if (ref.current != null && onUpdate) { | |
onUpdate(ref.current, grabbedByRef.current) | |
} | |
}) | |
return <group ref={mergeRefs([passedRef, ref])}>{children}</group> | |
}) | |
export function useSingleControllerGrabBehaviour( | |
onTransform?: (group: Group) => void, | |
changeMatrixDirectly = false | |
): GrabbaleOnUpdate { | |
const helpMatrix = useMemo(() => new Matrix4(), []) | |
return useCallback( | |
(group, grabbedBy) => { | |
if (grabbedBy.length > 0) { | |
//A # A' | |
helpMatrix | |
.copy(grabbedBy[0].controller.grip.matrixWorld) | |
.multiply(grabbedBy[0].controllerToObjectOffset) | |
if (changeMatrixDirectly) { | |
group.matrix.copy(helpMatrix) | |
} else { | |
group.position.setFromMatrixPosition(helpMatrix) | |
group.rotation.setFromRotationMatrix(helpMatrix) | |
} | |
if (onTransform != null) { | |
onTransform(group) | |
} | |
} | |
}, | |
[onTransform, helpMatrix] | |
) | |
} | |
export function useDoubleControllerGrabBehaviour(onTransform?: (group: Group) => void): GrabbaleOnUpdate { | |
const [helpMatrix, a, b, originalAbDistance, rotation, helpMatrix2, helpMatrix3] = useMemo( | |
() => [ | |
new Matrix4(), | |
new Vector3(), | |
new Vector3(), | |
new Vector3(), | |
new Quaternion(), | |
new Matrix4(), | |
new Matrix4(), | |
], | |
[] | |
) | |
return useCallback( | |
(group, grabbedBy) => { | |
if (grabbedBy.length > 0) { | |
//A # A' | |
helpMatrix.copy(grabbedBy[0].controller.grip.matrixWorld) | |
//.multiply(grabbedBy[0].controllerToObjectOffset); | |
if (grabbedBy.length > 1) { | |
a.setFromMatrixPosition(grabbedBy[0].controller.grip.matrixWorld) | |
b.setFromMatrixPosition(grabbedBy[1].controller.grip.matrixWorld) | |
helpMatrix3.copy(grabbedBy[1].controllerToObjectOffset).invert() | |
helpMatrix2 | |
//.copy(grabbedBy[0].controller.grip.matrixWorld) | |
.copy(grabbedBy[0].controllerToObjectOffset) | |
.multiply(helpMatrix3) | |
originalAbDistance.setFromMatrixPosition(helpMatrix2) //.sub(a); | |
const abDistance = b.sub(a) | |
abDistance.normalize() | |
originalAbDistance.normalize() | |
rotation.setFromUnitVectors(originalAbDistance, abDistance) | |
helpMatrix2.makeRotationFromQuaternion(rotation) | |
helpMatrix.multiply(helpMatrix2) | |
} | |
group.position.setFromMatrixPosition(helpMatrix) | |
group.rotation.setFromRotationMatrix(helpMatrix) | |
if (grabbedBy.length > 1) { | |
//group.scale.setFromMatrixScale(helpMatrix); | |
} | |
if (onTransform != null) { | |
onTransform(group) | |
} | |
} | |
}, | |
[helpMatrix, a, b, rotation, helpMatrix2] | |
) | |
} | |
export function useGrabbed( | |
ref: MutableRefObject<Object3D | undefined | null>, | |
boundingBox: Box3, | |
controllerRadius: number, | |
controllerOptions: { handedness?: XRHandedness } = {}, | |
onChange?: (grabbedBy: Array<Grabbed>, startedGrabbing: Array<Grabbed>, stoppedGrabbing: Array<Grabbed>) => void | |
): Array<Grabbed> { | |
const [grabbed, setGrabbed] = useState<Array<Grabbed>>([]) | |
useGrabbedEvent( | |
ref, | |
boundingBox, | |
controllerRadius, | |
controllerOptions, | |
(grabbedBy, startedGrabbing, stoppedGrabbing) => { | |
setGrabbed(grabbedBy) | |
if (onChange != null) { | |
onChange(grabbedBy, startedGrabbing, stoppedGrabbing) | |
} | |
} | |
) | |
return grabbed | |
} | |
export function useGrabbedEvent( | |
ref: MutableRefObject<Object3D | undefined | null>, | |
boundingBox: Box3, | |
controllerRadius: number, | |
controllerOptions: { handedness?: XRHandedness } = {}, | |
onChange: (grabbedBy: Array<Grabbed>, startedGrabbing: Array<Grabbed>, stoppedGrabbing: Array<Grabbed>) => void | |
): void { | |
const touchingControllersRef = useRef<Array<XRController>>([]) | |
const grabbingControllersRef = useRef<Array<Grabbed>>([]) | |
const onChangeTouchedControllers = useCallback((touchedBy) => (touchingControllersRef.current = touchedBy), []) | |
useTouchedEvent(ref, boundingBox, controllerRadius, controllerOptions, onChangeTouchedControllers) | |
const drop = useCallback( | |
(dropController: XRController) => { | |
const dropGrabbed = grabbingControllersRef.current.find(({ controller }) => controller === dropController) | |
if (dropGrabbed != null) { | |
grabbingControllersRef.current = grabbingControllersRef.current.filter( | |
(grabbed) => grabbed != dropGrabbed | |
) | |
onChange(grabbingControllersRef.current, [], [dropGrabbed]) | |
} | |
}, | |
[onChange, grabbingControllersRef] | |
) | |
const squeezestart = useCallback( | |
(evt: XREvent) => { | |
if (ref.current == null) { | |
return | |
} | |
if (touchingControllersRef.current.includes(evt.controller)) { | |
const grabbed = { | |
controller: evt.controller, | |
controllerToObjectOffset: calculateControllerOffset(evt.controller, ref.current), | |
drop: drop.bind(null, evt.controller), | |
} | |
grabbingControllersRef.current = [...grabbingControllersRef.current, grabbed] | |
onChange(grabbingControllersRef.current, [grabbed], []) | |
} | |
}, | |
[onChange, touchingControllersRef, ref] | |
) | |
useXREvent("squeezestart", squeezestart, controllerOptions) | |
useXREvent("squeezeend", (evt) => drop(evt.controller)) | |
} | |
function calculateControllerOffset(controller: XRController, obj: Object3D): Matrix4 { | |
return controller.grip.matrixWorld.clone().invert().multiply(obj.matrixWorld) //B^-1 # A | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { MutableRefObject } from "react" | |
import { useXR, XRController } from "@react-three/xr" | |
import { forwardRef, PropsWithChildren, useMemo, useRef, useState } from "react" | |
import { useFrame } from "@react-three/fiber" | |
import { Box3, Group, Object3D, Sphere, Vector3, XRHandedness } from "three" | |
import mergeRefs from "react-merge-refs" | |
//TODO support more than just Box3 | |
export const Touchable = forwardRef< | |
Group, | |
PropsWithChildren<{ | |
onChange: ( | |
touchedBy: Array<XRController>, | |
enteredControllers: Array<XRController>, | |
leftControllers: Array<XRController> | |
) => void | |
boundingBox: Box3 | |
controllerRadius: number | |
controllerOptions?: { handedness?: XRHandedness } | |
}> | |
>(({ children, controllerRadius, onChange, boundingBox, controllerOptions }, passedRef) => { | |
const ref = useRef<Group | undefined>() | |
useTouchedEvent(ref, boundingBox, controllerRadius, controllerOptions, onChange) | |
return <group ref={mergeRefs([passedRef, ref])}> {children} </group> | |
}) | |
export function useTouchedEvent( | |
ref: MutableRefObject<Object3D | undefined | null>, | |
boundingBox: Box3, | |
controllerRadius: number, | |
controllerOptions: { handedness?: XRHandedness } = {}, | |
onChange: ( | |
touchedBy: Array<XRController>, | |
enteredControllers: Array<XRController>, | |
leftControllers: Array<XRController> | |
) => void | |
) { | |
const controllers = useFilteredControllers(controllerOptions) | |
const sphere = useMemo(() => new Sphere(undefined, controllerRadius), []) | |
const helpVector = useMemo(() => new Vector3(), []) | |
const lastTouchedByRef = useRef<Array<XRController>>([]) | |
useFrame(() => { | |
const touchedBy = | |
ref.current == null ? [] : calculateTouchedBy(ref.current, boundingBox, controllers, sphere, helpVector) | |
const entered = touchedBy.filter((controller) => !lastTouchedByRef.current.includes(controller)) | |
const left = lastTouchedByRef.current.filter((controller) => !touchedBy.includes(controller)) | |
lastTouchedByRef.current = touchedBy | |
if (entered.length > 0 || left.length > 0) { | |
onChange(touchedBy, entered, left) | |
} | |
}) | |
} | |
export function useTouched( | |
ref: MutableRefObject<Object3D | undefined | null>, | |
boundingBox: Box3, | |
controllerRadius: number, | |
controllerOptions: { handedness?: XRHandedness } = {}, | |
onChange?: ( | |
touchedBy: Array<XRController>, | |
enteredControllers: Array<XRController>, | |
leftControllers: Array<XRController> | |
) => void | |
): Array<XRController> { | |
const [touchedBy, setTouchedBy] = useState<Array<XRController>>(() => []) | |
useTouchedEvent(ref, boundingBox, controllerRadius, controllerOptions, (touchedBy, entered, left) => { | |
if (onChange != null) { | |
onChange(touchedBy, entered, left) | |
} | |
setTouchedBy(touchedBy) | |
}) | |
return touchedBy | |
} | |
function useFilteredControllers({ handedness }: { handedness?: XRHandedness }) { | |
const { controllers: allControllers } = useXR() | |
const controllers = useMemo( | |
() => | |
handedness != null | |
? allControllers.filter((controller) => controller.inputSource.handedness === handedness) | |
: allControllers, | |
[allControllers, handedness] | |
) | |
return controllers | |
} | |
function calculateTouchedBy( | |
object: Object3D, | |
boundingBox: Box3, | |
controllers: Array<XRController>, | |
controllerBoundingSphere: Sphere, | |
helpVector: Vector3 | |
): Array<XRController> { | |
return controllers.filter((controller, i) => { | |
//todo this should not be done for every touchable | |
controller.grip.getWorldPosition(helpVector) | |
object.worldToLocal(helpVector) | |
controllerBoundingSphere.center.copy(helpVector) | |
return boundingBox.intersectsSphere(controllerBoundingSphere) | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment