scirs2_spatial/convex_hull/core.rs
1//! Core ConvexHull structure and basic methods
2//!
3//! This module contains the main ConvexHull struct and its fundamental
4//! operations, providing a foundation for convex hull computations.
5//!
6//! # Pure Rust Implementation
7//!
8//! This module uses pure Rust algorithms for convex hull computation:
9//! - 2D: Graham Scan, Jarvis March, or Andrew's Monotone Chain
10//! - 3D: Quickhull algorithm
11//! - nD: Incremental convex hull algorithm
12
13use crate::error::{SpatialError, SpatialResult};
14use scirs2_core::ndarray::{Array2, ArrayView2};
15
16/// Algorithms available for computing convex hulls
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ConvexHullAlgorithm {
19 /// Use Quickhull (default) - works for any dimension (pure Rust)
20 #[default]
21 Quickhull,
22 /// Graham scan algorithm - only for 2D (pure Rust)
23 GrahamScan,
24 /// Jarvis march (gift wrapping) algorithm - only for 2D (pure Rust)
25 JarvisMarch,
26}
27
28/// A ConvexHull represents the convex hull of a set of points.
29///
30/// It provides methods to access hull properties, check if points are
31/// inside the hull, and access hull facets and vertices.
32///
33/// # Pure Rust Implementation
34///
35/// This implementation uses pure Rust algorithms without any C library dependencies.
36#[derive(Debug, Clone)]
37pub struct ConvexHull {
38 /// Input points
39 pub(crate) points: Array2<f64>,
40 /// Vertex indices of the convex hull (indices into the original points array)
41 pub(crate) vertex_indices: Vec<usize>,
42 /// Simplex indices (facets) of the convex hull
43 pub(crate) simplices: Vec<Vec<usize>>,
44 /// Equations of the hull facets (shape: n_facets x (n_dim+1))
45 pub(crate) equations: Option<Array2<f64>>,
46}
47
48impl ConvexHull {
49 /// Create a new ConvexHull from a set of points.
50 ///
51 /// # Arguments
52 ///
53 /// * `points` - Input points (shape: npoints x n_dim)
54 ///
55 /// # Returns
56 ///
57 /// * Result containing a ConvexHull instance or an error
58 ///
59 /// # Errors
60 ///
61 /// * Returns error if hull computation fails or input is invalid
62 ///
63 /// # Examples
64 ///
65 /// ```
66 /// use scirs2_spatial::convex_hull::ConvexHull;
67 /// use scirs2_core::ndarray::array;
68 ///
69 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
70 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
71 /// ```
72 pub fn new(points: &ArrayView2<'_, f64>) -> SpatialResult<Self> {
73 Self::new_with_algorithm(points, ConvexHullAlgorithm::default())
74 }
75
76 /// Create a new ConvexHull from a set of points using a specific algorithm.
77 ///
78 /// # Arguments
79 ///
80 /// * `points` - Input points (shape: npoints x n_dim)
81 /// * `algorithm` - Algorithm to use for convex hull computation
82 ///
83 /// # Returns
84 ///
85 /// * Result containing a ConvexHull instance or an error
86 ///
87 /// # Errors
88 ///
89 /// * Returns error if hull computation fails or input is invalid
90 /// * Returns error if algorithm is not supported for the given dimensionality
91 ///
92 /// # Examples
93 ///
94 /// ```
95 /// use scirs2_spatial::convex_hull::{ConvexHull, ConvexHullAlgorithm};
96 /// use scirs2_core::ndarray::array;
97 ///
98 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
99 /// let hull = ConvexHull::new_with_algorithm(&points.view(), ConvexHullAlgorithm::GrahamScan).expect("Operation failed");
100 /// ```
101 pub fn new_with_algorithm(
102 points: &ArrayView2<'_, f64>,
103 algorithm: ConvexHullAlgorithm,
104 ) -> SpatialResult<Self> {
105 let npoints = points.nrows();
106 let ndim = points.ncols();
107
108 // For 1D, allow at least 1 point (degenerate case)
109 // For 2D, need at least 3 points for a proper hull, but allow 2 for degenerate line case
110 // For 3D and higher, require at least ndim + 1 points
111 let min_points = match ndim {
112 1 => 1, // Allow single points in 1D
113 2 => 3, // Need at least 3 points for a 2D convex hull
114 _ => ndim + 1,
115 };
116
117 if npoints < min_points {
118 return Err(SpatialError::ValueError(format!(
119 "Need at least {} points to construct a {}D convex hull",
120 min_points, ndim
121 )));
122 }
123
124 // Check if algorithm is compatible with dimensionality
125 match algorithm {
126 ConvexHullAlgorithm::GrahamScan | ConvexHullAlgorithm::JarvisMarch => {
127 if ndim != 2 {
128 return Err(SpatialError::ValueError(format!(
129 "{algorithm:?} algorithm only supports 2D points, got {ndim}D"
130 )));
131 }
132 }
133 ConvexHullAlgorithm::Quickhull => {
134 // Quickhull supports any dimension (pure Rust)
135 }
136 }
137
138 match algorithm {
139 ConvexHullAlgorithm::GrahamScan => {
140 crate::convex_hull::algorithms::graham_scan::compute_graham_scan(points)
141 }
142 ConvexHullAlgorithm::JarvisMarch => {
143 crate::convex_hull::algorithms::jarvis_march::compute_jarvis_march(points)
144 }
145 ConvexHullAlgorithm::Quickhull => {
146 crate::convex_hull::algorithms::quickhull::compute_quickhull(points)
147 }
148 }
149 }
150
151 /// Get the vertices of the convex hull
152 ///
153 /// # Returns
154 ///
155 /// * Array of vertices of the convex hull
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use scirs2_spatial::convex_hull::ConvexHull;
161 /// use scirs2_core::ndarray::array;
162 ///
163 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
164 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
165 ///
166 /// // The hull vertices should be the corners, not the interior point
167 /// // The number of vertices can vary depending on QHull implementation
168 /// assert!(hull.vertices().len() >= 3);
169 /// ```
170 pub fn vertices(&self) -> Vec<Vec<f64>> {
171 self.vertex_indices
172 .iter()
173 .map(|&idx| self.points.row(idx).to_vec())
174 .collect()
175 }
176
177 /// Get the vertices of the convex hull as an Array2
178 ///
179 /// # Returns
180 ///
181 /// * Array2 of shape (n_vertices, n_dim) containing the vertices of the convex hull
182 pub fn vertices_array(&self) -> Array2<f64> {
183 let ndim = self.points.ncols();
184 let n_vertices = self.vertex_indices.len();
185 let mut vertices = Array2::zeros((n_vertices, ndim));
186
187 for (i, &idx) in self.vertex_indices.iter().enumerate() {
188 // Make sure the index is valid
189 if idx < self.points.nrows() {
190 for j in 0..ndim {
191 vertices[[i, j]] = self.points[[idx, j]];
192 }
193 } else {
194 // If we have an invalid index (shouldn't happen, but for safety)
195 for j in 0..ndim {
196 vertices[[i, j]] = 0.0;
197 }
198 }
199 }
200
201 vertices
202 }
203
204 /// Get the indices of the vertices of the convex hull
205 ///
206 /// # Returns
207 ///
208 /// * Vector of indices of the hull vertices (into the original points array)
209 ///
210 /// # Examples
211 ///
212 /// ```
213 /// use scirs2_spatial::convex_hull::ConvexHull;
214 /// use scirs2_core::ndarray::array;
215 ///
216 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
217 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
218 ///
219 /// // Get the vertex indices of the hull
220 /// let vertices = hull.vertex_indices();
221 /// println!("Convex hull vertices: {:?}", vertices);
222 /// ```
223 pub fn vertex_indices(&self) -> &[usize] {
224 &self.vertex_indices
225 }
226
227 /// Get the simplices (facets) of the convex hull
228 ///
229 /// # Returns
230 ///
231 /// * Vector of simplices, where each simplex is a vector of vertex indices
232 ///
233 /// # Examples
234 ///
235 /// ```
236 /// use scirs2_spatial::convex_hull::ConvexHull;
237 /// use scirs2_core::ndarray::array;
238 ///
239 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
240 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
241 ///
242 /// // For a 2D hull, examine the simplices
243 /// for simplex in hull.simplices() {
244 /// // Print each simplex
245 /// println!("Simplex: {:?}", simplex);
246 /// }
247 /// ```
248 pub fn simplices(&self) -> &[Vec<usize>] {
249 &self.simplices
250 }
251
252 /// Get the dimensionality of the convex hull
253 ///
254 /// # Returns
255 ///
256 /// * Number of dimensions of the convex hull
257 ///
258 /// # Examples
259 ///
260 /// ```
261 /// use scirs2_spatial::convex_hull::ConvexHull;
262 /// use scirs2_core::ndarray::array;
263 ///
264 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
265 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
266 ///
267 /// assert_eq!(hull.ndim(), 2);
268 /// ```
269 pub fn ndim(&self) -> usize {
270 self.points.ncols()
271 }
272
273 /// Check if a point is inside the convex hull
274 ///
275 /// # Arguments
276 ///
277 /// * `point` - The point to check
278 ///
279 /// # Returns
280 ///
281 /// * Result containing a boolean indicating if the point is inside the hull
282 ///
283 /// # Errors
284 ///
285 /// * Returns error if point dimension doesn't match hull dimension
286 ///
287 /// # Examples
288 ///
289 /// ```
290 /// use scirs2_spatial::convex_hull::ConvexHull;
291 /// use scirs2_core::ndarray::array;
292 ///
293 /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
294 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
295 ///
296 /// // Check a point inside the hull
297 /// // Result may vary depending on QHull implementation and special case handling
298 /// let inside = hull.contains(&[0.25, 0.25]).expect("Operation failed");
299 /// println!("Point [0.25, 0.25] inside: {}", inside);
300 ///
301 /// // Check a point outside the hull
302 /// assert!(!hull.contains(&[2.0, 2.0]).expect("Operation failed"));
303 /// ```
304 pub fn contains<T: AsRef<[f64]>>(&self, point: T) -> SpatialResult<bool> {
305 crate::convex_hull::properties::containment::check_point_containment(self, point)
306 }
307
308 /// Get the volume of the convex hull
309 ///
310 /// For 2D hulls, this returns the area. For 3D hulls, this returns the volume.
311 /// For higher dimensions, this returns the hypervolume.
312 ///
313 /// # Returns
314 ///
315 /// * Result containing the volume/area of the convex hull
316 ///
317 /// # Errors
318 ///
319 /// * Returns error if volume computation fails
320 ///
321 /// # Examples
322 ///
323 /// ```
324 /// use scirs2_spatial::convex_hull::ConvexHull;
325 /// use scirs2_core::ndarray::array;
326 ///
327 /// // Create a square with area 1
328 /// let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
329 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
330 ///
331 /// let area = hull.volume().expect("Operation failed");
332 /// assert!((area - 1.0).abs() < 1e-10);
333 /// ```
334 pub fn volume(&self) -> SpatialResult<f64> {
335 crate::convex_hull::properties::volume::compute_volume(self)
336 }
337
338 /// Get the surface area of the convex hull
339 ///
340 /// For 2D hulls, this returns the perimeter. For 3D hulls, this returns the surface area.
341 /// For higher dimensions, this returns the surface hypervolume.
342 ///
343 /// # Returns
344 ///
345 /// * Result containing the surface area/perimeter of the convex hull
346 ///
347 /// # Errors
348 ///
349 /// * Returns error if area computation fails
350 ///
351 /// # Examples
352 ///
353 /// ```
354 /// use scirs2_spatial::convex_hull::ConvexHull;
355 /// use scirs2_core::ndarray::array;
356 ///
357 /// // Create a 3D cube
358 /// let points = array![
359 /// [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
360 /// [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]
361 /// ];
362 /// let hull = ConvexHull::new(&points.view()).expect("Operation failed");
363 ///
364 /// // Surface area of the cube should be 6 square units
365 /// // Note: Due to triangulation, the computed area may differ slightly
366 /// let area = hull.area().expect("Operation failed");
367 /// assert!(area > 5.5 && area < 7.0, "Expected area around 6.0, got {}", area);
368 /// ```
369 pub fn area(&self) -> SpatialResult<f64> {
370 crate::convex_hull::properties::surface_area::compute_surface_area(self)
371 }
372
373 /// Compute facet equations from the hull simplices (pure Rust)
374 ///
375 /// # Arguments
376 ///
377 /// * `points` - The points array
378 /// * `simplices` - The simplex indices
379 /// * `ndim` - Dimensionality of the hull
380 ///
381 /// # Returns
382 ///
383 /// * Option containing an Array2 of shape (n_facets, ndim+1) with the equations of the hull facets
384 /// or None if equations cannot be computed
385 pub fn compute_equations_from_simplices(
386 points: &Array2<f64>,
387 simplices: &[Vec<usize>],
388 ndim: usize,
389 ) -> Option<Array2<f64>> {
390 let n_facets = simplices.len();
391 if n_facets == 0 {
392 return None;
393 }
394
395 // Compute centroid for outward normal orientation
396 let npoints = points.nrows();
397 let mut centroid = vec![0.0; ndim];
398 for i in 0..npoints {
399 for j in 0..ndim {
400 centroid[j] += points[[i, j]];
401 }
402 }
403 for c in &mut centroid {
404 *c /= npoints as f64;
405 }
406
407 let mut equations = Array2::zeros((n_facets, ndim + 1));
408
409 for (i, simplex) in simplices.iter().enumerate() {
410 if simplex.len() < ndim {
411 continue;
412 }
413
414 // Compute the normal vector and offset for each facet
415 if ndim == 2 {
416 // For 2D: compute normal from edge
417 let p1 = [points[[simplex[0], 0]], points[[simplex[0], 1]]];
418 let p2 = [points[[simplex[1], 0]], points[[simplex[1], 1]]];
419
420 // Edge direction
421 let dx = p2[0] - p1[0];
422 let dy = p2[1] - p1[1];
423
424 // Normal (perpendicular to edge)
425 let len = (dx * dx + dy * dy).sqrt();
426 if len < 1e-10 {
427 continue;
428 }
429 let mut nx = -dy / len;
430 let mut ny = dx / len;
431
432 // Offset: -n · p1
433 let mut offset = -(nx * p1[0] + ny * p1[1]);
434
435 // Check if normal points outward (away from centroid)
436 // A point at centroid should satisfy n·c + offset < 0
437 let centroid_val = nx * centroid[0] + ny * centroid[1] + offset;
438 if centroid_val > 0.0 {
439 // Normal points inward, flip it
440 nx = -nx;
441 ny = -ny;
442 offset = -offset;
443 }
444
445 equations[[i, 0]] = nx;
446 equations[[i, 1]] = ny;
447 equations[[i, 2]] = offset;
448 } else if ndim == 3 {
449 // For 3D: compute normal from triangle
450 if simplex.len() < 3 {
451 continue;
452 }
453 let p1 = [
454 points[[simplex[0], 0]],
455 points[[simplex[0], 1]],
456 points[[simplex[0], 2]],
457 ];
458 let p2 = [
459 points[[simplex[1], 0]],
460 points[[simplex[1], 1]],
461 points[[simplex[1], 2]],
462 ];
463 let p3 = [
464 points[[simplex[2], 0]],
465 points[[simplex[2], 1]],
466 points[[simplex[2], 2]],
467 ];
468
469 // Two edge vectors
470 let v1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
471 let v2 = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
472
473 // Cross product for normal
474 let mut nx = v1[1] * v2[2] - v1[2] * v2[1];
475 let mut ny = v1[2] * v2[0] - v1[0] * v2[2];
476 let mut nz = v1[0] * v2[1] - v1[1] * v2[0];
477
478 let len = (nx * nx + ny * ny + nz * nz).sqrt();
479 if len < 1e-10 {
480 continue;
481 }
482 nx /= len;
483 ny /= len;
484 nz /= len;
485
486 // Offset: -n · p1
487 let mut offset = -(nx * p1[0] + ny * p1[1] + nz * p1[2]);
488
489 // Check if normal points outward (away from centroid)
490 let centroid_val = nx * centroid[0] + ny * centroid[1] + nz * centroid[2] + offset;
491 if centroid_val > 0.0 {
492 // Normal points inward, flip it
493 nx = -nx;
494 ny = -ny;
495 nz = -nz;
496 offset = -offset;
497 }
498
499 equations[[i, 0]] = nx;
500 equations[[i, 1]] = ny;
501 equations[[i, 2]] = nz;
502 equations[[i, 3]] = offset;
503 }
504 }
505
506 Some(equations)
507 }
508
509 /// Get the equations of the hull facets
510 ///
511 /// # Returns
512 ///
513 /// * Option containing an Array2 of shape (n_facets, ndim+1) with the equations of the hull facets
514 pub fn equations(&self) -> Option<&Array2<f64>> {
515 self.equations.as_ref()
516 }
517}