1use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
15pub struct ObjectRef {
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub object_id: Option<String>,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub item_id: Option<String>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub object_type: Option<String>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub object_class: Option<String>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub tracking_id: Option<String>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub dimensions_m: Option<Dimensions>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 #[validate(range(min = 0.0))]
43 pub mass_kg: Option<f64>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub pose: Option<Pose>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub frame_id: Option<String>,
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
56pub struct Dimensions {
57 #[serde(skip_serializing_if = "Option::is_none")]
59 #[validate(range(min = 0.0))]
60 pub length_m: Option<f64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 #[validate(range(min = 0.0))]
65 pub width_m: Option<f64>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 #[validate(range(min = 0.0))]
70 pub height_m: Option<f64>,
71}
72
73impl Dimensions {
74 pub fn new(length_m: f64, width_m: f64, height_m: f64) -> Self {
76 Self {
77 length_m: Some(length_m),
78 width_m: Some(width_m),
79 height_m: Some(height_m),
80 }
81 }
82
83 pub fn volume_m3(&self) -> Option<f64> {
85 match (self.length_m, self.width_m, self.height_m) {
86 (Some(l), Some(w), Some(h)) => Some(l * w * h),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
94pub struct Pose {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub position: Option<Position3D>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub orientation: Option<Quaternion>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub euler_deg: Option<EulerAngles>,
106}
107
108impl Pose {
109 pub fn from_position(x: f64, y: f64, z: f64) -> Self {
111 Self {
112 position: Some(Position3D::new(x, y, z)),
113 orientation: None,
114 euler_deg: None,
115 }
116 }
117
118 pub fn from_position_euler(x: f64, y: f64, z: f64, roll: f64, pitch: f64, yaw: f64) -> Self {
120 Self {
121 position: Some(Position3D::new(x, y, z)),
122 orientation: None,
123 euler_deg: Some(EulerAngles::new(roll, pitch, yaw)),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
130pub struct Position3D {
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub x_m: Option<f64>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub y_m: Option<f64>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub z_m: Option<f64>,
142}
143
144impl Position3D {
145 pub fn new(x: f64, y: f64, z: f64) -> Self {
147 Self {
148 x_m: Some(x),
149 y_m: Some(y),
150 z_m: Some(z),
151 }
152 }
153
154 pub fn distance_to(&self, other: &Position3D) -> Option<f64> {
156 match (
157 self.x_m, self.y_m, self.z_m, other.x_m, other.y_m, other.z_m,
158 ) {
159 (Some(x1), Some(y1), Some(z1), Some(x2), Some(y2), Some(z2)) => {
160 let dx = x2 - x1;
161 let dy = y2 - y1;
162 let dz = z2 - z1;
163 Some((dx * dx + dy * dy + dz * dz).sqrt())
164 }
165 _ => None,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
172pub struct Position2D {
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub x_m: Option<f64>,
176
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub y_m: Option<f64>,
180}
181
182impl Position2D {
183 pub fn new(x: f64, y: f64) -> Self {
185 Self {
186 x_m: Some(x),
187 y_m: Some(y),
188 }
189 }
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
194pub struct Quaternion {
195 pub w: f64,
197 pub x: f64,
199 pub y: f64,
201 pub z: f64,
203}
204
205impl Quaternion {
206 pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
208 Self { w, x, y, z }
209 }
210
211 pub fn identity() -> Self {
213 Self {
214 w: 1.0,
215 x: 0.0,
216 y: 0.0,
217 z: 0.0,
218 }
219 }
220
221 pub fn normalize(&self) -> Self {
223 let mag = (self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
224 if mag > 0.0 {
225 Self {
226 w: self.w / mag,
227 x: self.x / mag,
228 y: self.y / mag,
229 z: self.z / mag,
230 }
231 } else {
232 Self::identity()
233 }
234 }
235}
236
237#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
239pub struct EulerAngles {
240 #[serde(skip_serializing_if = "Option::is_none")]
242 #[validate(range(min = -180.0, max = 180.0))]
243 pub roll_deg: Option<f64>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 #[validate(range(min = -90.0, max = 90.0))]
248 pub pitch_deg: Option<f64>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
252 #[validate(range(min = -180.0, max = 180.0))]
253 pub yaw_deg: Option<f64>,
254}
255
256impl EulerAngles {
257 pub fn new(roll: f64, pitch: f64, yaw: f64) -> Self {
259 Self {
260 roll_deg: Some(roll),
261 pitch_deg: Some(pitch),
262 yaw_deg: Some(yaw),
263 }
264 }
265}
266
267#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
269pub struct Vector3D {
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub x: Option<f64>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub y: Option<f64>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub z: Option<f64>,
281}
282
283impl Vector3D {
284 pub fn new(x: f64, y: f64, z: f64) -> Self {
286 Self {
287 x: Some(x),
288 y: Some(y),
289 z: Some(z),
290 }
291 }
292
293 pub fn magnitude(&self) -> Option<f64> {
295 match (self.x, self.y, self.z) {
296 (Some(x), Some(y), Some(z)) => Some((x * x + y * y + z * z).sqrt()),
297 _ => None,
298 }
299 }
300}
301
302#[derive(Debug, Clone, Default, Serialize, Deserialize)]
304pub struct CovarianceMatrix {
305 pub values: Vec<f64>,
307
308 pub dimension: u8,
310}
311
312impl CovarianceMatrix {
313 pub fn position_3x3(values: [f64; 9]) -> Self {
315 Self {
316 values: values.to_vec(),
317 dimension: 3,
318 }
319 }
320
321 pub fn pose_6x6(values: [f64; 36]) -> Self {
323 Self {
324 values: values.to_vec(),
325 dimension: 6,
326 }
327 }
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
332pub struct BoundingBox {
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub center: Option<Position3D>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub dimensions: Option<Dimensions>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub orientation: Option<Quaternion>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub axis_aligned: Option<bool>,
348}
349
350#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
352pub struct BoundingBox2D {
353 #[serde(skip_serializing_if = "Option::is_none")]
355 #[validate(range(min = 0.0))]
356 pub x: Option<f64>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
360 #[validate(range(min = 0.0))]
361 pub y: Option<f64>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
365 #[validate(range(min = 0.0))]
366 pub width: Option<f64>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 #[validate(range(min = 0.0))]
371 pub height: Option<f64>,
372}
373
374#[derive(Debug, Clone, Default, Serialize, Deserialize)]
376pub struct Polygon2D {
377 pub vertices: Vec<Position2D>,
379
380 #[serde(default = "default_true")]
382 pub closed: bool,
383}
384
385fn default_true() -> bool {
386 true
387}
388
389impl Polygon2D {
390 pub fn new(vertices: Vec<Position2D>) -> Self {
392 Self {
393 vertices,
394 closed: true,
395 }
396 }
397
398 pub fn contains(&self, point: &Position2D) -> bool {
400 let (px, py) = match (point.x_m, point.y_m) {
401 (Some(x), Some(y)) => (x, y),
402 _ => return false,
403 };
404
405 let n = self.vertices.len();
406 if n < 3 {
407 return false;
408 }
409
410 let mut inside = false;
411 let mut j = n - 1;
412
413 for i in 0..n {
414 let (xi, yi) = match (self.vertices[i].x_m, self.vertices[i].y_m) {
415 (Some(x), Some(y)) => (x, y),
416 _ => continue,
417 };
418 let (xj, yj) = match (self.vertices[j].x_m, self.vertices[j].y_m) {
419 (Some(x), Some(y)) => (x, y),
420 _ => {
421 j = i;
422 continue;
423 }
424 };
425
426 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
427 inside = !inside;
428 }
429 j = i;
430 }
431
432 inside
433 }
434}
435
436#[derive(Debug, Clone, Default, Serialize, Deserialize)]
438pub struct TimeRange {
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub start: Option<chrono::DateTime<chrono::Utc>>,
442
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub end: Option<chrono::DateTime<chrono::Utc>>,
446}
447
448#[derive(Debug, Clone, Default, Serialize, Deserialize)]
450pub struct Metadata {
451 pub key: String,
453
454 pub value: String,
456}
457
458#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
460pub struct Confidence {
461 #[validate(range(min = 0.0, max = 1.0))]
463 pub score: f64,
464
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub source: Option<String>,
468}
469
470impl Confidence {
471 pub fn new(score: f64) -> Self {
473 Self {
474 score,
475 source: None,
476 }
477 }
478
479 pub fn with_source(score: f64, source: impl Into<String>) -> Self {
481 Self {
482 score,
483 source: Some(source.into()),
484 }
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn test_dimensions_volume() {
494 let dims = Dimensions::new(2.0, 3.0, 4.0);
495 assert_eq!(dims.volume_m3(), Some(24.0));
496 }
497
498 #[test]
499 fn test_position_distance() {
500 let p1 = Position3D::new(0.0, 0.0, 0.0);
501 let p2 = Position3D::new(3.0, 4.0, 0.0);
502 assert!((p1.distance_to(&p2).unwrap() - 5.0).abs() < 1e-10);
503 }
504
505 #[test]
506 fn test_quaternion_normalize() {
507 let q = Quaternion::new(2.0, 0.0, 0.0, 0.0);
508 let normalized = q.normalize();
509 assert!((normalized.w - 1.0).abs() < 1e-10);
510 }
511
512 #[test]
513 fn test_polygon_contains() {
514 let polygon = Polygon2D::new(vec![
515 Position2D::new(0.0, 0.0),
516 Position2D::new(4.0, 0.0),
517 Position2D::new(4.0, 4.0),
518 Position2D::new(0.0, 4.0),
519 ]);
520
521 assert!(polygon.contains(&Position2D::new(2.0, 2.0)));
522 assert!(!polygon.contains(&Position2D::new(5.0, 5.0)));
523 }
524
525 #[test]
530 fn test_object_ref_default() {
531 let obj = ObjectRef::default();
532 assert!(obj.object_id.is_none());
533 assert!(obj.item_id.is_none());
534 assert!(obj.object_type.is_none());
535 assert!(obj.object_class.is_none());
536 assert!(obj.tracking_id.is_none());
537 assert!(obj.dimensions_m.is_none());
538 assert!(obj.mass_kg.is_none());
539 assert!(obj.pose.is_none());
540 assert!(obj.frame_id.is_none());
541 }
542
543 #[test]
544 fn test_object_ref_full() {
545 let obj = ObjectRef {
546 object_id: Some("obj-001".to_string()),
547 item_id: Some("SKU12345".to_string()),
548 object_type: Some("package".to_string()),
549 object_class: Some("cardboard_box".to_string()),
550 tracking_id: Some("track-001".to_string()),
551 dimensions_m: Some(Dimensions::new(0.5, 0.3, 0.2)),
552 mass_kg: Some(2.5),
553 pose: Some(Pose::from_position(1.0, 2.0, 0.5)),
554 frame_id: Some("base_link".to_string()),
555 };
556
557 assert_eq!(obj.object_type, Some("package".to_string()));
558 assert_eq!(obj.mass_kg, Some(2.5));
559 }
560
561 #[test]
566 fn test_dimensions_new() {
567 let dims = Dimensions::new(1.0, 2.0, 3.0);
568 assert_eq!(dims.length_m, Some(1.0));
569 assert_eq!(dims.width_m, Some(2.0));
570 assert_eq!(dims.height_m, Some(3.0));
571 }
572
573 #[test]
574 fn test_dimensions_volume_partial() {
575 let dims = Dimensions {
576 length_m: Some(2.0),
577 width_m: None,
578 height_m: Some(3.0),
579 };
580 assert!(dims.volume_m3().is_none());
581 }
582
583 #[test]
584 fn test_dimensions_default() {
585 let dims = Dimensions::default();
586 assert!(dims.length_m.is_none());
587 assert!(dims.width_m.is_none());
588 assert!(dims.height_m.is_none());
589 assert!(dims.volume_m3().is_none());
590 }
591
592 #[test]
597 fn test_pose_from_position() {
598 let pose = Pose::from_position(1.0, 2.0, 3.0);
599 assert!(pose.position.is_some());
600 let pos = pose.position.unwrap();
601 assert_eq!(pos.x_m, Some(1.0));
602 assert_eq!(pos.y_m, Some(2.0));
603 assert_eq!(pos.z_m, Some(3.0));
604 }
605
606 #[test]
607 fn test_pose_from_position_euler() {
608 let pose = Pose::from_position_euler(1.0, 2.0, 3.0, 10.0, 20.0, 30.0);
609 assert!(pose.position.is_some());
610 assert!(pose.euler_deg.is_some());
611 let euler = pose.euler_deg.unwrap();
612 assert_eq!(euler.roll_deg, Some(10.0));
613 assert_eq!(euler.pitch_deg, Some(20.0));
614 assert_eq!(euler.yaw_deg, Some(30.0));
615 }
616
617 #[test]
618 fn test_pose_default() {
619 let pose = Pose::default();
620 assert!(pose.position.is_none());
621 assert!(pose.orientation.is_none());
622 assert!(pose.euler_deg.is_none());
623 }
624
625 #[test]
630 fn test_position_3d_new() {
631 let pos = Position3D::new(1.5, 2.5, 3.5);
632 assert_eq!(pos.x_m, Some(1.5));
633 assert_eq!(pos.y_m, Some(2.5));
634 assert_eq!(pos.z_m, Some(3.5));
635 }
636
637 #[test]
638 fn test_position_3d_distance_to_partial() {
639 let p1 = Position3D::new(0.0, 0.0, 0.0);
640 let p2 = Position3D::default();
641 assert!(p1.distance_to(&p2).is_none());
642 }
643
644 #[test]
645 fn test_position_3d_default() {
646 let pos = Position3D::default();
647 assert!(pos.x_m.is_none());
648 assert!(pos.y_m.is_none());
649 assert!(pos.z_m.is_none());
650 }
651
652 #[test]
657 fn test_position_2d_new() {
658 let pos = Position2D::new(10.0, 20.0);
659 assert_eq!(pos.x_m, Some(10.0));
660 assert_eq!(pos.y_m, Some(20.0));
661 }
662
663 #[test]
664 fn test_position_2d_default() {
665 let pos = Position2D::default();
666 assert!(pos.x_m.is_none());
667 assert!(pos.y_m.is_none());
668 }
669
670 #[test]
675 fn test_quaternion_new() {
676 let q = Quaternion::new(1.0, 0.0, 0.0, 0.0);
677 assert_eq!(q.w, 1.0);
678 assert_eq!(q.x, 0.0);
679 assert_eq!(q.y, 0.0);
680 assert_eq!(q.z, 0.0);
681 }
682
683 #[test]
684 fn test_quaternion_identity() {
685 let q = Quaternion::identity();
686 assert_eq!(q.w, 1.0);
687 assert_eq!(q.x, 0.0);
688 assert_eq!(q.y, 0.0);
689 assert_eq!(q.z, 0.0);
690 }
691
692 #[test]
693 fn test_quaternion_normalize_non_unit() {
694 let q = Quaternion::new(0.0, 3.0, 4.0, 0.0);
695 let normalized = q.normalize();
696 let mag_sq = normalized.w * normalized.w
698 + normalized.x * normalized.x
699 + normalized.y * normalized.y
700 + normalized.z * normalized.z;
701 assert!((mag_sq - 1.0).abs() < 1e-10);
702 }
703
704 #[test]
705 fn test_quaternion_default() {
706 let q = Quaternion::default();
707 assert_eq!(q.w, 0.0);
708 assert_eq!(q.x, 0.0);
709 assert_eq!(q.y, 0.0);
710 assert_eq!(q.z, 0.0);
711 }
712
713 #[test]
718 fn test_euler_angles_new() {
719 let euler = EulerAngles::new(10.0, 20.0, 30.0);
720 assert_eq!(euler.roll_deg, Some(10.0));
721 assert_eq!(euler.pitch_deg, Some(20.0));
722 assert_eq!(euler.yaw_deg, Some(30.0));
723 }
724
725 #[test]
726 fn test_euler_angles_default() {
727 let euler = EulerAngles::default();
728 assert!(euler.roll_deg.is_none());
729 assert!(euler.pitch_deg.is_none());
730 assert!(euler.yaw_deg.is_none());
731 }
732
733 #[test]
738 fn test_vector_3d_new() {
739 let v = Vector3D::new(1.0, 2.0, 3.0);
740 assert_eq!(v.x, Some(1.0));
741 assert_eq!(v.y, Some(2.0));
742 assert_eq!(v.z, Some(3.0));
743 }
744
745 #[test]
746 fn test_vector_3d_magnitude() {
747 let v = Vector3D::new(3.0, 4.0, 0.0);
748 assert!((v.magnitude().unwrap() - 5.0).abs() < 1e-10);
749 }
750
751 #[test]
752 fn test_vector_3d_magnitude_none() {
753 let v = Vector3D::default();
754 assert!(v.magnitude().is_none());
755 }
756
757 #[test]
758 fn test_vector_3d_default() {
759 let v = Vector3D::default();
760 assert!(v.x.is_none());
761 assert!(v.y.is_none());
762 assert!(v.z.is_none());
763 }
764
765 #[test]
770 fn test_bounding_box_default() {
771 let bb = BoundingBox::default();
772 assert!(bb.center.is_none());
773 assert!(bb.dimensions.is_none());
774 assert!(bb.orientation.is_none());
775 assert!(bb.axis_aligned.is_none());
776 }
777
778 #[test]
779 fn test_bounding_box_full() {
780 let bb = BoundingBox {
781 center: Some(Position3D::new(1.0, 2.0, 0.5)),
782 dimensions: Some(Dimensions::new(1.0, 0.5, 0.8)),
783 orientation: Some(Quaternion::identity()),
784 axis_aligned: Some(true),
785 };
786
787 assert!(bb.center.is_some());
788 assert!(bb.dimensions.is_some());
789 }
790
791 #[test]
796 fn test_polygon_2d_new() {
797 let vertices = vec![
798 Position2D::new(0.0, 0.0),
799 Position2D::new(1.0, 0.0),
800 Position2D::new(1.0, 1.0),
801 ];
802 let poly = Polygon2D::new(vertices);
803 assert_eq!(poly.vertices.len(), 3);
804 }
805
806 #[test]
807 fn test_polygon_2d_empty() {
808 let poly = Polygon2D::new(vec![]);
809 assert!(!poly.contains(&Position2D::new(0.0, 0.0)));
810 }
811
812 #[test]
813 fn test_polygon_2d_contains_partial_vertices() {
814 let poly = Polygon2D::new(vec![
815 Position2D::default(),
816 Position2D::new(1.0, 0.0),
817 Position2D::new(1.0, 1.0),
818 ]);
819 let _ = poly.contains(&Position2D::new(0.5, 0.5));
821 }
822
823 #[test]
824 fn test_polygon_2d_contains_point_no_coords() {
825 let poly = Polygon2D::new(vec![
826 Position2D::new(0.0, 0.0),
827 Position2D::new(1.0, 0.0),
828 Position2D::new(1.0, 1.0),
829 ]);
830 assert!(!poly.contains(&Position2D::default()));
831 }
832
833 #[test]
838 fn test_confidence_new() {
839 let conf = Confidence::new(0.95);
840 assert_eq!(conf.score, 0.95);
841 assert!(conf.source.is_none());
842 }
843
844 #[test]
845 fn test_confidence_with_source() {
846 let conf = Confidence::with_source(0.87, "model_v2");
847 assert_eq!(conf.score, 0.87);
848 assert_eq!(conf.source, Some("model_v2".to_string()));
849 }
850
851 #[test]
852 fn test_confidence_default() {
853 let conf = Confidence::default();
854 assert_eq!(conf.score, 0.0);
855 assert!(conf.source.is_none());
856 }
857
858 #[test]
863 fn test_time_range_default() {
864 let tr = TimeRange::default();
865 assert!(tr.start.is_none());
866 assert!(tr.end.is_none());
867 }
868
869 #[test]
874 fn test_metadata_default() {
875 let md = Metadata::default();
876 assert!(md.key.is_empty());
877 assert!(md.value.is_empty());
878 }
879
880 #[test]
881 fn test_metadata_full() {
882 let md = Metadata {
883 key: "version".to_string(),
884 value: "1.0.0".to_string(),
885 };
886 assert_eq!(md.key, "version");
887 assert_eq!(md.value, "1.0.0");
888 }
889
890 #[test]
895 fn test_object_ref_serialization() {
896 let obj = ObjectRef {
897 object_id: Some("obj-001".to_string()),
898 mass_kg: Some(5.0),
899 ..Default::default()
900 };
901
902 let json = serde_json::to_string(&obj).unwrap();
903 let deserialized: ObjectRef = serde_json::from_str(&json).unwrap();
904 assert_eq!(deserialized.object_id, Some("obj-001".to_string()));
905 }
906
907 #[test]
908 fn test_pose_serialization() {
909 let pose = Pose::from_position_euler(1.0, 2.0, 3.0, 0.0, 0.0, 90.0);
910 let json = serde_json::to_string(&pose).unwrap();
911 let deserialized: Pose = serde_json::from_str(&json).unwrap();
912 assert!(deserialized.position.is_some());
913 assert!(deserialized.euler_deg.is_some());
914 }
915}