phytrace_sdk/models/domains/
common.rs

1//! Common types shared across UDM domains.
2//!
3//! These types are used by multiple domains and represent shared concepts
4//! like object references, poses, dimensions, and coordinates.
5
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9/// Reference to an object in the world or payload.
10///
11/// Used for consistent object identification across domains (perception,
12/// manipulation, payload, etc.). Supports multiple ID schemes for different
13/// use cases.
14#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
15pub struct ObjectRef {
16    /// Session-stable object identifier (assigned by perception system)
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub object_id: Option<String>,
19
20    /// External business system ID (WMS, ERP, etc.)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub item_id: Option<String>,
23
24    /// Object type classification (package, pallet, tool, person, etc.)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub object_type: Option<String>,
27
28    /// Specific object class (e.g., "cardboard_box", "metal_pallet")
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub object_class: Option<String>,
31
32    /// Perception system tracking ID
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub tracking_id: Option<String>,
35
36    /// Object dimensions
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub dimensions_m: Option<Dimensions>,
39
40    /// Object mass in kilograms
41    #[serde(skip_serializing_if = "Option::is_none")]
42    #[validate(range(min = 0.0))]
43    pub mass_kg: Option<f64>,
44
45    /// Object pose (position and orientation)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub pose: Option<Pose>,
48
49    /// Reference frame for the pose
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub frame_id: Option<String>,
52}
53
54/// 3D dimensions of an object.
55#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
56pub struct Dimensions {
57    /// Length in meters (x-axis)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    #[validate(range(min = 0.0))]
60    pub length_m: Option<f64>,
61
62    /// Width in meters (y-axis)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[validate(range(min = 0.0))]
65    pub width_m: Option<f64>,
66
67    /// Height in meters (z-axis)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    #[validate(range(min = 0.0))]
70    pub height_m: Option<f64>,
71}
72
73impl Dimensions {
74    /// Create new dimensions with all values.
75    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    /// Calculate volume in cubic meters.
84    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/// 6-DOF pose (position + orientation).
93#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
94pub struct Pose {
95    /// Position component
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub position: Option<Position3D>,
98
99    /// Orientation as quaternion
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub orientation: Option<Quaternion>,
102
103    /// Orientation as Euler angles (alternative to quaternion)
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub euler_deg: Option<EulerAngles>,
106}
107
108impl Pose {
109    /// Create a pose from position only (identity orientation).
110    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    /// Create a pose from position and Euler angles.
119    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/// 3D position in Cartesian coordinates.
129#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
130pub struct Position3D {
131    /// X coordinate in meters
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub x_m: Option<f64>,
134
135    /// Y coordinate in meters
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub y_m: Option<f64>,
138
139    /// Z coordinate in meters
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub z_m: Option<f64>,
142}
143
144impl Position3D {
145    /// Create a new 3D position.
146    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    /// Calculate distance to another position.
155    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/// 2D position (for floor-plane navigation).
171#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
172pub struct Position2D {
173    /// X coordinate in meters
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub x_m: Option<f64>,
176
177    /// Y coordinate in meters
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub y_m: Option<f64>,
180}
181
182impl Position2D {
183    /// Create a new 2D position.
184    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/// Quaternion for orientation representation.
193#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
194pub struct Quaternion {
195    /// W component (scalar)
196    pub w: f64,
197    /// X component
198    pub x: f64,
199    /// Y component
200    pub y: f64,
201    /// Z component
202    pub z: f64,
203}
204
205impl Quaternion {
206    /// Create a new quaternion.
207    pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
208        Self { w, x, y, z }
209    }
210
211    /// Identity quaternion (no rotation).
212    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    /// Normalize the quaternion.
222    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/// Euler angles for orientation (roll, pitch, yaw).
238#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
239pub struct EulerAngles {
240    /// Roll angle in degrees (rotation about x-axis)
241    #[serde(skip_serializing_if = "Option::is_none")]
242    #[validate(range(min = -180.0, max = 180.0))]
243    pub roll_deg: Option<f64>,
244
245    /// Pitch angle in degrees (rotation about y-axis)
246    #[serde(skip_serializing_if = "Option::is_none")]
247    #[validate(range(min = -90.0, max = 90.0))]
248    pub pitch_deg: Option<f64>,
249
250    /// Yaw angle in degrees (rotation about z-axis)
251    #[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    /// Create new Euler angles.
258    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/// 3D vector for velocities, accelerations, etc.
268#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
269pub struct Vector3D {
270    /// X component
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub x: Option<f64>,
273
274    /// Y component
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub y: Option<f64>,
277
278    /// Z component
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub z: Option<f64>,
281}
282
283impl Vector3D {
284    /// Create a new 3D vector.
285    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    /// Calculate magnitude of the vector.
294    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/// Covariance matrix (6x6 for pose, 3x3 for position).
303#[derive(Debug, Clone, Default, Serialize, Deserialize)]
304pub struct CovarianceMatrix {
305    /// Row-major covariance values
306    pub values: Vec<f64>,
307
308    /// Matrix dimension (3 for 3x3, 6 for 6x6)
309    pub dimension: u8,
310}
311
312impl CovarianceMatrix {
313    /// Create a 3x3 covariance matrix for position uncertainty.
314    pub fn position_3x3(values: [f64; 9]) -> Self {
315        Self {
316            values: values.to_vec(),
317            dimension: 3,
318        }
319    }
320
321    /// Create a 6x6 covariance matrix for pose uncertainty.
322    pub fn pose_6x6(values: [f64; 36]) -> Self {
323        Self {
324            values: values.to_vec(),
325            dimension: 6,
326        }
327    }
328}
329
330/// Bounding box (axis-aligned or oriented).
331#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
332pub struct BoundingBox {
333    /// Center position
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub center: Option<Position3D>,
336
337    /// Dimensions
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub dimensions: Option<Dimensions>,
340
341    /// Orientation (for oriented bounding boxes)
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub orientation: Option<Quaternion>,
344
345    /// Whether this is axis-aligned
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub axis_aligned: Option<bool>,
348}
349
350/// 2D bounding box for image detections.
351#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
352pub struct BoundingBox2D {
353    /// Top-left X coordinate (pixels)
354    #[serde(skip_serializing_if = "Option::is_none")]
355    #[validate(range(min = 0.0))]
356    pub x: Option<f64>,
357
358    /// Top-left Y coordinate (pixels)
359    #[serde(skip_serializing_if = "Option::is_none")]
360    #[validate(range(min = 0.0))]
361    pub y: Option<f64>,
362
363    /// Width in pixels
364    #[serde(skip_serializing_if = "Option::is_none")]
365    #[validate(range(min = 0.0))]
366    pub width: Option<f64>,
367
368    /// Height in pixels
369    #[serde(skip_serializing_if = "Option::is_none")]
370    #[validate(range(min = 0.0))]
371    pub height: Option<f64>,
372}
373
374/// Polygon for 2D regions (zones, areas).
375#[derive(Debug, Clone, Default, Serialize, Deserialize)]
376pub struct Polygon2D {
377    /// List of vertices
378    pub vertices: Vec<Position2D>,
379
380    /// Whether the polygon is closed
381    #[serde(default = "default_true")]
382    pub closed: bool,
383}
384
385fn default_true() -> bool {
386    true
387}
388
389impl Polygon2D {
390    /// Create a new polygon from vertices.
391    pub fn new(vertices: Vec<Position2D>) -> Self {
392        Self {
393            vertices,
394            closed: true,
395        }
396    }
397
398    /// Check if a point is inside the polygon (2D ray casting).
399    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/// Time range.
437#[derive(Debug, Clone, Default, Serialize, Deserialize)]
438pub struct TimeRange {
439    /// Start time (ISO 8601)
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub start: Option<chrono::DateTime<chrono::Utc>>,
442
443    /// End time (ISO 8601)
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub end: Option<chrono::DateTime<chrono::Utc>>,
446}
447
448/// Key-value metadata pair.
449#[derive(Debug, Clone, Default, Serialize, Deserialize)]
450pub struct Metadata {
451    /// Key
452    pub key: String,
453
454    /// Value
455    pub value: String,
456}
457
458/// Confidence score with optional source.
459#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
460pub struct Confidence {
461    /// Confidence value (0.0 to 1.0)
462    #[validate(range(min = 0.0, max = 1.0))]
463    pub score: f64,
464
465    /// Source of the confidence (e.g., "model_v2", "sensor_fusion")
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub source: Option<String>,
468}
469
470impl Confidence {
471    /// Create a new confidence score.
472    pub fn new(score: f64) -> Self {
473        Self {
474            score,
475            source: None,
476        }
477    }
478
479    /// Create a confidence score with source.
480    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    // ==========================================================================
526    // ObjectRef Tests
527    // ==========================================================================
528
529    #[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    // ==========================================================================
562    // Dimensions Tests
563    // ==========================================================================
564
565    #[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    // ==========================================================================
593    // Pose Tests
594    // ==========================================================================
595
596    #[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    // ==========================================================================
626    // Position3D Tests
627    // ==========================================================================
628
629    #[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    // ==========================================================================
653    // Position2D Tests
654    // ==========================================================================
655
656    #[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    // ==========================================================================
671    // Quaternion Tests
672    // ==========================================================================
673
674    #[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        // Verify normalized quaternion has unit length (w^2 + x^2 + y^2 + z^2 = 1)
697        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    // ==========================================================================
714    // EulerAngles Tests
715    // ==========================================================================
716
717    #[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    // ==========================================================================
734    // Vector3D Tests
735    // ==========================================================================
736
737    #[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    // ==========================================================================
766    // BoundingBox Tests
767    // ==========================================================================
768
769    #[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    // ==========================================================================
792    // Polygon2D Tests
793    // ==========================================================================
794
795    #[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        // Should not crash with partial data
820        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    // ==========================================================================
834    // Confidence Tests
835    // ==========================================================================
836
837    #[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    // ==========================================================================
859    // TimeRange Tests
860    // ==========================================================================
861
862    #[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    // ==========================================================================
870    // Metadata Tests
871    // ==========================================================================
872
873    #[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    // ==========================================================================
891    // Serialization Roundtrip Tests
892    // ==========================================================================
893
894    #[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}