1use bincode::Decode;
4use bincode::Encode;
5use cu_spatial_payloads::Transform3D;
6use cu29::prelude::*;
7use cu29::units::si::angle::radian;
8use cu29::units::si::f64::Angle as Angle64;
9use cu29::units::si::f64::Length as Length64;
10use cu29::units::si::length::meter;
11use serde::Deserialize;
12use serde::Serialize;
13
14#[derive(
16 Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect,
17)]
18#[serde(rename_all = "snake_case")]
19pub enum CuJointAxis {
20 PosX,
21 NegX,
22 PosY,
23 NegY,
24 #[default]
25 PosZ,
26 NegZ,
27 Arbitrary([f64; 3]),
28}
29
30impl CuJointAxis {
31 pub fn unit_vector(self) -> [f64; 3] {
33 match self {
34 Self::PosX => [1.0, 0.0, 0.0],
35 Self::NegX => [-1.0, 0.0, 0.0],
36 Self::PosY => [0.0, 1.0, 0.0],
37 Self::NegY => [0.0, -1.0, 0.0],
38 Self::PosZ => [0.0, 0.0, 1.0],
39 Self::NegZ => [0.0, 0.0, -1.0],
40 Self::Arbitrary(v) => {
41 let norm = libm::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
42 v.map(|c| c / norm)
43 }
44 }
45 }
46}
47
48#[derive(
50 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Reflect,
51)]
52#[serde(rename_all = "snake_case")]
53pub enum CuJointType {
54 #[default]
55 Revolute,
56 Prismatic,
57 Fixed,
58}
59
60use crate::joint::CuJointLimits;
61use crate::joint::CuJointPosition;
62
63#[derive(
65 Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect,
66)]
67#[serde(deny_unknown_fields)]
68pub struct CuJointSpec {
69 pub joint_type: CuJointType,
71 pub origin_translation: [Length64; 3],
73 pub origin_rpy: [Angle64; 3],
75 pub axis: CuJointAxis,
77 pub limits: CuJointLimits,
79}
80
81impl CuJointSpec {
82 pub const fn revolute(
84 origin_translation: [Length64; 3],
85 origin_rpy: [Angle64; 3],
86 axis: CuJointAxis,
87 limits: CuJointLimits,
88 ) -> Self {
89 Self::actuated(
90 CuJointType::Revolute,
91 origin_translation,
92 origin_rpy,
93 axis,
94 limits,
95 )
96 }
97
98 pub const fn prismatic(
100 origin_translation: [Length64; 3],
101 origin_rpy: [Angle64; 3],
102 axis: CuJointAxis,
103 limits: CuJointLimits,
104 ) -> Self {
105 Self::actuated(
106 CuJointType::Prismatic,
107 origin_translation,
108 origin_rpy,
109 axis,
110 limits,
111 )
112 }
113
114 const fn actuated(
115 joint_type: CuJointType,
116 origin_translation: [Length64; 3],
117 origin_rpy: [Angle64; 3],
118 axis: CuJointAxis,
119 limits: CuJointLimits,
120 ) -> Self {
121 Self {
122 joint_type,
123 origin_translation,
124 origin_rpy,
125 axis,
126 limits,
127 }
128 }
129
130 pub fn validate_coordinate(&self, q: CuJointPosition) -> CuResult<()> {
132 self.limits.validate(q)?;
133 if self
134 .origin_translation
135 .iter()
136 .any(|v| !v.get::<meter>().is_finite())
137 || self
138 .origin_rpy
139 .iter()
140 .any(|v| !v.get::<radian>().is_finite())
141 || (self.joint_type != CuJointType::Fixed
142 && self.axis.unit_vector().iter().any(|v| !v.is_finite()))
143 {
144 return Err(CuError::from(
145 "Non-finite joint origin or invalid joint axis",
146 ));
147 }
148 if !matches!(
149 (self.joint_type, q),
150 (CuJointType::Revolute, CuJointPosition::Revolute(_))
151 | (CuJointType::Prismatic, CuJointPosition::Prismatic(_))
152 | (CuJointType::Fixed, CuJointPosition::Fixed)
153 ) {
154 return Err(CuError::from("Joint type and coordinate units disagree"));
155 }
156 Ok(())
157 }
158
159 pub fn origin_transform(&self) -> Transform3D<f64> {
161 Transform3D::<f64>::from_translation_euler_xyz(self.origin_translation, self.origin_rpy)
162 }
163
164 pub fn motion_transform(&self, q: CuJointPosition) -> CuResult<Transform3D<f64>> {
166 self.validate_coordinate(q)?;
167 let val = q.si_value();
168 let u = self.axis.unit_vector();
169 Ok(match self.joint_type {
170 CuJointType::Fixed => Transform3D::<f64>::identity(),
171 CuJointType::Revolute => axis_angle_transform(u, val),
172 CuJointType::Prismatic => Transform3D::<f64>::from_matrix([
173 [1.0, 0.0, 0.0, u[0] * val],
174 [0.0, 1.0, 0.0, u[1] * val],
175 [0.0, 0.0, 1.0, u[2] * val],
176 [0.0, 0.0, 0.0, 1.0],
177 ]),
178 })
179 }
180}
181
182#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)]
212#[reflect(no_field_bounds, from_reflect = false)]
213#[serde(deny_unknown_fields)]
214pub struct CuKinematicChain<const N: usize> {
215 #[serde(with = "crate::serde_arrays::joint_spec_array")]
216 joints: [CuJointSpec; N],
217 #[serde(default = "Transform3D::<f64>::identity")]
218 tool_offset: Transform3D<f64>,
219}
220
221impl<const N: usize> PartialEq for CuKinematicChain<N> {
222 fn eq(&self, other: &Self) -> bool {
223 self.joints == other.joints && self.tool_offset.to_matrix() == other.tool_offset.to_matrix()
224 }
225}
226
227impl<const N: usize> CuKinematicChain<N> {
228 pub const fn new(joints: [CuJointSpec; N]) -> Self {
230 Self {
231 joints,
232 tool_offset: Transform3D::<f64>::identity(),
233 }
234 }
235
236 pub const fn with_tool_offset(mut self, tool_offset: Transform3D<f64>) -> Self {
238 self.tool_offset = tool_offset;
239 self
240 }
241
242 pub fn validate(&self) -> CuResult<()> {
244 for joint in &self.joints {
245 joint.validate_coordinate(joint.limits.lower())?;
246 }
247 if self
248 .tool_offset
249 .to_matrix()
250 .iter()
251 .flatten()
252 .any(|v| !v.is_finite())
253 {
254 return Err(CuError::from("Non-finite chain tool offset"));
255 }
256 Ok(())
257 }
258
259 pub const fn joints(&self) -> &[CuJointSpec; N] {
261 &self.joints
262 }
263
264 pub const fn tool_offset(&self) -> &Transform3D<f64> {
266 &self.tool_offset
267 }
268
269 pub fn set_tool_offset(&mut self, tool_offset: Transform3D<f64>) {
271 self.tool_offset = tool_offset;
272 }
273
274 pub fn is_within_limits(&self, q: &[CuJointPosition; N]) -> bool {
276 let mut i = 0;
277 while i < N {
278 if self.joints[i].validate_coordinate(q[i]).is_err()
279 || !self.joints[i].limits.contains(q[i])
280 {
281 return false;
282 }
283 i += 1;
284 }
285 true
286 }
287
288 pub fn clamp_to_limits(&self, q: &mut [CuJointPosition; N]) -> CuResult<()> {
290 let mut i = 0;
291 while i < N {
292 self.joints[i].validate_coordinate(q[i])?;
293 q[i] = self.joints[i].limits.clamp(q[i])?;
294 i += 1;
295 }
296 Ok(())
297 }
298
299 pub fn forward_kinematics(
301 &self,
302 q: &[CuJointPosition; N],
303 out: &mut Transform3D<f64>,
304 ) -> CuResult<()> {
305 let mut acc = Transform3D::<f64>::identity();
306 let mut i = 0;
307 while i < N {
308 if !q[i].si_value().is_finite() {
309 return Err(CuError::from("repose_kinematics: non-finite joint angle"));
310 }
311 let step = self.joints[i]
312 .origin_transform()
313 .compose(self.joints[i].motion_transform(q[i])?);
314 acc = acc.compose(step);
315 i += 1;
316 }
317 *out = acc.compose(self.tool_offset);
318 Ok(())
319 }
320
321 pub fn forward_frames(
323 &self,
324 q: &[CuJointPosition; N],
325 out_frames: &mut [Transform3D<f64>; N],
326 out_ee: &mut Transform3D<f64>,
327 ) -> CuResult<()> {
328 let mut acc = Transform3D::<f64>::identity();
329 let mut i = 0;
330 while i < N {
331 if !q[i].si_value().is_finite() {
332 return Err(CuError::from("repose_kinematics: non-finite joint angle"));
333 }
334 let step = self.joints[i]
335 .origin_transform()
336 .compose(self.joints[i].motion_transform(q[i])?);
337 acc = acc.compose(step);
338 out_frames[i] = acc;
339 i += 1;
340 }
341 *out_ee = acc.compose(self.tool_offset);
342 Ok(())
343 }
344
345 pub fn geometric_jacobian(
351 &self,
352 q: &[CuJointPosition; N],
353 out_jacobian: &mut [[f64; N]; 6],
354 ) -> CuResult<()> {
355 let mut ee = Transform3D::<f64>::identity();
356 self.geometric_jacobian_and_pose(q, out_jacobian, &mut ee)
357 }
358
359 pub fn body_jacobian(
365 &self,
366 q: &[CuJointPosition; N],
367 out_jacobian: &mut [[f64; N]; 6],
368 ) -> CuResult<()> {
369 let mut space_jac = [[0.0_f64; N]; 6];
370 let mut ee = Transform3D::<f64>::identity();
371 self.geometric_jacobian_and_pose(q, &mut space_jac, &mut ee)?;
372
373 let ee_mat = ee.to_matrix();
374 let mut col = 0;
375 while col < N {
376 let vx = space_jac[0][col];
377 let vy = space_jac[1][col];
378 let vz = space_jac[2][col];
379 let wx = space_jac[3][col];
380 let wy = space_jac[4][col];
381 let wz = space_jac[5][col];
382
383 out_jacobian[0][col] = ee_mat[0][0] * vx + ee_mat[1][0] * vy + ee_mat[2][0] * vz;
385 out_jacobian[1][col] = ee_mat[0][1] * vx + ee_mat[1][1] * vy + ee_mat[2][1] * vz;
386 out_jacobian[2][col] = ee_mat[0][2] * vx + ee_mat[1][2] * vy + ee_mat[2][2] * vz;
387
388 out_jacobian[3][col] = ee_mat[0][0] * wx + ee_mat[1][0] * wy + ee_mat[2][0] * wz;
389 out_jacobian[4][col] = ee_mat[0][1] * wx + ee_mat[1][1] * wy + ee_mat[2][1] * wz;
390 out_jacobian[5][col] = ee_mat[0][2] * wx + ee_mat[1][2] * wy + ee_mat[2][2] * wz;
391 col += 1;
392 }
393 Ok(())
394 }
395
396 pub(crate) fn geometric_jacobian_and_pose(
397 &self,
398 q: &[CuJointPosition; N],
399 out_jacobian: &mut [[f64; N]; 6],
400 out_ee: &mut Transform3D<f64>,
401 ) -> CuResult<()> {
402 let mut joint_origins = [[0.0_f64; 3]; N];
403 let mut world_axes = [[0.0_f64; 3]; N];
404 let mut acc = Transform3D::<f64>::identity();
405
406 let mut i = 0;
407 while i < N {
408 if !q[i].si_value().is_finite() {
409 return Err(CuError::from("repose_kinematics: non-finite joint angle"));
410 }
411 let joint_frame = acc.compose(self.joints[i].origin_transform());
412 let mat = joint_frame.to_matrix();
413 joint_origins[i] = [mat[0][3], mat[1][3], mat[2][3]];
414
415 let local_axis = self.joints[i].axis.unit_vector();
416 world_axes[i] = [
417 mat[0][0] * local_axis[0] + mat[0][1] * local_axis[1] + mat[0][2] * local_axis[2],
418 mat[1][0] * local_axis[0] + mat[1][1] * local_axis[1] + mat[1][2] * local_axis[2],
419 mat[2][0] * local_axis[0] + mat[2][1] * local_axis[1] + mat[2][2] * local_axis[2],
420 ];
421
422 acc = joint_frame.compose(self.joints[i].motion_transform(q[i])?);
423 i += 1;
424 }
425
426 *out_ee = acc.compose(self.tool_offset);
427 let ee_mat = out_ee.to_matrix();
428 let p_ee = [ee_mat[0][3], ee_mat[1][3], ee_mat[2][3]];
429
430 let mut col = 0;
431 while col < N {
432 let w = world_axes[col];
433 match self.joints[col].joint_type {
434 CuJointType::Revolute => {
435 let r = [
436 p_ee[0] - joint_origins[col][0],
437 p_ee[1] - joint_origins[col][1],
438 p_ee[2] - joint_origins[col][2],
439 ];
440 let v = cross3(w, r);
441 out_jacobian[0][col] = v[0];
442 out_jacobian[1][col] = v[1];
443 out_jacobian[2][col] = v[2];
444 out_jacobian[3][col] = w[0];
445 out_jacobian[4][col] = w[1];
446 out_jacobian[5][col] = w[2];
447 }
448 CuJointType::Prismatic => {
449 out_jacobian[0][col] = w[0];
450 out_jacobian[1][col] = w[1];
451 out_jacobian[2][col] = w[2];
452 out_jacobian[3][col] = 0.0;
453 out_jacobian[4][col] = 0.0;
454 out_jacobian[5][col] = 0.0;
455 }
456 CuJointType::Fixed => {
457 out_jacobian[0][col] = 0.0;
458 out_jacobian[1][col] = 0.0;
459 out_jacobian[2][col] = 0.0;
460 out_jacobian[3][col] = 0.0;
461 out_jacobian[4][col] = 0.0;
462 out_jacobian[5][col] = 0.0;
463 }
464 }
465 col += 1;
466 }
467 Ok(())
468 }
469}
470
471const fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
472 [
473 a[1] * b[2] - a[2] * b[1],
474 a[2] * b[0] - a[0] * b[2],
475 a[0] * b[1] - a[1] * b[0],
476 ]
477}
478
479fn axis_angle_transform(axis: [f64; 3], angle_rad: f64) -> Transform3D<f64> {
480 let norm_sq = axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2];
481 if norm_sq <= 1e-15 {
482 return Transform3D::<f64>::identity();
483 }
484 let inv_norm = 1.0 / libm::sqrt(norm_sq);
485 let ux = axis[0] * inv_norm;
486 let uy = axis[1] * inv_norm;
487 let uz = axis[2] * inv_norm;
488
489 let s = libm::sin(angle_rad);
490 let c = libm::cos(angle_rad);
491 let one_minus_c = 1.0 - c;
492
493 Transform3D::<f64>::from_matrix([
494 [
495 c + ux * ux * one_minus_c,
496 ux * uy * one_minus_c - uz * s,
497 ux * uz * one_minus_c + uy * s,
498 0.0,
499 ],
500 [
501 uy * ux * one_minus_c + uz * s,
502 c + uy * uy * one_minus_c,
503 uy * uz * one_minus_c - ux * s,
504 0.0,
505 ],
506 [
507 uz * ux * one_minus_c - uy * s,
508 uz * uy * one_minus_c + ux * s,
509 c + uz * uz * one_minus_c,
510 0.0,
511 ],
512 [0.0, 0.0, 0.0, 1.0],
513 ])
514}