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