Skip to main content

jagua_rs/io/
import.rs

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/// Converts external representations of items and containers into internal ones.
17/// Direct configuration changes must satisfy [`ShapeModifyConfig`]'s documented ranges.
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    /// * `narrow_concavity_cutoff` - Optional definition for closing narrow concavities. If enabled, the shapes are modified to close "narrow" concavities. See [`ShapeModifyConfig`].
30    #[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    /// Set the minimum distance between items and other hazards.
47    /// Items are inflated and containers deflated by half this finite, nonnegative distance.
48    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    /// Import geometry with a caller-assigned internal index, independent of the external ID.
58    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    /// Imports container geometry and quality zones.
111    /// Returns an error for invalid geometry, unsupported shapes or out-of-range qualities.
112    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        //merge the container holes with quality == 0
181        shapes_inferior_qzones[0].extend(holes);
182
183        //convert the shapes to inferior quality zones
184        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
252// Normalize in the input unit before conversion so equivalent degree values stay identical.
253fn 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    //Strip the last vertex if it is the same as the first one
271    if points.len() > 1 && points[0] == points[points.len() - 1] {
272        points.pop();
273    }
274    //Remove duplicates that are consecutive (e.g. [1, 2, 2, 3] -> [1, 2, 3])
275    eliminate_degenerate_vertices(&mut points);
276    //Bail if there are any non-consecutive duplicates.
277    if points.len() != points.iter().unique().count() {
278        bail!("Simple polygon has non-consecutive duplicate vertices");
279    }
280    SPolygon::new(points)
281}
282
283/// Returns a transformation that translates the shape's centroid to the origin.
284#[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/// Converts an external transformation (applicable to the original shapes) to an internal transformation (used within `jagua-rs`).
291///
292/// * `ext_transf` - The external transformation.
293/// * `pre_transf` - The transformation that was applied to the original shape to derive the internal representation.
294#[must_use]
295pub fn ext_to_int_transformation(
296    ext_transf: &DTransformation,
297    pre_transf: &DTransformation,
298) -> DTransformation {
299    //1. undo pre-transform
300    //2. do the absolute transformation
301
302    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            //points are equal, mark for removal
317            indices_to_remove.push(i);
318        }
319    }
320    //remove points in reverse order to avoid shifting indices
321    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}