1use super::units::Rad;
27use std::fmt;
28
29const QUATERNION_NORM_THRESHOLD: f64 = 1e-10;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct Position3D {
38 pub x: f64,
40 pub y: f64,
42 pub z: f64,
44}
45
46impl Position3D {
47 pub const fn new(x: f64, y: f64, z: f64) -> Self {
49 Position3D { x, y, z }
50 }
51
52 pub const ZERO: Self = Position3D::new(0.0, 0.0, 0.0);
54
55 pub fn norm(&self) -> f64 {
57 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
58 }
59
60 pub fn normalize(&self) -> Self {
62 let n = self.norm();
63 if n < 1e-10 {
64 return Position3D::ZERO;
65 }
66 Position3D {
67 x: self.x / n,
68 y: self.y / n,
69 z: self.z / n,
70 }
71 }
72
73 pub fn dot(&self, other: &Position3D) -> f64 {
75 self.x * other.x + self.y * other.y + self.z * other.z
76 }
77
78 pub fn cross(&self, other: &Position3D) -> Position3D {
80 Position3D {
81 x: self.y * other.z - self.z * other.y,
82 y: self.z * other.x - self.x * other.z,
83 z: self.x * other.y - self.y * other.x,
84 }
85 }
86}
87
88impl fmt::Display for Position3D {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "({:.3}, {:.3}, {:.3})", self.x, self.y, self.z)
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99pub struct Quaternion {
100 pub w: f64,
102 pub x: f64,
104 pub y: f64,
106 pub z: f64,
108}
109
110impl Quaternion {
111 pub const IDENTITY: Self = Quaternion {
113 w: 1.0,
114 x: 0.0,
115 y: 0.0,
116 z: 0.0,
117 };
118
119 pub fn from_euler(roll: Rad, pitch: Rad, yaw: Rad) -> Self {
127 let cr = (roll.0 / 2.0).cos();
128 let sr = (roll.0 / 2.0).sin();
129 let cp = (pitch.0 / 2.0).cos();
130 let sp = (pitch.0 / 2.0).sin();
131 let cy = (yaw.0 / 2.0).cos();
132 let sy = (yaw.0 / 2.0).sin();
133
134 Quaternion {
135 w: cr * cp * cy + sr * sp * sy,
136 x: sr * cp * cy - cr * sp * sy,
137 y: cr * sp * cy + sr * cp * sy,
138 z: cr * cp * sy - sr * sp * cy,
139 }
140 }
141
142 pub fn to_euler(self) -> (Rad, Rad, Rad) {
146 let sinr_cosp = 2.0 * (self.w * self.x + self.y * self.z);
148 let cosr_cosp = 1.0 - 2.0 * (self.x * self.x + self.y * self.y);
149 let roll = Rad(sinr_cosp.atan2(cosr_cosp));
150
151 let sinp = 2.0 * (self.w * self.y - self.z * self.x);
153 let pitch = if sinp.abs() >= 1.0 {
154 Rad(std::f64::consts::FRAC_PI_2.copysign(sinp))
156 } else {
157 Rad(sinp.asin())
158 };
159
160 let siny_cosp = 2.0 * (self.w * self.z + self.x * self.y);
162 let cosy_cosp = 1.0 - 2.0 * (self.y * self.y + self.z * self.z);
163 let yaw = Rad(siny_cosp.atan2(cosy_cosp));
164
165 (roll, pitch, yaw)
166 }
167
168 pub fn normalize(&self) -> Self {
178 let norm_sq = self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z;
179
180 if norm_sq < QUATERNION_NORM_THRESHOLD {
182 tracing::warn!(
184 "Normalizing near-zero quaternion (norm²={:.2e} < {:.2e}): Q({:.3}, {:.3}, {:.3}, {:.3}), returning identity",
185 norm_sq,
186 QUATERNION_NORM_THRESHOLD,
187 self.w,
188 self.x,
189 self.y,
190 self.z
191 );
192 return Quaternion::IDENTITY;
193 }
194
195 let norm = norm_sq.sqrt();
196 Quaternion {
197 w: self.w / norm,
198 x: self.x / norm,
199 y: self.y / norm,
200 z: self.z / norm,
201 }
202 }
203
204 pub fn multiply(&self, other: &Quaternion) -> Quaternion {
206 Quaternion {
207 w: self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z,
208 x: self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y,
209 y: self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x,
210 z: self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w,
211 }
212 }
213
214 pub fn conjugate(&self) -> Quaternion {
216 Quaternion {
217 w: self.w,
218 x: -self.x,
219 y: -self.y,
220 z: -self.z,
221 }
222 }
223}
224
225impl fmt::Display for Quaternion {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(
228 f,
229 "Q({:.3}, {:.3}, {:.3}, {:.3})",
230 self.w, self.x, self.y, self.z
231 )
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238pub struct CartesianPose {
239 pub position: Position3D,
241 pub orientation: Quaternion,
243}
244
245impl CartesianPose {
246 pub fn from_position_euler(x: f64, y: f64, z: f64, roll: Rad, pitch: Rad, yaw: Rad) -> Self {
248 CartesianPose {
249 position: Position3D::new(x, y, z),
250 orientation: Quaternion::from_euler(roll, pitch, yaw),
251 }
252 }
253
254 pub fn from_position_quaternion(position: Position3D, orientation: Quaternion) -> Self {
256 CartesianPose {
257 position,
258 orientation,
259 }
260 }
261
262 pub const ZERO: Self = CartesianPose {
264 position: Position3D::ZERO,
265 orientation: Quaternion::IDENTITY,
266 };
267}
268
269impl fmt::Display for CartesianPose {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 write!(
272 f,
273 "Pose(pos: {}, quat: {})",
274 self.position, self.orientation
275 )
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Default)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294pub struct EulerAngles {
295 pub roll: f64,
297 pub pitch: f64,
299 pub yaw: f64,
301}
302
303impl EulerAngles {
304 pub fn new(roll: f64, pitch: f64, yaw: f64) -> Self {
306 EulerAngles { roll, pitch, yaw }
307 }
308
309 pub const ZERO: Self = EulerAngles {
311 roll: 0.0,
312 pitch: 0.0,
313 yaw: 0.0,
314 };
315}
316
317impl fmt::Display for EulerAngles {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 write!(
320 f,
321 "Euler(roll: {:.2}°, pitch: {:.2}°, yaw: {:.2}°)",
322 self.roll, self.pitch, self.yaw
323 )
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq)]
329#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
330pub struct CartesianVelocity {
331 pub linear: Position3D,
333 pub angular: Position3D,
335}
336
337impl CartesianVelocity {
338 pub fn new(linear: Position3D, angular: Position3D) -> Self {
340 CartesianVelocity { linear, angular }
341 }
342
343 pub const ZERO: Self = CartesianVelocity {
345 linear: Position3D::ZERO,
346 angular: Position3D::ZERO,
347 };
348}
349
350#[derive(Debug, Clone, Copy, PartialEq)]
352#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
353pub struct CartesianEffort {
354 pub force: Position3D,
356 pub torque: Position3D,
358}
359
360impl CartesianEffort {
361 pub fn new(force: Position3D, torque: Position3D) -> Self {
363 CartesianEffort { force, torque }
364 }
365
366 pub const ZERO: Self = CartesianEffort {
368 force: Position3D::ZERO,
369 torque: Position3D::ZERO,
370 };
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_position3d_basic() {
379 let pos = Position3D::new(1.0, 2.0, 3.0);
380 assert_eq!(pos.x, 1.0);
381 assert_eq!(pos.y, 2.0);
382 assert_eq!(pos.z, 3.0);
383 }
384
385 #[test]
386 fn test_position3d_norm() {
387 let pos = Position3D::new(3.0, 4.0, 0.0);
388 assert!((pos.norm() - 5.0).abs() < 1e-10);
389 }
390
391 #[test]
392 fn test_position3d_normalize() {
393 let pos = Position3D::new(3.0, 4.0, 0.0);
394 let normalized = pos.normalize();
395 assert!((normalized.norm() - 1.0).abs() < 1e-10);
396 }
397
398 #[test]
399 fn test_position3d_dot() {
400 let a = Position3D::new(1.0, 2.0, 3.0);
401 let b = Position3D::new(4.0, 5.0, 6.0);
402 assert_eq!(a.dot(&b), 32.0); }
404
405 #[test]
406 fn test_position3d_cross() {
407 let a = Position3D::new(1.0, 0.0, 0.0);
408 let b = Position3D::new(0.0, 1.0, 0.0);
409 let c = a.cross(&b);
410 assert_eq!(c.x, 0.0);
411 assert_eq!(c.y, 0.0);
412 assert_eq!(c.z, 1.0);
413 }
414
415 #[test]
416 fn test_quaternion_identity() {
417 let quat = Quaternion::IDENTITY;
418 assert_eq!(quat.w, 1.0);
419 assert_eq!(quat.x, 0.0);
420 }
421
422 #[test]
423 fn test_quaternion_euler_conversion() {
424 let roll = Rad(0.1);
425 let pitch = Rad(0.2);
426 let yaw = Rad(0.3);
427
428 let quat = Quaternion::from_euler(roll, pitch, yaw);
429 let (r2, p2, y2) = quat.to_euler();
430
431 assert!((roll.0 - r2.0).abs() < 1e-10);
432 assert!((pitch.0 - p2.0).abs() < 1e-10);
433 assert!((yaw.0 - y2.0).abs() < 1e-10);
434 }
435
436 #[test]
437 fn test_euler_angles_new() {
438 let euler = EulerAngles::new(10.0, 20.0, 30.0);
439 assert_eq!(euler.roll, 10.0);
440 assert_eq!(euler.pitch, 20.0);
441 assert_eq!(euler.yaw, 30.0);
442 }
443
444 #[test]
445 fn test_euler_angles_zero() {
446 let euler = EulerAngles::ZERO;
447 assert_eq!(euler.roll, 0.0);
448 assert_eq!(euler.pitch, 0.0);
449 assert_eq!(euler.yaw, 0.0);
450 }
451
452 #[test]
453 fn test_euler_angles_default() {
454 let euler = EulerAngles::default();
455 assert_eq!(euler.roll, 0.0);
456 assert_eq!(euler.pitch, 0.0);
457 assert_eq!(euler.yaw, 0.0);
458 }
459
460 #[test]
461 fn test_quaternion_normalization() {
462 let quat = Quaternion {
463 w: 1.0,
464 x: 1.0,
465 y: 1.0,
466 z: 1.0,
467 };
468 let normalized = quat.normalize();
469
470 let norm = (normalized.w * normalized.w
471 + normalized.x * normalized.x
472 + normalized.y * normalized.y
473 + normalized.z * normalized.z)
474 .sqrt();
475
476 assert!((norm - 1.0).abs() < 1e-10);
477 }
478
479 #[test]
480 fn test_quaternion_near_zero_stability() {
481 let near_zero = Quaternion {
483 w: 1e-20,
484 x: 1e-20,
485 y: 1e-20,
486 z: 1e-20,
487 };
488 let normalized = near_zero.normalize();
489
490 assert_eq!(normalized.w, 1.0);
492 assert_eq!(normalized.x, 0.0);
493 assert_eq!(normalized.y, 0.0);
494 assert_eq!(normalized.z, 0.0);
495
496 let zero = Quaternion {
498 w: 0.0,
499 x: 0.0,
500 y: 0.0,
501 z: 0.0,
502 };
503 let normalized_zero = zero.normalize();
504
505 assert!(!normalized_zero.w.is_nan());
507 assert!(!normalized_zero.x.is_nan());
508 assert_eq!(normalized_zero.w, 1.0);
509 }
510
511 #[test]
512 fn test_quaternion_norm_threshold() {
513 assert_eq!(QUATERNION_NORM_THRESHOLD, 1e-10);
515 }
516
517 #[test]
518 fn test_quaternion_multiply() {
519 let q1 = Quaternion::from_euler(Rad(0.1), Rad(0.0), Rad(0.0));
520 let q2 = Quaternion::from_euler(Rad(0.2), Rad(0.0), Rad(0.0));
521 let q3 = q1.multiply(&q2);
522 let q_expected = Quaternion::from_euler(Rad(0.3), Rad(0.0), Rad(0.0));
523
524 assert!((q3.w - q_expected.w).abs() < 1e-10);
525 assert!((q3.x - q_expected.x).abs() < 1e-10);
526 }
527
528 #[test]
529 fn test_quaternion_conjugate() {
530 let quat = Quaternion {
531 w: 0.7,
532 x: 0.1,
533 y: 0.2,
534 z: 0.3,
535 };
536 let conj = quat.conjugate();
537
538 assert_eq!(conj.w, 0.7);
539 assert_eq!(conj.x, -0.1);
540 assert_eq!(conj.y, -0.2);
541 assert_eq!(conj.z, -0.3);
542 }
543
544 #[test]
545 fn test_cartesian_pose_construction() {
546 let pose = CartesianPose::from_position_euler(1.0, 2.0, 3.0, Rad(0.0), Rad(0.0), Rad(0.0));
547
548 assert_eq!(pose.position.x, 1.0);
549 assert_eq!(pose.position.y, 2.0);
550 assert_eq!(pose.position.z, 3.0);
551 }
552
553 #[test]
554 fn test_cartesian_velocity() {
555 let vel = CartesianVelocity::new(
556 Position3D::new(0.1, 0.2, 0.3),
557 Position3D::new(0.01, 0.02, 0.03),
558 );
559
560 assert_eq!(vel.linear.x, 0.1);
561 assert_eq!(vel.angular.z, 0.03);
562 }
563
564 #[test]
565 fn test_cartesian_effort() {
566 let effort = CartesianEffort::new(
567 Position3D::new(10.0, 20.0, 30.0),
568 Position3D::new(1.0, 2.0, 3.0),
569 );
570
571 assert_eq!(effort.force.x, 10.0);
572 assert_eq!(effort.torque.z, 3.0);
573 }
574
575 #[test]
576 fn test_zero_constants() {
577 assert_eq!(Position3D::ZERO.x, 0.0);
578 assert_eq!(CartesianPose::ZERO.position.x, 0.0);
579 assert_eq!(CartesianVelocity::ZERO.linear.x, 0.0);
580 assert_eq!(CartesianEffort::ZERO.force.x, 0.0);
581 }
582}