Skip to main content

phyz_model/
joint.rs

1//! Joint types and definitions.
2
3use phyz_math::{DMat, Mat3, Quat, SpatialTransform, SpatialVec, Vec3};
4
5/// Joint type enumeration.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum JointType {
8    /// Single rotational DOF about an axis.
9    Revolute,
10    /// Single translational DOF along an axis.
11    Prismatic,
12    /// 3 DOF spherical joint (ball joint) using quaternions.
13    Spherical,
14    /// 6 DOF free joint (3 translation + 3 rotation).
15    Free,
16    /// 0 DOF fixed joint (rigid attachment).
17    Fixed,
18    /// Alias for Revolute (MuJoCo compatibility).
19    Hinge,
20    /// Alias for Prismatic (MuJoCo compatibility).
21    Slide,
22    /// Alias for Spherical (MuJoCo compatibility).
23    Ball,
24}
25
26/// A joint connecting two bodies.
27#[derive(Debug, Clone)]
28pub struct Joint {
29    /// Joint type.
30    pub joint_type: JointType,
31    /// Transform from parent body frame to joint frame (constant).
32    pub parent_to_joint: SpatialTransform,
33    /// Joint axis in local frame (for revolute: typically Z).
34    pub axis: Vec3,
35    /// Damping coefficient.
36    pub damping: f64,
37    /// Joint position limits [lower, upper] (None = unlimited).
38    pub limits: Option<[f64; 2]>,
39}
40
41impl Joint {
42    /// Create a revolute joint with the given parent-to-joint transform.
43    pub fn revolute(parent_to_joint: SpatialTransform) -> Self {
44        Self {
45            joint_type: JointType::Revolute,
46            parent_to_joint,
47            axis: Vec3::new(0.0, 0.0, 1.0), // revolute about Z
48            damping: 0.0,
49            limits: None,
50        }
51    }
52
53    /// Create a prismatic joint with the given parent-to-joint transform and axis.
54    pub fn prismatic(parent_to_joint: SpatialTransform, axis: Vec3) -> Self {
55        Self {
56            joint_type: JointType::Prismatic,
57            parent_to_joint,
58            axis,
59            damping: 0.0,
60            limits: None,
61        }
62    }
63
64    /// Create a spherical (ball) joint with the given parent-to-joint transform.
65    pub fn spherical(parent_to_joint: SpatialTransform) -> Self {
66        Self {
67            joint_type: JointType::Spherical,
68            parent_to_joint,
69            axis: Vec3::zeros(), // not used for spherical
70            damping: 0.0,
71            limits: None,
72        }
73    }
74
75    /// Create a free joint with the given parent-to-joint transform.
76    pub fn free(parent_to_joint: SpatialTransform) -> Self {
77        Self {
78            joint_type: JointType::Free,
79            parent_to_joint,
80            axis: Vec3::zeros(), // not used for free
81            damping: 0.0,
82            limits: None,
83        }
84    }
85
86    /// Create a fixed joint (rigid attachment).
87    pub fn fixed(parent_to_joint: SpatialTransform) -> Self {
88        Self {
89            joint_type: JointType::Fixed,
90            parent_to_joint,
91            axis: Vec3::zeros(),
92            damping: 0.0,
93            limits: None,
94        }
95    }
96
97    /// Number of degrees of freedom for this joint type.
98    pub fn ndof(&self) -> usize {
99        match self.joint_type {
100            JointType::Revolute | JointType::Hinge => 1,
101            JointType::Prismatic | JointType::Slide => 1,
102            JointType::Spherical | JointType::Ball => 3,
103            JointType::Free => 6,
104            JointType::Fixed => 0,
105        }
106    }
107
108    /// Compute the joint transform for the given joint position(s).
109    ///
110    /// Returns the Plücker transform from predecessor to successor frame.
111    /// `q` slice should have length >= ndof().
112    pub fn joint_transform_slice(&self, q: &[f64]) -> SpatialTransform {
113        match self.joint_type {
114            JointType::Revolute | JointType::Hinge => {
115                // Passive rotation: negate angle for coordinate transform
116                let angle = q[0];
117                let (s, c) = (-angle).sin_cos();
118                let a = &self.axis;
119                let ax = phyz_math::skew(a);
120                let rot = Mat3::identity() + ax * s + ax * ax * (1.0 - c);
121                SpatialTransform::new(rot, Vec3::zeros())
122            }
123            JointType::Prismatic | JointType::Slide => {
124                let distance = q[0];
125                let pos = self.axis * distance;
126                SpatialTransform::new(Mat3::identity(), pos)
127            }
128            JointType::Spherical | JointType::Ball => {
129                // q = [qw, qx, qy, qz] (quaternion components)
130                // But we store as exponential coordinates in integration
131                // For now, interpret as axis-angle representation (3 DOF)
132                let wx = q[0];
133                let wy = q[1];
134                let wz = q[2];
135                let w = Vec3::new(wx, wy, wz);
136                let quat = Quat::exp(&w);
137                let rot = quat.to_matrix();
138                SpatialTransform::new(rot, Vec3::zeros())
139            }
140            JointType::Free => {
141                // q = [x, y, z, wx, wy, wz] (position + exponential coordinates)
142                let pos = Vec3::new(q[0], q[1], q[2]);
143                let w = Vec3::new(q[3], q[4], q[5]);
144                let quat = Quat::exp(&w);
145                let rot = quat.to_matrix();
146                SpatialTransform::new(rot, pos)
147            }
148            JointType::Fixed => {
149                // No DOF, return identity
150                SpatialTransform::identity()
151            }
152        }
153    }
154
155    /// Compute the joint transform for a single-DOF joint (backward compat).
156    /// For multi-DOF joints, use joint_transform_slice instead.
157    pub fn joint_transform(&self, q: f64) -> SpatialTransform {
158        self.joint_transform_slice(&[q])
159    }
160
161    /// Motion subspace matrix S for this joint.
162    /// Returns a matrix of size 6 × ndof.
163    /// For single-DOF joints, returns a 6×1 column vector.
164    pub fn motion_subspace_matrix(&self) -> DMat {
165        match self.joint_type {
166            JointType::Revolute | JointType::Hinge => {
167                let s = SpatialVec::new(self.axis, Vec3::zeros());
168                DMat::from_column_slice(
169                    6,
170                    1,
171                    &[
172                        s.data[0], s.data[1], s.data[2], s.data[3], s.data[4], s.data[5],
173                    ],
174                )
175            }
176            JointType::Prismatic | JointType::Slide => {
177                let s = SpatialVec::new(Vec3::zeros(), self.axis);
178                DMat::from_column_slice(
179                    6,
180                    1,
181                    &[
182                        s.data[0], s.data[1], s.data[2], s.data[3], s.data[4], s.data[5],
183                    ],
184                )
185            }
186            JointType::Spherical | JointType::Ball => {
187                // 3 DOF: angular velocity in body frame
188                // S = [I_3×3; 0_3×3] (angular part is identity, linear is zero)
189                DMat::from_row_slice(
190                    6,
191                    3,
192                    &[
193                        1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
194                        0.0, 0.0, 0.0,
195                    ],
196                )
197            }
198            JointType::Free => {
199                // 6 DOF: [angular; linear] velocity
200                // S = I_6×6
201                DMat::identity(6, 6)
202            }
203            JointType::Fixed => {
204                // 0 DOF: empty 6×0 matrix
205                DMat::zeros(6, 0)
206            }
207        }
208    }
209
210    /// Motion subspace for single-DOF joints (backward compat).
211    /// For multi-DOF joints, use motion_subspace_matrix instead.
212    /// Fixed joints return a zero vector.
213    pub fn motion_subspace(&self) -> SpatialVec {
214        match self.joint_type {
215            JointType::Revolute | JointType::Hinge => SpatialVec::new(self.axis, Vec3::zeros()),
216            JointType::Prismatic | JointType::Slide => SpatialVec::new(Vec3::zeros(), self.axis),
217            JointType::Fixed => SpatialVec::zero(), // 0 DOF
218            _ => panic!(
219                "motion_subspace() only valid for single-DOF joints; use motion_subspace_matrix() for multi-DOF joints"
220            ),
221        }
222    }
223}