Skip to main content

rapier3d/dynamics/joint/
revolute_joint.rs

1#[cfg(all(not(feature = "std"), feature = "dim3"))]
2use simba::scalar::ComplexField;
3
4use crate::dynamics::integration_parameters::SpringCoefficients;
5use crate::dynamics::joint::{GenericJoint, GenericJointBuilder, JointAxesMask};
6use crate::dynamics::{JointAxis, JointLimits, JointMotor, MotorModel};
7use crate::math::{Real, Rotation, Vector};
8
9#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
10#[derive(Copy, Clone, Debug, PartialEq)]
11#[repr(transparent)]
12/// A hinge joint that allows rotation around one axis (like a door hinge or wheel axle).
13///
14/// Revolute joints lock all movement except rotation around a single axis. Use for:
15/// - Door hinges
16/// - Wheels and gears
17/// - Joints in robotic arms
18/// - Pendulums
19/// - Any rotating connection
20///
21/// You can optionally add:
22/// - **Limits**: Restrict rotation to a range (e.g., door that only opens 90°)
23/// - **Motor**: Powered rotation with target velocity or position
24///
25/// In 2D there's only one rotation axis (Z). In 3D you specify which axis (X, Y, or Z).
26pub struct RevoluteJoint {
27    /// The underlying joint data.
28    pub data: GenericJoint,
29}
30
31impl RevoluteJoint {
32    /// Creates a new revolute joint allowing only relative rotations.
33    #[cfg(feature = "dim2")]
34    #[allow(clippy::new_without_default)] // For symmetry with 3D which can’t have a Default impl.
35    pub fn new() -> Self {
36        let data = GenericJointBuilder::new(JointAxesMask::LOCKED_REVOLUTE_AXES);
37        Self { data: data.build() }
38    }
39
40    /// Creates a new revolute joint allowing only relative rotations along the specified axis.
41    ///
42    /// This axis is expressed in the local-space of both rigid-bodies.
43    #[cfg(feature = "dim3")]
44    pub fn new(axis: Vector) -> Self {
45        let data = GenericJointBuilder::new(JointAxesMask::LOCKED_REVOLUTE_AXES)
46            .local_axis1(axis)
47            .local_axis2(axis)
48            .build();
49        Self { data }
50    }
51
52    /// The underlying generic joint.
53    pub fn data(&self) -> &GenericJoint {
54        &self.data
55    }
56
57    /// Are contacts between the attached rigid-bodies enabled?
58    pub fn contacts_enabled(&self) -> bool {
59        self.data.contacts_enabled
60    }
61
62    /// Sets whether contacts between the attached rigid-bodies are enabled.
63    pub fn set_contacts_enabled(&mut self, enabled: bool) -> &mut Self {
64        self.data.set_contacts_enabled(enabled);
65        self
66    }
67
68    /// The joint’s anchor, expressed in the local-space of the first rigid-body.
69    #[must_use]
70    pub fn local_anchor1(&self) -> Vector {
71        self.data.local_anchor1()
72    }
73
74    /// Sets the joint’s anchor, expressed in the local-space of the first rigid-body.
75    pub fn set_local_anchor1(&mut self, anchor1: Vector) -> &mut Self {
76        self.data.set_local_anchor1(anchor1);
77        self
78    }
79
80    /// The joint’s anchor, expressed in the local-space of the second rigid-body.
81    #[must_use]
82    pub fn local_anchor2(&self) -> Vector {
83        self.data.local_anchor2()
84    }
85
86    /// Sets the joint’s anchor, expressed in the local-space of the second rigid-body.
87    pub fn set_local_anchor2(&mut self, anchor2: Vector) -> &mut Self {
88        self.data.set_local_anchor2(anchor2);
89        self
90    }
91
92    /// The angle along the free degree of freedom of this revolute joint in `[-π, π]`.
93    ///
94    /// # Parameters
95    /// - `rb_rot1`: the rotation of the first rigid-body attached to this revolute joint.
96    /// - `rb_rot2`: the rotation of the second rigid-body attached to this revolute joint.
97    pub fn angle(&self, rb_rot1: &Rotation, rb_rot2: &Rotation) -> Real {
98        let joint_rot1 = rb_rot1 * self.data.local_frame1.rotation;
99        let joint_rot2 = rb_rot2 * self.data.local_frame2.rotation;
100        let ang_err = joint_rot1.inverse() * joint_rot2;
101
102        #[cfg(feature = "dim3")]
103        if joint_rot1.dot(joint_rot2) < 0.0 {
104            -ang_err.x.clamp(-1.0, 1.0).asin() * 2.0
105        } else {
106            ang_err.x.clamp(-1.0, 1.0).asin() * 2.0
107        }
108
109        #[cfg(feature = "dim2")]
110        {
111            ang_err.angle()
112        }
113    }
114
115    /// The motor affecting the joint’s rotational degree of freedom.
116    #[must_use]
117    pub fn motor(&self) -> Option<&JointMotor> {
118        self.data.motor(JointAxis::AngX)
119    }
120
121    /// Set the spring-like model used by the motor to reach the desired target velocity and position.
122    pub fn set_motor_model(&mut self, model: MotorModel) -> &mut Self {
123        self.data.set_motor_model(JointAxis::AngX, model);
124        self
125    }
126
127    /// Sets the motor's target rotation speed.
128    ///
129    /// Makes the joint spin at a desired velocity (like a powered motor or wheel).
130    ///
131    /// # Parameters
132    /// * `target_vel` - Desired angular velocity in radians/second
133    /// * `factor` - Motor strength (higher = stronger, approaches target faster)
134    pub fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) -> &mut Self {
135        self.data
136            .set_motor_velocity(JointAxis::AngX, target_vel, factor);
137        self
138    }
139
140    /// Sets the motor's target angle (position control).
141    ///
142    /// Makes the joint rotate toward a specific angle using spring-like behavior.
143    ///
144    /// # Parameters
145    /// * `target_pos` - Desired angle in radians
146    /// * `stiffness` - How strongly to pull toward target (spring constant)
147    /// * `damping` - Resistance to motion (higher = less oscillation)
148    pub fn set_motor_position(
149        &mut self,
150        target_pos: Real,
151        stiffness: Real,
152        damping: Real,
153    ) -> &mut Self {
154        self.data
155            .set_motor_position(JointAxis::AngX, target_pos, stiffness, damping);
156        self
157    }
158
159    /// Configures both target angle and target velocity for the motor.
160    ///
161    /// Combines position and velocity control for precise motor behavior.
162    pub fn set_motor(
163        &mut self,
164        target_pos: Real,
165        target_vel: Real,
166        stiffness: Real,
167        damping: Real,
168    ) -> &mut Self {
169        self.data
170            .set_motor(JointAxis::AngX, target_pos, target_vel, stiffness, damping);
171        self
172    }
173
174    /// Sets the maximum torque the motor can apply.
175    ///
176    /// Limits how strong the motor is. Without this, motors can apply infinite force.
177    pub fn set_motor_max_force(&mut self, max_force: Real) -> &mut Self {
178        self.data.set_motor_max_force(JointAxis::AngX, max_force);
179        self
180    }
181
182    /// The rotation limits of this joint, if any.
183    ///
184    /// Returns `None` if no limits are set (unlimited rotation).
185    #[must_use]
186    pub fn limits(&self) -> Option<&JointLimits<Real>> {
187        self.data.limits(JointAxis::AngX)
188    }
189
190    /// Restricts rotation to a specific angle range.
191    ///
192    /// # Parameters
193    /// * `limits` - `[min_angle, max_angle]` in radians. The range may sit anywhere on the
194    ///   circle (e.g. `[0, 3π/2]`), but it can't be wider than a full turn: the joint's angle
195    ///   is derived from the bodies' relative rotation, which doesn't count revolutions, so a
196    ///   wider range is indistinguishable from no limit at all and leaves the joint free.
197    ///
198    /// # Example
199    /// ```
200    /// # use rapier3d::prelude::*;
201    /// # use rapier3d::dynamics::RevoluteJoint;
202    /// # let mut joint = RevoluteJoint::new(Vector::Y);
203    /// // Door that opens 0° to 90°
204    /// joint.set_limits([0.0, std::f32::consts::PI / 2.0]);
205    /// ```
206    pub fn set_limits(&mut self, limits: [Real; 2]) -> &mut Self {
207        self.data.set_limits(JointAxis::AngX, limits);
208        self
209    }
210
211    /// Gets the softness of this joint’s locked degrees of freedom.
212    #[must_use]
213    pub fn softness(&self) -> SpringCoefficients<Real> {
214        self.data.softness
215    }
216
217    /// Sets the softness of this joint’s locked degrees of freedom.
218    #[must_use]
219    pub fn set_softness(&mut self, softness: SpringCoefficients<Real>) -> &mut Self {
220        self.data.softness = softness;
221        self
222    }
223}
224
225impl From<RevoluteJoint> for GenericJoint {
226    fn from(val: RevoluteJoint) -> GenericJoint {
227        val.data
228    }
229}
230
231/// Create revolute joints using the builder pattern.
232///
233/// A revolute joint locks all relative motion except for rotations along the joint’s principal axis.
234#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
235#[derive(Copy, Clone, Debug, PartialEq)]
236pub struct RevoluteJointBuilder(pub RevoluteJoint);
237
238impl RevoluteJointBuilder {
239    /// Creates a new revolute joint builder.
240    #[cfg(feature = "dim2")]
241    #[allow(clippy::new_without_default)] // For symmetry with 3D which can’t have a Default impl.
242    pub fn new() -> Self {
243        Self(RevoluteJoint::new())
244    }
245
246    /// Creates a new revolute joint builder, allowing only relative rotations along the specified axis.
247    ///
248    /// This axis is expressed in the local-space of both rigid-bodies.
249    #[cfg(feature = "dim3")]
250    pub fn new(axis: Vector) -> Self {
251        Self(RevoluteJoint::new(axis))
252    }
253
254    /// Sets whether contacts between the attached rigid-bodies are enabled.
255    #[must_use]
256    pub fn contacts_enabled(mut self, enabled: bool) -> Self {
257        self.0.set_contacts_enabled(enabled);
258        self
259    }
260
261    /// Sets the joint’s anchor, expressed in the local-space of the first rigid-body.
262    #[must_use]
263    pub fn local_anchor1(mut self, anchor1: Vector) -> Self {
264        self.0.set_local_anchor1(anchor1);
265        self
266    }
267
268    /// Sets the joint’s anchor, expressed in the local-space of the second rigid-body.
269    #[must_use]
270    pub fn local_anchor2(mut self, anchor2: Vector) -> Self {
271        self.0.set_local_anchor2(anchor2);
272        self
273    }
274
275    /// Set the spring-like model used by the motor to reach the desired target velocity and position.
276    #[must_use]
277    pub fn motor_model(mut self, model: MotorModel) -> Self {
278        self.0.set_motor_model(model);
279        self
280    }
281
282    /// Sets the target velocity this motor needs to reach.
283    #[must_use]
284    pub fn motor_velocity(mut self, target_vel: Real, factor: Real) -> Self {
285        self.0.set_motor_velocity(target_vel, factor);
286        self
287    }
288
289    /// Sets the target angle this motor needs to reach.
290    #[must_use]
291    pub fn motor_position(mut self, target_pos: Real, stiffness: Real, damping: Real) -> Self {
292        self.0.set_motor_position(target_pos, stiffness, damping);
293        self
294    }
295
296    /// Configure both the target angle and target velocity of the motor.
297    #[must_use]
298    pub fn motor(
299        mut self,
300        target_pos: Real,
301        target_vel: Real,
302        stiffness: Real,
303        damping: Real,
304    ) -> Self {
305        self.0.set_motor(target_pos, target_vel, stiffness, damping);
306        self
307    }
308
309    /// Sets the maximum force the motor can deliver.
310    #[must_use]
311    pub fn motor_max_force(mut self, max_force: Real) -> Self {
312        self.0.set_motor_max_force(max_force);
313        self
314    }
315
316    /// Sets the `[min,max]` limit angles attached bodies can rotate along the joint's principal axis.
317    #[must_use]
318    pub fn limits(mut self, limits: [Real; 2]) -> Self {
319        self.0.set_limits(limits);
320        self
321    }
322
323    /// Sets the softness of this joint’s locked degrees of freedom.
324    #[must_use]
325    pub fn softness(mut self, softness: SpringCoefficients<Real>) -> Self {
326        self.0.data.softness = softness;
327        self
328    }
329
330    /// Builds the revolute joint.
331    #[must_use]
332    pub fn build(self) -> RevoluteJoint {
333        self.0
334    }
335}
336
337impl From<RevoluteJointBuilder> for GenericJoint {
338    fn from(val: RevoluteJointBuilder) -> GenericJoint {
339        val.0.into()
340    }
341}
342
343#[cfg(test)]
344mod test {
345    #[test]
346    fn test_revolute_joint_angle() {
347        #[cfg(feature = "dim3")]
348        use crate::math::{AngVector, Vector};
349        use crate::math::{Real, rotation_from_angle};
350        use crate::na::RealField;
351
352        #[cfg(feature = "dim2")]
353        let revolute = super::RevoluteJointBuilder::new().build();
354        #[cfg(feature = "dim2")]
355        let rot1 = rotation_from_angle(1.0);
356        #[cfg(feature = "dim3")]
357        let revolute = super::RevoluteJointBuilder::new(Vector::Y).build();
358        #[cfg(feature = "dim3")]
359        let rot1 = rotation_from_angle(AngVector::new(0.0, 1.0, 0.0));
360
361        let steps = 100;
362
363        // The -pi and pi values will be checked later.
364        for i in 1..steps {
365            let delta = -Real::pi() + i as Real * Real::two_pi() / steps as Real;
366            #[cfg(feature = "dim2")]
367            let rot2 = rotation_from_angle(1.0 + delta);
368            #[cfg(feature = "dim3")]
369            let rot2 = rotation_from_angle(AngVector::new(0.0, 1.0 + delta, 0.0));
370            approx::assert_relative_eq!(revolute.angle(&rot1, &rot2), delta, epsilon = 1.0e-5);
371        }
372
373        // Check the special case for -pi and pi that may return an angle with a flipped sign
374        // (because they are equivalent).
375        for delta in [-Real::pi(), Real::pi()] {
376            #[cfg(feature = "dim2")]
377            let rot2 = rotation_from_angle(1.0 + delta);
378            #[cfg(feature = "dim3")]
379            let rot2 = rotation_from_angle(AngVector::new(0.0, 1.0 + delta, 0.0));
380            approx::assert_relative_eq!(
381                revolute.angle(&rot1, &rot2).abs(),
382                delta.abs(),
383                epsilon = 1.0e-2
384            );
385        }
386    }
387}