1use itertools::Itertools;
2use log::{debug, error, info, warn};
3use ordered_float::OrderedFloat;
4use rand_distr::num_traits::ToPrimitive;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7
8use crate::geometry::geo_traits::{CollidesWith, DistanceTo};
9use crate::geometry::primitives::Edge;
10use crate::geometry::primitives::Point;
11use crate::geometry::primitives::SPolygon;
12
13use crate::io::ext_repr::ExtSPolygon;
14use crate::io::import;
15use anyhow::{Result, bail};
16
17#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum ShapeModifyMode {
21 Inflate,
23 Deflate,
25}
26
27#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)]
28pub struct ShapeModifyConfig {
29 pub simplify_tolerance: Option<f32>,
33 pub offset: Option<f32>,
37 pub narrow_concavity_cutoff: Option<(f32, f32)>,
45}
46
47pub fn simplify_shape(
53 shape: &SPolygon,
54 mode: ShapeModifyMode,
55 max_area_change_ratio: f32,
56) -> SPolygon {
57 let original_area = shape.area;
58
59 let mut ref_points = shape.vertices.clone();
60
61 for _ in 0..shape.n_vertices() {
62 let n_points = ref_points.len().cast_signed();
63 if n_points < 4 {
64 break;
66 }
67
68 let mut corners = (0..n_points)
69 .map(|i| {
70 let i_prev = (i - 1).rem_euclid(n_points);
71 let i_next = (i + 1).rem_euclid(n_points);
72 Corner(
73 i_prev.cast_unsigned(),
74 i.cast_unsigned(),
75 i_next.cast_unsigned(),
76 )
77 })
78 .collect_vec();
79
80 if mode == ShapeModifyMode::Deflate {
81 corners.reverse();
84 corners.iter_mut().for_each(Corner::flip);
86 }
87
88 let mut candidates = vec![];
89
90 let mut prev_corner = corners.last().expect("corners is empty");
91 let mut prev_corner_type = CornerType::from(prev_corner.to_points(&ref_points));
92
93 for corner in &corners {
95 let corner_type = CornerType::from(corner.to_points(&ref_points));
96
97 match (&corner_type, &prev_corner_type) {
99 (CornerType::Concave, _) => candidates.push(Candidate::Concave(*corner)),
100 (CornerType::Collinear, _) => candidates.push(Candidate::Collinear(*corner)),
101 (CornerType::Convex, CornerType::Convex) => {
102 candidates.push(Candidate::ConvexConvex(*prev_corner, *corner));
103 }
104 (_, _) => {}
105 }
106 (prev_corner, prev_corner_type) = (corner, corner_type);
107 }
108
109 let best_candidate = candidates
111 .iter()
112 .sorted_by_cached_key(|c| {
113 OrderedFloat(calculate_area_delta(&ref_points, c).unwrap_or(f32::INFINITY))
114 })
115 .find(|c| candidate_is_valid(&ref_points, c));
116
117 if let Some(best_candidate) = best_candidate {
119 let new_shape = execute_candidate(&ref_points, best_candidate);
120 let new_shape_area = SPolygon::calculate_area(&new_shape);
121 let area_delta = (new_shape_area - original_area).abs() / original_area;
122 if area_delta <= max_area_change_ratio {
123 debug!(
124 "[PS] executed {:?} simplification causing {:.2}% area change",
125 best_candidate,
126 area_delta * 100.0
127 );
128 ref_points = new_shape;
129 } else {
130 break; }
132 } else {
133 break; }
135 }
136
137 let simpl_shape = SPolygon::new(ref_points).unwrap();
139
140 if simpl_shape.n_vertices() < shape.n_vertices() {
141 info!(
142 "[PS] simplified from {} to {} edges with {:.3}% area difference",
143 shape.n_vertices(),
144 simpl_shape.n_vertices(),
145 (simpl_shape.area - shape.area) / shape.area * 100.0
146 );
147 } else {
148 info!("[PS] no simplification possible within area change constraints");
149 }
150
151 simpl_shape
152}
153
154fn calculate_area_delta(shape: &[Point], candidate: &Candidate) -> Result<f32, InvalidCandidate> {
155 let area = match candidate {
157 Candidate::Collinear(_) => 0.0,
158 Candidate::Concave(c) => {
159 let Point(x0, y0) = shape[c.0];
161 let Point(x1, y1) = shape[c.1];
162 let Point(x2, y2) = shape[c.2];
163
164 let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
165
166 area.abs()
167 }
168 Candidate::ConvexConvex(c1, c2) => {
169 let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))?;
170
171 let Point(x0, y0) = shape[c1.1];
173 let Point(x1, y1) = replacing_vertex;
174 let Point(x2, y2) = shape[c2.1];
175
176 let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
177
178 area.abs()
179 }
180 };
181 Ok(area)
182}
183
184fn candidate_is_valid(shape: &[Point], candidate: &Candidate) -> bool {
185 match candidate {
187 Candidate::Collinear(_) => true,
188 Candidate::Concave(c) => {
189 let new_edge = Edge::try_new(shape[c.0], shape[c.2]).unwrap();
190 let affected_points = [shape[c.0], shape[c.1], shape[c.2]];
191
192 edge_iter(shape)
194 .filter(|l| !affected_points.contains(&l.start))
195 .filter(|l| !affected_points.contains(&l.end))
196 .all(|l| !l.collides_with(&new_edge))
197 }
198 Candidate::ConvexConvex(c1, c2) => {
199 match replacing_vertex_convex_convex_candidate(shape, (*c1, *c2)) {
200 Err(_) => false,
201 Ok(new_vertex) => {
202 let new_edge_1 = Edge::try_new(shape[c1.0], new_vertex).unwrap();
203 let new_edge_2 = Edge::try_new(new_vertex, shape[c2.2]).unwrap();
204
205 let affected_points = [shape[c1.1], shape[c1.0], shape[c2.1], shape[c2.2]];
206
207 edge_iter(shape)
209 .filter(|l| !affected_points.contains(&l.start))
210 .filter(|l| !affected_points.contains(&l.end))
211 .all(|l| !l.collides_with(&new_edge_1) && !l.collides_with(&new_edge_2))
212 }
213 }
214 }
215 }
216}
217
218fn edge_iter(points: &[Point]) -> impl Iterator<Item = Edge> + '_ {
219 let n_points = points.len();
220 (0..n_points).map(move |i| {
221 let j = (i + 1) % n_points;
222 Edge::try_new(points[i], points[j]).unwrap()
223 })
224}
225
226fn execute_candidate(shape: &[Point], candidate: &Candidate) -> Vec<Point> {
227 let mut points = shape.iter().copied().collect_vec();
228 match candidate {
229 Candidate::Collinear(c) | Candidate::Concave(c) => {
230 points.remove(c.1);
231 }
232 Candidate::ConvexConvex(c1, c2) => {
233 let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))
234 .expect("invalid candidate cannot be executed");
235 points.remove(c1.1);
236 let other_index = if c1.1 < c2.1 { c2.1 - 1 } else { c2.1 };
237 points.remove(other_index);
238 points.insert(other_index, replacing_vertex);
239 }
240 }
241 points
242}
243
244fn replacing_vertex_convex_convex_candidate(
245 shape: &[Point],
246 (c1, c2): (Corner, Corner),
247) -> Result<Point, InvalidCandidate> {
248 assert_eq!(c1.2, c2.1, "non-consecutive corners {c1:?},{c2:?}");
249 assert_eq!(c1.1, c2.0, "non-consecutive corners {c1:?},{c2:?}");
250
251 let edge_prev = Edge::try_new(shape[c1.0], shape[c1.1]).unwrap();
252 let edge_next = Edge::try_new(shape[c2.2], shape[c2.1]).unwrap();
253
254 calculate_intersection_in_front(&edge_prev, &edge_next).ok_or(InvalidCandidate)
255}
256
257fn calculate_intersection_in_front(l1: &Edge, l2: &Edge) -> Option<Point> {
258 let Point(x1, y1) = l1.start;
263 let Point(x2, y2) = l1.end;
264 let Point(x3, y3) = l2.start;
265 let Point(x4, y4) = l2.end;
266
267 let t_nom = (x2 - x4) * (y4 - y3) - (y2 - y4) * (x4 - x3);
271 let t_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
272
273 let u_nom = (x2 - x4) * (y2 - y1) - (y2 - y4) * (x2 - x1);
274 let u_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
275
276 let t = if t_denom == 0.0 { 0.0 } else { t_nom / t_denom };
277
278 let u = if u_denom == 0.0 { 0.0 } else { u_nom / u_denom };
279
280 if t < 0.0 && u < 0.0 {
281 Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
283 } else {
284 None
286 }
287}
288
289#[derive(Debug, Clone)]
290struct InvalidCandidate;
291
292#[derive(Clone, Debug, PartialEq)]
293enum Candidate {
294 Concave(Corner),
295 ConvexConvex(Corner, Corner),
296 Collinear(Corner),
297}
298
299#[derive(Clone, Copy, Debug, PartialEq)]
300struct Corner(pub usize, pub usize, pub usize);
302
303impl Corner {
304 pub fn flip(&mut self) {
305 std::mem::swap(&mut self.0, &mut self.2);
306 }
307
308 pub fn to_points(self, points: &[Point]) -> [Point; 3] {
309 [points[self.0], points[self.1], points[self.2]]
310 }
311}
312
313#[derive(Clone, Copy, Debug, PartialEq)]
314enum CornerType {
315 Concave,
316 Convex,
317 Collinear,
318}
319
320impl CornerType {
321 pub fn from([p1, p2, p3]: [Point; 3]) -> Self {
322 let p1p2 = (p2.0 - p1.0, p2.1 - p1.1);
326 let p1p3 = (p3.0 - p1.0, p3.1 - p1.1);
327 let cross_prod = p1p2.0 * p1p3.1 - p1p2.1 * p1p3.0;
328
329 match cross_prod.partial_cmp(&0.0).expect("cross product is NaN") {
331 Ordering::Less => CornerType::Concave,
332 Ordering::Equal => CornerType::Collinear,
333 Ordering::Greater => CornerType::Convex,
334 }
335 }
336}
337
338pub fn offset_shape(sp: &SPolygon, mode: ShapeModifyMode, distance: f32) -> Result<SPolygon> {
341 let offset = match mode {
342 ShapeModifyMode::Deflate => -distance,
343 ShapeModifyMode::Inflate => distance,
344 };
345
346 let geo_poly = geo_types::Polygon::new(
348 sp.vertices
349 .iter()
350 .map(|p| (f64::from(p.0), f64::from(p.1)))
351 .collect(),
352 vec![],
353 );
354
355 let geo_poly_offsets = geo_buffer::buffer_polygon_rounded(&geo_poly, f64::from(offset)).0;
357
358 let geo_poly_offset = match geo_poly_offsets.len() {
359 0 => bail!("Offset resulted in an empty polygon"),
360 1 => &geo_poly_offsets[0],
361 _ => {
362 warn!("Offset resulted in multiple polygons, taking the first one.");
365 &geo_poly_offsets[0]
366 }
367 };
368
369 let ext_s_polygon = ExtSPolygon(
371 geo_poly_offset
372 .exterior()
373 .points()
374 .map(|p| (p.x().to_f32().unwrap(), p.y().to_f32().unwrap()))
375 .collect_vec(),
376 );
377
378 import::import_simple_polygon(&ext_s_polygon)
379}
380
381#[allow(clippy::too_many_lines)]
382#[must_use]
384pub fn close_narrow_concavities(
385 orig_shape: &SPolygon,
386 mode: ShapeModifyMode,
387 (cutoff_distance_ratio, cutoff_area_ratio): (f32, f32),
388) -> SPolygon {
389 let mut n_concav_closed = 0;
390 let mut shape = orig_shape.clone();
391
392 for _ in 0..shape.n_vertices() {
393 let n_points = shape.n_vertices();
394
395 let calc_vert_elim = |i, j| {
396 if j > i {
397 j - i - 1
398 } else {
399 n_points - i + j - 1
400 }
401 };
402
403 let mut best_candidate = None;
404 for i in 0..n_points {
405 for j in 0..n_points {
406 if i == j || (i + 1) % n_points == j || (j + 1) % n_points == i {
407 continue; }
409 let c_edge = Edge::try_new(shape.vertex(i), shape.vertex(j))
411 .expect("invalid edge in string candidate")
412 .scale(0.9999); if c_edge.length() > cutoff_distance_ratio * shape.diameter {
415 continue;
417 }
418
419 if mode == ShapeModifyMode::Inflate
420 && (shape.collides_with(&c_edge.start) || shape.collides_with(&c_edge.end))
421 {
422 continue;
424 }
425
426 if mode == ShapeModifyMode::Deflate
427 && !(shape.collides_with(&c_edge.start) && shape.collides_with(&c_edge.end))
428 {
429 continue;
431 }
432
433 if shape.edge_iter().any(|e| e.collides_with(&c_edge)) {
434 continue;
436 }
437 let sub_shape_area = {
439 let sub_shape_points = if j > i {
440 shape.vertices[i..j].to_vec()
441 } else {
442 [&shape.vertices[i..], &shape.vertices[..j]].concat()
443 };
444 SPolygon::calculate_area(&sub_shape_points)
445 };
446 if sub_shape_area >= 0.0 {
447 continue;
449 }
450 if sub_shape_area.abs() > cutoff_area_ratio * shape.area {
451 continue;
453 }
454
455 match best_candidate {
457 None => {
458 best_candidate = Some((i, j));
460 }
461 Some((best_i, best_j)) => {
462 if calc_vert_elim(i, j) > calc_vert_elim(best_i, best_j) {
464 best_candidate = Some((i, j));
465 }
466 }
467 }
468 }
469 }
470 if let Some((i, j)) = best_candidate {
471 let mut ref_points = shape.vertices.clone();
472 let start = i.cast_signed() + 1;
473 let end = j.cast_signed() - 1;
474 debug!(
475 "[PS] closing concavity between points (idx: {}, {:?}) and (idx: {}, {:?}) with edge length {:.3} ({} vertices eliminated)",
476 i,
477 shape.vertex(i),
478 j,
479 shape.vertex(j),
480 Edge::try_new(shape.vertex(i), shape.vertex(j))
481 .expect("invalid edge in string candidate")
482 .length(),
483 calc_vert_elim(i, j)
484 );
485 if start <= end {
486 ref_points.drain(start.cast_unsigned()..=end.cast_unsigned());
488 } else {
489 if start.cast_unsigned() < n_points {
491 ref_points.drain(start.cast_unsigned()..);
493 }
494 if end >= 0 {
495 ref_points.drain(0..=end.cast_unsigned());
497 }
498 }
499 shape = SPolygon::new(ref_points).expect("invalid shape after closing concavity");
500 n_concav_closed += 1;
501 } else {
502 break;
504 }
505 }
506
507 if n_concav_closed > 0 {
508 info!(
509 "[PS] [EXPERIMENTAL] closed {} concavities closer than {:.3}% of diameter and less than {:.3}% of area, reducing vertices from {} to {}",
510 n_concav_closed,
511 cutoff_distance_ratio * 100.0,
512 cutoff_area_ratio * 100.0,
513 orig_shape.n_vertices(),
514 shape.n_vertices()
515 );
516 }
517
518 shape
519}
520
521#[must_use]
522pub fn shape_modification_valid(orig: &SPolygon, simpl: &SPolygon, mode: ShapeModifyMode) -> bool {
523 let on_edge = |p: &Point| {
525 simpl
526 .edge_iter()
527 .any(|e| e.distance_to(p) < simpl.diameter * 1e-6)
528 };
529
530 for p in orig.vertices.iter().filter(|p| !simpl.vertices.contains(p)) {
531 let vertex_on_edge = on_edge(p);
532 let vertex_in_simpl = simpl.collides_with(p);
533
534 let error = match mode {
535 ShapeModifyMode::Inflate => !vertex_in_simpl && !vertex_on_edge,
536 ShapeModifyMode::Deflate => vertex_in_simpl && !vertex_on_edge,
537 };
538
539 if error {
540 error!(
541 "[PS] point {:?} from original shape is incorrect in simplified shape (original vertices: {:?}, simplified vertices: {:?})",
542 p,
543 orig.vertices.iter().map(|p| (p.0, p.1)).collect_vec(),
544 simpl.vertices.iter().map(|p| (p.0, p.1)).collect_vec()
545 );
546 return false; }
548 }
549 true
550}