react-three-rapier-unified 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ import React, {
2
+ createContext,
3
+ forwardRef,
4
+ memo,
5
+ ReactNode,
6
+ Ref,
7
+ RefObject,
8
+ useContext,
9
+ useEffect,
10
+ useMemo,
11
+ useRef
12
+ } from "react";
13
+ import { Object3D } from "three";
14
+ import { useChildColliderProps, useRapier } from "../hooks/hooks";
15
+ import { useForwardedRef } from "../hooks/use-forwarded-ref";
16
+ import { useImperativeInstance } from "../hooks/use-imperative-instance";
17
+ import { RapierRigidBody, RigidBodyOptions } from "../types";
18
+ import {
19
+ createRigidBodyState,
20
+ immutableRigidBodyOptions,
21
+ rigidBodyDescFromOptions,
22
+ useRigidBodyEvents,
23
+ useUpdateRigidBodyOptions
24
+ } from "../utils/utils-rigidbody";
25
+ import { AnyCollider } from "./AnyCollider";
26
+
27
+ type RigidBodyContextType = {
28
+ ref: RefObject<Object3D>;
29
+ getRigidBody: () => RapierRigidBody;
30
+ options: RigidBodyOptions;
31
+ };
32
+
33
+ export const RigidBodyContext = createContext<RigidBodyContextType>(undefined!);
34
+
35
+ export const useRigidBodyContext = () => useContext(RigidBodyContext);
36
+
37
+ export interface RigidBodyProps extends RigidBodyOptions {
38
+ children?: ReactNode;
39
+ ref?: Ref<RapierRigidBody>;
40
+ }
41
+
42
+ /**
43
+ * A rigid body is a physical object that can be simulated by the physics engine.
44
+ * @category Components
45
+ */
46
+ export const RigidBody = memo((props: RigidBodyProps) => {
47
+ const {
48
+ ref,
49
+ children,
50
+
51
+ type,
52
+ position,
53
+ rotation,
54
+ scale,
55
+
56
+ quaternion,
57
+ transformState,
58
+ ...objectProps
59
+ } = props;
60
+
61
+ const objectRef = useRef<Object3D>(null);
62
+ const rigidBodyRef = useForwardedRef(ref);
63
+ const { world, rigidBodyStates, physicsOptions, rigidBodyEvents } =
64
+ useRapier();
65
+
66
+ const mergedOptions = useMemo(() => {
67
+ return {
68
+ ...physicsOptions,
69
+ ...props,
70
+ children: undefined
71
+ };
72
+ }, [physicsOptions, props]);
73
+
74
+ const immutablePropArray = immutableRigidBodyOptions.flatMap((key) => {
75
+ return Array.isArray(mergedOptions[key])
76
+ ? [...mergedOptions[key]]
77
+ : mergedOptions[key];
78
+ });
79
+
80
+ const childColliderProps = useChildColliderProps(objectRef, mergedOptions);
81
+
82
+ // Provide a way to eagerly create rigidbody
83
+ const getRigidBody = useImperativeInstance(
84
+ () => {
85
+ const desc = rigidBodyDescFromOptions(mergedOptions);
86
+ const rigidBody = world.createRigidBody(desc);
87
+
88
+ if (typeof ref === "function") {
89
+ ref(rigidBody);
90
+ }
91
+ rigidBodyRef.current = rigidBody;
92
+
93
+ return rigidBody;
94
+ },
95
+ (rigidBody) => {
96
+ if (world.getRigidBody(rigidBody.handle)) {
97
+ world.removeRigidBody(rigidBody);
98
+ }
99
+ },
100
+ immutablePropArray
101
+ );
102
+
103
+ // Only provide a object state after the ref has been set
104
+ useEffect(() => {
105
+ const rigidBody = getRigidBody();
106
+
107
+ const state = createRigidBodyState({
108
+ rigidBody,
109
+ object: objectRef.current!
110
+ });
111
+
112
+ rigidBodyStates.set(
113
+ rigidBody.handle,
114
+ props.transformState ? props.transformState(state) : state
115
+ );
116
+
117
+ return () => {
118
+ rigidBodyStates.delete(rigidBody.handle);
119
+ };
120
+ }, [getRigidBody]);
121
+
122
+ useUpdateRigidBodyOptions(getRigidBody, mergedOptions, rigidBodyStates);
123
+ useRigidBodyEvents(getRigidBody, mergedOptions, rigidBodyEvents);
124
+
125
+ const contextValue = useMemo(() => {
126
+ return {
127
+ ref: objectRef as RigidBodyContextType["ref"],
128
+ getRigidBody: getRigidBody,
129
+ options: mergedOptions
130
+ };
131
+ }, [getRigidBody]);
132
+
133
+ return (
134
+ <RigidBodyContext.Provider value={contextValue}>
135
+ <object3D
136
+ ref={objectRef}
137
+ {...objectProps}
138
+ position={position}
139
+ rotation={rotation}
140
+ quaternion={quaternion}
141
+ scale={scale}
142
+ >
143
+ {children}
144
+
145
+ {childColliderProps.map((colliderProps, index) => (
146
+ <AnyCollider key={index} {...colliderProps} />
147
+ ))}
148
+ </object3D>
149
+ </RigidBodyContext.Provider>
150
+ );
151
+ });
152
+
153
+ RigidBody.displayName = "RigidBody";
@@ -0,0 +1,211 @@
1
+ import React, {
2
+ RefObject,
3
+ useContext,
4
+ useEffect,
5
+ useRef,
6
+ useState
7
+ } from "react";
8
+ import {
9
+ rapierContext,
10
+ RapierContext,
11
+ WorldStepCallback
12
+ } from "../components/Physics";
13
+ import { Object3D } from "three";
14
+
15
+ import { ColliderProps, RigidBodyProps } from "..";
16
+ import { createColliderPropsFromChildren } from "../utils/utils-collider";
17
+
18
+ // Utils
19
+ const useMutableCallback = <T>(fn: T) => {
20
+ const ref = useRef<T>(fn);
21
+ useEffect(() => {
22
+ ref.current = fn;
23
+ }, [fn]);
24
+ return ref;
25
+ };
26
+
27
+ // External hooks
28
+ /**
29
+ * Exposes the Rapier context, and world
30
+ * @category Hooks
31
+ */
32
+ export const useRapier = (): RapierContext => {
33
+ const rapier = useContext(rapierContext);
34
+ if (!rapier)
35
+ throw new Error(
36
+ "react-three-rapier: useRapier must be used within <Physics />!"
37
+ );
38
+ return rapier;
39
+ };
40
+
41
+ /**
42
+ * Registers a callback to be called before the physics step
43
+ * @category Hooks
44
+ */
45
+ export const useBeforePhysicsStep = (callback: WorldStepCallback) => {
46
+ const { beforeStepCallbacks } = useRapier();
47
+
48
+ const ref = useMutableCallback(callback);
49
+
50
+ useEffect(() => {
51
+ beforeStepCallbacks.add(ref);
52
+
53
+ return () => {
54
+ beforeStepCallbacks.delete(ref);
55
+ };
56
+ }, []);
57
+ };
58
+
59
+ /**
60
+ * Registers a callback to be called after the physics step
61
+ * @category Hooks
62
+ */
63
+ export const useAfterPhysicsStep = (callback: WorldStepCallback) => {
64
+ const { afterStepCallbacks } = useRapier();
65
+
66
+ const ref = useMutableCallback(callback);
67
+
68
+ useEffect(() => {
69
+ afterStepCallbacks.add(ref);
70
+
71
+ return () => {
72
+ afterStepCallbacks.delete(ref);
73
+ };
74
+ }, []);
75
+ };
76
+
77
+ /**
78
+ * Registers a callback to filter contact pairs.
79
+ *
80
+ * The callback determines if contact computation should happen between two colliders,
81
+ * and how the constraints solver should behave for these contacts.
82
+ *
83
+ * This will only be executed if at least one of the involved colliders contains the
84
+ * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks.
85
+ *
86
+ * @param callback - Function that returns:
87
+ * - `SolverFlags.COMPUTE_IMPULSE` (1) - Process the collision normally (compute impulses and resolve penetration)
88
+ * - `SolverFlags.EMPTY` (0) - Skip computing impulses for this collision pair (colliders pass through each other)
89
+ * - `null` - Skip this hook; let the next registered hook decide, or use Rapier's default behavior if no hook handles it
90
+ *
91
+ * When multiple hooks are registered, they are called in order until one returns a non-null value.
92
+ * That value is then passed to Rapier's physics engine.
93
+ *
94
+ * @category Hooks
95
+ *
96
+ * @example
97
+ * ```tsx
98
+ * import { useFilterContactPair } from '@react-three/rapier';
99
+ * import { SolverFlags } from '@dimforge/rapier3d-compat';
100
+ *
101
+ * useFilterContactPair((collider1, collider2, body1, body2) => {
102
+ * // Only process collisions for specific bodies
103
+ * if (body1 === myBodyHandle) {
104
+ * return SolverFlags.COMPUTE_IMPULSE;
105
+ * }
106
+ * // Let other hooks or default behavior handle it
107
+ * return null;
108
+ * });
109
+ * ```
110
+ */
111
+ export const useFilterContactPair = (
112
+ callback: (
113
+ collider1: number,
114
+ collider2: number,
115
+ body1: number,
116
+ body2: number
117
+ ) => number | null
118
+ ) => {
119
+ const { filterContactPairHooks } = useRapier();
120
+
121
+ const ref = useMutableCallback(callback);
122
+
123
+ useEffect(() => {
124
+ filterContactPairHooks.add(ref);
125
+
126
+ return () => {
127
+ filterContactPairHooks.delete(ref);
128
+ };
129
+ }, []);
130
+ };
131
+
132
+ /**
133
+ * Registers a callback to filter intersection pairs.
134
+ *
135
+ * The callback determines if intersection computation should happen between two colliders
136
+ * (where at least one is a sensor).
137
+ *
138
+ * This will only be executed if at least one of the involved colliders contains the
139
+ * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks.
140
+ *
141
+ * @param callback - Function that returns:
142
+ * - `true` - Allow the intersection to be detected (trigger intersection events)
143
+ * - `false` - Block the intersection (no intersection events will fire)
144
+ *
145
+ * When multiple hooks are registered, the **first hook that returns `false` blocks** the intersection.
146
+ * If all hooks return `true`, the intersection is allowed.
147
+ *
148
+ * @category Hooks
149
+ *
150
+ * @example
151
+ * ```tsx
152
+ * import { useFilterIntersectionPair } from '@react-three/rapier';
153
+ *
154
+ * useFilterIntersectionPair((collider1, collider2, body1, body2) => {
155
+ * // Block intersections for specific body pairs
156
+ * if (body1 === myBodyHandle && body2 === otherBodyHandle) {
157
+ * return false;
158
+ * }
159
+ * // Allow all other intersections
160
+ * return true;
161
+ * });
162
+ * ```
163
+ */
164
+ export const useFilterIntersectionPair = (
165
+ callback: (
166
+ collider1: number,
167
+ collider2: number,
168
+ body1: number,
169
+ body2: number
170
+ ) => boolean
171
+ ) => {
172
+ const { filterIntersectionPairHooks } = useRapier();
173
+
174
+ const ref = useMutableCallback(callback);
175
+
176
+ useEffect(() => {
177
+ filterIntersectionPairHooks.add(ref);
178
+
179
+ return () => {
180
+ filterIntersectionPairHooks.delete(ref);
181
+ };
182
+ }, []);
183
+ };
184
+
185
+ // Internal hooks
186
+ /**
187
+ * @internal
188
+ */
189
+ export const useChildColliderProps = <O extends Object3D>(
190
+ ref: RefObject<O | undefined | null>,
191
+ options: RigidBodyProps,
192
+ ignoreMeshColliders = true
193
+ ) => {
194
+ const [colliderProps, setColliderProps] = useState<ColliderProps[]>([]);
195
+
196
+ useEffect(() => {
197
+ const object = ref.current;
198
+
199
+ if (object && options.colliders !== false) {
200
+ setColliderProps(
201
+ createColliderPropsFromChildren({
202
+ object: ref.current!,
203
+ options,
204
+ ignoreMeshColliders
205
+ })
206
+ );
207
+ }
208
+ }, [options.colliders]);
209
+
210
+ return colliderProps;
211
+ };
@@ -0,0 +1,221 @@
1
+ import {
2
+ FixedImpulseJoint,
3
+ ImpulseJoint,
4
+ PrismaticImpulseJoint,
5
+ RevoluteImpulseJoint,
6
+ RopeImpulseJoint,
7
+ SphericalImpulseJoint,
8
+ SpringImpulseJoint
9
+ } from "@dimforge/rapier3d-compat";
10
+ import { RefObject, useRef } from "react";
11
+ import {
12
+ FixedJointParams,
13
+ PrismaticJointParams,
14
+ RapierRigidBody,
15
+ RevoluteJointParams,
16
+ RopeJointParams,
17
+ SphericalJointParams,
18
+ SpringJointParams,
19
+ UseImpulseJoint,
20
+ useRapier
21
+ } from "..";
22
+ import {
23
+ vector3ToRapierVector,
24
+ quaternionToRapierQuaternion
25
+ } from "../utils/utils";
26
+
27
+ import type Rapier from "@dimforge/rapier3d-compat";
28
+ import { useImperativeInstance } from "./use-imperative-instance";
29
+
30
+ /**
31
+ * @internal
32
+ */
33
+ export const useImpulseJoint = <JointType extends ImpulseJoint>(
34
+ body1: RefObject<RapierRigidBody>,
35
+ body2: RefObject<RapierRigidBody>,
36
+ params: Rapier.JointData
37
+ ) => {
38
+ const { world } = useRapier();
39
+ const jointRef = useRef<JointType | undefined>(undefined);
40
+
41
+ useImperativeInstance(
42
+ () => {
43
+ if (body1.current && body2.current) {
44
+ const newJoint = world.createImpulseJoint(
45
+ params,
46
+ body1.current,
47
+ body2.current,
48
+ true
49
+ ) as JointType;
50
+
51
+ jointRef.current = newJoint;
52
+
53
+ return newJoint;
54
+ }
55
+ },
56
+ (joint) => {
57
+ if (joint) {
58
+ jointRef.current = undefined;
59
+ if (world.getImpulseJoint(joint.handle)) {
60
+ world.removeImpulseJoint(joint, true);
61
+ }
62
+ }
63
+ },
64
+ []
65
+ );
66
+
67
+ return jointRef;
68
+ };
69
+
70
+ /**
71
+ * A fixed joint ensures that two rigid-bodies don't move relative to each other.
72
+ * Fixed joints are characterized by one local frame (represented by an isometry) on each rigid-body.
73
+ * The fixed-joint makes these frames coincide in world-space.
74
+ *
75
+ * @category Hooks - Joints
76
+ */
77
+ export const useFixedJoint: UseImpulseJoint<
78
+ FixedJointParams,
79
+ FixedImpulseJoint
80
+ > = (
81
+ body1,
82
+ body2,
83
+ [body1Anchor, body1LocalFrame, body2Anchor, body2LocalFrame]
84
+ ) => {
85
+ const { rapier } = useRapier();
86
+
87
+ return useImpulseJoint<FixedImpulseJoint>(
88
+ body1,
89
+ body2,
90
+ rapier.JointData.fixed(
91
+ vector3ToRapierVector(body1Anchor),
92
+ quaternionToRapierQuaternion(body1LocalFrame),
93
+ vector3ToRapierVector(body2Anchor),
94
+ quaternionToRapierQuaternion(body2LocalFrame)
95
+ )
96
+ );
97
+ };
98
+
99
+ /**
100
+ * The spherical joint ensures that two points on the local-spaces of two rigid-bodies always coincide (it prevents any relative
101
+ * translational motion at this points). This is typically used to simulate ragdolls arms, pendulums, etc.
102
+ * They are characterized by one local anchor on each rigid-body. Each anchor represents the location of the
103
+ * points that need to coincide on the local-space of each rigid-body.
104
+ *
105
+ * @category Hooks - Joints
106
+ */
107
+ export const useSphericalJoint: UseImpulseJoint<
108
+ SphericalJointParams,
109
+ SphericalImpulseJoint
110
+ > = (body1, body2, [body1Anchor, body2Anchor]) => {
111
+ const { rapier } = useRapier();
112
+
113
+ return useImpulseJoint<SphericalImpulseJoint>(
114
+ body1,
115
+ body2,
116
+ rapier.JointData.spherical(
117
+ vector3ToRapierVector(body1Anchor),
118
+ vector3ToRapierVector(body2Anchor)
119
+ )
120
+ );
121
+ };
122
+
123
+ /**
124
+ * The revolute joint prevents any relative movement between two rigid-bodies, except for relative
125
+ * rotations along one axis. This is typically used to simulate wheels, fans, etc.
126
+ * They are characterized by one local anchor as well as one local axis on each rigid-body.
127
+ *
128
+ * @category Hooks - Joints
129
+ */
130
+ export const useRevoluteJoint: UseImpulseJoint<
131
+ RevoluteJointParams,
132
+ RevoluteImpulseJoint
133
+ > = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
134
+ const { rapier } = useRapier();
135
+
136
+ const params = rapier.JointData.revolute(
137
+ vector3ToRapierVector(body1Anchor),
138
+ vector3ToRapierVector(body2Anchor),
139
+ vector3ToRapierVector(axis)
140
+ );
141
+
142
+ if (limits) {
143
+ params.limitsEnabled = true;
144
+ params.limits = limits;
145
+ }
146
+
147
+ return useImpulseJoint<RevoluteImpulseJoint>(body1, body2, params);
148
+ };
149
+
150
+ /**
151
+ * The prismatic joint prevents any relative movement between two rigid-bodies, except for relative translations along one axis.
152
+ * It is characterized by one local anchor as well as one local axis on each rigid-body. In 3D, an optional
153
+ * local tangent axis can be specified for each rigid-body.
154
+ *
155
+ * @category Hooks - Joints
156
+ */
157
+ export const usePrismaticJoint: UseImpulseJoint<
158
+ PrismaticJointParams,
159
+ PrismaticImpulseJoint
160
+ > = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
161
+ const { rapier } = useRapier();
162
+
163
+ const params = rapier.JointData.prismatic(
164
+ vector3ToRapierVector(body1Anchor),
165
+ vector3ToRapierVector(body2Anchor),
166
+ vector3ToRapierVector(axis)
167
+ );
168
+
169
+ if (limits) {
170
+ params.limitsEnabled = true;
171
+ params.limits = limits;
172
+ }
173
+
174
+ return useImpulseJoint<PrismaticImpulseJoint>(body1, body2, params);
175
+ };
176
+
177
+ /**
178
+ * The rope joint limits the max distance between two bodies.
179
+ * @category Hooks - Joints
180
+ */
181
+ export const useRopeJoint: UseImpulseJoint<
182
+ RopeJointParams,
183
+ RopeImpulseJoint
184
+ > = (body1, body2, [body1Anchor, body2Anchor, length]) => {
185
+ const { rapier } = useRapier();
186
+
187
+ const vBody1Anchor = vector3ToRapierVector(body1Anchor);
188
+ const vBody2Anchor = vector3ToRapierVector(body2Anchor);
189
+
190
+ const params = rapier.JointData.rope(length, vBody1Anchor, vBody2Anchor);
191
+
192
+ return useImpulseJoint<RopeImpulseJoint>(body1, body2, params);
193
+ };
194
+
195
+ /**
196
+ * The spring joint applies a force proportional to the distance between two objects.
197
+ * @category Hooks - Joints
198
+ */
199
+ export const useSpringJoint: UseImpulseJoint<
200
+ SpringJointParams,
201
+ SpringImpulseJoint
202
+ > = (
203
+ body1,
204
+ body2,
205
+ [body1Anchor, body2Anchor, restLength, stiffness, damping]
206
+ ) => {
207
+ const { rapier } = useRapier();
208
+
209
+ const vBody1Anchor = vector3ToRapierVector(body1Anchor);
210
+ const vBody2Anchor = vector3ToRapierVector(body2Anchor);
211
+
212
+ const params = rapier.JointData.spring(
213
+ restLength,
214
+ stiffness,
215
+ damping,
216
+ vBody1Anchor,
217
+ vBody2Anchor
218
+ );
219
+
220
+ return useImpulseJoint<SpringImpulseJoint>(body1, body2, params);
221
+ };
@@ -0,0 +1,19 @@
1
+ import { ForwardedRef, RefObject, useRef } from "react";
2
+
3
+ // Need to catch the case where forwardedRef is a function... how to do that?
4
+ export const useForwardedRef = <T>(
5
+ forwardedRef: ForwardedRef<T> | undefined,
6
+ defaultValue: T | null = null
7
+ ): RefObject<T> => {
8
+ const innerRef = useRef<T>(defaultValue);
9
+
10
+ // Update the forwarded ref when the inner ref changes
11
+ if (forwardedRef && typeof forwardedRef !== "function") {
12
+ if (!forwardedRef.current) {
13
+ forwardedRef.current = innerRef.current;
14
+ }
15
+ return forwardedRef as RefObject<T>;
16
+ }
17
+
18
+ return innerRef as RefObject<T>;
19
+ };
@@ -0,0 +1,33 @@
1
+ import { DependencyList, useCallback, useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Initiate an instance and return a safe getter
5
+ */
6
+ export const useImperativeInstance = <InstanceType>(
7
+ createFn: () => InstanceType,
8
+ destroyFn: (instance: InstanceType) => void,
9
+ dependencyList: DependencyList
10
+ ) => {
11
+ const ref = useRef<InstanceType | undefined>(undefined);
12
+
13
+ const getInstance = useCallback(() => {
14
+ if (!ref.current) {
15
+ ref.current = createFn();
16
+ }
17
+
18
+ return ref.current;
19
+ }, dependencyList);
20
+
21
+ useEffect(() => {
22
+ // Save the destroy function and instance
23
+ const instance = getInstance();
24
+ const destroy = () => destroyFn(instance);
25
+
26
+ return () => {
27
+ destroy();
28
+ ref.current = undefined;
29
+ };
30
+ }, [getInstance]);
31
+
32
+ return getInstance;
33
+ };
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ // Core exports from react-three-rapier
2
+ export * from "./types";
3
+
4
+ // Component exports
5
+ export type { RigidBodyProps } from "./components/RigidBody";
6
+ export type {
7
+ InstancedRigidBodiesProps,
8
+ InstancedRigidBodyProps
9
+ } from "./components/InstancedRigidBodies";
10
+ export type {
11
+ CylinderColliderProps,
12
+ BallColliderProps,
13
+ CapsuleColliderProps,
14
+ ConeColliderProps,
15
+ ConvexHullColliderProps,
16
+ CuboidColliderProps,
17
+ HeightfieldColliderProps,
18
+ RoundCuboidColliderProps,
19
+ TrimeshColliderProps,
20
+ ColliderOptionsRequiredArgs
21
+ } from "./components/AnyCollider";
22
+
23
+ export type {
24
+ PhysicsProps,
25
+ RapierContext,
26
+ WorldStepCallback,
27
+ FilterContactPairCallback,
28
+ FilterIntersectionPairCallback
29
+ } from "./components/Physics";
30
+ export type { MeshColliderProps } from "./components/MeshCollider";
31
+
32
+ export { Physics } from "./components/Physics";
33
+ export { RigidBody } from "./components/RigidBody";
34
+ export { MeshCollider } from "./components/MeshCollider";
35
+ export { InstancedRigidBodies } from "./components/InstancedRigidBodies";
36
+ export * from "./components/AnyCollider";
37
+
38
+ // Hooks exports
39
+ export * from "./hooks/joints";
40
+ export {
41
+ useRapier,
42
+ useBeforePhysicsStep,
43
+ useAfterPhysicsStep,
44
+ useFilterContactPair,
45
+ useFilterIntersectionPair
46
+ } from "./hooks/hooks";
47
+
48
+ // Utils exports
49
+ export * from "./utils/interaction-groups";
50
+ export * from "./utils/three-object-helpers";
51
+
52
+ // Addons exports
53
+ export * from "./addons/attractor/Attractor";
54
+ export * from "./addons/attractor/AttractorDebugHelper";
55
+