import type { AtomicName, AtomicProps, BodyProps, BodyShapeType, BoxProps, CannonWorkerAPI, CompoundBodyProps, ConeTwistConstraintOpts, ConstraintOptns, ConstraintTypes, ContactMaterialOptions, ConvexPolyhedronArgs, ConvexPolyhedronProps, CylinderProps, DistanceConstraintOpts, HeightfieldProps, HingeConstraintOpts, LockConstraintOpts, MaterialOptions, ParticleProps, PlaneProps, PointToPointConstraintOpts, PropValue, Quad, RayhitEvent, RayMode, RayOptions, SetOpName, SphereArgs, SphereProps, SpringOptns, SubscriptionName, Subscriptions, SubscriptionTarget, TrimeshProps, Triplet, VectorName, WheelInfoOptions, } from '@pmndrs/cannon-worker-api' import type { DependencyList, MutableRefObject, Ref, RefObject } from 'react' import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { DynamicDrawUsage, Euler, InstancedMesh, MathUtils, Object3D, Quaternion, Vector3 } from 'three' import { useDebugContext } from './debug-context' import type { CannonEvents } from './physics-context' import { usePhysicsContext } from './physics-context' export type AtomicApi = { set: (value: AtomicProps[K]) => void subscribe: (callback: (value: AtomicProps[K]) => void) => () => void } export type QuaternionApi = { copy: ({ w, x, y, z }: Quaternion) => void set: (x: number, y: number, z: number, w: number) => void subscribe: (callback: (value: Quad) => void) => () => void } export type VectorApi = { copy: ({ x, y, z }: Vector3 | Euler) => void set: (x: number, y: number, z: number) => void subscribe: (callback: (value: Triplet) => void) => () => void } export type WorkerApi = { [K in AtomicName]: AtomicApi } & { [K in VectorName]: VectorApi } & { applyForce: (force: Triplet, worldPoint: Triplet) => void applyImpulse: (impulse: Triplet, worldPoint: Triplet) => void applyLocalForce: (force: Triplet, localPoint: Triplet) => void applyLocalImpulse: (impulse: Triplet, localPoint: Triplet) => void applyTorque: (torque: Triplet) => void quaternion: QuaternionApi rotation: VectorApi scaleOverride: (scale: Triplet) => void sleep: () => void wakeUp: () => void } export interface PublicApi extends WorkerApi { at: (index: number) => WorkerApi } export type Api = [RefObject, PublicApi] const temp = new Object3D() function useForwardedRef(ref: Ref): MutableRefObject { const nullRef = useRef(null) return ref && typeof ref !== 'function' ? ref : nullRef } function capitalize(str: T): Capitalize { return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize } function getUUID(ref: Ref, index?: number): string | null { const suffix = index === undefined ? '' : `/${index}` if (typeof ref === 'function') return null return ref && ref.current && `${ref.current.uuid}${suffix}` } const e = new Euler() const q = new Quaternion() const quaternionToRotation = (callback: (v: Triplet) => void) => { return (v: Quad) => callback(e.setFromQuaternion(q.fromArray(v)).toArray() as Triplet) } let incrementingId = 0 function subscribe( ref: RefObject, worker: CannonWorkerAPI, subscriptions: Subscriptions, type: T, index?: number, target: SubscriptionTarget = 'bodies', ) { return (callback: (value: PropValue) => void) => { const id = incrementingId++ subscriptions[id] = { [type]: callback } const uuid = getUUID(ref, index) uuid && worker.subscribe({ props: { id, target, type }, uuid }) return () => { delete subscriptions[id] worker.unsubscribe({ props: id }) } } } function prepare(object: Object3D, { position = [0, 0, 0], rotation = [0, 0, 0], userData = {} }: BodyProps) { object.userData = userData object.position.set(...position) object.rotation.set(...rotation) object.updateMatrix() } function setupCollision( events: CannonEvents, { onCollide, onCollideBegin, onCollideEnd }: Partial, uuid: string, ) { events[uuid] = { collide: onCollide, collideBegin: onCollideBegin, collideEnd: onCollideEnd, } } type GetByIndex = (index: number) => T type ArgFn = (args: T) => unknown[] function useBody, O extends Object3D>( type: BodyShapeType, fn: GetByIndex, argsFn: ArgFn, fwdRef: Ref = null, deps: DependencyList = [], ): Api { const ref = useForwardedRef(fwdRef) const { events, refs, scaleOverrides, subscriptions, worker } = usePhysicsContext() const debugApi = useDebugContext() useLayoutEffect(() => { if (!ref.current) { // When the reference isn't used we create a stub // The body doesn't have a visual representation but can still be constrained // Yes, this type may be technically incorrect ref.current = new Object3D() as O } const object = ref.current const currentWorker = worker const objectCount = object instanceof InstancedMesh ? (object.instanceMatrix.setUsage(DynamicDrawUsage), object.count) : 1 const uuid = object instanceof InstancedMesh ? new Array(objectCount).fill(0).map((_, i) => `${object.uuid}/${i}`) : [object.uuid] const props: (B & { args: unknown })[] = object instanceof InstancedMesh ? uuid.map((id, i) => { const props = fn(i) prepare(temp, props) object.setMatrixAt(i, temp.matrix) object.instanceMatrix.needsUpdate = true refs[id] = object debugApi?.add(id, props, type) setupCollision(events, props, id) return { ...props, args: argsFn(props.args) } }) : uuid.map((id, i) => { const props = fn(i) prepare(object, props) refs[id] = object debugApi?.add(id, props, type) setupCollision(events, props, id) return { ...props, args: argsFn(props.args) } }) // Register on mount, unregister on unmount currentWorker.addBodies({ props: props.map(({ onCollide, onCollideBegin, onCollideEnd, ...serializableProps }) => { return { onCollide: Boolean(onCollide), ...serializableProps } }), type, uuid, }) return () => { uuid.forEach((id) => { delete refs[id] debugApi?.remove(id) delete events[id] }) currentWorker.removeBodies({ uuid }) } }, deps) const api = useMemo(() => { const makeAtomic = (type: T, index?: number) => { const op: SetOpName = `set${capitalize(type)}` return { set: (value: PropValue) => { const uuid = getUUID(ref, index) uuid && worker[op]({ props: value, uuid, } as never) }, subscribe: subscribe(ref, worker, subscriptions, type, index), } } const makeQuaternion = (index?: number) => { const type = 'quaternion' return { copy: ({ w, x, y, z }: Quaternion) => { const uuid = getUUID(ref, index) uuid && worker.setQuaternion({ props: [x, y, z, w], uuid }) }, set: (x: number, y: number, z: number, w: number) => { const uuid = getUUID(ref, index) uuid && worker.setQuaternion({ props: [x, y, z, w], uuid }) }, subscribe: subscribe(ref, worker, subscriptions, type, index), } } const makeRotation = (index?: number) => { return { copy: ({ x, y, z }: Vector3 | Euler) => { const uuid = getUUID(ref, index) uuid && worker.setRotation({ props: [x, y, z], uuid }) }, set: (x: number, y: number, z: number) => { const uuid = getUUID(ref, index) uuid && worker.setRotation({ props: [x, y, z], uuid }) }, subscribe: (callback: (value: Triplet) => void) => { const id = incrementingId++ const target = 'bodies' const type = 'quaternion' const uuid = getUUID(ref, index) subscriptions[id] = { [type]: quaternionToRotation(callback) } uuid && worker.subscribe({ props: { id, target, type }, uuid }) return () => { delete subscriptions[id] worker.unsubscribe({ props: id }) } }, } } const makeVec = (type: VectorName, index?: number) => { const op: SetOpName = `set${capitalize(type)}` return { copy: ({ x, y, z }: Vector3 | Euler) => { const uuid = getUUID(ref, index) uuid && worker[op]({ props: [x, y, z], uuid }) }, set: (x: number, y: number, z: number) => { const uuid = getUUID(ref, index) uuid && worker[op]({ props: [x, y, z], uuid }) }, subscribe: subscribe(ref, worker, subscriptions, type, index), } } function makeApi(index?: number): WorkerApi { return { allowSleep: makeAtomic('allowSleep', index), angularDamping: makeAtomic('angularDamping', index), angularFactor: makeVec('angularFactor', index), angularVelocity: makeVec('angularVelocity', index), applyForce(force: Triplet, worldPoint: Triplet) { const uuid = getUUID(ref, index) uuid && worker.applyForce({ props: [force, worldPoint], uuid }) }, applyImpulse(impulse: Triplet, worldPoint: Triplet) { const uuid = getUUID(ref, index) uuid && worker.applyImpulse({ props: [impulse, worldPoint], uuid }) }, applyLocalForce(force: Triplet, localPoint: Triplet) { const uuid = getUUID(ref, index) uuid && worker.applyLocalForce({ props: [force, localPoint], uuid }) }, applyLocalImpulse(impulse: Triplet, localPoint: Triplet) { const uuid = getUUID(ref, index) uuid && worker.applyLocalImpulse({ props: [impulse, localPoint], uuid }) }, applyTorque(torque: Triplet) { const uuid = getUUID(ref, index) uuid && worker.applyTorque({ props: [torque], uuid }) }, collisionFilterGroup: makeAtomic('collisionFilterGroup', index), collisionFilterMask: makeAtomic('collisionFilterMask', index), collisionResponse: makeAtomic('collisionResponse', index), fixedRotation: makeAtomic('fixedRotation', index), isTrigger: makeAtomic('isTrigger', index), linearDamping: makeAtomic('linearDamping', index), linearFactor: makeVec('linearFactor', index), mass: makeAtomic('mass', index), material: makeAtomic('material', index), position: makeVec('position', index), quaternion: makeQuaternion(index), rotation: makeRotation(index), scaleOverride(scale) { const uuid = getUUID(ref, index) if (uuid) scaleOverrides[uuid] = new Vector3(...scale) }, sleep() { const uuid = getUUID(ref, index) uuid && worker.sleep({ uuid }) }, sleepSpeedLimit: makeAtomic('sleepSpeedLimit', index), sleepTimeLimit: makeAtomic('sleepTimeLimit', index), userData: makeAtomic('userData', index), velocity: makeVec('velocity', index), wakeUp() { const uuid = getUUID(ref, index) uuid && worker.wakeUp({ uuid }) }, } } const cache: { [index: number]: WorkerApi } = {} return { ...makeApi(undefined), at: (index: number) => cache[index] || (cache[index] = makeApi(index)), } }, []) return [ref, api] } function makeTriplet(v: Vector3 | Triplet): Triplet { return v instanceof Vector3 ? [v.x, v.y, v.z] : v } export function usePlane( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody('Plane', fn, () => [], fwdRef, deps) } export function useBox(fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList) { const defaultBoxArgs: Triplet = [1, 1, 1] return useBody('Box', fn, (args = defaultBoxArgs): Triplet => args, fwdRef, deps) } export function useCylinder( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody('Cylinder', fn, (args = [] as []) => args, fwdRef, deps) } export function useHeightfield( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody('Heightfield', fn, (args) => args, fwdRef, deps) } export function useParticle( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody('Particle', fn, () => [], fwdRef, deps) } export function useSphere( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody( 'Sphere', fn, (args: SphereArgs = [1]): SphereArgs => { if (!Array.isArray(args)) throw new Error('useSphere args must be an array') return [args[0]] }, fwdRef, deps, ) } export function useTrimesh( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody('Trimesh', fn, (args) => args, fwdRef, deps) } export function useConvexPolyhedron( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody( 'ConvexPolyhedron', fn, ([vertices, faces, normals, axes, boundingSphereRadius] = []): ConvexPolyhedronArgs => [ vertices && vertices.map(makeTriplet), faces, normals && normals.map(makeTriplet), axes && axes.map(makeTriplet), boundingSphereRadius, ], fwdRef, deps, ) } export function useCompoundBody( fn: GetByIndex, fwdRef?: Ref, deps?: DependencyList, ) { return useBody('Compound', fn, (args) => args as unknown[], fwdRef, deps) } type ConstraintApi = [ RefObject, RefObject, { disable: () => void enable: () => void }, ] type HingeConstraintApi = [ RefObject, RefObject, { disable: () => void disableMotor: () => void enable: () => void enableMotor: () => void setMotorMaxForce: (value: number) => void setMotorSpeed: (value: number) => void }, ] type SpringApi = [ RefObject, RefObject, { setDamping: (value: number) => void setRestLength: (value: number) => void setStiffness: (value: number) => void }, ] type ConstraintORHingeApi< T extends 'Hinge' | ConstraintTypes, A extends Object3D, B extends Object3D, > = T extends ConstraintTypes ? ConstraintApi : HingeConstraintApi function useConstraint( type: T, bodyA: Ref, bodyB: Ref, optns: ConstraintOptns | HingeConstraintOpts = {}, deps: DependencyList = [], ): ConstraintORHingeApi { const { worker } = usePhysicsContext() const uuid = MathUtils.generateUUID() const refA = useForwardedRef(bodyA) const refB = useForwardedRef(bodyB) useEffect(() => { if (refA.current && refB.current) { worker.addConstraint({ props: [refA.current.uuid, refB.current.uuid, optns], type, uuid, }) return () => worker.removeConstraint({ uuid }) } }, deps) const api = useMemo(() => { const enableDisable = { disable: () => worker.disableConstraint({ uuid }), enable: () => worker.enableConstraint({ uuid }), } if (type === 'Hinge') { return { ...enableDisable, disableMotor: () => worker.disableConstraintMotor({ uuid }), enableMotor: () => worker.enableConstraintMotor({ uuid }), setMotorMaxForce: (value: number) => worker.setConstraintMotorMaxForce({ props: value, uuid }), setMotorSpeed: (value: number) => worker.setConstraintMotorSpeed({ props: value, uuid }), } } return enableDisable }, deps) return [refA, refB, api] as ConstraintORHingeApi } export function usePointToPointConstraint( bodyA: Ref = null, bodyB: Ref = null, optns: PointToPointConstraintOpts, deps: DependencyList = [], ) { return useConstraint('PointToPoint', bodyA, bodyB, optns, deps) } export function useConeTwistConstraint( bodyA: Ref = null, bodyB: Ref = null, optns: ConeTwistConstraintOpts, deps: DependencyList = [], ) { return useConstraint('ConeTwist', bodyA, bodyB, optns, deps) } export function useDistanceConstraint( bodyA: Ref = null, bodyB: Ref = null, optns: DistanceConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Distance', bodyA, bodyB, optns, deps) } export function useHingeConstraint( bodyA: Ref = null, bodyB: Ref = null, optns: HingeConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Hinge', bodyA, bodyB, optns, deps) } export function useLockConstraint( bodyA: Ref = null, bodyB: Ref = null, optns: LockConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Lock', bodyA, bodyB, optns, deps) } export function useSpring( bodyA: Ref = null, bodyB: Ref = null, optns: SpringOptns, deps: DependencyList = [], ): SpringApi { const { worker } = usePhysicsContext() const [uuid] = useState(() => MathUtils.generateUUID()) const refA = useForwardedRef(bodyA) const refB = useForwardedRef(bodyB) useEffect(() => { if (refA.current && refB.current) { worker.addSpring({ props: [refA.current.uuid, refB.current.uuid, optns], uuid, }) return () => { worker.removeSpring({ uuid }) } } }, deps) const api = useMemo( () => ({ setDamping: (value: number) => worker.setSpringDamping({ props: value, uuid }), setRestLength: (value: number) => worker.setSpringRestLength({ props: value, uuid }), setStiffness: (value: number) => worker.setSpringStiffness({ props: value, uuid }), }), deps, ) return [refA, refB, api] } function useRay( mode: RayMode, options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { const { worker, events } = usePhysicsContext() const [uuid] = useState(() => MathUtils.generateUUID()) useEffect(() => { events[uuid] = { rayhit: callback } worker.addRay({ props: { ...options, mode }, uuid }) return () => { worker.removeRay({ uuid }) delete events[uuid] } }, deps) } export function useRaycastClosest( options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { useRay('Closest', options, callback, deps) } export function useRaycastAny( options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { useRay('Any', options, callback, deps) } export function useRaycastAll( options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { useRay('All', options, callback, deps) } export interface RaycastVehiclePublicApi { applyEngineForce: (value: number, wheelIndex: number) => void setBrake: (brake: number, wheelIndex: number) => void setSteeringValue: (value: number, wheelIndex: number) => void sliding: { subscribe: (callback: (sliding: boolean) => void) => void } } function isString(v: unknown): v is string { return typeof v === 'string' } export interface RaycastVehicleProps { chassisBody: Ref indexForwardAxis?: number indexRightAxis?: number indexUpAxis?: number wheelInfos: WheelInfoOptions[] wheels: Ref[] } export function useRaycastVehicle( fn: () => RaycastVehicleProps, fwdRef: Ref = null, deps: DependencyList = [], ): [RefObject, RaycastVehiclePublicApi] { const ref = useForwardedRef(fwdRef) const { worker, subscriptions } = usePhysicsContext() useLayoutEffect(() => { if (!ref.current) { // When the reference isn't used we create a stub // The body doesn't have a visual representation but can still be constrained // Yes, this type may be technically incorrect ref.current = new Object3D() as O } const currentWorker = worker const uuid: string = ref.current.uuid const { chassisBody, indexForwardAxis = 2, indexRightAxis = 0, indexUpAxis = 1, wheelInfos, wheels, } = fn() const chassisBodyUUID = getUUID(chassisBody) const wheelUUIDs = wheels.map((ref) => getUUID(ref)) if (!chassisBodyUUID || !wheelUUIDs.every(isString)) return currentWorker.addRaycastVehicle({ props: [chassisBodyUUID, wheelUUIDs, wheelInfos, indexForwardAxis, indexRightAxis, indexUpAxis], uuid, }) return () => { currentWorker.removeRaycastVehicle({ uuid }) } }, deps) const api = useMemo(() => { return { applyEngineForce(value: number, wheelIndex: number) { const uuid = getUUID(ref) uuid && worker.applyRaycastVehicleEngineForce({ props: [value, wheelIndex], uuid, }) }, setBrake(brake: number, wheelIndex: number) { const uuid = getUUID(ref) uuid && worker.setRaycastVehicleBrake({ props: [brake, wheelIndex], uuid }) }, setSteeringValue(value: number, wheelIndex: number) { const uuid = getUUID(ref) uuid && worker.setRaycastVehicleSteeringValue({ props: [value, wheelIndex], uuid, }) }, sliding: { subscribe: subscribe(ref, worker, subscriptions, 'sliding', undefined, 'vehicles'), }, } }, deps) return [ref, api] } export function useContactMaterial( materialA: MaterialOptions, materialB: MaterialOptions, options: ContactMaterialOptions, deps: DependencyList = [], ): void { const { worker } = usePhysicsContext() const [uuid] = useState(() => MathUtils.generateUUID()) useEffect(() => { worker.addContactMaterial({ props: [materialA, materialB, options], uuid, }) return () => { worker.removeContactMaterial({ uuid }) } }, deps) }