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 #[expect(
415 clippy::indexing_slicing,
416 reason = "i and j are bounded by n = self.vertices.len()"
417 )]
418 let (xi, yi) = match (self.vertices[i].x_m, self.vertices[i].y_m) {
419 (Some(x), Some(y)) => (x, y),
420 _ => continue,
421 };
422 #[expect(
423 clippy::indexing_slicing,
424 reason = "j is bounded by n = self.vertices.len()"
425 )]
426 let (xj, yj) = match (self.vertices[j].x_m, self.vertices[j].y_m) {
427 (Some(x), Some(y)) => (x, y),
428 _ => {
429 j = i;
430 continue;
431 }
432 };
433
434 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
435 inside = !inside;
436 }
437 j = i;
438 }
439
440 inside
441 }
442}
443
444#[derive(Debug, Clone, Default, Serialize, Deserialize)]
446pub struct TimeRange {
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub start: Option<chrono::DateTime<chrono::Utc>>,
450
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub end: Option<chrono::DateTime<chrono::Utc>>,
454}
455
456#[derive(Debug, Clone, Default, Serialize, Deserialize)]
458pub struct Metadata {
459 pub key: String,
461
462 pub value: String,
464}
465
466#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
468pub struct Confidence {
469 #[validate(range(min = 0.0, max = 1.0))]
471 pub score: f64,
472
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub source: Option<String>,
476}
477
478impl Confidence {
479 pub fn new(score: f64) -> Self {
481 Self {
482 score,
483 source: None,
484 }
485 }
486
487 pub fn with_source(score: f64, source: impl Into<String>) -> Self {
489 Self {
490 score,
491 source: Some(source.into()),
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_dimensions_volume() {
502 let dims = Dimensions::new(2.0, 3.0, 4.0);
503 assert_eq!(dims.volume_m3(), Some(24.0));
504 }
505
506 #[test]
507 fn test_position_distance() {
508 let p1 = Position3D::new(0.0, 0.0, 0.0);
509 let p2 = Position3D::new(3.0, 4.0, 0.0);
510 assert!((p1.distance_to(&p2).unwrap() - 5.0).abs() < 1e-10);
511 }
512
513 #[test]
514 fn test_quaternion_normalize() {
515 let q = Quaternion::new(2.0, 0.0, 0.0, 0.0);
516 let normalized = q.normalize();
517 assert!((normalized.w - 1.0).abs() < 1e-10);
518 }
519
520 #[test]
521 fn test_polygon_contains() {
522 let polygon = Polygon2D::new(vec![
523 Position2D::new(0.0, 0.0),
524 Position2D::new(4.0, 0.0),
525 Position2D::new(4.0, 4.0),
526 Position2D::new(0.0, 4.0),
527 ]);
528
529 assert!(polygon.contains(&Position2D::new(2.0, 2.0)));
530 assert!(!polygon.contains(&Position2D::new(5.0, 5.0)));
531 }
532
533 #[test]
538 fn test_object_ref_default() {
539 let obj = ObjectRef::default();
540 assert!(obj.object_id.is_none());
541 assert!(obj.item_id.is_none());
542 assert!(obj.object_type.is_none());
543 assert!(obj.object_class.is_none());
544 assert!(obj.tracking_id.is_none());
545 assert!(obj.dimensions_m.is_none());
546 assert!(obj.mass_kg.is_none());
547 assert!(obj.pose.is_none());
548 assert!(obj.frame_id.is_none());
549 }
550
551 #[test]
552 fn test_object_ref_full() {
553 let obj = ObjectRef {
554 object_id: Some("obj-001".to_string()),
555 item_id: Some("SKU12345".to_string()),
556 object_type: Some("package".to_string()),
557 object_class: Some("cardboard_box".to_string()),
558 tracking_id: Some("track-001".to_string()),
559 dimensions_m: Some(Dimensions::new(0.5, 0.3, 0.2)),
560 mass_kg: Some(2.5),
561 pose: Some(Pose::from_position(1.0, 2.0, 0.5)),
562 frame_id: Some("base_link".to_string()),
563 };
564
565 assert_eq!(obj.object_type, Some("package".to_string()));
566 assert_eq!(obj.mass_kg, Some(2.5));
567 }
568
569 #[test]
574 fn test_dimensions_new() {
575 let dims = Dimensions::new(1.0, 2.0, 3.0);
576 assert_eq!(dims.length_m, Some(1.0));
577 assert_eq!(dims.width_m, Some(2.0));
578 assert_eq!(dims.height_m, Some(3.0));
579 }
580
581 #[test]
582 fn test_dimensions_volume_partial() {
583 let dims = Dimensions {
584 length_m: Some(2.0),
585 width_m: None,
586 height_m: Some(3.0),
587 };
588 assert!(dims.volume_m3().is_none());
589 }
590
591 #[test]
592 fn test_dimensions_default() {
593 let dims = Dimensions::default();
594 assert!(dims.length_m.is_none());
595 assert!(dims.width_m.is_none());
596 assert!(dims.height_m.is_none());
597 assert!(dims.volume_m3().is_none());
598 }
599
600 #[test]
605 fn test_pose_from_position() {
606 let pose = Pose::from_position(1.0, 2.0, 3.0);
607 assert!(pose.position.is_some());
608 let pos = pose.position.unwrap();
609 assert_eq!(pos.x_m, Some(1.0));
610 assert_eq!(pos.y_m, Some(2.0));
611 assert_eq!(pos.z_m, Some(3.0));
612 }
613
614 #[test]
615 fn test_pose_from_position_euler() {
616 let pose = Pose::from_position_euler(1.0, 2.0, 3.0, 10.0, 20.0, 30.0);
617 assert!(pose.position.is_some());
618 assert!(pose.euler_deg.is_some());
619 let euler = pose.euler_deg.unwrap();
620 assert_eq!(euler.roll_deg, Some(10.0));
621 assert_eq!(euler.pitch_deg, Some(20.0));
622 assert_eq!(euler.yaw_deg, Some(30.0));
623 }
624
625 #[test]
626 fn test_pose_default() {
627 let pose = Pose::default();
628 assert!(pose.position.is_none());
629 assert!(pose.orientation.is_none());
630 assert!(pose.euler_deg.is_none());
631 }
632
633 #[test]
638 fn test_position_3d_new() {
639 let pos = Position3D::new(1.5, 2.5, 3.5);
640 assert_eq!(pos.x_m, Some(1.5));
641 assert_eq!(pos.y_m, Some(2.5));
642 assert_eq!(pos.z_m, Some(3.5));
643 }
644
645 #[test]
646 fn test_position_3d_distance_to_partial() {
647 let p1 = Position3D::new(0.0, 0.0, 0.0);
648 let p2 = Position3D::default();
649 assert!(p1.distance_to(&p2).is_none());
650 }
651
652 #[test]
653 fn test_position_3d_default() {
654 let pos = Position3D::default();
655 assert!(pos.x_m.is_none());
656 assert!(pos.y_m.is_none());
657 assert!(pos.z_m.is_none());
658 }
659
660 #[test]
665 fn test_position_2d_new() {
666 let pos = Position2D::new(10.0, 20.0);
667 assert_eq!(pos.x_m, Some(10.0));
668 assert_eq!(pos.y_m, Some(20.0));
669 }
670
671 #[test]
672 fn test_position_2d_default() {
673 let pos = Position2D::default();
674 assert!(pos.x_m.is_none());
675 assert!(pos.y_m.is_none());
676 }
677
678 #[test]
683 fn test_quaternion_new() {
684 let q = Quaternion::new(1.0, 0.0, 0.0, 0.0);
685 assert!((q.w - 1.0).abs() < f64::EPSILON);
686 assert!((q.x - 0.0).abs() < f64::EPSILON);
687 assert!((q.y - 0.0).abs() < f64::EPSILON);
688 assert!((q.z - 0.0).abs() < f64::EPSILON);
689 }
690
691 #[test]
692 fn test_quaternion_identity() {
693 let q = Quaternion::identity();
694 assert!((q.w - 1.0).abs() < f64::EPSILON);
695 assert!((q.x - 0.0).abs() < f64::EPSILON);
696 assert!((q.y - 0.0).abs() < f64::EPSILON);
697 assert!((q.z - 0.0).abs() < f64::EPSILON);
698 }
699
700 #[test]
701 fn test_quaternion_normalize_non_unit() {
702 let q = Quaternion::new(0.0, 3.0, 4.0, 0.0);
703 let normalized = q.normalize();
704 let mag_sq = normalized.w * normalized.w
706 + normalized.x * normalized.x
707 + normalized.y * normalized.y
708 + normalized.z * normalized.z;
709 assert!((mag_sq - 1.0).abs() < 1e-10);
710 }
711
712 #[test]
713 fn test_quaternion_default() {
714 let q = Quaternion::default();
715 assert!((q.w - 0.0).abs() < f64::EPSILON);
716 assert!((q.x - 0.0).abs() < f64::EPSILON);
717 assert!((q.y - 0.0).abs() < f64::EPSILON);
718 assert!((q.z - 0.0).abs() < f64::EPSILON);
719 }
720
721 #[test]
726 fn test_euler_angles_new() {
727 let euler = EulerAngles::new(10.0, 20.0, 30.0);
728 assert_eq!(euler.roll_deg, Some(10.0));
729 assert_eq!(euler.pitch_deg, Some(20.0));
730 assert_eq!(euler.yaw_deg, Some(30.0));
731 }
732
733 #[test]
734 fn test_euler_angles_default() {
735 let euler = EulerAngles::default();
736 assert!(euler.roll_deg.is_none());
737 assert!(euler.pitch_deg.is_none());
738 assert!(euler.yaw_deg.is_none());
739 }
740
741 #[test]
746 fn test_vector_3d_new() {
747 let v = Vector3D::new(1.0, 2.0, 3.0);
748 assert_eq!(v.x, Some(1.0));
749 assert_eq!(v.y, Some(2.0));
750 assert_eq!(v.z, Some(3.0));
751 }
752
753 #[test]
754 fn test_vector_3d_magnitude() {
755 let v = Vector3D::new(3.0, 4.0, 0.0);
756 assert!((v.magnitude().unwrap() - 5.0).abs() < 1e-10);
757 }
758
759 #[test]
760 fn test_vector_3d_magnitude_none() {
761 let v = Vector3D::default();
762 assert!(v.magnitude().is_none());
763 }
764
765 #[test]
766 fn test_vector_3d_default() {
767 let v = Vector3D::default();
768 assert!(v.x.is_none());
769 assert!(v.y.is_none());
770 assert!(v.z.is_none());
771 }
772
773 #[test]
778 fn test_bounding_box_default() {
779 let bb = BoundingBox::default();
780 assert!(bb.center.is_none());
781 assert!(bb.dimensions.is_none());
782 assert!(bb.orientation.is_none());
783 assert!(bb.axis_aligned.is_none());
784 }
785
786 #[test]
787 fn test_bounding_box_full() {
788 let bb = BoundingBox {
789 center: Some(Position3D::new(1.0, 2.0, 0.5)),
790 dimensions: Some(Dimensions::new(1.0, 0.5, 0.8)),
791 orientation: Some(Quaternion::identity()),
792 axis_aligned: Some(true),
793 };
794
795 assert!(bb.center.is_some());
796 assert!(bb.dimensions.is_some());
797 }
798
799 #[test]
804 fn test_polygon_2d_new() {
805 let vertices = vec![
806 Position2D::new(0.0, 0.0),
807 Position2D::new(1.0, 0.0),
808 Position2D::new(1.0, 1.0),
809 ];
810 let poly = Polygon2D::new(vertices);
811 assert_eq!(poly.vertices.len(), 3);
812 }
813
814 #[test]
815 fn test_polygon_2d_empty() {
816 let poly = Polygon2D::new(vec![]);
817 assert!(!poly.contains(&Position2D::new(0.0, 0.0)));
818 }
819
820 #[test]
821 fn test_polygon_2d_contains_partial_vertices() {
822 let poly = Polygon2D::new(vec![
823 Position2D::default(),
824 Position2D::new(1.0, 0.0),
825 Position2D::new(1.0, 1.0),
826 ]);
827 let _ = poly.contains(&Position2D::new(0.5, 0.5));
829 }
830
831 #[test]
832 fn test_polygon_2d_contains_point_no_coords() {
833 let poly = Polygon2D::new(vec![
834 Position2D::new(0.0, 0.0),
835 Position2D::new(1.0, 0.0),
836 Position2D::new(1.0, 1.0),
837 ]);
838 assert!(!poly.contains(&Position2D::default()));
839 }
840
841 #[test]
846 fn test_confidence_new() {
847 let conf = Confidence::new(0.95);
848 assert!((conf.score - 0.95).abs() < f64::EPSILON);
849 assert!(conf.source.is_none());
850 }
851
852 #[test]
853 fn test_confidence_with_source() {
854 let conf = Confidence::with_source(0.87, "model_v2");
855 assert!((conf.score - 0.87).abs() < f64::EPSILON);
856 assert_eq!(conf.source, Some("model_v2".to_string()));
857 }
858
859 #[test]
860 fn test_confidence_default() {
861 let conf = Confidence::default();
862 assert!((conf.score - 0.0).abs() < f64::EPSILON);
863 assert!(conf.source.is_none());
864 }
865
866 #[test]
871 fn test_time_range_default() {
872 let tr = TimeRange::default();
873 assert!(tr.start.is_none());
874 assert!(tr.end.is_none());
875 }
876
877 #[test]
882 fn test_metadata_default() {
883 let md = Metadata::default();
884 assert!(md.key.is_empty());
885 assert!(md.value.is_empty());
886 }
887
888 #[test]
889 fn test_metadata_full() {
890 let md = Metadata {
891 key: "version".to_string(),
892 value: "1.0.0".to_string(),
893 };
894 assert_eq!(md.key, "version");
895 assert_eq!(md.value, "1.0.0");
896 }
897
898 #[test]
903 fn test_object_ref_serialization() {
904 let obj = ObjectRef {
905 object_id: Some("obj-001".to_string()),
906 mass_kg: Some(5.0),
907 ..Default::default()
908 };
909
910 let json = serde_json::to_string(&obj).unwrap();
911 let deserialized: ObjectRef = serde_json::from_str(&json).unwrap();
912 assert_eq!(deserialized.object_id, Some("obj-001".to_string()));
913 }
914
915 #[test]
916 fn test_pose_serialization() {
917 let pose = Pose::from_position_euler(1.0, 2.0, 3.0, 0.0, 0.0, 90.0);
918 let json = serde_json::to_string(&pose).unwrap();
919 let deserialized: Pose = serde_json::from_str(&json).unwrap();
920 assert!(deserialized.position.is_some());
921 assert!(deserialized.euler_deg.is_some());
922 }
923}