1use crate::collision_detection::CDEConfig;
2use crate::entities::Item;
3use crate::entities::{Container, InferiorQualityZone, N_QUALITIES};
4use crate::geometry::geo_enums::RotationRange;
5use crate::geometry::primitives::{Point, Rect, SPolygon};
6use crate::geometry::shape_modification::{ShapeModifyConfig, ShapeModifyMode};
7use crate::geometry::{AllowedOrientations, OriginalShape};
8use crate::geometry::{DTransformation, Transformation};
9use crate::io::ext_repr::{ExtContainer, ExtItem, ExtRotation, ExtSPolygon, ExtShape};
10use anyhow::{Result, bail, ensure};
11use float_cmp::approx_eq;
12use itertools::Itertools;
13use log::debug;
14use std::sync::Arc;
15
16#[derive(Clone, Debug, Copy)]
19pub struct Importer {
20 pub shape_modify_config: ShapeModifyConfig,
21 pub cde_config: CDEConfig,
22}
23
24impl Importer {
25 #[must_use]
31 pub fn new(
32 cde_config: CDEConfig,
33 simplify_tolerance: Option<f32>,
34 narrow_concavity_cutoff: Option<(f32, f32)>,
35 ) -> Importer {
36 Importer {
37 shape_modify_config: ShapeModifyConfig {
38 offset: None,
39 simplify_tolerance,
40 narrow_concavity_cutoff,
41 },
42 cde_config,
43 }
44 }
45
46 pub fn with_min_item_separation(mut self, separation: f32) -> Result<Self> {
49 ensure!(
50 separation.is_finite() && separation >= 0.0,
51 "min_item_separation must be finite and nonnegative"
52 );
53 self.shape_modify_config.offset = (separation > 0.0).then_some(separation / 2.0);
54 Ok(self)
55 }
56
57 pub fn import_item(&self, ext_item: &ExtItem, idx: usize) -> Result<Arc<Item>> {
59 debug!("[IMPORT] starting item {:?}", ext_item.id);
60
61 let original_shape = {
62 let shape = match &ext_item.shape {
63 ExtShape::Rectangle {
64 x_min,
65 y_min,
66 width,
67 height,
68 } => {
69 let rect = Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)?;
70 SPolygon::from(rect)
71 }
72 ExtShape::SimplePolygon(esp) => import_simple_polygon(esp)?,
73 ExtShape::Polygon(ep) => {
74 ensure!(
75 ep.inner.is_empty(),
76 "item {} has unsupported holes",
77 ext_item.id
78 );
79 import_simple_polygon(&ep.outer)?
80 }
81 ExtShape::MultiPolygon(_) => {
82 bail!("No support for multipolygons yet")
83 }
84 };
85 OriginalShape {
86 pre_transform: centering_transformation(&shape),
87 shape,
88 modify_mode: ShapeModifyMode::Inflate,
89 modify_config: self.shape_modify_config,
90 }
91 };
92
93 let base_quality = ext_item.min_quality;
94
95 let rotations = import_rotation(&ext_item.orientation.rotation)?;
96 let axes = normalized_radians(&ext_item.orientation.reflection_axes, 180.0)?;
97 let allowed_orientations = AllowedOrientations::new(rotations, axes)?;
98
99 Item::new(
100 idx,
101 ext_item.id,
102 original_shape,
103 allowed_orientations,
104 base_quality,
105 self.cde_config.item_surrogate_config,
106 )
107 .map(Arc::new)
108 }
109
110 pub fn import_container(&self, ext_cont: &ExtContainer) -> Result<Container> {
113 ensure!(
114 ext_cont.zones.iter().all(|zone| zone.quality < N_QUALITIES),
115 "Quality zones must have quality below {N_QUALITIES}"
116 );
117
118 let original_outer = {
119 let outer = match &ext_cont.shape {
120 ExtShape::Rectangle {
121 x_min,
122 y_min,
123 width,
124 height,
125 } => Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)?.into(),
126 ExtShape::SimplePolygon(esp) => import_simple_polygon(esp)?,
127 ExtShape::Polygon(ep) => import_simple_polygon(&ep.outer)?,
128 ExtShape::MultiPolygon(_) => {
129 bail!("No support for multipolygon shapes yet")
130 }
131 };
132 OriginalShape {
133 shape: outer,
134 pre_transform: DTransformation::empty(),
135 modify_mode: ShapeModifyMode::Deflate,
136 modify_config: self.shape_modify_config,
137 }
138 };
139
140 let holes = match &ext_cont.shape {
141 ExtShape::SimplePolygon(_) | ExtShape::Rectangle { .. } => vec![],
142 ExtShape::Polygon(jp) => {
143 let json_holes = &jp.inner;
144 json_holes
145 .iter()
146 .map(import_simple_polygon)
147 .collect::<Result<Vec<SPolygon>>>()?
148 }
149 ExtShape::MultiPolygon(_) => {
150 bail!("No support for multipolygon shapes yet")
151 }
152 };
153
154 let mut shapes_inferior_qzones = (0..N_QUALITIES)
155 .map(|q| {
156 ext_cont
157 .zones
158 .iter()
159 .filter(|zone| zone.quality == q)
160 .map(|zone| match &zone.shape {
161 ExtShape::Rectangle {
162 x_min,
163 y_min,
164 width,
165 height,
166 } => Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)
167 .map(Into::into),
168 ExtShape::SimplePolygon(esp) => import_simple_polygon(esp),
169 ExtShape::Polygon(_) => {
170 bail!("Quality zones require rectangles or simple polygons")
171 }
172 ExtShape::MultiPolygon(_) => {
173 bail!("Quality zones do not support multipolygons")
174 }
175 })
176 .collect::<Result<Vec<SPolygon>>>()
177 })
178 .collect::<Result<Vec<Vec<SPolygon>>>>()?;
179
180 shapes_inferior_qzones[0].extend(holes);
182
183 let quality_zones = shapes_inferior_qzones
185 .into_iter()
186 .enumerate()
187 .map(|(q, zone_shapes)| {
188 let original_shapes = zone_shapes
189 .into_iter()
190 .map(|s| OriginalShape {
191 shape: s,
192 pre_transform: DTransformation::empty(),
193 modify_mode: ShapeModifyMode::Inflate,
194 modify_config: self.shape_modify_config,
195 })
196 .collect_vec();
197 InferiorQualityZone::new(q, original_shapes, self.cde_config.item_surrogate_config)
198 })
199 .collect::<Result<Vec<InferiorQualityZone>>>()?;
200
201 Container::new(original_outer, quality_zones, self.cde_config)
202 }
203}
204
205#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
206fn import_rotation(rotation: &ExtRotation) -> Result<RotationRange> {
207 const MAX_ANGLES: usize = 65_536;
208 let mut angles = match rotation {
209 ExtRotation::Continuous {} => return Ok(RotationRange::Continuous),
210 ExtRotation::Discrete { angles } => {
211 ensure!(
212 !angles.is_empty() && angles.len() <= MAX_ANGLES,
213 "discrete rotation requires 1..=65536 angles"
214 );
215 ensure!(
216 angles.iter().all(|angle| angle.is_finite()),
217 "rotation angles must be finite"
218 );
219 angles
220 .iter()
221 .map(|angle| angle.rem_euclid(360.0) % 360.0)
222 .collect::<Vec<_>>()
223 }
224 ExtRotation::Stepped { step } => {
225 ensure!(
226 step.is_finite() && *step > 0.0 && *step <= 360.0,
227 "rotation step must be finite and in (0, 360]"
228 );
229 let step = f64::from(*step);
230 let count = (360.0 / step).round();
231 ensure!(
232 count <= 65_536.0
233 && (count * step - 360.0).abs() <= 360.0 * f64::from(f32::EPSILON),
234 "rotation step must divide 360 into at most 65536 angles"
235 );
236 (0..count as u32)
237 .map(|i| (f64::from(i) * 360.0 / count) as f32)
238 .collect()
239 }
240 };
241 angles.sort_by(f32::total_cmp);
242 angles.dedup();
243 if angles == [0.0] {
244 Ok(RotationRange::None)
245 } else {
246 Ok(RotationRange::Discrete(
247 angles.into_iter().map(f32::to_radians).collect(),
248 ))
249 }
250}
251
252fn normalized_radians(angles: &[f32], period: f32) -> Result<Vec<f32>> {
254 ensure!(
255 angles.len() <= AllowedOrientations::MAX_ANGLES,
256 "orientation angle list exceeds the count limit"
257 );
258 ensure!(
259 angles.iter().all(|a| a.is_finite()),
260 "orientation angles must be finite"
261 );
262 Ok(angles
263 .iter()
264 .map(|a| (a.rem_euclid(period) % period).to_radians())
265 .collect())
266}
267
268pub fn import_simple_polygon(sp: &ExtSPolygon) -> Result<SPolygon> {
269 let mut points = sp.0.iter().map(|(x, y)| Point(*x, *y)).collect_vec();
270 if points.len() > 1 && points[0] == points[points.len() - 1] {
272 points.pop();
273 }
274 eliminate_degenerate_vertices(&mut points);
276 if points.len() != points.iter().unique().count() {
278 bail!("Simple polygon has non-consecutive duplicate vertices");
279 }
280 SPolygon::new(points)
281}
282
283#[must_use]
285pub fn centering_transformation(shape: &SPolygon) -> DTransformation {
286 let Point(cx, cy) = shape.centroid();
287 DTransformation::new(0.0, (-cx, -cy))
288}
289
290#[must_use]
295pub fn ext_to_int_transformation(
296 ext_transf: &DTransformation,
297 pre_transf: &DTransformation,
298) -> DTransformation {
299 Transformation::empty()
303 .transform(&pre_transf.compose().inverse())
304 .transform_from_decomposed(ext_transf)
305 .decompose()
306}
307
308fn eliminate_degenerate_vertices(points: &mut Vec<Point>) {
309 let mut indices_to_remove = vec![];
310 let n_points = points.len();
311 for i in 0..n_points {
312 let j = (i + 1) % n_points;
313 let p_i = points[i];
314 let p_j = points[j];
315 if approx_eq!(f32, p_i.0, p_j.0) && approx_eq!(f32, p_i.1, p_j.1) {
316 indices_to_remove.push(i);
318 }
319 }
320 indices_to_remove.sort_unstable_by(|a, b| b.cmp(a));
322 for index in indices_to_remove {
323 if index < points.len() {
324 let j = (index + 1) % points.len();
325 debug!(
326 "[IMPORT] degenerate vertex eliminated (idx: {}, {:?}, {:?})",
327 index, points[index], points[j]
328 );
329 points.remove(index);
330 }
331 }
332}