rapier3d/dynamics/joint/
revolute_joint.rs

1use crate::dynamics::integration_parameters::SpringCoefficients;
2use crate::dynamics::joint::{GenericJoint, GenericJointBuilder, JointAxesMask};
3use crate::dynamics::{JointAxis, JointLimits, JointMotor, MotorModel};
4use crate::math::{Point, Real, Rotation};
5
6#[cfg(feature = "dim3")]
7use crate::math::UnitVector;
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: UnitVector<Real>) -> 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) -> Point<Real> {
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: Point<Real>) -> &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) -> Point<Real> {
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: Point<Real>) -> &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<Real>, rb_rot2: &Rotation<Real>) -> 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.i.clamp(-1.0, 1.0).asin() * 2.0
105        } else {
106            ang_err.i.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
194    ///
195    /// # Example
196    /// ```
197    /// # use rapier3d::prelude::*;
198    /// # use rapier3d::dynamics::RevoluteJoint;
199    /// # let mut joint = RevoluteJoint::new(Vector::y_axis());
200    /// // Door that opens 0° to 90°
201    /// joint.set_limits([0.0, std::f32::consts::PI / 2.0]);
202    /// ```
203    pub fn set_limits(&mut self, limits: [Real; 2]) -> &mut Self {
204        self.data.set_limits(JointAxis::AngX, limits);
205        self
206    }
207
208    /// Gets the softness of this joint’s locked degrees of freedom.
209    #[must_use]
210    pub fn softness(&self) -> SpringCoefficients<Real> {
211        self.data.softness
212    }
213
214    /// Sets the softness of this joint’s locked degrees of freedom.
215    #[must_use]
216    pub fn set_softness(&mut self, softness: SpringCoefficients<Real>) -> &mut Self {
217        self.data.softness = softness;
218        self
219    }
220}
221
222impl From<RevoluteJoint> for GenericJoint {
223    fn from(val: RevoluteJoint) -> GenericJoint {
224        val.data
225    }
226}
227
228/// Create revolute joints using the builder pattern.
229///
230/// A revolute joint locks all relative motion except for rotations along the joint’s principal axis.
231#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
232#[derive(Copy, Clone, Debug, PartialEq)]
233pub struct RevoluteJointBuilder(pub RevoluteJoint);
234
235impl RevoluteJointBuilder {
236    /// Creates a new revolute joint builder.
237    #[cfg(feature = "dim2")]
238    #[allow(clippy::new_without_default)] // For symmetry with 3D which can’t have a Default impl.
239    pub fn new() -> Self {
240        Self(RevoluteJoint::new())
241    }
242
243    /// Creates a new revolute joint builder, allowing only relative rotations along the specified axis.
244    ///
245    /// This axis is expressed in the local-space of both rigid-bodies.
246    #[cfg(feature = "dim3")]
247    pub fn new(axis: UnitVector<Real>) -> Self {
248        Self(RevoluteJoint::new(axis))
249    }
250
251    /// Sets whether contacts between the attached rigid-bodies are enabled.
252    #[must_use]
253    pub fn contacts_enabled(mut self, enabled: bool) -> Self {
254        self.0.set_contacts_enabled(enabled);
255        self
256    }
257
258    /// Sets the joint’s anchor, expressed in the local-space of the first rigid-body.
259    #[must_use]
260    pub fn local_anchor1(mut self, anchor1: Point<Real>) -> Self {
261        self.0.set_local_anchor1(anchor1);
262        self
263    }
264
265    /// Sets the joint’s anchor, expressed in the local-space of the second rigid-body.
266    #[must_use]
267    pub fn local_anchor2(mut self, anchor2: Point<Real>) -> Self {
268        self.0.set_local_anchor2(anchor2);
269        self
270    }
271
272    /// Set the spring-like model used by the motor to reach the desired target velocity and position.
273    #[must_use]
274    pub fn motor_model(mut self, model: MotorModel) -> Self {
275        self.0.set_motor_model(model);
276        self
277    }
278
279    /// Sets the target velocity this motor needs to reach.
280    #[must_use]
281    pub fn motor_velocity(mut self, target_vel: Real, factor: Real) -> Self {
282        self.0.set_motor_velocity(target_vel, factor);
283        self
284    }
285
286    /// Sets the target angle this motor needs to reach.
287    #[must_use]
288    pub fn motor_position(mut self, target_pos: Real, stiffness: Real, damping: Real) -> Self {
289        self.0.set_motor_position(target_pos, stiffness, damping);
290        self
291    }
292
293    /// Configure both the target angle and target velocity of the motor.
294    #[must_use]
295    pub fn motor(
296        mut self,
297        target_pos: Real,
298        target_vel: Real,
299        stiffness: Real,
300        damping: Real,
301    ) -> Self {
302        self.0.set_motor(target_pos, target_vel, stiffness, damping);
303        self
304    }
305
306    /// Sets the maximum force the motor can deliver.
307    #[must_use]
308    pub fn motor_max_force(mut self, max_force: Real) -> Self {
309        self.0.set_motor_max_force(max_force);
310        self
311    }
312
313    /// Sets the `[min,max]` limit angles attached bodies can rotate along the joint's principal axis.
314    #[must_use]
315    pub fn limits(mut self, limits: [Real; 2]) -> Self {
316        self.0.set_limits(limits);
317        self
318    }
319
320    /// Sets the softness of this joint’s locked degrees of freedom.
321    #[must_use]
322    pub fn softness(mut self, softness: SpringCoefficients<Real>) -> Self {
323        self.0.data.softness = softness;
324        self
325    }
326
327    /// Builds the revolute joint.
328    #[must_use]
329    pub fn build(self) -> RevoluteJoint {
330        self.0
331    }
332}
333
334impl From<RevoluteJointBuilder> for GenericJoint {
335    fn from(val: RevoluteJointBuilder) -> GenericJoint {
336        val.0.into()
337    }
338}
339
340#[cfg(test)]
341mod test {
342    #[test]
343    fn test_revolute_joint_angle() {
344        use crate::math::{Real, Rotation};
345        use crate::na::RealField;
346        #[cfg(feature = "dim3")]
347        use crate::{math::Vector, na::vector};
348
349        #[cfg(feature = "dim2")]
350        let revolute = super::RevoluteJointBuilder::new().build();
351        #[cfg(feature = "dim2")]
352        let rot1 = Rotation::new(1.0);
353        #[cfg(feature = "dim3")]
354        let revolute = super::RevoluteJointBuilder::new(Vector::y_axis()).build();
355        #[cfg(feature = "dim3")]
356        let rot1 = Rotation::new(vector![0.0, 1.0, 0.0]);
357
358        let steps = 100;
359
360        // The -pi and pi values will be checked later.
361        for i in 1..steps {
362            let delta = -Real::pi() + i as Real * Real::two_pi() / steps as Real;
363            #[cfg(feature = "dim2")]
364            let rot2 = Rotation::new(1.0 + delta);
365            #[cfg(feature = "dim3")]
366            let rot2 = Rotation::new(vector![0.0, 1.0 + delta, 0.0]);
367            approx::assert_relative_eq!(revolute.angle(&rot1, &rot2), delta, epsilon = 1.0e-5);
368        }
369
370        // Check the special case for -pi and pi that may return an angle with a flipped sign
371        // (because they are equivalent).
372        for delta in [-Real::pi(), Real::pi()] {
373            #[cfg(feature = "dim2")]
374            let rot2 = Rotation::new(1.0 + delta);
375            #[cfg(feature = "dim3")]
376            let rot2 = Rotation::new(vector![0.0, 1.0 + delta, 0.0]);
377            approx::assert_relative_eq!(
378                revolute.angle(&rot1, &rot2).abs(),
379                delta.abs(),
380                epsilon = 1.0e-2
381            );
382        }
383    }
384}