honeycomb_kernels/triangulation/
mod.rs

1//! Polygon triangulation functions
2//!
3//! This module contains implementations of simple polygon triangulation methods. These are not
4//! meshing functions; our goal with these is to cut existing cells of an irregular mesh into
5//! triangular cells.
6//!
7//! With consideration to the above, we implement two polygon triangulation methods:
8//! - fanning -- two versions of this are implemented:
9//!     - a defensive one where the function actively search for a valid vertex to fan from
10//!     - a specific one which assume the cell is convex; it fans the polygon from its first vertex
11//! - ear clipping -- this method isn't algorithmically efficient, but (a) we operate on small
12//!   cells, and (b) it covers our needs (non-fannable polygons without holes)
13
14mod ear_clipping;
15mod fan;
16
17pub use ear_clipping::process_cell as earclip_cell;
18pub use fan::process_cell as fan_cell;
19pub use fan::process_convex_cell as fan_convex_cell;
20
21use honeycomb_core::cmap::{CMap2, DartIdType};
22use honeycomb_core::geometry::{CoordsFloat, Vertex2};
23use thiserror::Error;
24
25/// Error-modeling enum for triangulation routines.
26#[derive(Error, Debug, PartialEq, Eq)]
27pub enum TriangulateError {
28    /// The face to triangulate is already a triangle.
29    #[error("face is already a triangle")]
30    AlreadyTriangulated,
31    /// The face has no ear to use for triangulation using the ear clipping method.
32    #[error("no ear found in the polygon to triangulate")]
33    NoEar,
34    /// The face is not fannable, i.e. there is no "star" vertex.
35    #[error("no star in the polygon to triangulate")]
36    NonFannable,
37    /// The number of darts passed to create the new segments is too low. The `usize` value
38    /// is the number of missing darts.
39    #[error("not enough darts were passed to the triangulation function - missing `{0}`")]
40    NotEnoughDarts(usize),
41    /// The number of darts passed to create the new segments is too high. The `usize` value
42    /// is the number of excess darts.
43    #[error("too many darts were passed to the triangulation function - missing `{0}`")]
44    TooManyDarts(usize),
45    /// The face is not fit for triangulation. The `String` contains information about the reason.
46    #[error("face isn't defined correctly - {0}")]
47    UndefinedFace(&'static str),
48}
49
50#[allow(clippy::missing_errors_doc)]
51/// Checks if a face meets the requirements for triangulation.
52///
53/// This function performs several checks on a face before it can be triangulated:
54/// 1. Ensures the face has at least 3 vertices.
55/// 2. Verifies that the face is not already triangulated.
56/// 3. Confirms that the correct number of darts have been allocated for triangulation.
57///
58/// # Arguments
59///
60/// * `n_darts_face` - The number of darts in the face to be triangulated.
61/// * `n_darts_allocated` - The number of darts allocated for the triangulation process.
62/// * `face_id` - The identifier of the face being checked.
63///
64/// # Return / Errors
65///
66/// This function can return:
67/// - `Ok(())` if all checks pass and the face is ready for triangulation.
68/// - `Err(TriangulateError)` if any check fails, with a specific error describing the issue.
69///
70/// A failed check can result in the following errors:
71/// - `TriangulateError::UndefinedFace` - If the face has fewer than 3 vertices.
72/// - `TriangulateError::AlreadyTriangulated` - If the face already has exactly 3 vertices.
73/// - `TriangulateError::NotEnoughDarts` - If there aren't enough darts allocated for triangulation.
74/// - `TriangulateError::TooManyDarts` - If there are too many darts allocated for triangulation.
75#[allow(
76    clippy::cast_possible_wrap,
77    clippy::cast_sign_loss,
78    clippy::cast_abs_to_unsigned
79)]
80pub fn check_requirements(
81    n_darts_face: usize,
82    n_darts_allocated: usize,
83) -> Result<(), TriangulateError> {
84    match n_darts_face {
85        1 | 2 => {
86            return Err(TriangulateError::UndefinedFace("less than 3 vertices"));
87        }
88        3 => {
89            return Err(TriangulateError::AlreadyTriangulated);
90        }
91        _ => {}
92    }
93
94    // check the value of n_allocated - n_expected
95    match n_darts_allocated as isize - (n_darts_face as isize - 3) * 2 {
96        diff @ ..0 => {
97            return Err(TriangulateError::NotEnoughDarts(diff.abs() as usize));
98        }
99        0 => {}
100        diff @ 1.. => {
101            return Err(TriangulateError::TooManyDarts(diff as usize));
102        }
103    }
104
105    Ok(())
106}
107
108fn fetch_face_vertices<T: CoordsFloat>(
109    cmap: &CMap2<T>,
110    darts: &[DartIdType],
111) -> Result<Vec<Vertex2<T>>, TriangulateError> {
112    let tmp = darts
113        .iter()
114        .map(|dart_id| cmap.force_read_vertex(cmap.vertex_id(*dart_id)));
115    if tmp.clone().any(|v| v.is_none()) {
116        Err(TriangulateError::UndefinedFace(
117            "one or more undefined vertices",
118        ))
119    } else {
120        Ok(tmp.map(Option::unwrap).collect()) // safe unwrap due to if
121    }
122}
123
124/// Compute the cross product: `v1v2 x v2v3`.
125fn crossp_from_verts<T: CoordsFloat>(v1: &Vertex2<T>, v2: &Vertex2<T>, v3: &Vertex2<T>) -> T {
126    (v2.x() - v1.x()) * (v3.y() - v2.y()) - (v2.y() - v1.y()) * (v3.x() - v2.x())
127}
128
129#[cfg(test)]
130mod tests;