jagua_rs/geometry/
shape_modification.rs

1use itertools::Itertools;
2use log::{debug, error, info, warn};
3use ordered_float::OrderedFloat;
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6
7use crate::geometry::geo_traits::{CollidesWith, DistanceTo};
8use crate::geometry::primitives::Edge;
9use crate::geometry::primitives::Point;
10use crate::geometry::primitives::SPolygon;
11
12use crate::io::ext_repr::ExtSPolygon;
13use crate::io::import;
14use anyhow::{Result, bail};
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    /// Maximum distance between two vertices for which a concavity is considered narrow and can be closed.
37    /// Defined as a fraction of the item's diameter.
38    /// If undefined, no concavity closing is performed.
39    /// See [`close_narrow_concavities`]
40    pub narrow_concavity_cutoff_ratio: Option<f32>,
41}
42
43/// Simplifies a [`SPolygon`] by reducing the number of edges.
44///
45/// The simplified shape will either be a subset or a superset of the original shape, depending on the [`ShapeModifyMode`].
46/// The procedure sequentially eliminates edges until either the change in area (ratio)
47/// exceeds `max_area_delta` or the number of edges < 4.
48pub fn simplify_shape(
49    shape: &SPolygon,
50    mode: ShapeModifyMode,
51    max_area_change_ratio: f32,
52) -> SPolygon {
53    let original_area = shape.area;
54
55    let mut ref_points = shape.vertices.clone();
56
57    for _ in 0..shape.n_vertices() {
58        let n_points = ref_points.len() as isize;
59        if n_points < 4 {
60            //can't simplify further
61            break;
62        }
63
64        let mut corners = (0..n_points)
65            .map(|i| {
66                let i_prev = (i - 1).rem_euclid(n_points);
67                let i_next = (i + 1).rem_euclid(n_points);
68                Corner(i_prev as usize, i as usize, i_next as usize)
69            })
70            .collect_vec();
71
72        if mode == ShapeModifyMode::Deflate {
73            //default mode is to inflate, so we need to reverse the order of the corners and flip the corners for deflate mode
74            //reverse the order of the corners
75            corners.reverse();
76            //reverse each corner
77            corners.iter_mut().for_each(|c| c.flip());
78        }
79
80        let mut candidates = vec![];
81
82        let mut prev_corner = corners.last().expect("corners is empty");
83        let mut prev_corner_type = CornerType::from(prev_corner.to_points(&ref_points));
84
85        //Go over all corners and generate candidates
86        for corner in corners.iter() {
87            let corner_type = CornerType::from(corner.to_points(&ref_points));
88
89            //Generate a removal candidate (or not)
90            match (&corner_type, &prev_corner_type) {
91                (CornerType::Concave, _) => candidates.push(Candidate::Concave(*corner)),
92                (CornerType::Collinear, _) => candidates.push(Candidate::Collinear(*corner)),
93                (CornerType::Convex, CornerType::Convex) => {
94                    candidates.push(Candidate::ConvexConvex(*prev_corner, *corner))
95                }
96                (_, _) => {}
97            };
98            (prev_corner, prev_corner_type) = (corner, corner_type);
99        }
100
101        //search the candidate with the smallest change in area that is valid
102        let best_candidate = candidates
103            .iter()
104            .sorted_by_cached_key(|c| {
105                OrderedFloat(calculate_area_delta(&ref_points, c).unwrap_or(f32::INFINITY))
106            })
107            .find(|c| candidate_is_valid(&ref_points, c));
108
109        //if it is within the area change constraints, execute the candidate
110        if let Some(best_candidate) = best_candidate {
111            let new_shape = execute_candidate(&ref_points, best_candidate);
112            let new_shape_area = SPolygon::calculate_area(&new_shape);
113            let area_delta = (new_shape_area - original_area).abs() / original_area;
114            if area_delta <= max_area_change_ratio {
115                debug!(
116                    "[PS] executed {:?} simplification causing {:.2}% area change",
117                    best_candidate,
118                    area_delta * 100.0
119                );
120                ref_points = new_shape;
121            } else {
122                break; //area change too significant
123            }
124        } else {
125            break; //no candidate found
126        }
127    }
128
129    //Convert it back to a simple polygon
130    let simpl_shape = SPolygon::new(ref_points).unwrap();
131
132    if simpl_shape.n_vertices() < shape.n_vertices() {
133        info!(
134            "[PS] simplified from {} to {} edges with {:.3}% area difference",
135            shape.n_vertices(),
136            simpl_shape.n_vertices(),
137            (simpl_shape.area - shape.area) / shape.area * 100.0
138        );
139    } else {
140        info!("[PS] no simplification possible within area change constraints");
141    }
142
143    simpl_shape
144}
145
146fn calculate_area_delta(shape: &[Point], candidate: &Candidate) -> Result<f32, InvalidCandidate> {
147    //calculate the difference in area of the shape if the candidate were to be executed
148    let area = match candidate {
149        Candidate::Collinear(_) => 0.0,
150        Candidate::Concave(c) => {
151            //Triangle formed by i_prev, i and i_next will correspond to the change area
152            let Point(x0, y0) = shape[c.0];
153            let Point(x1, y1) = shape[c.1];
154            let Point(x2, y2) = shape[c.2];
155
156            let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
157
158            area.abs()
159        }
160        Candidate::ConvexConvex(c1, c2) => {
161            let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))?;
162
163            //the triangle formed by corner c1, c2, and replacing vertex will correspond to the change in area
164            let Point(x0, y0) = shape[c1.1];
165            let Point(x1, y1) = replacing_vertex;
166            let Point(x2, y2) = shape[c2.1];
167
168            let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
169
170            area.abs()
171        }
172    };
173    Ok(area)
174}
175
176fn candidate_is_valid(shape: &[Point], candidate: &Candidate) -> bool {
177    //ensure the removal/replacement does not create any self intersections
178    match candidate {
179        Candidate::Collinear(_) => true,
180        Candidate::Concave(c) => {
181            let new_edge = Edge::try_new(shape[c.0], shape[c.2]).unwrap();
182            let affected_points = [shape[c.0], shape[c.1], shape[c.2]];
183
184            //check for self-intersections
185            edge_iter(shape)
186                .filter(|l| !affected_points.contains(&l.start))
187                .filter(|l| !affected_points.contains(&l.end))
188                .all(|l| !l.collides_with(&new_edge))
189        }
190        Candidate::ConvexConvex(c1, c2) => {
191            match replacing_vertex_convex_convex_candidate(shape, (*c1, *c2)) {
192                Err(_) => false,
193                Ok(new_vertex) => {
194                    let new_edge_1 = Edge::try_new(shape[c1.0], new_vertex).unwrap();
195                    let new_edge_2 = Edge::try_new(new_vertex, shape[c2.2]).unwrap();
196
197                    let affected_points = [shape[c1.1], shape[c1.0], shape[c2.1], shape[c2.2]];
198
199                    //check for self-intersections
200                    edge_iter(shape)
201                        .filter(|l| !affected_points.contains(&l.start))
202                        .filter(|l| !affected_points.contains(&l.end))
203                        .all(|l| !l.collides_with(&new_edge_1) && !l.collides_with(&new_edge_2))
204                }
205            }
206        }
207    }
208}
209
210fn edge_iter(points: &[Point]) -> impl Iterator<Item = Edge> + '_ {
211    let n_points = points.len();
212    (0..n_points).map(move |i| {
213        let j = (i + 1) % n_points;
214        Edge::try_new(points[i], points[j]).unwrap()
215    })
216}
217
218fn execute_candidate(shape: &[Point], candidate: &Candidate) -> Vec<Point> {
219    let mut points = shape.iter().cloned().collect_vec();
220    match candidate {
221        Candidate::Collinear(c) | Candidate::Concave(c) => {
222            points.remove(c.1);
223        }
224        Candidate::ConvexConvex(c1, c2) => {
225            let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))
226                .expect("invalid candidate cannot be executed");
227            points.remove(c1.1);
228            let other_index = if c1.1 < c2.1 { c2.1 - 1 } else { c2.1 };
229            points.remove(other_index);
230            points.insert(other_index, replacing_vertex);
231        }
232    }
233    points
234}
235
236fn replacing_vertex_convex_convex_candidate(
237    shape: &[Point],
238    (c1, c2): (Corner, Corner),
239) -> Result<Point, InvalidCandidate> {
240    assert_eq!(c1.2, c2.1, "non-consecutive corners {c1:?},{c2:?}");
241    assert_eq!(c1.1, c2.0, "non-consecutive corners {c1:?},{c2:?}");
242
243    let edge_prev = Edge::try_new(shape[c1.0], shape[c1.1]).unwrap();
244    let edge_next = Edge::try_new(shape[c2.2], shape[c2.1]).unwrap();
245
246    calculate_intersection_in_front(&edge_prev, &edge_next).ok_or(InvalidCandidate)
247}
248
249fn calculate_intersection_in_front(l1: &Edge, l2: &Edge) -> Option<Point> {
250    //Calculates the intersection point between l1 and l2 if both were extended in front to infinity.
251
252    //https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment
253    //vector 1 = [(x1,y1),(x2,y2)[ and vector 2 = [(x3,y3),(x4,y4)[
254    let Point(x1, y1) = l1.start;
255    let Point(x2, y2) = l1.end;
256    let Point(x3, y3) = l2.start;
257    let Point(x4, y4) = l2.end;
258
259    //used formula is slightly different to the one on wikipedia. The orientation of the line segments are flipped
260    //We consider an intersection if t == ]0,1] && u == ]0,1]
261
262    let t_nom = (x2 - x4) * (y4 - y3) - (y2 - y4) * (x4 - x3);
263    let t_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
264
265    let u_nom = (x2 - x4) * (y2 - y1) - (y2 - y4) * (x2 - x1);
266    let u_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
267
268    let t = if t_denom != 0.0 { t_nom / t_denom } else { 0.0 };
269
270    let u = if u_denom != 0.0 { u_nom / u_denom } else { 0.0 };
271
272    if t < 0.0 && u < 0.0 {
273        //intersection is in front both vectors
274        Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
275    } else {
276        //no intersection (parallel or not in front)
277        None
278    }
279}
280
281#[derive(Debug, Clone)]
282struct InvalidCandidate;
283
284#[derive(Clone, Debug, PartialEq)]
285enum Candidate {
286    Concave(Corner),
287    ConvexConvex(Corner, Corner),
288    Collinear(Corner),
289}
290
291#[derive(Clone, Copy, Debug, PartialEq)]
292///Corner is defined as the left hand side of points 0-1-2
293struct Corner(pub usize, pub usize, pub usize);
294
295impl Corner {
296    pub fn flip(&mut self) {
297        std::mem::swap(&mut self.0, &mut self.2);
298    }
299
300    pub fn to_points(self, points: &[Point]) -> [Point; 3] {
301        [points[self.0], points[self.1], points[self.2]]
302    }
303}
304
305#[derive(Clone, Copy, Debug, PartialEq)]
306enum CornerType {
307    Concave,
308    Convex,
309    Collinear,
310}
311
312impl CornerType {
313    pub fn from([p1, p2, p3]: [Point; 3]) -> Self {
314        //returns the corner type on the left-hand side p1->p2->p3
315        //From: https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/
316
317        let p1p2 = (p2.0 - p1.0, p2.1 - p1.1);
318        let p1p3 = (p3.0 - p1.0, p3.1 - p1.1);
319        let cross_prod = p1p2.0 * p1p3.1 - p1p2.1 * p1p3.0;
320
321        //a positive cross product indicates that p2p3 turns to the left with respect to p1p2
322        match cross_prod.partial_cmp(&0.0).expect("cross product is NaN") {
323            Ordering::Less => CornerType::Concave,
324            Ordering::Equal => CornerType::Collinear,
325            Ordering::Greater => CornerType::Convex,
326        }
327    }
328}
329
330/// Offsets a [`SPolygon`] by a certain `distance` either inwards or outwards depending on the [`ShapeModifyMode`].
331/// Relies on the [`geo_offset`](https://crates.io/crates/geo_offset) crate.
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 = geo_types::Polygon::new(
340        sp.vertices
341            .iter()
342            .map(|p| (p.0 as f64, p.1 as f64))
343            .collect(),
344        vec![],
345    );
346
347    // Create the offset polygon
348    let geo_poly_offsets = geo_buffer::buffer_polygon_rounded(&geo_poly, offset as f64).0;
349
350    let geo_poly_offset = match geo_poly_offsets.len() {
351        0 => bail!("Offset resulted in an empty polygon"),
352        1 => &geo_poly_offsets[0],
353        _ => {
354            // If there are multiple polygons, we take the first one.
355            // This can happen if the offset creates multiple disconnected parts.
356            warn!("Offset resulted in multiple polygons, taking the first one.");
357            &geo_poly_offsets[0]
358        }
359    };
360
361    // Convert back to internal representation (by using the import function)
362    let ext_s_polygon = ExtSPolygon(
363        geo_poly_offset
364            .exterior()
365            .points()
366            .map(|p| (p.x() as f32, p.y() as f32))
367            .collect_vec(),
368    );
369
370    import::import_simple_polygon(&ext_s_polygon)
371}
372
373/// Closes narrow concavities in a [`SPolygon`] by replacing them with a straight edge, eliminating the vertices in between.
374pub fn close_narrow_concavities(
375    orig_shape: &SPolygon,
376    mode: ShapeModifyMode,
377    max_distance_ratio: f32,
378) -> SPolygon {
379    let mut n_concav_closed = 0;
380    let mut shape = orig_shape.clone();
381
382    for _ in 0..shape.n_vertices() {
383        let n_points = shape.n_vertices();
384
385        let calc_vert_elim = |i, j| {
386            if j > i {
387                j - i - 1
388            } else {
389                n_points - i + j - 1
390            }
391        };
392
393        let mut best_candidate = None;
394        for i in 0..n_points {
395            for j in 0..n_points {
396                if i == j || (i + 1) % n_points == j || (j + 1) % n_points == i {
397                    continue; //skip adjacent points
398                }
399                //Simulate the replacing edge
400                let c_edge = Edge::try_new(shape.vertex(i), shape.vertex(j))
401                    .expect("invalid edge in string candidate")
402                    .scale(0.9999); //slightly shrink the edge to avoid self-intersections
403
404                if c_edge.length() > max_distance_ratio * shape.diameter {
405                    //If the edge is too long, skip it
406                    continue;
407                }
408
409                if mode == ShapeModifyMode::Inflate
410                    && (shape.collides_with(&c_edge.start) || shape.collides_with(&c_edge.end))
411                {
412                    //If we are only allowed to inflate the shape and any end point is inside the shape, skip it
413                    continue;
414                } else if mode == ShapeModifyMode::Deflate
415                    && !(shape.collides_with(&c_edge.start) && shape.collides_with(&c_edge.end))
416                {
417                    //If we are only allowed to deflate the shape and both end points are not inside the shape, skip it
418                    continue;
419                }
420
421                if shape.edge_iter().any(|e| e.collides_with(&c_edge)) {
422                    //If the edge collides with any edge of the shape, reject always
423                    continue;
424                }
425                //the eliminated vertices should form a negative area (in inflation mode) or positive area (in deflation mode)
426                let sub_shape_area = {
427                    let sub_shape_points = if j > i {
428                        shape.vertices[i..j].to_vec()
429                    } else {
430                        [&shape.vertices[i..], &shape.vertices[..j]].concat()
431                    };
432                    SPolygon::calculate_area(&sub_shape_points)
433                };
434                if sub_shape_area >= 0.0 {
435                    //if the area is not negative, skip it
436                    continue;
437                }
438
439                //Valid candidate found...
440                match best_candidate {
441                    None => {
442                        //first candidate found
443                        best_candidate = Some((i, j));
444                    }
445                    Some((best_i, best_j)) => {
446                        //check the number of points that would be removed
447                        if calc_vert_elim(i, j) > calc_vert_elim(best_i, best_j) {
448                            best_candidate = Some((i, j));
449                        }
450                    }
451                }
452            }
453        }
454        if let Some((i, j)) = best_candidate {
455            let mut ref_points = shape.vertices.clone();
456            let start = i as isize + 1;
457            let end = j as isize - 1;
458            debug!(
459                "[PS] closing concavity between points (idx: {}, {:?}) and (idx: {}, {:?}) with edge length {:.3} ({} vertices eliminated)",
460                i,
461                shape.vertex(i),
462                j,
463                shape.vertex(j),
464                Edge::try_new(shape.vertex(i), shape.vertex(j))
465                    .expect("invalid edge in string candidate")
466                    .length(),
467                calc_vert_elim(i, j)
468            );
469            if start <= end {
470                // if j does not wrap around the shape
471                ref_points.drain((start as usize)..=(end as usize));
472            } else {
473                // if j wraps around the shape
474                if (start as usize) < n_points {
475                    //remove from `start` to back
476                    ref_points.drain(start as usize..);
477                }
478                if end >= 0 {
479                    //remove from front to `end`
480                    ref_points.drain(0..=(end as usize));
481                }
482            }
483            shape = SPolygon::new(ref_points).expect("invalid shape after closing concavity");
484            n_concav_closed += 1;
485        } else {
486            //no more candidates found, break the loop
487            break;
488        }
489    }
490
491    if n_concav_closed > 0 {
492        info!(
493            "[PS] [EXPERIMENTAL] closed {} concavities closer than {:.3}% of diameter, reducing vertices from {} to {}",
494            n_concav_closed,
495            max_distance_ratio * 100.0,
496            orig_shape.n_vertices(),
497            shape.n_vertices()
498        );
499    }
500
501    shape
502}
503
504pub fn shape_modification_valid(orig: &SPolygon, simpl: &SPolygon, mode: ShapeModifyMode) -> bool {
505    //make sure each point of the original shape is either in the new shape or included (in case of inflation)/excluded (in case of deflation) in the new shape
506    let on_edge = |p: &Point| {
507        simpl
508            .edge_iter()
509            .any(|e| e.distance_to(p) < simpl.diameter * 1e-6)
510    };
511
512    for p in orig.vertices.iter().filter(|p| !simpl.vertices.contains(p)) {
513        let vertex_on_edge = on_edge(p);
514        let vertex_in_simpl = simpl.collides_with(p);
515
516        let error = match mode {
517            ShapeModifyMode::Inflate => !vertex_in_simpl && !vertex_on_edge,
518            ShapeModifyMode::Deflate => vertex_in_simpl && !vertex_on_edge,
519        };
520
521        if error {
522            error!(
523                "[PS] point {:?} from original shape is incorrect in simplified shape (original vertices: {:?}, simplified vertices: {:?})",
524                p,
525                orig.vertices.iter().map(|p| (p.0, p.1)).collect_vec(),
526                simpl.vertices.iter().map(|p| (p.0, p.1)).collect_vec()
527            );
528            return false; //point is not in the new shape and does not collide with it
529        }
530    }
531    true
532}