@react-three/rapier 0.11.0 → 0.11.2

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.
@@ -109,6 +109,7 @@ interface RapierWorldProps {
109
109
  /**
110
110
  * The update priority at which the physics simulation should run.
111
111
  *
112
+ * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#taking-over-the-render-loop
112
113
  * @defaultValue undefined
113
114
  */
114
115
  updatePriority?: number;
@@ -332,12 +332,14 @@ export declare type PrismaticJointParams = [
332
332
  body1Anchor: Vector3Array,
333
333
  body1LocalFrame: Vector3Array,
334
334
  body2Anchor: Vector3Array,
335
- body2LocalFrame: Vector3Array
335
+ body2LocalFrame: Vector3Array,
336
+ limits?: [min: number, max: number]
336
337
  ];
337
338
  export declare type RevoluteJointParams = [
338
339
  body1Anchor: Vector3Array,
339
340
  body2Anchor: Vector3Array,
340
- axis: Vector3Array
341
+ axis: Vector3Array,
342
+ limits?: [min: number, max: number]
341
343
  ];
342
344
  export declare type RigidBodyApiRef = MutableRefObject<undefined | null | RigidBodyApi>;
343
345
  export interface UseImpulseJoint<P> {
@@ -8,3 +8,4 @@ export declare const rapierQuaternionToQuaternion: ({ x, y, z, w }: RapierQuater
8
8
  export declare const rigidBodyTypeFromString: (type: RigidBodyTypeString) => number;
9
9
  export declare const scaleVertices: (vertices: ArrayLike<number>, scale: Vector3) => number[];
10
10
  export declare const vectorToTuple: (v: Vector3 | Quaternion | any[] | undefined | number | Euler) => any[];
11
+ export declare function useConst<T>(initialValue: T | (() => T)): T;
@@ -131,6 +131,17 @@ const vectorToTuple = v => {
131
131
 
132
132
  return [v];
133
133
  };
134
+ function useConst(initialValue) {
135
+ const ref = React.useRef();
136
+
137
+ if (ref.current === undefined) {
138
+ ref.current = {
139
+ value: typeof initialValue === "function" ? initialValue() : initialValue
140
+ };
141
+ }
142
+
143
+ return ref.current.value;
144
+ }
134
145
 
135
146
  const createRigidBodyApi = ref => {
136
147
  return {
@@ -579,8 +590,12 @@ const useColliderEvents = (collidersRef, props, events) => {
579
590
  };
580
591
 
581
592
  const rigidBodyDescFromOptions = options => {
593
+ var _options$canSleep;
594
+
582
595
  const type = rigidBodyTypeFromString((options === null || options === void 0 ? void 0 : options.type) || "dynamic");
583
- const desc = new rapier3dCompat.RigidBodyDesc(type);
596
+ const desc = new rapier3dCompat.RigidBodyDesc(type); // Apply immutable options
597
+
598
+ desc.canSleep = (_options$canSleep = options === null || options === void 0 ? void 0 : options.canSleep) !== null && _options$canSleep !== void 0 ? _options$canSleep : true;
584
599
  return desc;
585
600
  };
586
601
  const createRigidBodyState = ({
@@ -646,6 +661,11 @@ const mutableRigidBodyOptions = {
646
661
  userData: (rb, value) => {
647
662
  rb.userData = value;
648
663
  },
664
+
665
+ type(rb, value) {
666
+ rb.setBodyType(rigidBodyTypeFromString(value));
667
+ },
668
+
649
669
  position: () => {},
650
670
  rotation: () => {},
651
671
  quaternion: () => {},
@@ -865,11 +885,18 @@ const useSphericalJoint = (body1, body2, [body1Anchor, body2Anchor]) => {
865
885
  * They are characterized by one local anchor as well as one local axis on each rigid-body.
866
886
  */
867
887
 
868
- const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
888
+ const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
869
889
  const {
870
890
  rapier
871
891
  } = useRapier();
872
- return useImpulseJoint(body1, body2, rapier.JointData.revolute(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
892
+ const params = rapier.JointData.revolute(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis));
893
+
894
+ if (limits) {
895
+ params.limitsEnabled = true;
896
+ params.limits = limits;
897
+ }
898
+
899
+ return useImpulseJoint(body1, body2, params);
873
900
  };
874
901
  /**
875
902
  * The prismatic joint prevents any relative movement between two rigid-bodies, except for relative translations along one axis.
@@ -877,11 +904,18 @@ const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
877
904
  * local tangent axis can be specified for each rigid-body.
878
905
  */
879
906
 
880
- const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
907
+ const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
881
908
  const {
882
909
  rapier
883
910
  } = useRapier();
884
- return useImpulseJoint(body1, body2, rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
911
+ const params = rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis));
912
+
913
+ if (limits) {
914
+ params.limitsEnabled = true;
915
+ params.limits = limits;
916
+ }
917
+
918
+ return useImpulseJoint(body1, body2, params);
885
919
  };
886
920
 
887
921
  const calcForceByType = {
@@ -1019,12 +1053,12 @@ const Physics = ({
1019
1053
 
1020
1054
  return worldRef.current;
1021
1055
  });
1022
- const [rigidBodyStates] = React.useState(() => new Map());
1023
- const [colliderStates] = React.useState(() => new Map());
1024
- const [rigidBodyEvents] = React.useState(() => new Map());
1025
- const [colliderEvents] = React.useState(() => new Map());
1026
- const [eventQueue] = React.useState(() => new rapier3dCompat.EventQueue(false));
1027
- const [attractorStates] = React.useState(() => new Map()); // Init world
1056
+ const rigidBodyStates = useConst(() => new Map());
1057
+ const colliderStates = useConst(() => new Map());
1058
+ const rigidBodyEvents = useConst(() => new Map());
1059
+ const colliderEvents = useConst(() => new Map());
1060
+ const eventQueue = useConst(() => new rapier3dCompat.EventQueue(false));
1061
+ const attractorStates = useConst(() => new Map()); // Init world
1028
1062
 
1029
1063
  React.useEffect(() => {
1030
1064
  const world = getWorldRef.current();
@@ -131,6 +131,17 @@ const vectorToTuple = v => {
131
131
 
132
132
  return [v];
133
133
  };
134
+ function useConst(initialValue) {
135
+ const ref = React.useRef();
136
+
137
+ if (ref.current === undefined) {
138
+ ref.current = {
139
+ value: typeof initialValue === "function" ? initialValue() : initialValue
140
+ };
141
+ }
142
+
143
+ return ref.current.value;
144
+ }
134
145
 
135
146
  const createRigidBodyApi = ref => {
136
147
  return {
@@ -579,8 +590,12 @@ const useColliderEvents = (collidersRef, props, events) => {
579
590
  };
580
591
 
581
592
  const rigidBodyDescFromOptions = options => {
593
+ var _options$canSleep;
594
+
582
595
  const type = rigidBodyTypeFromString((options === null || options === void 0 ? void 0 : options.type) || "dynamic");
583
- const desc = new rapier3dCompat.RigidBodyDesc(type);
596
+ const desc = new rapier3dCompat.RigidBodyDesc(type); // Apply immutable options
597
+
598
+ desc.canSleep = (_options$canSleep = options === null || options === void 0 ? void 0 : options.canSleep) !== null && _options$canSleep !== void 0 ? _options$canSleep : true;
584
599
  return desc;
585
600
  };
586
601
  const createRigidBodyState = ({
@@ -646,6 +661,11 @@ const mutableRigidBodyOptions = {
646
661
  userData: (rb, value) => {
647
662
  rb.userData = value;
648
663
  },
664
+
665
+ type(rb, value) {
666
+ rb.setBodyType(rigidBodyTypeFromString(value));
667
+ },
668
+
649
669
  position: () => {},
650
670
  rotation: () => {},
651
671
  quaternion: () => {},
@@ -865,11 +885,18 @@ const useSphericalJoint = (body1, body2, [body1Anchor, body2Anchor]) => {
865
885
  * They are characterized by one local anchor as well as one local axis on each rigid-body.
866
886
  */
867
887
 
868
- const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
888
+ const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
869
889
  const {
870
890
  rapier
871
891
  } = useRapier();
872
- return useImpulseJoint(body1, body2, rapier.JointData.revolute(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
892
+ const params = rapier.JointData.revolute(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis));
893
+
894
+ if (limits) {
895
+ params.limitsEnabled = true;
896
+ params.limits = limits;
897
+ }
898
+
899
+ return useImpulseJoint(body1, body2, params);
873
900
  };
874
901
  /**
875
902
  * The prismatic joint prevents any relative movement between two rigid-bodies, except for relative translations along one axis.
@@ -877,11 +904,18 @@ const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
877
904
  * local tangent axis can be specified for each rigid-body.
878
905
  */
879
906
 
880
- const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
907
+ const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
881
908
  const {
882
909
  rapier
883
910
  } = useRapier();
884
- return useImpulseJoint(body1, body2, rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
911
+ const params = rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis));
912
+
913
+ if (limits) {
914
+ params.limitsEnabled = true;
915
+ params.limits = limits;
916
+ }
917
+
918
+ return useImpulseJoint(body1, body2, params);
885
919
  };
886
920
 
887
921
  const calcForceByType = {
@@ -1019,12 +1053,12 @@ const Physics = ({
1019
1053
 
1020
1054
  return worldRef.current;
1021
1055
  });
1022
- const [rigidBodyStates] = React.useState(() => new Map());
1023
- const [colliderStates] = React.useState(() => new Map());
1024
- const [rigidBodyEvents] = React.useState(() => new Map());
1025
- const [colliderEvents] = React.useState(() => new Map());
1026
- const [eventQueue] = React.useState(() => new rapier3dCompat.EventQueue(false));
1027
- const [attractorStates] = React.useState(() => new Map()); // Init world
1056
+ const rigidBodyStates = useConst(() => new Map());
1057
+ const colliderStates = useConst(() => new Map());
1058
+ const rigidBodyEvents = useConst(() => new Map());
1059
+ const colliderEvents = useConst(() => new Map());
1060
+ const eventQueue = useConst(() => new rapier3dCompat.EventQueue(false));
1061
+ const attractorStates = useConst(() => new Map()); // Init world
1028
1062
 
1029
1063
  React.useEffect(() => {
1030
1064
  const world = getWorldRef.current();
@@ -1,7 +1,7 @@
1
1
  import { ColliderDesc, ActiveEvents, RigidBodyDesc, EventQueue } from '@dimforge/rapier3d-compat';
2
2
  export { CoefficientCombineRule, Collider as RapierCollider, RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat';
3
3
  import { useFrame, useThree } from '@react-three/fiber';
4
- import React, { useMemo, useEffect, useContext, useState, useRef, memo, createContext, useCallback, forwardRef, useImperativeHandle, useLayoutEffect } from 'react';
4
+ import React, { useRef, useMemo, useEffect, useContext, useState, memo, createContext, useCallback, forwardRef, useImperativeHandle, useLayoutEffect } from 'react';
5
5
  import { Quaternion, Euler, Vector3, Object3D, Matrix4, MathUtils, InstancedMesh, BufferAttribute, DynamicDrawUsage } from 'three';
6
6
  import { useAsset } from 'use-asset';
7
7
  import { mergeVertices, VertexNormalsHelper } from 'three-stdlib';
@@ -106,6 +106,17 @@ const vectorToTuple = v => {
106
106
 
107
107
  return [v];
108
108
  };
109
+ function useConst(initialValue) {
110
+ const ref = useRef();
111
+
112
+ if (ref.current === undefined) {
113
+ ref.current = {
114
+ value: typeof initialValue === "function" ? initialValue() : initialValue
115
+ };
116
+ }
117
+
118
+ return ref.current.value;
119
+ }
109
120
 
110
121
  const createRigidBodyApi = ref => {
111
122
  return {
@@ -554,8 +565,12 @@ const useColliderEvents = (collidersRef, props, events) => {
554
565
  };
555
566
 
556
567
  const rigidBodyDescFromOptions = options => {
568
+ var _options$canSleep;
569
+
557
570
  const type = rigidBodyTypeFromString((options === null || options === void 0 ? void 0 : options.type) || "dynamic");
558
- const desc = new RigidBodyDesc(type);
571
+ const desc = new RigidBodyDesc(type); // Apply immutable options
572
+
573
+ desc.canSleep = (_options$canSleep = options === null || options === void 0 ? void 0 : options.canSleep) !== null && _options$canSleep !== void 0 ? _options$canSleep : true;
559
574
  return desc;
560
575
  };
561
576
  const createRigidBodyState = ({
@@ -621,6 +636,11 @@ const mutableRigidBodyOptions = {
621
636
  userData: (rb, value) => {
622
637
  rb.userData = value;
623
638
  },
639
+
640
+ type(rb, value) {
641
+ rb.setBodyType(rigidBodyTypeFromString(value));
642
+ },
643
+
624
644
  position: () => {},
625
645
  rotation: () => {},
626
646
  quaternion: () => {},
@@ -840,11 +860,18 @@ const useSphericalJoint = (body1, body2, [body1Anchor, body2Anchor]) => {
840
860
  * They are characterized by one local anchor as well as one local axis on each rigid-body.
841
861
  */
842
862
 
843
- const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
863
+ const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
844
864
  const {
845
865
  rapier
846
866
  } = useRapier();
847
- return useImpulseJoint(body1, body2, rapier.JointData.revolute(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
867
+ const params = rapier.JointData.revolute(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis));
868
+
869
+ if (limits) {
870
+ params.limitsEnabled = true;
871
+ params.limits = limits;
872
+ }
873
+
874
+ return useImpulseJoint(body1, body2, params);
848
875
  };
849
876
  /**
850
877
  * The prismatic joint prevents any relative movement between two rigid-bodies, except for relative translations along one axis.
@@ -852,11 +879,18 @@ const useRevoluteJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
852
879
  * local tangent axis can be specified for each rigid-body.
853
880
  */
854
881
 
855
- const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
882
+ const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis, limits]) => {
856
883
  const {
857
884
  rapier
858
885
  } = useRapier();
859
- return useImpulseJoint(body1, body2, rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
886
+ const params = rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis));
887
+
888
+ if (limits) {
889
+ params.limitsEnabled = true;
890
+ params.limits = limits;
891
+ }
892
+
893
+ return useImpulseJoint(body1, body2, params);
860
894
  };
861
895
 
862
896
  const calcForceByType = {
@@ -994,12 +1028,12 @@ const Physics = ({
994
1028
 
995
1029
  return worldRef.current;
996
1030
  });
997
- const [rigidBodyStates] = useState(() => new Map());
998
- const [colliderStates] = useState(() => new Map());
999
- const [rigidBodyEvents] = useState(() => new Map());
1000
- const [colliderEvents] = useState(() => new Map());
1001
- const [eventQueue] = useState(() => new EventQueue(false));
1002
- const [attractorStates] = useState(() => new Map()); // Init world
1031
+ const rigidBodyStates = useConst(() => new Map());
1032
+ const colliderStates = useConst(() => new Map());
1033
+ const rigidBodyEvents = useConst(() => new Map());
1034
+ const colliderEvents = useConst(() => new Map());
1035
+ const eventQueue = useConst(() => new EventQueue(false));
1036
+ const attractorStates = useConst(() => new Map()); // Init world
1003
1037
 
1004
1038
  useEffect(() => {
1005
1039
  const world = getWorldRef.current();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/rapier",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "source": "src/index.ts",
5
5
  "main": "dist/react-three-rapier.cjs.js",
6
6
  "module": "dist/react-three-rapier.esm.js",
@@ -12,17 +12,17 @@
12
12
  "ts": "tsc -w"
13
13
  },
14
14
  "devDependencies": {
15
- "@react-three/drei": "^9.34.1",
16
- "@react-three/fiber": "^8.8.9",
15
+ "@react-three/drei": "9.45.0",
16
+ "@react-three/fiber": "8.9.1",
17
17
  "@react-three/test-renderer": "^8.0.17",
18
18
  "@types/react-dom": "^18.0.2",
19
19
  "@types/three": "^0.139.0",
20
20
  "@vitejs/plugin-react": "^2.1.0",
21
21
  "@vitest/ui": "^0.25.2",
22
22
  "happy-dom": "^7.5.5",
23
- "react": "^18.1.0",
24
- "react-dom": "^18.1.0",
25
- "three": "^0.139.2",
23
+ "react": "18.2.0",
24
+ "react-dom": "18.2.0",
25
+ "three": "0.146.0",
26
26
  "vitest": "^0.25.2"
27
27
  },
28
28
  "peerDependencies": {
package/readme.md CHANGED
@@ -41,19 +41,51 @@ const App = () => {
41
41
 
42
42
  ## Readme Topics
43
43
 
44
- - [Automatic Colliders](#automatic-colliders)
44
+ - [Basic Usage](#basic-usage)
45
+ - [Readme Topics](#readme-topics)
46
+ - [The Physics Component](#the-physics-component)
47
+ - [Automatic colliders](#automatic-colliders)
48
+ - [Collider Examples](#collider-examples)
45
49
  - [Instanced Meshes](#instanced-meshes)
46
50
  - [Debug](#debug)
47
51
  - [Collision Events](#collision-events)
48
- - [Collision Groups](#configuring-collision-and-solver-groups)
49
- - [Contact Force Events](#contact-force-events)
52
+ - [Configuring collision and solver groups](#configuring-collision-and-solver-groups)
53
+ - [Contact force events](#contact-force-events)
50
54
  - [Sensors](#sensors)
55
+ - [Sensors Example](#sensors-example)
51
56
  - [Attractors](#attractors)
52
- - [The timeStep](#configuring-time-step-size)
57
+ - [Attractors Example](#attractors-example)
58
+ - [Configuring Time Step Size](#configuring-time-step-size)
53
59
  - [Manual stepping](#manual-stepping)
60
+ - [Joints](#joints)
61
+ - [Joints Example](#joints-example)
54
62
 
55
63
  ---
56
64
 
65
+ ## The Physics Component
66
+ The `<Physics />` component is the root component of your physics world. It is responsible for creating the physics world and managing the simulation. It relies on lazily initiating `Rapier` and needs to be wrapped in `<Suspense />`.
67
+
68
+ ```tsx
69
+ // The gravity of the physics workd
70
+ gravity?: Vector3Array; // default [0, -9.81, 0]
71
+
72
+ // Which collider shape to generate for meshes by default
73
+ colliders?: RigidBodyAutoCollider; // default "cuboid"
74
+
75
+ // The number of physics steps per second
76
+ timeStep?: number | "vary"; // default 1/60
77
+
78
+ // Pause the physic simulation
79
+ paused?: boolean; // default false
80
+
81
+ // Which order to run the physics simulation
82
+ updatePriority?: number;
83
+
84
+ // If the physics updates slower than the monitor refreshes,
85
+ // interpolation will smooth out the steps between frames
86
+ interpolate?: boolean; // default true
87
+ ```
88
+
57
89
  ## Automatic colliders
58
90
 
59
91
  RigidBodies generate automatic colliders by default for all meshes that it contains. You can control the default collider by setting the `colliders` prop on a `<RigidBody />`, or change it globally by setting `colliders` on `<Physics />`. Setting `colliders={false}` disables auto-generation.
@@ -149,6 +181,10 @@ If part of our meshes are invisible and you want to include them in the collider
149
181
  </RigidBody>
150
182
  ```
151
183
 
184
+ ### Collider Examples
185
+ <a href="https://codesandbox.io/s/react-three-rapier-auto-colliders-b4coz1"><img src="https://raw.githubusercontent.com/pmndrs/react-three-rapier/HEAD/packages/react-three-rapier/misc/example-auto-colliders.jpg" width="240" /></a>
186
+ <a href="https://codesandbox.io/s/react-three-rapier-compound-colliders-ol5ybn"><img src="https://raw.githubusercontent.com/pmndrs/react-three-rapier/HEAD/packages/react-three-rapier/misc/example-compound-shapes.jpg" width="240" /></a>
187
+
152
188
  ## Instanced Meshes
153
189
 
154
190
  Instanced meshes can also be used and have automatic colliders generated from their mesh.
@@ -393,6 +429,9 @@ To detect when a collider enters or leaves another collider, you can use the `on
393
429
  </RigidBody>
394
430
  ```
395
431
 
432
+ ### Sensors Example
433
+ <a href="https://codesandbox.io/s/react-three-rapier-sensors-byjmsk"><img src="https://raw.githubusercontent.com/pmndrs/react-three-rapier/HEAD/packages/react-three-rapier/misc/example-sensors.jpg" width="240" /></a>
434
+
396
435
  ## Attractors
397
436
 
398
437
  An attractor simulates a source of gravity. Any `RigidBody` within range will be _pulled_ (attracted) toward the attractor.
@@ -427,6 +466,9 @@ Gravity types:
427
466
  - `mass2` is the mass of the rigid-body at the time of calculation. Note that rigid-bodies with colliders will use the mass provided to the collider. This is not a value you can control from the attractor, only from wherever you're creating rigid-body components in the scene.
428
467
  - `distance` is the distance between the attractor and rigid-body at the time of calculation
429
468
 
469
+ ### Attractors Example
470
+ <a href="https://codesandbox.io/s/react-three-rapier-attractors-oyj640"><img src="https://raw.githubusercontent.com/pmndrs/react-three-rapier/HEAD/packages/react-three-rapier/misc/example-attractors.jpg" width="240" /></a>
471
+
430
472
  ## Configuring Time Step Size
431
473
 
432
474
  By default, `<Physics>` will simulate the physics world at a fixed rate of 60 frames per second. This can be changed by setting the `timeStep` prop on `<Physics>`:
@@ -455,4 +497,7 @@ step(1 / 60);
455
497
 
456
498
  ## Joints
457
499
 
458
- WIP
500
+ - WIP
501
+
502
+ ### Joints Example
503
+ <a href="https://codesandbox.io/s/react-three-rapier-joints-mhhbd4"><img src="https://raw.githubusercontent.com/pmndrs/react-three-rapier/HEAD/packages/react-three-rapier/misc/example-joints.jpg" width="240" /></a>