jagua_rs/io/
import.rs

1use crate::collision_detection::CDEConfig;
2use crate::entities::Item;
3use crate::entities::{Container, InferiorQualityZone, N_QUALITIES};
4use crate::geometry::OriginalShape;
5use crate::geometry::geo_enums::RotationRange;
6use crate::geometry::primitives::{Point, Rect, SPolygon};
7use crate::geometry::shape_modification::{ShapeModifyConfig, ShapeModifyMode};
8use crate::geometry::{DTransformation, Transformation};
9use crate::io::ext_repr::{ExtContainer, ExtItem, ExtSPolygon, ExtShape};
10use anyhow::{Result, bail};
11use float_cmp::approx_eq;
12use itertools::Itertools;
13use log::{debug, warn};
14
15/// Converts external representations of items and containers into internal ones.
16#[derive(Clone, Debug, Copy)]
17pub struct Importer {
18    pub shape_modify_config: ShapeModifyConfig,
19    pub cde_config: CDEConfig,
20}
21
22impl Importer {
23    /// Creates a new instance with the given configuration.
24    ///
25    /// * `cde_config` - Configuration for the CDE (Collision Detection Engine).
26    /// * `simplify_tolerance` - See [`ShapeModifyConfig`].
27    /// * `min_item_separation` - Optional minimum separation distance between items and any other hazard. If enabled, every hazard is inflated/deflated by half this value. See [`ShapeModifyConfig`].
28    /// * `narrow_concavity_cutoff` - Optional definition for closing narrow concavities. If enabled, the shapes are modified to close "narrow" concavities. See [`ShapeModifyConfig`].
29    #[must_use]
30    pub fn new(
31        cde_config: CDEConfig,
32        simplify_tolerance: Option<f32>,
33        min_item_separation: Option<f32>,
34        narrow_concavity_cutoff: Option<(f32, f32)>,
35    ) -> Importer {
36        Importer {
37            shape_modify_config: ShapeModifyConfig {
38                offset: min_item_separation.map(|f| f / 2.0),
39                simplify_tolerance,
40                narrow_concavity_cutoff,
41            },
42            cde_config,
43        }
44    }
45
46    pub fn import_item(&self, ext_item: &ExtItem) -> Result<Item> {
47        debug!("[IMPORT] starting item {:?}", ext_item.id);
48
49        let original_shape = {
50            let shape = match &ext_item.shape {
51                ExtShape::Rectangle {
52                    x_min,
53                    y_min,
54                    width,
55                    height,
56                } => {
57                    let rect = Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)?;
58                    SPolygon::from(rect)
59                }
60                ExtShape::SimplePolygon(esp) => import_simple_polygon(esp)?,
61                ExtShape::Polygon(ep) => {
62                    warn!("No native support for polygons yet, ignoring the holes");
63                    import_simple_polygon(&ep.outer)?
64                }
65                ExtShape::MultiPolygon(_) => {
66                    bail!("No support for multipolygons yet")
67                }
68            };
69            OriginalShape {
70                pre_transform: centering_transformation(&shape),
71                shape,
72                modify_mode: ShapeModifyMode::Inflate,
73                modify_config: self.shape_modify_config,
74            }
75        };
76
77        let base_quality = ext_item.min_quality;
78
79        let allowed_orientations = match ext_item.allowed_orientations.as_ref() {
80            Some(a_o) => {
81                if a_o.is_empty() || (a_o.len() == 1 && a_o[0] == 0.0) {
82                    RotationRange::None
83                } else {
84                    RotationRange::Discrete(a_o.iter().map(|angle| angle.to_radians()).collect())
85                }
86            }
87            None => RotationRange::Continuous,
88        };
89
90        Item::new(
91            usize::try_from(ext_item.id).unwrap(),
92            original_shape,
93            allowed_orientations,
94            base_quality,
95            self.cde_config.item_surrogate_config,
96        )
97    }
98
99    pub fn import_container(&self, ext_cont: &ExtContainer) -> Result<Container> {
100        assert!(
101            ext_cont.zones.iter().all(|zone| zone.quality < N_QUALITIES),
102            "All quality zones must have lower quality than N_QUALITIES, set N_QUALITIES to a higher value if required"
103        );
104
105        let original_outer = {
106            let outer = match &ext_cont.shape {
107                ExtShape::Rectangle {
108                    x_min,
109                    y_min,
110                    width,
111                    height,
112                } => Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)?.into(),
113                ExtShape::SimplePolygon(esp) => import_simple_polygon(esp)?,
114                ExtShape::Polygon(ep) => import_simple_polygon(&ep.outer)?,
115                ExtShape::MultiPolygon(_) => {
116                    bail!("No support for multipolygon shapes yet")
117                }
118            };
119            OriginalShape {
120                shape: outer,
121                pre_transform: DTransformation::empty(),
122                modify_mode: ShapeModifyMode::Deflate,
123                modify_config: self.shape_modify_config,
124            }
125        };
126
127        let holes = match &ext_cont.shape {
128            ExtShape::SimplePolygon(_) | ExtShape::Rectangle { .. } => vec![],
129            ExtShape::Polygon(jp) => {
130                let json_holes = &jp.inner;
131                json_holes
132                    .iter()
133                    .map(import_simple_polygon)
134                    .collect::<Result<Vec<SPolygon>>>()?
135            }
136            ExtShape::MultiPolygon(_) => {
137                unimplemented!("No support for multipolygon shapes yet")
138            }
139        };
140
141        let mut shapes_inferior_qzones = (0..N_QUALITIES)
142            .map(|q| {
143                ext_cont
144                    .zones
145                    .iter()
146                    .filter(|zone| zone.quality == q)
147                    .map(|zone| match &zone.shape {
148                        ExtShape::Rectangle {
149                            x_min,
150                            y_min,
151                            width,
152                            height,
153                        } => Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)
154                            .map(Into::into),
155                        ExtShape::SimplePolygon(esp) => import_simple_polygon(esp),
156                        ExtShape::Polygon(_) => {
157                            unimplemented!("No support for polygon to simplepolygon conversion yet")
158                        }
159                        ExtShape::MultiPolygon(_) => {
160                            unimplemented!("No support for multipolygon shapes yet")
161                        }
162                    })
163                    .collect::<Result<Vec<SPolygon>>>()
164            })
165            .collect::<Result<Vec<Vec<SPolygon>>>>()?;
166
167        //merge the container holes with quality == 0
168        shapes_inferior_qzones[0].extend(holes);
169
170        //convert the shapes to inferior quality zones
171        let quality_zones = shapes_inferior_qzones
172            .into_iter()
173            .enumerate()
174            .map(|(q, zone_shapes)| {
175                let original_shapes = zone_shapes
176                    .into_iter()
177                    .map(|s| OriginalShape {
178                        shape: s,
179                        pre_transform: DTransformation::empty(),
180                        modify_mode: ShapeModifyMode::Inflate,
181                        modify_config: self.shape_modify_config,
182                    })
183                    .collect_vec();
184                InferiorQualityZone::new(q, original_shapes)
185            })
186            .collect::<Result<Vec<InferiorQualityZone>>>()?;
187
188        Container::new(
189            usize::try_from(ext_cont.id).unwrap(),
190            original_outer,
191            quality_zones,
192            self.cde_config,
193        )
194    }
195}
196
197pub fn import_simple_polygon(sp: &ExtSPolygon) -> Result<SPolygon> {
198    let mut points = sp.0.iter().map(|(x, y)| Point(*x, *y)).collect_vec();
199    //Strip the last vertex if it is the same as the first one
200    if points.len() > 1 && points[0] == points[points.len() - 1] {
201        points.pop();
202    }
203    //Remove duplicates that are consecutive (e.g. [1, 2, 2, 3] -> [1, 2, 3])
204    eliminate_degenerate_vertices(&mut points);
205    //Bail if there are any non-consecutive duplicates.
206    if points.len() != points.iter().unique().count() {
207        bail!("Simple polygon has non-consecutive duplicate vertices");
208    }
209    SPolygon::new(points)
210}
211
212/// Returns a transformation that translates the shape's centroid to the origin.
213#[must_use]
214pub fn centering_transformation(shape: &SPolygon) -> DTransformation {
215    let Point(cx, cy) = shape.centroid();
216    DTransformation::new(0.0, (-cx, -cy))
217}
218
219/// Converts an external transformation (applicable to the original shapes) to an internal transformation (used within `jagua-rs`).
220///
221/// * `ext_transf` - The external transformation.
222/// * `pre_transf` - The transformation that was applied to the original shape to derive the internal representation.
223#[must_use]
224pub fn ext_to_int_transformation(
225    ext_transf: &DTransformation,
226    pre_transf: &DTransformation,
227) -> DTransformation {
228    //1. undo pre-transform
229    //2. do the absolute transformation
230
231    Transformation::empty()
232        .transform(&pre_transf.compose().inverse())
233        .transform_from_decomposed(ext_transf)
234        .decompose()
235}
236
237pub fn eliminate_degenerate_vertices(points: &mut Vec<Point>) {
238    let mut indices_to_remove = vec![];
239    let n_points = points.len();
240    for i in 0..n_points {
241        let j = (i + 1) % n_points;
242        let p_i = points[i];
243        let p_j = points[j];
244        if approx_eq!(f32, p_i.0, p_j.0) && approx_eq!(f32, p_i.1, p_j.1) {
245            //points are equal, mark for removal
246            indices_to_remove.push(i);
247        }
248    }
249    //remove points in reverse order to avoid shifting indices
250    indices_to_remove.sort_unstable_by(|a, b| b.cmp(a));
251    for index in indices_to_remove {
252        if index < points.len() {
253            let j = (index + 1) % points.len();
254            debug!(
255                "[IMPORT] degenerate vertex eliminated (idx: {}, {:?}, {:?})",
256                index, points[index], points[j]
257            );
258            points.remove(index);
259        }
260    }
261}