Skip to main content

piper_client/types/
cartesian.rs

1//! 笛卡尔空间类型
2//!
3//! 提供3D位姿、速度和力的表示,用于笛卡尔空间控制。
4//!
5//! # 设计目标
6//!
7//! - **完整表示**: 位姿(位置+姿态)、速度、力
8//! - **数值稳定**: 四元数归一化防止NaN传播
9//! - **易用转换**: 欧拉角 ↔ 四元数
10//!
11//! # 示例
12//!
13//! ```rust
14//! use piper_client::types::{CartesianPose, Quaternion, Rad};
15//!
16//! // 创建位姿
17//! let pose = CartesianPose::from_position_euler(
18//!     0.5, 0.0, 0.3,  // x, y, z (米)
19//!     Rad(0.0), Rad(0.0), Rad(1.57),  // roll, pitch, yaw
20//! );
21//!
22//! // 四元数转欧拉角
23//! let (roll, pitch, yaw) = pose.orientation.to_euler();
24//! ```
25
26use super::units::Rad;
27use std::fmt;
28
29/// 四元数归一化阈值(避免除零)
30///
31/// 当四元数的模平方小于此值时,归一化会返回单位四元数。
32const QUATERNION_NORM_THRESHOLD: f64 = 1e-10;
33
34/// 三维位置向量(米)
35#[derive(Debug, Clone, Copy, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct Position3D {
38    /// X 坐标(米)
39    pub x: f64,
40    /// Y 坐标(米)
41    pub y: f64,
42    /// Z 坐标(米)
43    pub z: f64,
44}
45
46impl Position3D {
47    /// 创建新的三维位置
48    pub const fn new(x: f64, y: f64, z: f64) -> Self {
49        Position3D { x, y, z }
50    }
51
52    /// 零向量
53    pub const ZERO: Self = Position3D::new(0.0, 0.0, 0.0);
54
55    /// 计算向量长度(范数)
56    pub fn norm(&self) -> f64 {
57        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
58    }
59
60    /// 归一化(单位向量)
61    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    /// 点积
74    pub fn dot(&self, other: &Position3D) -> f64 {
75        self.x * other.x + self.y * other.y + self.z * other.z
76    }
77
78    /// 叉积
79    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/// 四元数(用于表示3D旋转)
95///
96/// 四元数是表示3D旋转的数学工具,避免了欧拉角的万向节锁问题。
97#[derive(Debug, Clone, Copy, PartialEq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99pub struct Quaternion {
100    /// 实部
101    pub w: f64,
102    /// 虚部 i
103    pub x: f64,
104    /// 虚部 j
105    pub y: f64,
106    /// 虚部 k
107    pub z: f64,
108}
109
110impl Quaternion {
111    /// 单位四元数(无旋转)
112    pub const IDENTITY: Self = Quaternion {
113        w: 1.0,
114        x: 0.0,
115        y: 0.0,
116        z: 0.0,
117    };
118
119    /// 从欧拉角创建四元数(Roll-Pitch-Yaw, ZYX顺序)
120    ///
121    /// # 参数
122    ///
123    /// - `roll`: 绕X轴旋转
124    /// - `pitch`: 绕Y轴旋转
125    /// - `yaw`: 绕Z轴旋转
126    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    /// 转换为欧拉角(Roll-Pitch-Yaw)
143    ///
144    /// 返回 `(roll, pitch, yaw)`
145    pub fn to_euler(self) -> (Rad, Rad, Rad) {
146        // Roll (x-axis rotation)
147        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        // Pitch (y-axis rotation)
152        let sinp = 2.0 * (self.w * self.y - self.z * self.x);
153        let pitch = if sinp.abs() >= 1.0 {
154            // Gimbal lock
155            Rad(std::f64::consts::FRAC_PI_2.copysign(sinp))
156        } else {
157            Rad(sinp.asin())
158        };
159
160        // Yaw (z-axis rotation)
161        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    /// 归一化(确保单位四元数)
169    ///
170    /// # 数值稳定性
171    ///
172    /// 如果四元数的模接近 0(< 1e-10),返回默认单位四元数 (1, 0, 0, 0)
173    /// 以避免除零错误和 NaN 扩散。
174    ///
175    /// 这种情况理论上不应发生,但在初始化错误、序列化错误或数值计算
176    /// 累积误差时可能出现。
177    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        // ✅ 数值稳定性检查:避免除零
181        if norm_sq < QUATERNION_NORM_THRESHOLD {
182            // 返回默认单位四元数(无旋转)
183            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    /// 四元数乘法(组合旋转)
205    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    /// 共轭(逆旋转)
215    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/// 笛卡尔空间位姿(位置 + 姿态)
236#[derive(Debug, Clone, Copy, PartialEq)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238pub struct CartesianPose {
239    /// 位置(米)
240    pub position: Position3D,
241    /// 姿态(四元数)
242    pub orientation: Quaternion,
243}
244
245impl CartesianPose {
246    /// 从位置和欧拉角创建
247    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    /// 从位置和四元数创建
255    pub fn from_position_quaternion(position: Position3D, orientation: Quaternion) -> Self {
256        CartesianPose {
257            position,
258            orientation,
259        }
260    }
261
262    /// 零位姿(原点,无旋转)
263    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/// 欧拉角(用于表示3D旋转姿态)
280///
281/// 使用 **Intrinsic RPY (Roll-Pitch-Yaw)** 顺序,即:
282/// - 先绕 X 轴旋转(Roll)
283/// - 再绕 Y 轴旋转(Pitch)
284/// - 最后绕 Z 轴旋转(Yaw)
285///
286/// **协议映射**:
287/// - Roll (RX): 对应协议 0x153 的 RX 角度
288/// - Pitch (RY): 对应协议 0x154 的 RY 角度
289/// - Yaw (RZ): 对应协议 0x154 的 RZ 角度
290///
291/// **单位**:度(degree)
292#[derive(Debug, Clone, Copy, PartialEq, Default)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294pub struct EulerAngles {
295    /// Roll:绕 X 轴旋转(度)
296    pub roll: f64,
297    /// Pitch:绕 Y 轴旋转(度)
298    pub pitch: f64,
299    /// Yaw:绕 Z 轴旋转(度)
300    pub yaw: f64,
301}
302
303impl EulerAngles {
304    /// 创建新的欧拉角
305    pub fn new(roll: f64, pitch: f64, yaw: f64) -> Self {
306        EulerAngles { roll, pitch, yaw }
307    }
308
309    /// 零角度(无旋转)
310    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/// 笛卡尔空间速度(线速度 + 角速度)
328#[derive(Debug, Clone, Copy, PartialEq)]
329#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
330pub struct CartesianVelocity {
331    /// 线速度(米/秒)
332    pub linear: Position3D,
333    /// 角速度(弧度/秒)
334    pub angular: Position3D,
335}
336
337impl CartesianVelocity {
338    /// 创建新的笛卡尔速度
339    pub fn new(linear: Position3D, angular: Position3D) -> Self {
340        CartesianVelocity { linear, angular }
341    }
342
343    /// 零速度
344    pub const ZERO: Self = CartesianVelocity {
345        linear: Position3D::ZERO,
346        angular: Position3D::ZERO,
347    };
348}
349
350/// 笛卡尔空间力/力矩
351#[derive(Debug, Clone, Copy, PartialEq)]
352#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
353pub struct CartesianEffort {
354    /// 力(牛顿)
355    pub force: Position3D,
356    /// 力矩(牛顿·米)
357    pub torque: Position3D,
358}
359
360impl CartesianEffort {
361    /// 创建新的笛卡尔力/力矩
362    pub fn new(force: Position3D, torque: Position3D) -> Self {
363        CartesianEffort { force, torque }
364    }
365
366    /// 零力/力矩
367    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); // 1*4 + 2*5 + 3*6
403    }
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        // 测试近零四元数的数值稳定性
482        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        // 应该返回单位四元数(无旋转)
491        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        // 测试完全为零的情况
497        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        // 不应该是 NaN
506        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        // 验证阈值常量的正确使用
514        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}