1use alloc::vec::Vec;
9
10#[cfg(not(feature = "std"))]
11use geometry_coords::math::Float;
12use geometry_coords::precise_math;
13use geometry_model::{Polygon, Ring};
14use geometry_strategy::{CollectPoints, ConvexHullStrategy, MonotoneChain};
15use geometry_trait::{Point, PointMut};
16
17use crate::convex_hull::convex_hull;
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct ConcaveHullParams {
22 pub concavity: f64,
25 pub length_threshold: f64,
27}
28
29impl Default for ConcaveHullParams {
30 fn default() -> Self {
31 Self {
32 concavity: 2.0,
33 length_threshold: 0.0,
34 }
35 }
36}
37
38#[inline]
40#[must_use]
41pub fn concave_hull<G, P>(geometry: &G) -> Polygon<P>
42where
43 G: CollectPoints<Point = P>,
44 P: Point<Scalar = f64> + PointMut + Default + Copy,
45 MonotoneChain: ConvexHullStrategy<G, Output = Ring<P, true, true>>,
46{
47 concave_hull_with(geometry, ConcaveHullParams::default())
48}
49
50#[inline]
52#[must_use]
53pub fn concave_hull_with<G, P>(geometry: &G, parameters: ConcaveHullParams) -> Polygon<P>
54where
55 G: CollectPoints<Point = P>,
56 P: Point<Scalar = f64> + PointMut + Default + Copy,
57 MonotoneChain: ConvexHullStrategy<G, Output = Ring<P, true, true>>,
58{
59 refine_hull(geometry, parameters, None)
60}
61
62#[inline]
68#[must_use]
69pub fn k_nearest_concave_hull<G, P>(geometry: &G, k: usize) -> Polygon<P>
70where
71 G: CollectPoints<Point = P>,
72 P: Point<Scalar = f64> + PointMut + Default + Copy,
73 MonotoneChain: ConvexHullStrategy<G, Output = Ring<P, true, true>>,
74{
75 if k == 0 {
76 return Polygon::new(convex_hull(geometry));
77 }
78 refine_hull(
79 geometry,
80 ConcaveHullParams {
81 concavity: f64::INFINITY,
82 length_threshold: 0.0,
83 },
84 Some(k),
85 )
86}
87
88fn refine_hull<G, P>(
89 geometry: &G,
90 parameters: ConcaveHullParams,
91 nearest_limit: Option<usize>,
92) -> Polygon<P>
93where
94 G: CollectPoints<Point = P>,
95 P: Point<Scalar = f64> + PointMut + Default + Copy,
96 MonotoneChain: ConvexHullStrategy<G, Output = Ring<P, true, true>>,
97{
98 let mut all_points = Vec::new();
99 geometry.collect_points(&mut all_points);
100 deduplicate(&mut all_points);
101
102 let mut boundary = convex_hull(geometry).0;
103 while boundary.len() > 1 && same_xy(boundary.first(), boundary.last()) {
104 boundary.pop();
105 }
106 if boundary.len() < 3 {
107 close(&mut boundary);
108 return Polygon::new(Ring::from_vec(boundary));
109 }
110
111 let mut candidates: Vec<P> = all_points
112 .into_iter()
113 .filter(|point| !boundary.iter().any(|hull| same_point(point, hull)))
114 .collect();
115 let concavity = parameters.concavity.max(1.0);
116 let length_threshold = parameters.length_threshold.max(0.0);
117
118 while !candidates.is_empty() {
119 let mut best: Option<Insertion> = None;
120 for edge in 0..boundary.len() {
121 let first = boundary[edge];
122 let second = boundary[(edge + 1) % boundary.len()];
123 let edge_length = distance(first, second);
124 if edge_length <= length_threshold.max(f64::EPSILON) {
125 continue;
126 }
127
128 let mut candidate_indices: Vec<usize> = (0..candidates.len()).collect();
129 candidate_indices.sort_by(|&left, &right| {
130 midpoint_distance(first, second, candidates[left]).total_cmp(&midpoint_distance(
131 first,
132 second,
133 candidates[right],
134 ))
135 });
136 if let Some(limit) = nearest_limit {
137 candidate_indices.truncate(limit.min(candidate_indices.len()));
138 }
139
140 for candidate_index in candidate_indices {
141 let candidate = candidates[candidate_index];
142 let detour =
143 (distance(first, candidate) + distance(candidate, second)) / edge_length;
144 if detour > concavity
145 || point_segment_distance(candidate, first, second) <= f64::EPSILON
146 || !insertion_is_simple(&boundary, edge, candidate)
147 {
148 continue;
149 }
150 let score = point_segment_distance(candidate, first, second);
151 let insertion = Insertion {
152 edge,
153 candidate: candidate_index,
154 score,
155 };
156 if best.is_none_or(|current| insertion.score < current.score) {
157 best = Some(insertion);
158 }
159 }
160 }
161
162 let Some(insertion) = best else {
163 break;
164 };
165 let point = candidates.swap_remove(insertion.candidate);
166 boundary.insert(insertion.edge + 1, point);
167 }
168
169 close(&mut boundary);
170 Polygon::new(Ring::from_vec(boundary))
171}
172
173#[derive(Clone, Copy)]
174struct Insertion {
175 edge: usize,
176 candidate: usize,
177 score: f64,
178}
179
180fn deduplicate<P: Point<Scalar = f64> + Copy>(points: &mut Vec<P>) {
181 let mut unique = Vec::with_capacity(points.len());
182 for point in points.iter().copied() {
183 if !unique.iter().any(|other| same_point(&point, other)) {
184 unique.push(point);
185 }
186 }
187 *points = unique;
188}
189
190fn close<P: Copy>(points: &mut Vec<P>) {
191 if let Some(first) = points.first().copied() {
192 points.push(first);
193 }
194}
195
196fn insertion_is_simple<P>(boundary: &[P], edge: usize, candidate: P) -> bool
197where
198 P: Point<Scalar = f64> + Copy,
199{
200 let first = boundary[edge];
201 let second_index = (edge + 1) % boundary.len();
202 let second = boundary[second_index];
203 for other_edge in 0..boundary.len() {
204 if other_edge == edge {
205 continue;
206 }
207 let other_first_index = other_edge;
208 let other_second_index = (other_edge + 1) % boundary.len();
209 let other_first = boundary[other_first_index];
210 let other_second = boundary[other_second_index];
211
212 let first_segment_shares_endpoint = other_first_index == edge || other_second_index == edge;
213 if !first_segment_shares_endpoint
214 && segments_intersect(first, candidate, other_first, other_second)
215 {
216 return false;
217 }
218 let second_segment_shares_endpoint =
219 other_first_index == second_index || other_second_index == second_index;
220 if !second_segment_shares_endpoint
221 && segments_intersect(candidate, second, other_first, other_second)
222 {
223 return false;
224 }
225 }
226 true
227}
228
229fn segments_intersect<P>(a: P, b: P, c: P, d: P) -> bool
230where
231 P: Point<Scalar = f64> + Copy,
232{
233 let ab_c = orientation(a, b, c);
234 let ab_d = orientation(a, b, d);
235 let cd_a = orientation(c, d, a);
236 let cd_b = orientation(c, d, b);
237 if ab_c == 0.0 && on_segment(a, b, c) {
238 return true;
239 }
240 if ab_d == 0.0 && on_segment(a, b, d) {
241 return true;
242 }
243 if cd_a == 0.0 && on_segment(c, d, a) {
244 return true;
245 }
246 if cd_b == 0.0 && on_segment(c, d, b) {
247 return true;
248 }
249 (ab_c > 0.0) != (ab_d > 0.0) && (cd_a > 0.0) != (cd_b > 0.0)
250}
251
252#[allow(
253 clippy::needless_pass_by_value,
254 reason = "the hull operates on Copy point handles throughout"
255)]
256fn orientation<P: Point<Scalar = f64>>(first: P, second: P, third: P) -> f64 {
257 precise_math::orient2d(
258 [first.get::<0>(), first.get::<1>()],
259 [second.get::<0>(), second.get::<1>()],
260 [third.get::<0>(), third.get::<1>()],
261 )
262}
263
264#[allow(
265 clippy::needless_pass_by_value,
266 reason = "the hull operates on Copy point handles throughout"
267)]
268fn on_segment<P: Point<Scalar = f64>>(first: P, second: P, point: P) -> bool {
269 point.get::<0>() >= first.get::<0>().min(second.get::<0>())
270 && point.get::<0>() <= first.get::<0>().max(second.get::<0>())
271 && point.get::<1>() >= first.get::<1>().min(second.get::<1>())
272 && point.get::<1>() <= first.get::<1>().max(second.get::<1>())
273}
274
275#[allow(
276 clippy::needless_pass_by_value,
277 reason = "the hull operates on Copy point handles throughout"
278)]
279fn midpoint_distance<P: Point<Scalar = f64>>(first: P, second: P, point: P) -> f64 {
280 let x = first.get::<0>() / 2.0 + second.get::<0>() / 2.0 - point.get::<0>();
281 let y = first.get::<1>() / 2.0 + second.get::<1>() / 2.0 - point.get::<1>();
282 x.hypot(y)
283}
284
285#[allow(
286 clippy::needless_pass_by_value,
287 reason = "the hull operates on Copy point handles throughout"
288)]
289fn point_segment_distance<P: Point<Scalar = f64>>(point: P, first: P, second: P) -> f64 {
290 let dx = second.get::<0>() - first.get::<0>();
291 let dy = second.get::<1>() - first.get::<1>();
292 let length_squared = dx * dx + dy * dy;
293 if length_squared <= f64::EPSILON {
294 return distance(point, first);
295 }
296 let projection = ((point.get::<0>() - first.get::<0>()) * dx
297 + (point.get::<1>() - first.get::<1>()) * dy)
298 / length_squared;
299 let projection = projection.clamp(0.0, 1.0);
300 let x = first.get::<0>() + projection * dx;
301 let y = first.get::<1>() + projection * dy;
302 (point.get::<0>() - x).hypot(point.get::<1>() - y)
303}
304
305#[allow(
306 clippy::needless_pass_by_value,
307 reason = "the hull operates on Copy point handles throughout"
308)]
309fn distance<P: Point<Scalar = f64>>(first: P, second: P) -> f64 {
310 (second.get::<0>() - first.get::<0>()).hypot(second.get::<1>() - first.get::<1>())
311}
312
313fn same_xy<P: Point<Scalar = f64>>(first: Option<&P>, second: Option<&P>) -> bool {
314 first
315 .zip(second)
316 .is_some_and(|(first, second)| same_point(first, second))
317}
318
319#[allow(
320 clippy::float_cmp,
321 reason = "coordinate identity, not approximate geometric equality, is required"
322)]
323fn same_point<P: Point<Scalar = f64>>(first: &P, second: &P) -> bool {
324 first.get::<0>() == second.get::<0>() && first.get::<1>() == second.get::<1>()
325}
326
327#[cfg(test)]
328mod tests {
329 use geometry_cs::Cartesian;
330 use geometry_model::{MultiPoint, Point2D};
331
332 use super::*;
333 use crate::area::area;
334
335 #[test]
336 fn square_digs_toward_an_interior_point() {
337 type P = Point2D<f64, Cartesian>;
338 let points = MultiPoint::from_vec(alloc::vec![
339 P::new(0.0, 0.0),
340 P::new(0.0, 4.0),
341 P::new(4.0, 4.0),
342 P::new(4.0, 0.0),
343 P::new(2.0, 1.0),
344 ]);
345 let hull = concave_hull_with(
346 &points,
347 ConcaveHullParams {
348 concavity: 1.2,
349 length_threshold: 0.0,
350 },
351 );
352 assert!(hull.outer.0.contains(&P::new(2.0, 1.0)));
353 assert!(area(&hull).abs() < 16.0);
354 }
355
356 #[test]
357 fn private_intersection_guards_cover_invalid_insertions() {
358 type P = Point2D<f64, Cartesian>;
359 let boundary = [
360 P::new(0.0, 0.0),
361 P::new(0.0, 4.0),
362 P::new(4.0, 4.0),
363 P::new(4.0, 0.0),
364 ];
365 assert!(!insertion_is_simple(&boundary, 0, P::new(5.0, 2.0)));
366 assert!(!insertion_is_simple(&boundary, 0, P::new(5.0, -1.0)));
367
368 assert!(segments_intersect(
369 P::new(0.0, 0.0),
370 P::new(2.0, 0.0),
371 P::new(1.0, 0.0),
372 P::new(1.0, 1.0),
373 ));
374 assert!(segments_intersect(
375 P::new(0.0, 0.0),
376 P::new(2.0, 0.0),
377 P::new(1.0, 1.0),
378 P::new(1.0, 0.0),
379 ));
380 assert!(segments_intersect(
381 P::new(1.0, 0.0),
382 P::new(1.0, 1.0),
383 P::new(0.0, 0.0),
384 P::new(2.0, 0.0),
385 ));
386 assert!(segments_intersect(
387 P::new(1.0, 1.0),
388 P::new(1.0, 0.0),
389 P::new(0.0, 0.0),
390 P::new(2.0, 0.0),
391 ));
392 let distance = point_segment_distance(P::new(3.0, 4.0), P::new(0.0, 0.0), P::new(0.0, 0.0));
393 assert!((distance - 5.0).abs() < f64::EPSILON);
394 }
395}