doc_quad/geom/
simplify.rs1use geo::ConvexHull;
3use geo::Simplify;
4use geo_types::{Coord, LineString, Polygon};
5use std::time::Instant;
6
7pub struct GeometrySimplifier;
9
10impl GeometrySimplifier {
11 pub fn simplify_to_quad(contour: Vec<Coord<f32>>) -> Option<LineString<f32>> {
28 let start = Instant::now();
29
30 if contour.len() < 4 {
31 log::debug!(
32 "[Geom::Simplify] - Contour too short (len={}), skipping.",
33 contour.len()
34 );
35 return None;
36 }
37
38 let original_len = contour.len();
39
40 let mut coords = contour;
42 if coords.first() != coords.last() {
43 if let Some(&first) = coords.first() {
44 coords.push(first);
45 }
46 }
47
48 let line_string = LineString::new(coords);
49
50 let perimeter = geo::EuclideanLength::euclidean_length(&line_string);
52 if perimeter < f32::EPSILON {
53 log::debug!(
54 "[Geom::Simplify] - Contour has zero perimeter (len={}), skipping.",
55 original_len
56 );
57 return None;
58 }
59
60 let polygon = Polygon::new(line_string.clone(), vec![]);
64 let hull: LineString<f32> = polygon.convex_hull().exterior().clone();
65 let hull_point_count = hull.0.len();
66
67 log::debug!(
68 "[Geom::Simplify] - Convex hull: original_len={}, hull_points={}, perimeter={:.1}",
69 original_len,
70 hull_point_count,
71 perimeter
72 );
73
74 if hull_point_count < 5 {
76 log::debug!(
77 "[Geom::Simplify] - Hull degenerate (hull_points={} < 5), skipping.",
78 hull_point_count
79 );
80 return None;
81 }
82
83 let hull_perimeter = geo::EuclideanLength::euclidean_length(&hull);
84
85 let epsilon_ratios: &[f32] = &[0.01, 0.02, 0.03, 0.05, 0.08, 0.12, 0.20, 0.30, 0.40];
87
88 for &ratio in epsilon_ratios {
89 let epsilon = hull_perimeter * ratio;
90 let simplified = hull.simplify(&epsilon);
91 let simplified_len = simplified.0.len();
92
93 log::trace!(
94 "[Geom::Simplify] - DP trial: ratio={:.0}%, epsilon={:.1}, simplified_points={}",
95 ratio * 100.0,
96 epsilon,
97 simplified_len
98 );
99
100 if simplified_len == 5 {
101 log::debug!(
102 "[Geom::Simplify] - Found quad via convex-hull+DP at epsilon_ratio={:.0}%. \
103 original_len={}, hull_points={}, Elapsed: {}µs",
104 ratio * 100.0,
105 original_len,
106 hull_point_count,
107 start.elapsed().as_micros()
108 );
109 return Some(simplified);
110 }
111 }
112
113 log::debug!(
114 "[Geom::Simplify] - DP failed to find 5-point result for hull (hull_points={}). \
115 Falling back to extreme-point method. Elapsed: {}µs",
116 hull_point_count,
117 start.elapsed().as_micros()
118 );
119
120 let result = Self::extract_quad_by_extremes(&hull);
122
123 if result.is_some() {
124 log::debug!(
125 "[Geom::Simplify] - Extreme-point fallback SUCCEEDED (hull_points={}). Elapsed: {}µs",
126 hull_point_count,
127 start.elapsed().as_micros()
128 );
129 } else {
130 log::debug!(
131 "[Geom::Simplify] - Extreme-point fallback FAILED (hull_points={}). Elapsed: {}µs",
132 hull_point_count,
133 start.elapsed().as_micros()
134 );
135 }
136
137 result
138 }
139
140 fn extract_quad_by_extremes(line_string: &LineString<f32>) -> Option<LineString<f32>> {
153 let pts = &line_string.0;
154 if pts.len() < 4 {
155 return None;
156 }
157
158 let tl = pts.iter().min_by(|a, b| {
164 (a.x + a.y)
165 .partial_cmp(&(b.x + b.y))
166 .unwrap_or(std::cmp::Ordering::Equal)
167 })?;
168 let tr = pts.iter().max_by(|a, b| {
169 (a.x - a.y)
170 .partial_cmp(&(b.x - b.y))
171 .unwrap_or(std::cmp::Ordering::Equal)
172 })?;
173 let br = pts.iter().max_by(|a, b| {
174 (a.x + a.y)
175 .partial_cmp(&(b.x + b.y))
176 .unwrap_or(std::cmp::Ordering::Equal)
177 })?;
178 let bl = pts.iter().min_by(|a, b| {
179 (a.x - a.y)
180 .partial_cmp(&(b.x - b.y))
181 .unwrap_or(std::cmp::Ordering::Equal)
182 })?;
183
184 log::debug!(
185 "[Geom::Simplify] - Extreme points: TL=({:.1},{:.1}), TR=({:.1},{:.1}), BR=({:.1},{:.1}), BL=({:.1},{:.1})",
186 tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y
187 );
188
189 let corners = [tl, tr, br, bl];
194 for i in 0..4 {
195 for j in (i + 1)..4 {
196 let dx = corners[i].x - corners[j].x;
197 let dy = corners[i].y - corners[j].y;
198 let dist = (dx * dx + dy * dy).sqrt();
199 if dist < 5.0 {
200 log::warn!(
201 "[Geom::Simplify] - Extreme-point fallback produced degenerate quad: \
202 corners[{}]=({:.1},{:.1}) and corners[{}]=({:.1},{:.1}) are too close (dist={:.1}px < 5px).",
203 i, corners[i].x, corners[i].y,
204 j, corners[j].x, corners[j].y,
205 dist
206 );
207 return None;
208 }
209 }
210 }
211
212 let quad_coords = vec![*tl, *tr, *br, *bl, *tl];
214 Some(LineString::new(quad_coords))
215 }
216}