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#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum ShapeModifyMode {
20 Inflate,
22 Deflate,
24}
25
26#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)]
27pub struct ShapeModifyConfig {
28 pub simplify_tolerance: Option<f32>,
32 pub offset: Option<f32>,
36}
37
38pub 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 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 corners.reverse();
71 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 for corner in corners.iter() {
82 let corner_type = CornerType::from(corner.to_points(&ref_points));
83
84 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 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 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; }
120 } else {
121 break; }
123 }
124
125 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 let area = match candidate {
148 Candidate::Collinear(_) => 0.0,
149 Candidate::Concave(c) => {
150 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 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 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 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 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 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 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 Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
274 } else {
275 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)]
291struct 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 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 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#[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 let geo_poly =
340 geo_types::Polygon::new(sp.vertices.iter().map(|p| (p.0, p.1)).collect(), vec![]);
341
342 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 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}