Skip to main content

zendo_protocol/
frames.rs

1//! Decoded frame structs with one named field per joint or landmark.
2
3use crate::constants::{
4    BODY_ISB_ANGLE_COUNT, BODY_JOINT_COUNT, BODY_LANDMARK_COUNT, HAND_JOINT_COUNT,
5    HAND_LANDMARK_COUNT,
6};
7use crate::types::{
8    HandJoint, HandLandmarkName, IsbAngleName, Joint, Landmark, LandmarkName, Quaternion,
9};
10
11/// Generates a frame struct: one public named field per joint/landmark, plus
12/// wire-order conversion, name lookup, and iteration.
13macro_rules! named_frame {
14    (
15        $(#[$attr:meta])*
16        $name:ident, elem = $elem:ty, key = $key:ty, count = $count:expr,
17        { $( $field:ident => $variant:ident ),+ $(,)? }
18    ) => {
19        $(#[$attr])*
20        #[derive(Clone, Copy, Debug, Default, PartialEq)]
21        pub struct $name {
22            $( pub $field: $elem ),+
23        }
24
25        impl $name {
26            /// Builds a frame from its values in wire order.
27            ///
28            /// Useful for the Zendo server, which produces frames to encode
29            /// rather than decoding them.
30            pub const fn from_array(values: [$elem; $count]) -> Self {
31                let [ $( $field ),+ ] = values;
32                Self { $( $field ),+ }
33            }
34
35            /// Returns the frame's values in wire order.
36            pub const fn to_array(&self) -> [$elem; $count] {
37                [ $( self.$field ),+ ]
38            }
39
40            /// Looks up a single value by name.
41            pub const fn get(&self, key: $key) -> $elem {
42                match key {
43                    $( <$key>::$variant => self.$field ),+
44                }
45            }
46
47            /// Iterates over `(name, value)` pairs in wire order.
48            pub fn iter(&self) -> impl Iterator<Item = ($key, $elem)> {
49                [ $( (<$key>::$variant, self.$field) ),+ ].into_iter()
50            }
51        }
52    };
53}
54
55named_frame! {
56    /// Body-joint orientations for one frame (13 joints).
57    BodyQuaternionFrame, elem = Quaternion, key = Joint, count = BODY_JOINT_COUNT,
58    {
59        hips => Hips,
60        spine => Spine,
61        neck => Neck,
62        right_arm => RightArm,
63        right_forearm => RightForearm,
64        left_arm => LeftArm,
65        left_forearm => LeftForearm,
66        right_upleg => RightUpLeg,
67        right_leg => RightLeg,
68        right_foot => RightFoot,
69        left_upleg => LeftUpLeg,
70        left_leg => LeftLeg,
71        left_foot => LeftFoot,
72    }
73}
74
75named_frame! {
76    /// Body-landmark positions for one frame (19 MAIA landmarks).
77    BodyLandmarkFrame, elem = Landmark, key = LandmarkName, count = BODY_LANDMARK_COUNT,
78    {
79        sacroiliac_joint => SacroiliacJoint,
80        suprasternal_notch => SuprasternalNotch,
81        nose => Nose,
82        left_ear => LeftEar,
83        right_ear => RightEar,
84        left_shoulder => LeftShoulder,
85        right_shoulder => RightShoulder,
86        left_elbow => LeftElbow,
87        right_elbow => RightElbow,
88        left_wrist => LeftWrist,
89        right_wrist => RightWrist,
90        left_hip => LeftHip,
91        right_hip => RightHip,
92        left_knee => LeftKnee,
93        right_knee => RightKnee,
94        left_ankle => LeftAnkle,
95        right_ankle => RightAnkle,
96        left_foot_index => LeftFootIndex,
97        right_foot_index => RightFootIndex,
98    }
99}
100
101named_frame! {
102    /// Hand-joint orientations for one frame (16 joints).
103    HandQuaternionFrame, elem = Quaternion, key = HandJoint, count = HAND_JOINT_COUNT,
104    {
105        wrist => Wrist,
106        thumb_mcp => ThumbMcp,
107        thumb_pip => ThumbPip,
108        thumb_dip => ThumbDip,
109        index_mcp => IndexMcp,
110        index_pip => IndexPip,
111        index_dip => IndexDip,
112        middle_mcp => MiddleMcp,
113        middle_pip => MiddlePip,
114        middle_dip => MiddleDip,
115        ring_mcp => RingMcp,
116        ring_pip => RingPip,
117        ring_dip => RingDip,
118        pinky_mcp => PinkyMcp,
119        pinky_pip => PinkyPip,
120        pinky_dip => PinkyDip,
121    }
122}
123
124named_frame! {
125    /// Hand-landmark positions for one frame (21 BlazePose hand landmarks).
126    HandLandmarkFrame, elem = Landmark, key = HandLandmarkName, count = HAND_LANDMARK_COUNT,
127    {
128        wrist => Wrist,
129        thumb_cmc => ThumbCmc,
130        thumb_mcp => ThumbMcp,
131        thumb_ip => ThumbIp,
132        thumb_tip => ThumbTip,
133        index_mcp => IndexMcp,
134        index_pip => IndexPip,
135        index_dip => IndexDip,
136        index_tip => IndexTip,
137        middle_mcp => MiddleMcp,
138        middle_pip => MiddlePip,
139        middle_dip => MiddleDip,
140        middle_tip => MiddleTip,
141        ring_mcp => RingMcp,
142        ring_pip => RingPip,
143        ring_dip => RingDip,
144        ring_tip => RingTip,
145        pinky_mcp => PinkyMcp,
146        pinky_pip => PinkyPip,
147        pinky_dip => PinkyDip,
148        pinky_tip => PinkyTip,
149    }
150}
151
152named_frame! {
153    /// Body ISB joint angles for one frame (21 scalars, radians).
154    ///
155    /// Shoulder `plane_of_elevation` fields are `NaN` when the shoulder is near
156    /// neutral (singularity). All other values are always finite after a solve.
157    BodyIsbAnglesFrame, elem = f64, key = IsbAngleName, count = BODY_ISB_ANGLE_COUNT,
158    {
159        thorax_lateral_bend => ThoraxLateralBend,
160        thorax_axial_rotation => ThoraxAxialRotation,
161        neck_flexion => NeckFlexion,
162        neck_lateral_bend => NeckLateralBend,
163        neck_axial_rotation => NeckAxialRotation,
164        right_shoulder_plane_of_elevation => RightShoulderPlaneOfElevation,
165        right_shoulder_elevation => RightShoulderElevation,
166        left_shoulder_plane_of_elevation => LeftShoulderPlaneOfElevation,
167        left_shoulder_elevation => LeftShoulderElevation,
168        right_elbow_flexion => RightElbowFlexion,
169        left_elbow_flexion => LeftElbowFlexion,
170        right_hip_flexion => RightHipFlexion,
171        right_hip_adduction => RightHipAdduction,
172        right_hip_internal_rotation => RightHipInternalRotation,
173        left_hip_flexion => LeftHipFlexion,
174        left_hip_adduction => LeftHipAdduction,
175        left_hip_internal_rotation => LeftHipInternalRotation,
176        right_knee_flexion => RightKneeFlexion,
177        left_knee_flexion => LeftKneeFlexion,
178        right_ankle_dorsiflexion => RightAnkleDorsiflexion,
179        left_ankle_dorsiflexion => LeftAnkleDorsiflexion,
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn from_array_then_to_array_round_trips() {
189        // Arrange
190        let mut values = [Quaternion::default(); BODY_JOINT_COUNT];
191        for (i, q) in values.iter_mut().enumerate() {
192            *q = Quaternion {
193                w: i as f64,
194                x: 0.0,
195                y: 0.0,
196                z: 0.0,
197            };
198        }
199
200        // Act
201        let frame = BodyQuaternionFrame::from_array(values);
202
203        // Assert
204        assert_eq!(frame.to_array(), values);
205        assert_eq!(frame.hips.w, 0.0);
206        assert_eq!(frame.left_foot.w, 12.0);
207    }
208
209    #[test]
210    fn get_matches_field_access() {
211        // Arrange
212        let frame = HandLandmarkFrame {
213            thumb_tip: Landmark {
214                x: 1.0,
215                y: 2.0,
216                z: 3.0,
217                confidence: 0.5,
218            },
219            ..HandLandmarkFrame::default()
220        };
221
222        // Act / Assert
223        assert_eq!(frame.get(HandLandmarkName::ThumbTip), frame.thumb_tip);
224    }
225
226    #[test]
227    fn iter_yields_wire_order() {
228        // Arrange
229        let frame = BodyLandmarkFrame::default();
230
231        // Act
232        let names: Vec<LandmarkName> = frame.iter().map(|(name, _)| name).collect();
233
234        // Assert
235        assert_eq!(names, LandmarkName::ALL.to_vec());
236    }
237}