1use crate::geometry::GeometryId;
4use crate::transform::{Transform2D, Transform3D};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct Placement<S> {
13 pub geometry_id: GeometryId,
15
16 pub instance: usize,
18
19 pub position: Vec<S>,
21
22 pub rotation: Vec<S>,
26
27 pub boundary_index: usize,
29
30 pub mirrored: bool,
32
33 pub rotation_index: Option<usize>,
35}
36
37impl<S: Copy + Default> Placement<S> {
38 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 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 pub fn with_boundary(mut self, index: usize) -> Self {
75 self.boundary_index = index;
76 self
77 }
78
79 pub fn with_mirrored(mut self, mirrored: bool) -> Self {
81 self.mirrored = mirrored;
82 self
83 }
84
85 pub fn with_rotation_index(mut self, index: usize) -> Self {
87 self.rotation_index = Some(index);
88 self
89 }
90
91 pub fn is_2d(&self) -> bool {
93 self.position.len() == 2
94 }
95
96 pub fn is_3d(&self) -> bool {
98 self.position.len() == 3
99 }
100
101 pub fn x(&self) -> S {
103 self.position.first().copied().unwrap_or_default()
104 }
105
106 pub fn y(&self) -> S {
108 self.position.get(1).copied().unwrap_or_default()
109 }
110
111 pub fn z(&self) -> Option<S> {
113 self.position.get(2).copied()
114 }
115
116 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 pub fn to_transform_2d(&self) -> Transform2D<S> {
125 Transform2D::new(self.x(), self.y(), self.angle())
126 }
127
128 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 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 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#[derive(Debug, Clone, Default)]
173#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
174pub struct PlacementStats {
175 pub count: usize,
177 pub mirrored_count: usize,
179 pub rotation_distribution: std::collections::HashMap<usize, usize>,
181 pub boundary_distribution: std::collections::HashMap<usize, usize>,
183}
184
185impl PlacementStats {
186 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}