1use geo::Buffer;
2use geo::algorithm::Area;
3use geo::algorithm::buffer::{BufferStyle, LineJoin};
4use itertools::Itertools;
5use log::{debug, error, info};
6use ordered_float::OrderedFloat;
7use rand_distr::num_traits::ToPrimitive;
8use serde::{Deserialize, Serialize};
9use std::cmp::Ordering;
10
11use crate::geometry::geo_traits::{CollidesWith, DistanceTo};
12use crate::geometry::primitives::Edge;
13use crate::geometry::primitives::Point;
14use crate::geometry::primitives::Rect;
15use crate::geometry::primitives::SPolygon;
16
17use crate::io::ext_repr::ExtSPolygon;
18use crate::io::import;
19use anyhow::{Result, bail};
20
21#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum ShapeModifyMode {
25 Inflate,
27 Deflate,
29}
30
31#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)]
32pub struct ShapeModifyConfig {
34 pub simplify_tolerance: Option<f32>,
38 pub offset: Option<f32>,
42 pub narrow_concavity_cutoff: Option<(f32, f32)>,
50}
51
52pub fn simplify_shape(
59 shape: &SPolygon,
60 mode: ShapeModifyMode,
61 max_area_change_ratio: f32,
62) -> SPolygon {
63 let original_area = shape.area;
64
65 let mut ref_points = shape.vertices.clone();
66
67 for _ in 0..shape.n_vertices() {
68 let n_points = ref_points.len().cast_signed();
69 if n_points < 4 {
70 break;
72 }
73
74 let mut corners = (0..n_points)
75 .map(|i| {
76 let i_prev = (i - 1).rem_euclid(n_points);
77 let i_next = (i + 1).rem_euclid(n_points);
78 Corner(
79 i_prev.cast_unsigned(),
80 i.cast_unsigned(),
81 i_next.cast_unsigned(),
82 )
83 })
84 .collect_vec();
85
86 if mode == ShapeModifyMode::Deflate {
87 corners.reverse();
90 corners.iter_mut().for_each(Corner::flip);
92 }
93
94 let mut candidates = vec![];
95
96 let mut prev_corner = corners.last().expect("corners is empty");
97 let mut prev_corner_type = CornerType::from(prev_corner.to_points(&ref_points));
98
99 for corner in &corners {
101 let corner_type = CornerType::from(corner.to_points(&ref_points));
102
103 match (&corner_type, &prev_corner_type) {
105 (CornerType::Concave, _) => candidates.push(Candidate::Concave(*corner)),
106 (CornerType::Collinear, _) => candidates.push(Candidate::Collinear(*corner)),
107 (CornerType::Convex, CornerType::Convex) => {
108 candidates.push(Candidate::ConvexConvex(*prev_corner, *corner));
109 }
110 (_, _) => {}
111 }
112 (prev_corner, prev_corner_type) = (corner, corner_type);
113 }
114
115 let best_candidate = candidates
117 .iter()
118 .sorted_by_cached_key(|c| {
119 OrderedFloat(calculate_area_delta(&ref_points, c).unwrap_or(f32::INFINITY))
120 })
121 .find(|c| candidate_is_valid(&ref_points, c, &shape.bbox));
122
123 if let Some(best_candidate) = best_candidate {
125 let new_shape = execute_candidate(&ref_points, best_candidate);
126 let new_shape_area = SPolygon::calculate_area(&new_shape);
127 let area_delta = (new_shape_area - original_area).abs() / original_area;
128 if area_delta <= max_area_change_ratio {
129 debug!(
130 "[PS] executed {:?} simplification causing {:.2}% area change",
131 best_candidate,
132 area_delta * 100.0
133 );
134 ref_points = new_shape;
135 } else {
136 break; }
138 } else {
139 break; }
141 }
142
143 let simpl_shape = SPolygon::new(ref_points).unwrap();
145
146 if simpl_shape.n_vertices() < shape.n_vertices() {
147 info!(
148 "[PS] simplified from {} to {} edges with {:.3}% area difference",
149 shape.n_vertices(),
150 simpl_shape.n_vertices(),
151 (simpl_shape.area - shape.area) / shape.area * 100.0
152 );
153 } else {
154 info!("[PS] no simplification possible within area change constraints");
155 }
156
157 simpl_shape
158}
159
160fn calculate_area_delta(shape: &[Point], candidate: &Candidate) -> Result<f32, InvalidCandidate> {
161 let area = match candidate {
163 Candidate::Collinear(_) => 0.0,
164 Candidate::Concave(c) => {
165 let Point(x0, y0) = shape[c.0];
167 let Point(x1, y1) = shape[c.1];
168 let Point(x2, y2) = shape[c.2];
169
170 let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
171
172 area.abs()
173 }
174 Candidate::ConvexConvex(c1, c2) => {
175 let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))?;
176
177 let Point(x0, y0) = shape[c1.1];
179 let Point(x1, y1) = replacing_vertex;
180 let Point(x2, y2) = shape[c2.1];
181
182 let area = (x0 * y1 + x1 * y2 + x2 * y0 - x0 * y2 - x1 * y0 - x2 * y1) / 2.0;
183
184 area.abs()
185 }
186 };
187 Ok(area)
188}
189
190fn candidate_is_valid(shape: &[Point], candidate: &Candidate, bbox: &Rect) -> bool {
191 match candidate {
193 Candidate::Collinear(_) => true,
194 Candidate::Concave(c) => {
195 let new_edge = Edge::try_new(shape[c.0], shape[c.2]).unwrap();
196 let affected_points = [shape[c.0], shape[c.1], shape[c.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))
203 }
204 Candidate::ConvexConvex(c1, c2) => {
205 match replacing_vertex_convex_convex_candidate(shape, (*c1, *c2)) {
206 Err(_) => false,
207 Ok(new_vertex) if !bbox.collides_with(&new_vertex) => false,
210 Ok(new_vertex) => {
211 let new_edge_1 = Edge::try_new(shape[c1.0], new_vertex).unwrap();
212 let new_edge_2 = Edge::try_new(new_vertex, shape[c2.2]).unwrap();
213
214 let affected_points = [shape[c1.1], shape[c1.0], shape[c2.1], shape[c2.2]];
215
216 edge_iter(shape)
218 .filter(|l| !affected_points.contains(&l.start))
219 .filter(|l| !affected_points.contains(&l.end))
220 .all(|l| !l.collides_with(&new_edge_1) && !l.collides_with(&new_edge_2))
221 }
222 }
223 }
224 }
225}
226
227fn edge_iter(points: &[Point]) -> impl Iterator<Item = Edge> + '_ {
228 let n_points = points.len();
229 (0..n_points).map(move |i| {
230 let j = (i + 1) % n_points;
231 Edge::try_new(points[i], points[j]).unwrap()
232 })
233}
234
235fn execute_candidate(shape: &[Point], candidate: &Candidate) -> Vec<Point> {
236 let mut points = shape.iter().copied().collect_vec();
237 match candidate {
238 Candidate::Collinear(c) | Candidate::Concave(c) => {
239 points.remove(c.1);
240 }
241 Candidate::ConvexConvex(c1, c2) => {
242 let replacing_vertex = replacing_vertex_convex_convex_candidate(shape, (*c1, *c2))
243 .expect("invalid candidate cannot be executed");
244 points.remove(c1.1);
245 let other_index = if c1.1 < c2.1 { c2.1 - 1 } else { c2.1 };
246 points.remove(other_index);
247 points.insert(other_index, replacing_vertex);
248 }
249 }
250 points
251}
252
253fn replacing_vertex_convex_convex_candidate(
254 shape: &[Point],
255 (c1, c2): (Corner, Corner),
256) -> Result<Point, InvalidCandidate> {
257 assert_eq!(c1.2, c2.1, "non-consecutive corners {c1:?},{c2:?}");
258 assert_eq!(c1.1, c2.0, "non-consecutive corners {c1:?},{c2:?}");
259
260 let edge_prev = Edge::try_new(shape[c1.0], shape[c1.1]).unwrap();
261 let edge_next = Edge::try_new(shape[c2.2], shape[c2.1]).unwrap();
262
263 calculate_intersection_in_front(&edge_prev, &edge_next).ok_or(InvalidCandidate)
264}
265
266fn calculate_intersection_in_front(l1: &Edge, l2: &Edge) -> Option<Point> {
267 let Point(x1, y1) = l1.start;
272 let Point(x2, y2) = l1.end;
273 let Point(x3, y3) = l2.start;
274 let Point(x4, y4) = l2.end;
275
276 let t_nom = (x2 - x4) * (y4 - y3) - (y2 - y4) * (x4 - x3);
280 let t_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
281
282 let u_nom = (x2 - x4) * (y2 - y1) - (y2 - y4) * (x2 - x1);
283 let u_denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
284
285 let t = if t_denom == 0.0 { 0.0 } else { t_nom / t_denom };
286
287 let u = if u_denom == 0.0 { 0.0 } else { u_nom / u_denom };
288
289 if t < 0.0 && u < 0.0 {
290 Some(Point(x2 + t * (x1 - x2), y2 + t * (y1 - y2)))
292 } else {
293 None
295 }
296}
297
298#[derive(Debug, Clone)]
299struct InvalidCandidate;
300
301#[derive(Clone, Debug, PartialEq)]
302enum Candidate {
303 Concave(Corner),
304 ConvexConvex(Corner, Corner),
305 Collinear(Corner),
306}
307
308#[derive(Clone, Copy, Debug, PartialEq)]
309struct Corner(pub usize, pub usize, pub usize);
311
312impl Corner {
313 pub fn flip(&mut self) {
314 std::mem::swap(&mut self.0, &mut self.2);
315 }
316
317 pub fn to_points(self, points: &[Point]) -> [Point; 3] {
318 [points[self.0], points[self.1], points[self.2]]
319 }
320}
321
322#[derive(Clone, Copy, Debug, PartialEq)]
323enum CornerType {
324 Concave,
325 Convex,
326 Collinear,
327}
328
329impl CornerType {
330 pub fn from([p1, p2, p3]: [Point; 3]) -> Self {
331 let p1p2 = (p2.0 - p1.0, p2.1 - p1.1);
335 let p1p3 = (p3.0 - p1.0, p3.1 - p1.1);
336 let cross_prod = p1p2.0 * p1p3.1 - p1p2.1 * p1p3.0;
337
338 match cross_prod.partial_cmp(&0.0).expect("cross product is NaN") {
340 Ordering::Less => CornerType::Concave,
341 Ordering::Equal => CornerType::Collinear,
342 Ordering::Greater => CornerType::Convex,
343 }
344 }
345}
346
347pub fn offset_shape(sp: &SPolygon, mode: ShapeModifyMode, distance: f32) -> Result<SPolygon> {
350 let offset = match mode {
351 ShapeModifyMode::Deflate => -distance,
352 ShapeModifyMode::Inflate => distance,
353 };
354
355 let geo_poly = geo_types::Polygon::new(
357 sp.vertices
358 .iter()
359 .map(|p| (f64::from(p.0), f64::from(p.1)))
360 .collect(),
361 vec![],
362 );
363
364 let style = BufferStyle::new(f64::from(offset)).line_join(LineJoin::Round(0.1));
367 let geo_poly_offsets = geo_poly.buffer_with_style(style).0;
368
369 let geo_poly_offset = match geo_poly_offsets.len() {
370 0 => bail!("Offset resulted in an empty polygon"),
371 1 => &geo_poly_offsets[0],
372 n => {
373 let areas = geo_poly_offsets
376 .iter()
377 .map(Area::unsigned_area)
378 .collect_vec();
379 let largest_idx = areas
380 .iter()
381 .position_max_by_key(|a| OrderedFloat(**a))
382 .unwrap();
383 let dropped_area = areas.iter().sum::<f64>() - areas[largest_idx];
384 error!(
385 "Offset split the shape into {n} disconnected parts, keeping the largest (area {:.3}) and dropping {} (total area {dropped_area:.3})",
386 areas[largest_idx],
387 n - 1
388 );
389 &geo_poly_offsets[largest_idx]
390 }
391 };
392
393 let ext_s_polygon = ExtSPolygon(
395 geo_poly_offset
396 .exterior()
397 .points()
398 .map(|p| (p.x().to_f32().unwrap(), p.y().to_f32().unwrap()))
399 .collect_vec(),
400 );
401
402 import::import_simple_polygon(&ext_s_polygon)
403}
404
405#[allow(clippy::too_many_lines)]
406#[must_use]
408pub fn close_narrow_concavities(
409 orig_shape: &SPolygon,
410 mode: ShapeModifyMode,
411 (cutoff_distance_ratio, cutoff_area_ratio): (f32, f32),
412) -> SPolygon {
413 let mut n_concav_closed = 0;
414 let mut shape = orig_shape.clone();
415
416 for _ in 0..shape.n_vertices() {
417 let n_points = shape.n_vertices();
418
419 let calc_vert_elim = |i, j| {
420 if j > i {
421 j - i - 1
422 } else {
423 n_points - i + j - 1
424 }
425 };
426
427 let mut best_candidate = None;
428 for i in 0..n_points {
429 for j in 0..n_points {
430 if i == j || (i + 1) % n_points == j || (j + 1) % n_points == i {
431 continue; }
433 let c_edge = Edge::try_new(shape.vertex(i), shape.vertex(j))
435 .expect("invalid edge in string candidate")
436 .scale(0.9999); if c_edge.length() > cutoff_distance_ratio * shape.diameter {
439 continue;
441 }
442
443 if mode == ShapeModifyMode::Inflate
444 && (shape.collides_with(&c_edge.start) || shape.collides_with(&c_edge.end))
445 {
446 continue;
448 }
449
450 if mode == ShapeModifyMode::Deflate
451 && !(shape.collides_with(&c_edge.start) && shape.collides_with(&c_edge.end))
452 {
453 continue;
455 }
456
457 if shape.edge_iter().any(|e| e.collides_with(&c_edge)) {
458 continue;
460 }
461 let sub_shape_area = {
463 let sub_shape_points = if j > i {
464 shape.vertices[i..j].to_vec()
465 } else {
466 [&shape.vertices[i..], &shape.vertices[..j]].concat()
467 };
468 SPolygon::calculate_area(&sub_shape_points)
469 };
470 if sub_shape_area >= 0.0 {
471 continue;
473 }
474 if sub_shape_area.abs() > cutoff_area_ratio * shape.area {
475 continue;
477 }
478
479 match best_candidate {
481 None => {
482 best_candidate = Some((i, j));
484 }
485 Some((best_i, best_j)) => {
486 if calc_vert_elim(i, j) > calc_vert_elim(best_i, best_j) {
488 best_candidate = Some((i, j));
489 }
490 }
491 }
492 }
493 }
494 if let Some((i, j)) = best_candidate {
495 let mut ref_points = shape.vertices.clone();
496 let start = i.cast_signed() + 1;
497 let end = j.cast_signed() - 1;
498 debug!(
499 "[PS] closing concavity between points (idx: {}, {:?}) and (idx: {}, {:?}) with edge length {:.3} ({} vertices eliminated)",
500 i,
501 shape.vertex(i),
502 j,
503 shape.vertex(j),
504 Edge::try_new(shape.vertex(i), shape.vertex(j))
505 .expect("invalid edge in string candidate")
506 .length(),
507 calc_vert_elim(i, j)
508 );
509 if start <= end {
510 ref_points.drain(start.cast_unsigned()..=end.cast_unsigned());
512 } else {
513 if start.cast_unsigned() < n_points {
515 ref_points.drain(start.cast_unsigned()..);
517 }
518 if end >= 0 {
519 ref_points.drain(0..=end.cast_unsigned());
521 }
522 }
523 shape = SPolygon::new(ref_points).expect("invalid shape after closing concavity");
524 n_concav_closed += 1;
525 } else {
526 break;
528 }
529 }
530
531 if n_concav_closed > 0 {
532 info!(
533 "[PS] [EXPERIMENTAL] closed {} concavities closer than {:.3}% of diameter and less than {:.3}% of area, reducing vertices from {} to {}",
534 n_concav_closed,
535 cutoff_distance_ratio * 100.0,
536 cutoff_area_ratio * 100.0,
537 orig_shape.n_vertices(),
538 shape.n_vertices()
539 );
540 }
541
542 shape
543}
544
545#[must_use]
546pub fn shape_modification_valid(orig: &SPolygon, simpl: &SPolygon, mode: ShapeModifyMode) -> bool {
547 let on_edge = |p: &Point| {
549 simpl
550 .edge_iter()
551 .any(|e| e.distance_to(p) < simpl.diameter * 1e-6)
552 };
553
554 for p in orig.vertices.iter().filter(|p| !simpl.vertices.contains(p)) {
555 let vertex_on_edge = on_edge(p);
556 let vertex_in_simpl = simpl.collides_with(p);
557
558 let error = match mode {
559 ShapeModifyMode::Inflate => !vertex_in_simpl && !vertex_on_edge,
560 ShapeModifyMode::Deflate => vertex_in_simpl && !vertex_on_edge,
561 };
562
563 if error {
564 error!(
565 "[PS] point {:?} from original shape is incorrect in simplified shape (original vertices: {:?}, simplified vertices: {:?})",
566 p,
567 orig.vertices.iter().map(|p| (p.0, p.1)).collect_vec(),
568 simpl.vertices.iter().map(|p| (p.0, p.1)).collect_vec()
569 );
570 return false; }
572 }
573 true
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn simplification_does_not_grow_bounding_box() {
582 let points = [
586 (0.0, 0.0),
587 (15.0, 0.0),
588 (30.0, 30.0),
589 (45.0, 0.0),
590 (557.3, 0.0),
591 (557.3, 970.0),
592 (0.0, 970.0),
593 ];
594 let shape = SPolygon::new(points.map(|(x, y)| Point(x, y)).to_vec()).unwrap();
595
596 let simplified = simplify_shape(&shape, ShapeModifyMode::Inflate, 0.001);
597
598 assert!(simplified.n_vertices() < shape.n_vertices());
599 assert!(shape_modification_valid(
600 &shape,
601 &simplified,
602 ShapeModifyMode::Inflate
603 ));
604 assert_eq!(simplified.bbox, shape.bbox);
605 }
606}