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#[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 pub narrow_concavity_cutoff_ratio: Option<f32>,
41}
42
43pub 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 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 corners.reverse();
76 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 for corner in corners.iter() {
87 let corner_type = CornerType::from(corner.to_points(&ref_points));
88
89 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 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 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; }
124 } else {
125 break; }
127 }
128
129 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 let area = match candidate {
149 Candidate::Collinear(_) => 0.0,
150 Candidate::Concave(c) => {
151 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 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 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 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 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 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 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 Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
275 } else {
276 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)]
292struct 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 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 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
330pub 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 = 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 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 warn!("Offset resulted in multiple polygons, taking the first one.");
357 &geo_poly_offsets[0]
358 }
359 };
360
361 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
373pub 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; }
399 let c_edge = Edge::try_new(shape.vertex(i), shape.vertex(j))
401 .expect("invalid edge in string candidate")
402 .scale(0.9999); if c_edge.length() > max_distance_ratio * shape.diameter {
405 continue;
407 }
408
409 if mode == ShapeModifyMode::Inflate
410 && (shape.collides_with(&c_edge.start) || shape.collides_with(&c_edge.end))
411 {
412 continue;
414 } else if mode == ShapeModifyMode::Deflate
415 && !(shape.collides_with(&c_edge.start) && shape.collides_with(&c_edge.end))
416 {
417 continue;
419 }
420
421 if shape.edge_iter().any(|e| e.collides_with(&c_edge)) {
422 continue;
424 }
425 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 continue;
437 }
438
439 match best_candidate {
441 None => {
442 best_candidate = Some((i, j));
444 }
445 Some((best_i, best_j)) => {
446 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 ref_points.drain((start as usize)..=(end as usize));
472 } else {
473 if (start as usize) < n_points {
475 ref_points.drain(start as usize..);
477 }
478 if end >= 0 {
479 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 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 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; }
530 }
531 true
532}