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 itertools::Itertools;
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    /// * `poly_simpl_tolerance` - See [`ShapeModifyConfig::simplify_tolerance`].
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::offset`].
28    pub fn new(
29        cde_config: CDEConfig,
30        poly_simpl_tolerance: Option<f32>,
31        min_item_separation: Option<f32>,
32    ) -> Importer {
33        Importer {
34            shape_modify_config: ShapeModifyConfig {
35                offset: min_item_separation.map(|f| f / 2.0),
36                simplify_tolerance: poly_simpl_tolerance,
37            },
38            cde_config,
39        }
40    }
41
42    pub fn import_item(&self, ext_item: &ExtItem) -> Result<Item> {
43        let original_shape = {
44            let shape = match &ext_item.shape {
45                ExtShape::Rectangle {
46                    x_min,
47                    y_min,
48                    width,
49                    height,
50                } => {
51                    let rect = Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)?;
52                    SPolygon::from(rect)
53                }
54                ExtShape::SimplePolygon(esp) => import_simple_polygon(esp)?,
55                ExtShape::Polygon(_) => {
56                    bail!("No support for polygons with holes yet")
57                }
58                ExtShape::MultiPolygon(_) => {
59                    bail!("No support for multipolygons yet")
60                }
61            };
62            OriginalShape {
63                pre_transform: centering_transformation(&shape),
64                shape,
65                modify_mode: ShapeModifyMode::Inflate,
66                modify_config: self.shape_modify_config,
67            }
68        };
69
70        let base_quality = ext_item.min_quality;
71
72        let allowed_orientations = match ext_item.allowed_orientations.as_ref() {
73            Some(a_o) => {
74                if a_o.is_empty() || (a_o.len() == 1 && a_o[0] == 0.0) {
75                    RotationRange::None
76                } else {
77                    RotationRange::Discrete(a_o.iter().map(|angle| angle.to_radians()).collect())
78                }
79            }
80            None => RotationRange::Continuous,
81        };
82
83        Item::new(
84            ext_item.id as usize,
85            original_shape,
86            allowed_orientations,
87            base_quality,
88            self.cde_config.item_surrogate_config,
89        )
90    }
91
92    pub fn import_container(&self, ext_cont: &ExtContainer) -> Result<Container> {
93        assert!(
94            ext_cont.zones.iter().all(|zone| zone.quality < N_QUALITIES),
95            "All quality zones must have lower quality than N_QUALITIES, set N_QUALITIES to a higher value if required"
96        );
97
98        let original_outer = {
99            let outer = match &ext_cont.shape {
100                ExtShape::Rectangle {
101                    x_min,
102                    y_min,
103                    width,
104                    height,
105                } => Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)?.into(),
106                ExtShape::SimplePolygon(esp) => import_simple_polygon(esp)?,
107                ExtShape::Polygon(ep) => import_simple_polygon(&ep.outer)?,
108                ExtShape::MultiPolygon(_) => {
109                    bail!("No support for multipolygon shapes yet")
110                }
111            };
112            OriginalShape {
113                shape: outer,
114                pre_transform: DTransformation::empty(),
115                modify_mode: ShapeModifyMode::Deflate,
116                modify_config: self.shape_modify_config,
117            }
118        };
119
120        let holes = match &ext_cont.shape {
121            ExtShape::SimplePolygon(_) | ExtShape::Rectangle { .. } => vec![],
122            ExtShape::Polygon(jp) => {
123                let json_holes = &jp.inner;
124                json_holes
125                    .iter()
126                    .map(import_simple_polygon)
127                    .collect::<Result<Vec<SPolygon>>>()?
128            }
129            ExtShape::MultiPolygon(_) => {
130                unimplemented!("No support for multipolygon shapes yet")
131            }
132        };
133
134        let mut shapes_inferior_qzones = (0..N_QUALITIES)
135            .map(|q| {
136                ext_cont
137                    .zones
138                    .iter()
139                    .filter(|zone| zone.quality == q)
140                    .map(|zone| match &zone.shape {
141                        ExtShape::Rectangle {
142                            x_min,
143                            y_min,
144                            width,
145                            height,
146                        } => Rect::try_new(*x_min, *y_min, x_min + width, y_min + height)
147                            .map(|r| r.into()),
148                        ExtShape::SimplePolygon(esp) => import_simple_polygon(esp),
149                        ExtShape::Polygon(_) => {
150                            unimplemented!("No support for polygon to simplepolygon conversion yet")
151                        }
152                        ExtShape::MultiPolygon(_) => {
153                            unimplemented!("No support for multipolygon shapes yet")
154                        }
155                    })
156                    .collect::<Result<Vec<SPolygon>>>()
157            })
158            .collect::<Result<Vec<Vec<SPolygon>>>>()?;
159
160        //merge the container holes with quality == 0
161        shapes_inferior_qzones[0].extend(holes);
162
163        //convert the shapes to inferior quality zones
164        let quality_zones = shapes_inferior_qzones
165            .into_iter()
166            .enumerate()
167            .map(|(q, zone_shapes)| {
168                let original_shapes = zone_shapes
169                    .into_iter()
170                    .map(|s| OriginalShape {
171                        shape: s,
172                        pre_transform: DTransformation::empty(),
173                        modify_mode: ShapeModifyMode::Inflate,
174                        modify_config: self.shape_modify_config,
175                    })
176                    .collect_vec();
177                InferiorQualityZone::new(q, original_shapes)
178            })
179            .collect::<Result<Vec<InferiorQualityZone>>>()?;
180
181        Container::new(
182            ext_cont.id as usize,
183            original_outer,
184            quality_zones,
185            self.cde_config,
186        )
187    }
188}
189
190fn import_simple_polygon(sp: &ExtSPolygon) -> Result<SPolygon> {
191    let mut points = sp.0.iter().map(|(x, y)| Point(*x, *y)).collect_vec();
192    //Strip the last vertex if it is the same as the first one
193    if points.len() > 1 && points[0] == points[points.len() - 1] {
194        points.pop();
195    }
196    //Remove duplicates that are consecutive (e.g. [1, 2, 2, 3] -> [1, 2, 3])
197    points.dedup();
198    //Bail if there are any non-consecutive duplicates.
199    if points.len() != points.iter().unique().count() {
200        bail!("Simple polygon has non-consecutive duplicate vertices");
201    }
202    SPolygon::new(points)
203}
204
205/// Returns a transformation that translates the shape's centroid to the origin.
206pub fn centering_transformation(shape: &SPolygon) -> DTransformation {
207    let Point(cx, cy) = shape.centroid();
208    DTransformation::new(0.0, (-cx, -cy))
209}
210
211/// Converts an external transformation (applicable to the original shapes) to an internal transformation (used within `jagua-rs`).
212///
213/// * `ext_transf` - The external transformation.
214/// * `pre_transf` - The transformation that was applied to the original shape to derive the internal representation.
215pub fn ext_to_int_transformation(
216    ext_transf: &DTransformation,
217    pre_transf: &DTransformation,
218) -> DTransformation {
219    //1. undo pre-transform
220    //2. do the absolute transformation
221
222    Transformation::empty()
223        .transform(&pre_transf.compose().inverse())
224        .transform_from_decomposed(ext_transf)
225        .decompose()
226}