Skip to main content

zendo_protocol/
types.rs

1//! Value types and the joint / landmark vocabularies.
2
3use crate::constants::{HAND_SIDE_LEFT, HAND_SIDE_RIGHT};
4
5/// A unit quaternion describing a joint orientation: `w + xi + yj + zk`.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct Quaternion {
8    pub w: f64,
9    pub x: f64,
10    pub y: f64,
11    pub z: f64,
12}
13
14/// A 3D landmark position with a detection confidence in `[0, 1]`.
15#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct Landmark {
17    pub x: f64,
18    pub y: f64,
19    pub z: f64,
20    pub confidence: f64,
21}
22
23/// Which hand a hand frame describes.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum HandSide {
26    Right,
27    Left,
28}
29
30impl HandSide {
31    /// Decodes the side byte that prefixes hand-frame payloads.
32    pub const fn from_byte(byte: u8) -> Option<Self> {
33        match byte {
34            HAND_SIDE_RIGHT => Some(Self::Right),
35            HAND_SIDE_LEFT => Some(Self::Left),
36            _ => None,
37        }
38    }
39
40    /// The side byte for this hand.
41    pub const fn as_byte(self) -> u8 {
42        match self {
43            Self::Right => HAND_SIDE_RIGHT,
44            Self::Left => HAND_SIDE_LEFT,
45        }
46    }
47
48    /// The lowercase name of this hand (`"right"` or `"left"`).
49    pub const fn as_str(self) -> &'static str {
50        match self {
51            Self::Right => "right",
52            Self::Left => "left",
53        }
54    }
55}
56
57/// Generates a fieldless enum whose variants carry a stable wire order and a
58/// lowercase string label (the same labels Zendo writes to its CSV logs).
59macro_rules! name_enum {
60    (
61        $(#[$attr:meta])*
62        $name:ident { $( $variant:ident => $label:literal ),+ $(,)? }
63    ) => {
64        $(#[$attr])*
65        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66        pub enum $name {
67            $( $variant ),+
68        }
69
70        impl $name {
71            /// Every variant, in wire order.
72            pub const ALL: &'static [$name] = &[ $( $name::$variant ),+ ];
73
74            /// The lowercase string label for this variant.
75            pub const fn as_str(self) -> &'static str {
76                match self {
77                    $( $name::$variant => $label ),+
78                }
79            }
80
81            /// This variant's position in wire order.
82            pub const fn index(self) -> usize {
83                self as usize
84            }
85
86            /// The variant at `index` in wire order, or `None` if out of range.
87            pub const fn from_index(index: usize) -> Option<$name> {
88                let all: &[$name] = $name::ALL;
89                if index < all.len() {
90                    Some(all[index])
91                } else {
92                    None
93                }
94            }
95        }
96    };
97}
98
99name_enum! {
100    /// A body joint in a [`BodyQuaternionFrame`](crate::BodyQuaternionFrame).
101    Joint {
102        Hips => "hips",
103        Spine => "spine",
104        Neck => "neck",
105        RightArm => "right_arm",
106        RightForearm => "right_forearm",
107        LeftArm => "left_arm",
108        LeftForearm => "left_forearm",
109        RightUpLeg => "right_upleg",
110        RightLeg => "right_leg",
111        RightFoot => "right_foot",
112        LeftUpLeg => "left_upleg",
113        LeftLeg => "left_leg",
114        LeftFoot => "left_foot",
115    }
116}
117
118name_enum! {
119    /// A body landmark in a [`BodyLandmarkFrame`](crate::BodyLandmarkFrame).
120    LandmarkName {
121        SacroiliacJoint => "sacroiliac_joint",
122        SuprasternalNotch => "suprasternal_notch",
123        Nose => "nose",
124        LeftEar => "left_ear",
125        RightEar => "right_ear",
126        LeftShoulder => "left_shoulder",
127        RightShoulder => "right_shoulder",
128        LeftElbow => "left_elbow",
129        RightElbow => "right_elbow",
130        LeftWrist => "left_wrist",
131        RightWrist => "right_wrist",
132        LeftHip => "left_hip",
133        RightHip => "right_hip",
134        LeftKnee => "left_knee",
135        RightKnee => "right_knee",
136        LeftAnkle => "left_ankle",
137        RightAnkle => "right_ankle",
138        LeftFootIndex => "left_foot_index",
139        RightFootIndex => "right_foot_index",
140    }
141}
142
143name_enum! {
144    /// A hand joint in a [`HandQuaternionFrame`](crate::HandQuaternionFrame).
145    HandJoint {
146        Wrist => "wrist",
147        ThumbMcp => "thumb_mcp",
148        ThumbPip => "thumb_pip",
149        ThumbDip => "thumb_dip",
150        IndexMcp => "index_mcp",
151        IndexPip => "index_pip",
152        IndexDip => "index_dip",
153        MiddleMcp => "middle_mcp",
154        MiddlePip => "middle_pip",
155        MiddleDip => "middle_dip",
156        RingMcp => "ring_mcp",
157        RingPip => "ring_pip",
158        RingDip => "ring_dip",
159        PinkyMcp => "pinky_mcp",
160        PinkyPip => "pinky_pip",
161        PinkyDip => "pinky_dip",
162    }
163}
164
165name_enum! {
166    /// A scalar angle in a [`BodyIsbAnglesFrame`](crate::BodyIsbAnglesFrame).
167    ///
168    /// All angles are in radians, in ISB joint coordinate systems. Shoulder
169    /// `plane_of_elevation` is `NaN` when the shoulder is near neutral (singularity).
170    IsbAngleName {
171        ThoraxLateralBend => "thorax_lateral_bend",
172        ThoraxAxialRotation => "thorax_axial_rotation",
173        NeckFlexion => "neck_flexion",
174        NeckLateralBend => "neck_lateral_bend",
175        NeckAxialRotation => "neck_axial_rotation",
176        RightShoulderPlaneOfElevation => "right_shoulder_plane_of_elevation",
177        RightShoulderElevation => "right_shoulder_elevation",
178        LeftShoulderPlaneOfElevation => "left_shoulder_plane_of_elevation",
179        LeftShoulderElevation => "left_shoulder_elevation",
180        RightElbowFlexion => "right_elbow_flexion",
181        LeftElbowFlexion => "left_elbow_flexion",
182        RightHipFlexion => "right_hip_flexion",
183        RightHipAdduction => "right_hip_adduction",
184        RightHipInternalRotation => "right_hip_internal_rotation",
185        LeftHipFlexion => "left_hip_flexion",
186        LeftHipAdduction => "left_hip_adduction",
187        LeftHipInternalRotation => "left_hip_internal_rotation",
188        RightKneeFlexion => "right_knee_flexion",
189        LeftKneeFlexion => "left_knee_flexion",
190        RightAnkleDorsiflexion => "right_ankle_dorsiflexion",
191        LeftAnkleDorsiflexion => "left_ankle_dorsiflexion",
192    }
193}
194
195name_enum! {
196    /// A hand landmark in a [`HandLandmarkFrame`](crate::HandLandmarkFrame).
197    HandLandmarkName {
198        Wrist => "wrist",
199        ThumbCmc => "thumb_cmc",
200        ThumbMcp => "thumb_mcp",
201        ThumbIp => "thumb_ip",
202        ThumbTip => "thumb_tip",
203        IndexMcp => "index_mcp",
204        IndexPip => "index_pip",
205        IndexDip => "index_dip",
206        IndexTip => "index_tip",
207        MiddleMcp => "middle_mcp",
208        MiddlePip => "middle_pip",
209        MiddleDip => "middle_dip",
210        MiddleTip => "middle_tip",
211        RingMcp => "ring_mcp",
212        RingPip => "ring_pip",
213        RingDip => "ring_dip",
214        RingTip => "ring_tip",
215        PinkyMcp => "pinky_mcp",
216        PinkyPip => "pinky_pip",
217        PinkyDip => "pinky_dip",
218        PinkyTip => "pinky_tip",
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::constants::{
226        BODY_ISB_ANGLE_COUNT, BODY_JOINT_COUNT, BODY_LANDMARK_COUNT, HAND_JOINT_COUNT,
227        HAND_LANDMARK_COUNT,
228    };
229
230    #[test]
231    fn enum_lengths_match_protocol_counts() {
232        // Arrange / Act / Assert
233        assert_eq!(Joint::ALL.len(), BODY_JOINT_COUNT);
234        assert_eq!(LandmarkName::ALL.len(), BODY_LANDMARK_COUNT);
235        assert_eq!(HandJoint::ALL.len(), HAND_JOINT_COUNT);
236        assert_eq!(HandLandmarkName::ALL.len(), HAND_LANDMARK_COUNT);
237        assert_eq!(IsbAngleName::ALL.len(), BODY_ISB_ANGLE_COUNT);
238    }
239
240    #[test]
241    fn index_round_trips_through_from_index() {
242        // Arrange
243        for (i, joint) in Joint::ALL.iter().enumerate() {
244            // Act
245            let recovered = Joint::from_index(i);
246
247            // Assert
248            assert_eq!(recovered, Some(*joint));
249            assert_eq!(joint.index(), i);
250        }
251    }
252
253    #[test]
254    fn from_index_rejects_out_of_range() {
255        // Arrange / Act / Assert
256        assert_eq!(Joint::from_index(BODY_JOINT_COUNT), None);
257        assert_eq!(HandLandmarkName::from_index(usize::MAX), None);
258    }
259
260    #[test]
261    fn hand_side_byte_round_trips() {
262        // Arrange / Act / Assert
263        assert_eq!(
264            HandSide::from_byte(HandSide::Right.as_byte()),
265            Some(HandSide::Right)
266        );
267        assert_eq!(
268            HandSide::from_byte(HandSide::Left.as_byte()),
269            Some(HandSide::Left)
270        );
271        assert_eq!(HandSide::from_byte(2), None);
272    }
273
274    #[test]
275    fn labels_are_stable() {
276        // Arrange / Act / Assert
277        assert_eq!(Joint::Hips.as_str(), "hips");
278        assert_eq!(Joint::LeftFoot.as_str(), "left_foot");
279        assert_eq!(LandmarkName::SacroiliacJoint.as_str(), "sacroiliac_joint");
280        assert_eq!(HandJoint::PinkyDip.as_str(), "pinky_dip");
281        assert_eq!(HandLandmarkName::ThumbTip.as_str(), "thumb_tip");
282    }
283}