u_nesting_core/
placement.rs

1//! Placement representation for positioned geometries.
2
3use crate::geometry::GeometryId;
4use crate::transform::{Transform2D, Transform3D};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// Represents the placement of a geometry within a boundary.
10#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct Placement<S> {
13    /// The ID of the placed geometry.
14    pub geometry_id: GeometryId,
15
16    /// Instance index (0-based) when multiple copies exist.
17    pub instance: usize,
18
19    /// Position coordinates (x, y for 2D; x, y, z for 3D).
20    pub position: Vec<S>,
21
22    /// Rotation angle(s) in radians.
23    /// - 2D: single angle `[θ]`
24    /// - 3D: Euler angles `[rx, ry, rz]` or quaternion components
25    pub rotation: Vec<S>,
26
27    /// Index of the boundary this geometry is placed in (for multi-bin scenarios).
28    pub boundary_index: usize,
29
30    /// Whether the geometry was mirrored/flipped.
31    pub mirrored: bool,
32
33    /// The rotation option index used (if discrete rotations).
34    pub rotation_index: Option<usize>,
35}
36
37impl<S: Copy + Default> Placement<S> {
38    /// Creates a new 2D placement.
39    pub fn new_2d(geometry_id: GeometryId, instance: usize, x: S, y: S, angle: S) -> Self {
40        Self {
41            geometry_id,
42            instance,
43            position: vec![x, y],
44            rotation: vec![angle],
45            boundary_index: 0,
46            mirrored: false,
47            rotation_index: None,
48        }
49    }
50
51    /// Creates a new 3D placement.
52    pub fn new_3d(
53        geometry_id: GeometryId,
54        instance: usize,
55        x: S,
56        y: S,
57        z: S,
58        rx: S,
59        ry: S,
60        rz: S,
61    ) -> Self {
62        Self {
63            geometry_id,
64            instance,
65            position: vec![x, y, z],
66            rotation: vec![rx, ry, rz],
67            boundary_index: 0,
68            mirrored: false,
69            rotation_index: None,
70        }
71    }
72
73    /// Sets the boundary index.
74    pub fn with_boundary(mut self, index: usize) -> Self {
75        self.boundary_index = index;
76        self
77    }
78
79    /// Sets the mirrored flag.
80    pub fn with_mirrored(mut self, mirrored: bool) -> Self {
81        self.mirrored = mirrored;
82        self
83    }
84
85    /// Sets the rotation index.
86    pub fn with_rotation_index(mut self, index: usize) -> Self {
87        self.rotation_index = Some(index);
88        self
89    }
90
91    /// Returns true if this is a 2D placement.
92    pub fn is_2d(&self) -> bool {
93        self.position.len() == 2
94    }
95
96    /// Returns true if this is a 3D placement.
97    pub fn is_3d(&self) -> bool {
98        self.position.len() == 3
99    }
100
101    /// Returns the x coordinate.
102    pub fn x(&self) -> S {
103        self.position.first().copied().unwrap_or_default()
104    }
105
106    /// Returns the y coordinate.
107    pub fn y(&self) -> S {
108        self.position.get(1).copied().unwrap_or_default()
109    }
110
111    /// Returns the z coordinate (for 3D placements).
112    pub fn z(&self) -> Option<S> {
113        self.position.get(2).copied()
114    }
115
116    /// Returns the rotation angle (for 2D placements).
117    pub fn angle(&self) -> S {
118        self.rotation.first().copied().unwrap_or_default()
119    }
120}
121
122impl<S: nalgebra::RealField + Copy + Default> Placement<S> {
123    /// Converts a 2D placement to a Transform2D.
124    pub fn to_transform_2d(&self) -> Transform2D<S> {
125        Transform2D::new(self.x(), self.y(), self.angle())
126    }
127
128    /// Creates a 2D placement from a Transform2D.
129    pub fn from_transform_2d(
130        geometry_id: GeometryId,
131        instance: usize,
132        transform: &Transform2D<S>,
133    ) -> Self {
134        Self::new_2d(
135            geometry_id,
136            instance,
137            transform.tx,
138            transform.ty,
139            transform.angle,
140        )
141    }
142
143    /// Converts a 3D placement to a Transform3D.
144    pub fn to_transform_3d(&self) -> Transform3D<S> {
145        let z = self.position.get(2).copied().unwrap_or(S::zero());
146        let rx = self.rotation.first().copied().unwrap_or(S::zero());
147        let ry = self.rotation.get(1).copied().unwrap_or(S::zero());
148        let rz = self.rotation.get(2).copied().unwrap_or(S::zero());
149        Transform3D::new(self.x(), self.y(), z, rx, ry, rz)
150    }
151
152    /// Creates a 3D placement from a Transform3D.
153    pub fn from_transform_3d(
154        geometry_id: GeometryId,
155        instance: usize,
156        transform: &Transform3D<S>,
157    ) -> Self {
158        Self::new_3d(
159            geometry_id,
160            instance,
161            transform.tx,
162            transform.ty,
163            transform.tz,
164            transform.rx,
165            transform.ry,
166            transform.rz,
167        )
168    }
169}
170
171/// Placement statistics for a set of placements.
172#[derive(Debug, Clone, Default)]
173#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
174pub struct PlacementStats {
175    /// Total number of placements.
176    pub count: usize,
177    /// Number of mirrored placements.
178    pub mirrored_count: usize,
179    /// Distribution of rotation indices used.
180    pub rotation_distribution: std::collections::HashMap<usize, usize>,
181    /// Distribution of placements per boundary.
182    pub boundary_distribution: std::collections::HashMap<usize, usize>,
183}
184
185impl PlacementStats {
186    /// Computes statistics from a set of placements.
187    pub fn from_placements<S>(placements: &[Placement<S>]) -> Self {
188        let mut stats = Self {
189            count: placements.len(),
190            ..Default::default()
191        };
192
193        for p in placements {
194            if p.mirrored {
195                stats.mirrored_count += 1;
196            }
197
198            if let Some(rot_idx) = p.rotation_index {
199                *stats.rotation_distribution.entry(rot_idx).or_insert(0) += 1;
200            }
201
202            *stats
203                .boundary_distribution
204                .entry(p.boundary_index)
205                .or_insert(0) += 1;
206        }
207
208        stats
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_placement_2d() {
218        let p = Placement::new_2d("test".to_string(), 0, 10.0, 20.0, 0.5);
219        assert!(p.is_2d());
220        assert!(!p.is_3d());
221        assert_eq!(p.x(), 10.0);
222        assert_eq!(p.y(), 20.0);
223        assert_eq!(p.angle(), 0.5);
224    }
225
226    #[test]
227    fn test_placement_3d() {
228        let p = Placement::new_3d("test".to_string(), 0, 10.0, 20.0, 30.0, 0.1, 0.2, 0.3);
229        assert!(p.is_3d());
230        assert!(!p.is_2d());
231        assert_eq!(p.z(), Some(30.0));
232    }
233
234    #[test]
235    fn test_transform_conversion() {
236        let p = Placement::new_2d("test".to_string(), 0, 10.0_f64, 20.0, 0.5);
237        let t = p.to_transform_2d();
238        assert_eq!(t.tx, 10.0);
239        assert_eq!(t.ty, 20.0);
240        assert_eq!(t.angle, 0.5);
241    }
242
243    #[test]
244    fn test_placement_stats() {
245        let placements = vec![
246            Placement::new_2d("a".to_string(), 0, 0.0, 0.0, 0.0).with_rotation_index(0),
247            Placement::new_2d("b".to_string(), 0, 0.0, 0.0, 0.0)
248                .with_rotation_index(1)
249                .with_mirrored(true),
250            Placement::new_2d("c".to_string(), 0, 0.0, 0.0, 0.0)
251                .with_rotation_index(0)
252                .with_boundary(1),
253        ];
254
255        let stats = PlacementStats::from_placements(&placements);
256        assert_eq!(stats.count, 3);
257        assert_eq!(stats.mirrored_count, 1);
258        assert_eq!(stats.rotation_distribution.get(&0), Some(&2));
259        assert_eq!(stats.rotation_distribution.get(&1), Some(&1));
260    }
261}