rust_igraph/algorithms/spatial/
convex_hull.rs1use crate::core::{IgraphError, IgraphResult};
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct ConvexHullResult {
15 pub hull_vertices: Vec<usize>,
19 pub hull_coords: Vec<(f64, f64)>,
21}
22
23pub fn convex_hull_2d(points: &[(f64, f64)]) -> IgraphResult<ConvexHullResult> {
44 let n = points.len();
45
46 if n == 0 {
47 return Ok(ConvexHullResult {
48 hull_vertices: Vec::new(),
49 hull_coords: Vec::new(),
50 });
51 }
52
53 for (i, &(x, y)) in points.iter().enumerate() {
54 if x.is_nan() || y.is_nan() {
55 return Err(IgraphError::InvalidArgument(format!(
56 "convex_hull_2d: NaN coordinate at index {i}"
57 )));
58 }
59 }
60
61 if n <= 2 {
62 let hull_vertices: Vec<usize> = (0..n).collect();
63 let hull_coords: Vec<(f64, f64)> = points.to_vec();
64 return Ok(ConvexHullResult {
65 hull_vertices,
66 hull_coords,
67 });
68 }
69
70 let mut pivot_idx: usize = 0;
72 for i in 1..n {
73 let cmp_y = points[i].1.total_cmp(&points[pivot_idx].1);
74 let cmp_x = points[i].0.total_cmp(&points[pivot_idx].0);
75 if cmp_y == std::cmp::Ordering::Less
76 || (cmp_y == std::cmp::Ordering::Equal && cmp_x == std::cmp::Ordering::Less)
77 {
78 pivot_idx = i;
79 }
80 }
81
82 let px = points[pivot_idx].0;
83 let py = points[pivot_idx].1;
84
85 let mut angles: Vec<f64> = Vec::with_capacity(n);
87 for (i, &(x, y)) in points.iter().enumerate() {
88 if i == pivot_idx {
89 angles.push(f64::NEG_INFINITY);
90 } else {
91 angles.push(f64::atan2(y - py, x - px));
92 }
93 }
94
95 let mut order: Vec<usize> = (0..n).collect();
97 order.sort_by(|&a, &b| {
98 angles[a].total_cmp(&angles[b]).then_with(|| {
99 let da = dist_sq(px, py, points[a].0, points[a].1);
100 let db = dist_sq(px, py, points[b].0, points[b].1);
101 da.total_cmp(&db)
102 })
103 });
104
105 let mut filtered: Vec<usize> = Vec::with_capacity(n);
107 let mut i = 0;
108 while i < order.len() {
109 let mut j = i;
110 while j + 1 < order.len()
111 && angles[order[j + 1]].total_cmp(&angles[order[i]]) == std::cmp::Ordering::Equal
112 {
113 j += 1;
114 }
115 filtered.push(order[j]);
117 i = j + 1;
118 }
119
120 if filtered.len() <= 2 {
121 let hull_vertices = filtered;
122 let hull_coords: Vec<(f64, f64)> = hull_vertices.iter().map(|&i| points[i]).collect();
123 return Ok(ConvexHullResult {
124 hull_vertices,
125 hull_coords,
126 });
127 }
128
129 let mut stack: Vec<usize> = Vec::with_capacity(filtered.len());
131 stack.push(filtered[0]);
132 stack.push(filtered[1]);
133
134 for &idx in filtered.iter().skip(2) {
135 while stack.len() >= 2 {
136 let top = stack[stack.len() - 1];
137 let below = stack[stack.len() - 2];
138 let cp = cross_product(points[below], points[top], points[idx]);
139 if cp > 0.0 {
140 break;
141 }
142 stack.pop();
143 }
144 stack.push(idx);
145 }
146
147 let hull_coords: Vec<(f64, f64)> = stack.iter().map(|&i| points[i]).collect();
148 Ok(ConvexHullResult {
149 hull_vertices: stack,
150 hull_coords,
151 })
152}
153
154fn dist_sq(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
155 (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
156}
157
158fn cross_product(o: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
159 (a.0 - o.0) * (b.1 - o.1) - (b.0 - o.0) * (a.1 - o.1)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn empty() {
168 let r = convex_hull_2d(&[]).unwrap();
169 assert!(r.hull_vertices.is_empty());
170 assert!(r.hull_coords.is_empty());
171 }
172
173 #[test]
174 fn single_point() {
175 let r = convex_hull_2d(&[(1.0, 2.0)]).unwrap();
176 assert_eq!(r.hull_vertices, vec![0]);
177 assert_eq!(r.hull_coords, vec![(1.0, 2.0)]);
178 }
179
180 #[test]
181 fn two_points() {
182 let r = convex_hull_2d(&[(0.0, 0.0), (1.0, 1.0)]).unwrap();
183 assert_eq!(r.hull_vertices.len(), 2);
184 }
185
186 #[test]
187 fn triangle() {
188 let pts = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)];
189 let r = convex_hull_2d(&pts).unwrap();
190 assert_eq!(r.hull_vertices.len(), 3);
191 let mut sorted = r.hull_vertices.clone();
192 sorted.sort_unstable();
193 assert_eq!(sorted, vec![0, 1, 2]);
194 }
195
196 #[test]
197 fn square_with_interior() {
198 let pts = vec![
199 (0.0, 0.0),
200 (1.0, 0.0),
201 (1.0, 1.0),
202 (0.0, 1.0),
203 (0.5, 0.5), ];
205 let r = convex_hull_2d(&pts).unwrap();
206 assert_eq!(r.hull_vertices.len(), 4);
207 assert!(!r.hull_vertices.contains(&4));
208 }
209
210 #[test]
211 fn collinear_points() {
212 let pts = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0)];
213 let r = convex_hull_2d(&pts).unwrap();
214 assert_eq!(r.hull_vertices.len(), 2);
216 let mut sorted = r.hull_vertices.clone();
217 sorted.sort_unstable();
218 assert_eq!(sorted, vec![0, 3]);
219 }
220
221 #[test]
222 fn regular_hexagon() {
223 let pts: Vec<(f64, f64)> = (0..6)
224 .map(|i| {
225 let angle = std::f64::consts::PI / 3.0 * f64::from(i);
226 (angle.cos(), angle.sin())
227 })
228 .collect();
229 let r = convex_hull_2d(&pts).unwrap();
230 assert_eq!(r.hull_vertices.len(), 6);
231 }
232
233 #[test]
234 fn nan_coordinate_error() {
235 let pts = vec![(0.0, 0.0), (f64::NAN, 1.0)];
236 assert!(convex_hull_2d(&pts).is_err());
237 }
238
239 #[test]
240 fn duplicate_points() {
241 let pts = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)];
242 let r = convex_hull_2d(&pts).unwrap();
243 assert_eq!(r.hull_vertices.len(), 3);
244 }
245
246 #[test]
247 fn large_random_circle() {
248 let n: u32 = 20;
250 let pts: Vec<(f64, f64)> = (0..n)
251 .map(|i| {
252 let angle = 2.0 * std::f64::consts::PI * f64::from(i) / f64::from(n);
253 (angle.cos(), angle.sin())
254 })
255 .collect();
256 let r = convex_hull_2d(&pts).unwrap();
257 assert_eq!(r.hull_vertices.len(), n as usize);
258 }
259
260 #[test]
261 fn hull_is_ccw() {
262 let pts = vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (1.0, 1.0)];
264 let r = convex_hull_2d(&pts).unwrap();
265 let hull = &r.hull_coords;
267 let len = hull.len();
268 for i in 0..len {
269 let cp = cross_product(hull[i], hull[(i + 1) % len], hull[(i + 2) % len]);
270 assert!(
271 cp >= 0.0,
272 "hull is not CCW at index {i}: cross product = {cp}"
273 );
274 }
275 }
276}