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: Option<(f32, f32)>,
44}
45
46pub fn simplify_shape(
52 shape: &SPolygon,
53 mode: ShapeModifyMode,
54 max_area_change_ratio: f32,
55) -> SPolygon {
56 let original_area = shape.area;
57
58 let mut ref_points = shape.vertices.clone();
59
60 for _ in 0..shape.n_vertices() {
61 let n_points = ref_points.len() as isize;
62 if n_points < 4 {
63 break;
65 }
66
67 let mut corners = (0..n_points)
68 .map(|i| {
69 let i_prev = (i - 1).rem_euclid(n_points);
70 let i_next = (i + 1).rem_euclid(n_points);
71 Corner(i_prev as usize, i as usize, i_next as usize)
72 })
73 .collect_vec();
74
75 if mode == ShapeModifyMode::Deflate {
76 corners.reverse();
79 corners.iter_mut().for_each(|c| c.flip());
81 }
82
83 let mut candidates = vec![];
84
85 let mut prev_corner = corners.last().expect("corners is empty");
86 let mut prev_corner_type = CornerType::from(prev_corner.to_points(&ref_points));
87
88 for corner in corners.iter() {
90 let corner_type = CornerType::from(corner.to_points(&ref_points));
91
92 match (&corner_type, &prev_corner_type) {
94 (CornerType::Concave, _) => candidates.push(Candidate::Concave(*corner)),
95 (CornerType::Collinear, _) => candidates.push(Candidate::Collinear(*corner)),
96 (CornerType::Convex, CornerType::Convex) => {
97 candidates.push(Candidate::ConvexConvex(*prev_corner, *corner))
98 }
99 (_, _) => {}
100 };
101 (prev_corner, prev_corner_type) = (corner, corner_type);
102 }
103
104 let best_candidate = candidates
106 .iter()
107 .sorted_by_cached_key(|c| {
108 OrderedFloat(calculate_area_delta(&ref_points, c).unwrap_or(f32::INFINITY))
109 })
110 .find(|c| candidate_is_valid(&ref_points, c));
111
112 if let Some(best_candidate) = best_candidate {
114 let new_shape = execute_candidate(&ref_points, best_candidate);
115 let new_shape_area = SPolygon::calculate_area(&new_shape);
116 let area_delta = (new_shape_area - original_area).abs() / original_area;
117 if area_delta <= max_area_change_ratio {
118 debug!(
119 "[PS] executed {:?} simplification causing {:.2}% area change",
120 best_candidate,
121 area_delta * 100.0
122 );
123 ref_points = new_shape;
124 } else {
125 break; }
127 } else {
128 break; }
130 }
131
132 let simpl_shape = SPolygon::new(ref_points).unwrap();
134
135 if simpl_shape.n_vertices() < shape.n_vertices() {
136 info!(
137 "[PS] simplified from {} to {} edges with {:.3}% area difference",
138 shape.n_vertices(),
139 simpl_shape.n_vertices(),
140 (simpl_shape.area - shape.area) / shape.area * 100.0
141 );
142 } else {
143 info!("[PS] no simplification possible within area change constraints");
144 }
145
146 simpl_shape
147}
148
149fn calculate_area_delta(shape: &[Point], candidate: &Candidate) -> Result<f32, InvalidCandidate> {
150 let area = match candidate {
152 Candidate::Collinear(_) => 0.0,
153 Candidate::Concave(c) => {
154 let Point(x0, y0) = shape[c.0];
156 let Point(x1, y1) = shape[c.1];
157 let Point(x2, y2) = shape[c.2];
158
159 let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
160
161 area.abs()
162 }
163 Candidate::ConvexConvex(c1, c2) => {
164 let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))?;
165
166 let Point(x0, y0) = shape[c1.1];
168 let Point(x1, y1) = replacing_vertex;
169 let Point(x2, y2) = shape[c2.1];
170
171 let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
172
173 area.abs()
174 }
175 };
176 Ok(area)
177}
178
179fn candidate_is_valid(shape: &[Point], candidate: &Candidate) -> bool {
180 match candidate {
182 Candidate::Collinear(_) => true,
183 Candidate::Concave(c) => {
184 let new_edge = Edge::try_new(shape[c.0], shape[c.2]).unwrap();
185 let affected_points = [shape[c.0], shape[c.1], shape[c.2]];
186
187 edge_iter(shape)
189 .filter(|l| !affected_points.contains(&l.start))
190 .filter(|l| !affected_points.contains(&l.end))
191 .all(|l| !l.collides_with(&new_edge))
192 }
193 Candidate::ConvexConvex(c1, c2) => {
194 match replacing_vertex_convex_convex_candidate(shape, (*c1, *c2)) {
195 Err(_) => false,
196 Ok(new_vertex) => {
197 let new_edge_1 = Edge::try_new(shape[c1.0], new_vertex).unwrap();
198 let new_edge_2 = Edge::try_new(new_vertex, shape[c2.2]).unwrap();
199
200 let affected_points = [shape[c1.1], shape[c1.0], shape[c2.1], shape[c2.2]];
201
202 edge_iter(shape)
204 .filter(|l| !affected_points.contains(&l.start))
205 .filter(|l| !affected_points.contains(&l.end))
206 .all(|l| !l.collides_with(&new_edge_1) && !l.collides_with(&new_edge_2))
207 }
208 }
209 }
210 }
211}
212
213fn edge_iter(points: &[Point]) -> impl Iterator<Item = Edge> + '_ {
214 let n_points = points.len();
215 (0..n_points).map(move |i| {
216 let j = (i + 1) % n_points;
217 Edge::try_new(points[i], points[j]).unwrap()
218 })
219}
220
221fn execute_candidate(shape: &[Point], candidate: &Candidate) -> Vec<Point> {
222 let mut points = shape.iter().cloned().collect_vec();
223 match candidate {
224 Candidate::Collinear(c) | Candidate::Concave(c) => {
225 points.remove(c.1);
226 }
227 Candidate::ConvexConvex(c1, c2) => {
228 let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))
229 .expect("invalid candidate cannot be executed");
230 points.remove(c1.1);
231 let other_index = if c1.1 < c2.1 { c2.1 - 1 } else { c2.1 };
232 points.remove(other_index);
233 points.insert(other_index, replacing_vertex);
234 }
235 }
236 points
237}
238
239fn replacing_vertex_convex_convex_candidate(
240 shape: &[Point],
241 (c1, c2): (Corner, Corner),
242) -> Result<Point, InvalidCandidate> {
243 assert_eq!(c1.2, c2.1, "non-consecutive corners {c1:?},{c2:?}");
244 assert_eq!(c1.1, c2.0, "non-consecutive corners {c1:?},{c2:?}");
245
246 let edge_prev = Edge::try_new(shape[c1.0], shape[c1.1]).unwrap();
247 let edge_next = Edge::try_new(shape[c2.2], shape[c2.1]).unwrap();
248
249 calculate_intersection_in_front(&edge_prev, &edge_next).ok_or(InvalidCandidate)
250}
251
252fn calculate_intersection_in_front(l1: &Edge, l2: &Edge) -> Option<Point> {
253 let Point(x1, y1) = l1.start;
258 let Point(x2, y2) = l1.end;
259 let Point(x3, y3) = l2.start;
260 let Point(x4, y4) = l2.end;
261
262 let t_nom = (x2 - x4) * (y4 - y3) - (y2 - y4) * (x4 - x3);
266 let t_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
267
268 let u_nom = (x2 - x4) * (y2 - y1) - (y2 - y4) * (x2 - x1);
269 let u_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
270
271 let t = if t_denom != 0.0 { t_nom / t_denom } else { 0.0 };
272
273 let u = if u_denom != 0.0 { u_nom / u_denom } else { 0.0 };
274
275 if t < 0.0 && u < 0.0 {
276 Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
278 } else {
279 None
281 }
282}
283
284#[derive(Debug, Clone)]
285struct InvalidCandidate;
286
287#[derive(Clone, Debug, PartialEq)]
288enum Candidate {
289 Concave(Corner),
290 ConvexConvex(Corner, Corner),
291 Collinear(Corner),
292}
293
294#[derive(Clone, Copy, Debug, PartialEq)]
295struct Corner(pub usize, pub usize, pub usize);
297
298impl Corner {
299 pub fn flip(&mut self) {
300 std::mem::swap(&mut self.0, &mut self.2);
301 }
302
303 pub fn to_points(self, points: &[Point]) -> [Point; 3] {
304 [points[self.0], points[self.1], points[self.2]]
305 }
306}
307
308#[derive(Clone, Copy, Debug, PartialEq)]
309enum CornerType {
310 Concave,
311 Convex,
312 Collinear,
313}
314
315impl CornerType {
316 pub fn from([p1, p2, p3]: [Point; 3]) -> Self {
317 let p1p2 = (p2.0 - p1.0, p2.1 - p1.1);
321 let p1p3 = (p3.0 - p1.0, p3.1 - p1.1);
322 let cross_prod = p1p2.0 * p1p3.1 - p1p2.1 * p1p3.0;
323
324 match cross_prod.partial_cmp(&0.0).expect("cross product is NaN") {
326 Ordering::Less => CornerType::Concave,
327 Ordering::Equal => CornerType::Collinear,
328 Ordering::Greater => CornerType::Convex,
329 }
330 }
331}
332
333pub fn offset_shape(sp: &SPolygon, mode: ShapeModifyMode, distance: f32) -> Result<SPolygon> {
336 let offset = match mode {
337 ShapeModifyMode::Deflate => -distance,
338 ShapeModifyMode::Inflate => distance,
339 };
340
341 let geo_poly = geo_types::Polygon::new(
343 sp.vertices
344 .iter()
345 .map(|p| (p.0 as f64, p.1 as f64))
346 .collect(),
347 vec![],
348 );
349
350 let geo_poly_offsets = geo_buffer::buffer_polygon_rounded(&geo_poly, offset as f64).0;
352
353 let geo_poly_offset = match geo_poly_offsets.len() {
354 0 => bail!("Offset resulted in an empty polygon"),
355 1 => &geo_poly_offsets[0],
356 _ => {
357 warn!("Offset resulted in multiple polygons, taking the first one.");
360 &geo_poly_offsets[0]
361 }
362 };
363
364 let ext_s_polygon = ExtSPolygon(
366 geo_poly_offset
367 .exterior()
368 .points()
369 .map(|p| (p.x() as f32, p.y() as f32))
370 .collect_vec(),
371 );
372
373 import::import_simple_polygon(&ext_s_polygon)
374}
375
376pub fn close_narrow_concavities(
378 orig_shape: &SPolygon,
379 mode: ShapeModifyMode,
380 (cutoff_distance_ratio, cutoff_area_ratio): (f32, f32),
381) -> SPolygon {
382 let mut n_concav_closed = 0;
383 let mut shape = orig_shape.clone();
384
385 for _ in 0..shape.n_vertices() {
386 let n_points = shape.n_vertices();
387
388 let calc_vert_elim = |i, j| {
389 if j > i {
390 j - i - 1
391 } else {
392 n_points - i + j - 1
393 }
394 };
395
396 let mut best_candidate = None;
397 for i in 0..n_points {
398 for j in 0..n_points {
399 if i == j || (i + 1) % n_points == j || (j + 1) % n_points == i {
400 continue; }
402 let c_edge = Edge::try_new(shape.vertex(i), shape.vertex(j))
404 .expect("invalid edge in string candidate")
405 .scale(0.9999); if c_edge.length() > cutoff_distance_ratio * shape.diameter {
408 continue;
410 }
411
412 if mode == ShapeModifyMode::Inflate
413 && (shape.collides_with(&c_edge.start) || shape.collides_with(&c_edge.end))
414 {
415 continue;
417 } else if mode == ShapeModifyMode::Deflate
418 && !(shape.collides_with(&c_edge.start) && shape.collides_with(&c_edge.end))
419 {
420 continue;
422 }
423
424 if shape.edge_iter().any(|e| e.collides_with(&c_edge)) {
425 continue;
427 }
428 let sub_shape_area = {
430 let sub_shape_points = if j > i {
431 shape.vertices[i..j].to_vec()
432 } else {
433 [&shape.vertices[i..], &shape.vertices[..j]].concat()
434 };
435 SPolygon::calculate_area(&sub_shape_points)
436 };
437 if sub_shape_area >= 0.0 {
438 continue;
440 }
441 if sub_shape_area.abs() > cutoff_area_ratio * shape.area {
442 continue;
444 }
445
446 match best_candidate {
448 None => {
449 best_candidate = Some((i, j));
451 }
452 Some((best_i, best_j)) => {
453 if calc_vert_elim(i, j) > calc_vert_elim(best_i, best_j) {
455 best_candidate = Some((i, j));
456 }
457 }
458 }
459 }
460 }
461 if let Some((i, j)) = best_candidate {
462 let mut ref_points = shape.vertices.clone();
463 let start = i as isize + 1;
464 let end = j as isize - 1;
465 debug!(
466 "[PS] closing concavity between points (idx: {}, {:?}) and (idx: {}, {:?}) with edge length {:.3} ({} vertices eliminated)",
467 i,
468 shape.vertex(i),
469 j,
470 shape.vertex(j),
471 Edge::try_new(shape.vertex(i), shape.vertex(j))
472 .expect("invalid edge in string candidate")
473 .length(),
474 calc_vert_elim(i, j)
475 );
476 if start <= end {
477 ref_points.drain((start as usize)..=(end as usize));
479 } else {
480 if (start as usize) < n_points {
482 ref_points.drain(start as usize..);
484 }
485 if end >= 0 {
486 ref_points.drain(0..=(end as usize));
488 }
489 }
490 shape = SPolygon::new(ref_points).expect("invalid shape after closing concavity");
491 n_concav_closed += 1;
492 } else {
493 break;
495 }
496 }
497
498 if n_concav_closed > 0 {
499 info!(
500 "[PS] [EXPERIMENTAL] closed {} concavities closer than {:.3}% of diameter and less than {:.3}% of area, reducing vertices from {} to {}",
501 n_concav_closed,
502 cutoff_distance_ratio * 100.0,
503 cutoff_area_ratio * 100.0,
504 orig_shape.n_vertices(),
505 shape.n_vertices()
506 );
507 }
508
509 shape
510}
511
512pub fn shape_modification_valid(orig: &SPolygon, simpl: &SPolygon, mode: ShapeModifyMode) -> bool {
513 let on_edge = |p: &Point| {
515 simpl
516 .edge_iter()
517 .any(|e| e.distance_to(p) < simpl.diameter * 1e-6)
518 };
519
520 for p in orig.vertices.iter().filter(|p| !simpl.vertices.contains(p)) {
521 let vertex_on_edge = on_edge(p);
522 let vertex_in_simpl = simpl.collides_with(p);
523
524 let error = match mode {
525 ShapeModifyMode::Inflate => !vertex_in_simpl && !vertex_on_edge,
526 ShapeModifyMode::Deflate => vertex_in_simpl && !vertex_on_edge,
527 };
528
529 if error {
530 error!(
531 "[PS] point {:?} from original shape is incorrect in simplified shape (original vertices: {:?}, simplified vertices: {:?})",
532 p,
533 orig.vertices.iter().map(|p| (p.0, p.1)).collect_vec(),
534 simpl.vertices.iter().map(|p| (p.0, p.1)).collect_vec()
535 );
536 return false; }
538 }
539 true
540}