jagua_rs/geometry/
shape_modification.rs

1#[cfg(feature = "separation-distance")]
2use geo_offset::Offset;
3use itertools::Itertools;
4use log::{debug, info};
5use ordered_float::NotNan;
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8
9use crate::geometry::geo_traits::CollidesWith;
10use crate::geometry::primitives::Edge;
11use crate::geometry::primitives::Point;
12use crate::geometry::primitives::SPolygon;
13
14use anyhow::Result;
15
16/// Whether to strictly inflate or deflate when making any modifications to shape.
17/// Depends on the [`position`](crate::collision_detection::hazards::HazardEntity::scope) of the [`HazardEntity`](crate::collision_detection::hazards::HazardEntity) that the shape represents.
18#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum ShapeModifyMode {
20    /// Modify the shape to be strictly larger than the original (superset).
21    Inflate,
22    /// Modify the shape to be strictly smaller than the original (subset).
23    Deflate,
24}
25
26#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)]
27pub struct ShapeModifyConfig {
28    /// Maximum deviation of the simplified polygon with respect to the original polygon area as a ratio.
29    /// If undefined, no simplification is performed.
30    /// See [`simplify_shape`]
31    pub simplify_tolerance: Option<f32>,
32    /// Offset by which to inflate or deflate the polygon.
33    /// If undefined, no offset is applied.
34    /// See [`offset_shape`]
35    pub offset: Option<f32>,
36}
37
38/// Simplifies a [`SPolygon`] by reducing the number of edges.
39///
40/// The simplified shape will either be a subset or a superset of the original shape, depending on the [`ShapeModifyMode`].
41/// The procedure sequentially eliminates edges until either the change in area (ratio)
42/// exceeds `max_area_delta` or the number of edges < 4.
43pub fn simplify_shape(
44    shape: &SPolygon,
45    mode: ShapeModifyMode,
46    max_area_change_ratio: f32,
47) -> SPolygon {
48    let original_area = shape.area;
49
50    let mut ref_points = shape.vertices.clone();
51
52    for _ in 0..shape.n_vertices() {
53        let n_points = ref_points.len() as isize;
54        if n_points < 4 {
55            //can't simplify further
56            break;
57        }
58
59        let mut corners = (0..n_points)
60            .map(|i| {
61                let i_prev = (i - 1).rem_euclid(n_points);
62                let i_next = (i + 1).rem_euclid(n_points);
63                Corner(i_prev as usize, i as usize, i_next as usize)
64            })
65            .collect_vec();
66
67        if mode == ShapeModifyMode::Deflate {
68            //default mode is to inflate, so we need to reverse the order of the corners and flip the corners for deflate mode
69            //reverse the order of the corners
70            corners.reverse();
71            //reverse each corner
72            corners.iter_mut().for_each(|c| c.flip());
73        }
74
75        let mut candidates = vec![];
76
77        let mut prev_corner = corners.last().expect("corners is empty");
78        let mut prev_corner_type = CornerType::from(prev_corner.to_points(&ref_points));
79
80        //Go over all corners and generate candidates
81        for corner in corners.iter() {
82            let corner_type = CornerType::from(corner.to_points(&ref_points));
83
84            //Generate a removal candidate (or not)
85            match (&corner_type, &prev_corner_type) {
86                (CornerType::Concave, _) => candidates.push(Candidate::Concave(*corner)),
87                (CornerType::Collinear, _) => candidates.push(Candidate::Collinear(*corner)),
88                (CornerType::Convex, CornerType::Convex) => {
89                    candidates.push(Candidate::ConvexConvex(*prev_corner, *corner))
90                }
91                (_, _) => {}
92            };
93            (prev_corner, prev_corner_type) = (corner, corner_type);
94        }
95
96        //search the candidate with the smallest change in area that is valid
97        let best_candidate = candidates
98            .iter()
99            .sorted_by_cached_key(|c| {
100                calculate_area_delta(&ref_points, c)
101                    .unwrap_or_else(|_| NotNan::new(f32::INFINITY).expect("area delta is NaN"))
102            })
103            .find(|c| candidate_is_valid(&ref_points, c));
104
105        //if it is within the area change constraints, execute the candidate
106        if let Some(best_candidate) = best_candidate {
107            let new_shape = execute_candidate(&ref_points, best_candidate);
108            let new_shape_area = SPolygon::calculate_area(&new_shape);
109            let area_delta = (new_shape_area - original_area).abs() / original_area;
110            if area_delta <= max_area_change_ratio {
111                debug!(
112                    "Simplified {:?} causing {:.2}% area change",
113                    best_candidate,
114                    area_delta * 100.0
115                );
116                ref_points = new_shape;
117            } else {
118                break; //area change too significant
119            }
120        } else {
121            break; //no candidate found
122        }
123    }
124
125    //Convert it back to a simple polygon
126    let simpl_shape = SPolygon::new(ref_points).unwrap();
127
128    if simpl_shape.n_vertices() < shape.n_vertices() {
129        info!(
130            "[PS] simplified from {} to {} edges with {:.3}% area difference",
131            shape.n_vertices(),
132            simpl_shape.n_vertices(),
133            (simpl_shape.area - shape.area) / shape.area * 100.0
134        );
135    } else {
136        info!("[PS] no simplification possible within area change constraints");
137    }
138
139    simpl_shape
140}
141
142fn calculate_area_delta(
143    shape: &[Point],
144    candidate: &Candidate,
145) -> Result<NotNan<f32>, InvalidCandidate> {
146    //calculate the difference in area of the shape if the candidate were to be executed
147    let area = match candidate {
148        Candidate::Collinear(_) => 0.0,
149        Candidate::Concave(c) => {
150            //Triangle formed by i_prev, i and i_next will correspond to the change area
151            let Point(x0, y0) = shape[c.0];
152            let Point(x1, y1) = shape[c.1];
153            let Point(x2, y2) = shape[c.2];
154
155            let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
156
157            area.abs()
158        }
159        Candidate::ConvexConvex(c1, c2) => {
160            let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))?;
161
162            //the triangle formed by corner c1, c2, and replacing vertex will correspond to the change in area
163            let Point(x0, y0) = shape[c1.1];
164            let Point(x1, y1) = replacing_vertex;
165            let Point(x2, y2) = shape[c2.1];
166
167            let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
168
169            area.abs()
170        }
171    };
172    Ok(NotNan::new(area).expect("area is NaN"))
173}
174
175fn candidate_is_valid(shape: &[Point], candidate: &Candidate) -> bool {
176    //ensure the removal/replacement does not create any self intersections
177    match candidate {
178        Candidate::Collinear(_) => true,
179        Candidate::Concave(c) => {
180            let new_edge = Edge::try_new(shape[c.0], shape[c.2]).unwrap();
181            let affected_points = [shape[c.0], shape[c.1], shape[c.2]];
182
183            //check for self-intersections
184            edge_iter(shape)
185                .filter(|l| !affected_points.contains(&l.start))
186                .filter(|l| !affected_points.contains(&l.end))
187                .all(|l| !l.collides_with(&new_edge))
188        }
189        Candidate::ConvexConvex(c1, c2) => {
190            match replacing_vertex_convex_convex_candidate(shape, (*c1, *c2)) {
191                Err(_) => false,
192                Ok(new_vertex) => {
193                    let new_edge_1 = Edge::try_new(shape[c1.0], new_vertex).unwrap();
194                    let new_edge_2 = Edge::try_new(new_vertex, shape[c2.2]).unwrap();
195
196                    let affected_points = [shape[c1.1], shape[c1.0], shape[c2.1], shape[c2.2]];
197
198                    //check for self-intersections
199                    edge_iter(shape)
200                        .filter(|l| !affected_points.contains(&l.start))
201                        .filter(|l| !affected_points.contains(&l.end))
202                        .all(|l| !l.collides_with(&new_edge_1) && !l.collides_with(&new_edge_2))
203                }
204            }
205        }
206    }
207}
208
209fn edge_iter(points: &[Point]) -> impl Iterator<Item = Edge> + '_ {
210    let n_points = points.len();
211    (0..n_points).map(move |i| {
212        let j = (i + 1) % n_points;
213        Edge::try_new(points[i], points[j]).unwrap()
214    })
215}
216
217fn execute_candidate(shape: &[Point], candidate: &Candidate) -> Vec<Point> {
218    let mut points = shape.iter().cloned().collect_vec();
219    match candidate {
220        Candidate::Collinear(c) | Candidate::Concave(c) => {
221            points.remove(c.1);
222        }
223        Candidate::ConvexConvex(c1, c2) => {
224            let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))
225                .expect("invalid candidate cannot be executed");
226            points.remove(c1.1);
227            let other_index = if c1.1 < c2.1 { c2.1 - 1 } else { c2.1 };
228            points.remove(other_index);
229            points.insert(other_index, replacing_vertex);
230        }
231    }
232    points
233}
234
235fn replacing_vertex_convex_convex_candidate(
236    shape: &[Point],
237    (c1, c2): (Corner, Corner),
238) -> Result<Point, InvalidCandidate> {
239    assert_eq!(c1.2, c2.1, "non-consecutive corners {c1:?},{c2:?}");
240    assert_eq!(c1.1, c2.0, "non-consecutive corners {c1:?},{c2:?}");
241
242    let edge_prev = Edge::try_new(shape[c1.0], shape[c1.1]).unwrap();
243    let edge_next = Edge::try_new(shape[c2.2], shape[c2.1]).unwrap();
244
245    calculate_intersection_in_front(&edge_prev, &edge_next).ok_or(InvalidCandidate)
246}
247
248fn calculate_intersection_in_front(l1: &Edge, l2: &Edge) -> Option<Point> {
249    //Calculates the intersection point between l1 and l2 if both were extended in front to infinity.
250
251    //https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment
252    //vector 1 = [(x1,y1),(x2,y2)[ and vector 2 = [(x3,y3),(x4,y4)[
253    let Point(x1, y1) = l1.start;
254    let Point(x2, y2) = l1.end;
255    let Point(x3, y3) = l2.start;
256    let Point(x4, y4) = l2.end;
257
258    //used formula is slightly different to the one on wikipedia. The orientation of the line segments are flipped
259    //We consider an intersection if t == ]0,1] && u == ]0,1]
260
261    let t_nom = (x2 - x4) * (y4 - y3) - (y2 - y4) * (x4 - x3);
262    let t_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
263
264    let u_nom = (x2 - x4) * (y2 - y1) - (y2 - y4) * (x2 - x1);
265    let u_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
266
267    let t = if t_denom != 0.0 { t_nom / t_denom } else { 0.0 };
268
269    let u = if u_denom != 0.0 { u_nom / u_denom } else { 0.0 };
270
271    if t < 0.0 && u < 0.0 {
272        //intersection is in front both vectors
273        Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
274    } else {
275        //no intersection (parallel or not in front)
276        None
277    }
278}
279
280#[derive(Debug, Clone)]
281struct InvalidCandidate;
282
283#[derive(Clone, Debug, PartialEq)]
284enum Candidate {
285    Concave(Corner),
286    ConvexConvex(Corner, Corner),
287    Collinear(Corner),
288}
289
290#[derive(Clone, Copy, Debug, PartialEq)]
291///Corner is defined as the left hand side of points 0-1-2
292struct Corner(pub usize, pub usize, pub usize);
293
294impl Corner {
295    pub fn flip(&mut self) {
296        std::mem::swap(&mut self.0, &mut self.2);
297    }
298
299    pub fn to_points(self, points: &[Point]) -> [Point; 3] {
300        [points[self.0], points[self.1], points[self.2]]
301    }
302}
303
304#[derive(Clone, Copy, Debug, PartialEq)]
305enum CornerType {
306    Concave,
307    Convex,
308    Collinear,
309}
310
311impl CornerType {
312    pub fn from([p1, p2, p3]: [Point; 3]) -> Self {
313        //returns the corner type on the left-hand side p1->p2->p3
314        //From: https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/
315
316        let p1p2 = (p2.0 - p1.0, p2.1 - p1.1);
317        let p1p3 = (p3.0 - p1.0, p3.1 - p1.1);
318        let cross_prod = p1p2.0 * p1p3.1 - p1p2.1 * p1p3.0;
319
320        //a positive cross product indicates that p2p3 turns to the left with respect to p1p2
321        match cross_prod.partial_cmp(&0.0).expect("cross product is NaN") {
322            Ordering::Less => CornerType::Concave,
323            Ordering::Equal => CornerType::Collinear,
324            Ordering::Greater => CornerType::Convex,
325        }
326    }
327}
328
329/// Offsets a [`SPolygon`] by a certain `distance` either inwards or outwards depending on the [`ShapeModifyMode`].
330/// Relies on the [`geo_offset`](https://crates.io/crates/geo_offset) crate.
331#[cfg(feature = "separation-distance")]
332pub fn offset_shape(sp: &SPolygon, mode: ShapeModifyMode, distance: f32) -> Result<SPolygon> {
333    let offset = match mode {
334        ShapeModifyMode::Deflate => -distance,
335        ShapeModifyMode::Inflate => distance,
336    };
337
338    // Convert the SPolygon to a geo_types::Polygon
339    let geo_poly =
340        geo_types::Polygon::new(sp.vertices.iter().map(|p| (p.0, p.1)).collect(), vec![]);
341
342    // Create the offset geo_types::Polygon
343    let geo_poly_offset = geo_poly
344        .offset(offset)
345        .map_err(|e| anyhow::anyhow!("Error while offsetting polygon: {:?}", e))?
346        .0
347        .remove(0);
348
349    let mut points_offset = geo_poly_offset
350        .exterior()
351        .points()
352        .map(|p| Point(p.x(), p.y()))
353        .collect_vec();
354
355    //pop the last point if it is the same as the first
356    if points_offset.first() == points_offset.last() {
357        points_offset.pop();
358    }
359
360    SPolygon::new(points_offset)
361}
362
363#[cfg(not(feature = "separation-distance"))]
364pub fn offset_shape(_sp: &SPolygon, _mode: ShapeModifyMode, _distance: f32) -> Result<SPolygon> {
365    anyhow::bail!(
366        "cannot offset shape without geo_offset dependency, compile with --features separation to enable this"
367    )
368}