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}