Skip to content

Instantly share code, notes, and snippets.

@bbohlender
Created July 18, 2022 18:07
Show Gist options
  • Save bbohlender/d7531b86a1e0899d879ed287b13171b7 to your computer and use it in GitHub Desktop.
Save bbohlender/d7531b86a1e0899d879ed287b13171b7 to your computer and use it in GitHub Desktop.
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
}
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