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}