Skip to main content

use_triangle/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_bounds::Aabb2;
5use use_coordinate::GeometryError;
6use use_distance::distance_2d;
7use use_orientation::{Orientation2, orientation_2d, signed_twice_area_2d};
8use use_point::Point2;
9
10/// A constructed 2D triangle represented by three vertices.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Triangle {
13    a: Point2,
14    b: Point2,
15    c: Point2,
16}
17
18impl Triangle {
19    /// Creates a triangle from three points.
20    #[must_use]
21    pub const fn new(a: Point2, b: Point2, c: Point2) -> Self {
22        Self { a, b, c }
23    }
24
25    /// Creates a triangle from three points with finite coordinates.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`GeometryError::NonFiniteComponent`] when any vertex contains a
30    /// non-finite coordinate.
31    pub fn try_new(a: Point2, b: Point2, c: Point2) -> Result<Self, GeometryError> {
32        Ok(Self::new(a.validate()?, b.validate()?, c.validate()?))
33    }
34
35    /// Returns the first vertex.
36    #[must_use]
37    pub const fn a(self) -> Point2 {
38        self.a
39    }
40
41    /// Returns the second vertex.
42    #[must_use]
43    pub const fn b(self) -> Point2 {
44        self.b
45    }
46
47    /// Returns the third vertex.
48    #[must_use]
49    pub const fn c(self) -> Point2 {
50        self.c
51    }
52
53    /// Returns the triangle vertices in `[a, b, c]` order.
54    #[must_use]
55    pub const fn vertices(self) -> [Point2; 3] {
56        [self.a(), self.b(), self.c()]
57    }
58
59    /// Returns twice the signed area of the triangle.
60    #[must_use]
61    pub fn twice_signed_area(self) -> f64 {
62        triangle_twice_signed_area(self.a(), self.b(), self.c())
63    }
64
65    /// Returns twice the unsigned area of the triangle.
66    #[must_use]
67    pub fn twice_area(self) -> f64 {
68        triangle_twice_area(self.a(), self.b(), self.c())
69    }
70
71    /// Returns the triangle orientation implied by the vertex winding order.
72    #[must_use]
73    pub fn orientation(self) -> Orientation2 {
74        orientation_2d(self.a(), self.b(), self.c())
75    }
76
77    /// Returns the triangle area.
78    #[must_use]
79    pub fn area(self) -> f64 {
80        self.twice_area() * 0.5
81    }
82
83    /// Returns the triangle side lengths in `[ab, bc, ca]` order.
84    #[must_use]
85    pub fn sides(self) -> [f64; 3] {
86        [
87            distance_2d(self.a(), self.b()),
88            distance_2d(self.b(), self.c()),
89            distance_2d(self.c(), self.a()),
90        ]
91    }
92
93    /// Returns the triangle perimeter.
94    #[must_use]
95    pub fn perimeter(self) -> f64 {
96        self.sides().into_iter().sum()
97    }
98
99    /// Returns the triangle centroid.
100    #[must_use]
101    pub fn centroid(self) -> Point2 {
102        let [a, b, c] = self.vertices();
103
104        Point2::new((a.x() + b.x() + c.x()) / 3.0, (a.y() + b.y() + c.y()) / 3.0)
105    }
106
107    /// Returns `true` when the triangle is exactly degenerate.
108    #[must_use]
109    pub fn is_degenerate(self) -> bool {
110        self.twice_signed_area() == 0.0
111    }
112
113    /// Returns `true` when the triangle's unsigned twice-area is within `tolerance` of zero.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
118    /// or infinite.
119    ///
120    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
121    pub fn is_degenerate_with_tolerance(self, tolerance: f64) -> Result<bool, GeometryError> {
122        let tolerance = GeometryError::validate_tolerance(tolerance)?;
123
124        Ok(self.twice_signed_area().abs() <= tolerance)
125    }
126
127    /// Returns the triangle bounding box.
128    #[must_use]
129    pub const fn aabb(self) -> Aabb2 {
130        let [a, b, c] = self.vertices();
131        let min_x = a.x().min(b.x()).min(c.x());
132        let min_y = a.y().min(b.y()).min(c.y());
133        let max_x = a.x().max(b.x()).max(c.x());
134        let max_y = a.y().max(b.y()).max(c.y());
135
136        Aabb2::from_points(Point2::new(min_x, min_y), Point2::new(max_x, max_y))
137    }
138}
139
140/// Returns twice the signed 2D triangle area using the shoelace formula.
141#[must_use]
142pub fn triangle_twice_signed_area(a: Point2, b: Point2, c: Point2) -> f64 {
143    signed_twice_area_2d(a, b, c)
144}
145
146/// Returns twice the unsigned 2D triangle area.
147#[must_use]
148pub fn triangle_twice_area(a: Point2, b: Point2, c: Point2) -> f64 {
149    triangle_twice_signed_area(a, b, c).abs()
150}
151
152/// Returns the 2D triangle area.
153#[must_use]
154pub fn triangle_area(a: Point2, b: Point2, c: Point2) -> f64 {
155    triangle_twice_area(a, b, c) * 0.5
156}
157
158#[cfg(test)]
159mod tests {
160    use super::{Triangle, triangle_area, triangle_twice_area, triangle_twice_signed_area};
161    use use_coordinate::GeometryError;
162    use use_orientation::Orientation2;
163    use use_point::Point2;
164
165    fn approx_eq(left: f64, right: f64) -> bool {
166        (left - right).abs() < 1.0e-10
167    }
168
169    fn approx_eq_slice(left: [f64; 3], right: [f64; 3]) -> bool {
170        left.into_iter()
171            .zip(right)
172            .all(|(left_value, right_value)| approx_eq(left_value, right_value))
173    }
174
175    #[test]
176    fn constructs_triangles() {
177        let triangle = Triangle::new(
178            Point2::new(0.0, 0.0),
179            Point2::new(4.0, 0.0),
180            Point2::new(0.0, 3.0),
181        );
182
183        assert_eq!(triangle.a(), Point2::new(0.0, 0.0));
184    }
185
186    #[test]
187    fn constructs_triangles_with_try_new() {
188        assert_eq!(
189            Triangle::try_new(
190                Point2::new(0.0, 0.0),
191                Point2::new(4.0, 0.0),
192                Point2::new(0.0, 3.0),
193            ),
194            Ok(Triangle::new(
195                Point2::new(0.0, 0.0),
196                Point2::new(4.0, 0.0),
197                Point2::new(0.0, 3.0),
198            ))
199        );
200    }
201
202    #[test]
203    fn rejects_non_finite_triangle_vertices() {
204        assert!(matches!(
205            Triangle::try_new(
206                Point2::new(0.0, 0.0),
207                Point2::new(4.0, 0.0),
208                Point2::new(0.0, f64::NAN),
209            ),
210            Err(GeometryError::NonFiniteComponent {
211                type_name: "Point2",
212                component: "y",
213                value,
214            }) if value.is_nan()
215        ));
216    }
217
218    #[test]
219    fn computes_triangle_area() {
220        let triangle = Triangle::new(
221            Point2::new(0.0, 0.0),
222            Point2::new(4.0, 0.0),
223            Point2::new(0.0, 3.0),
224        );
225
226        assert!(approx_eq(triangle.twice_signed_area(), 12.0));
227        assert!(approx_eq(triangle.twice_area(), 12.0));
228        assert!(approx_eq(triangle.area(), 6.0));
229        assert!(approx_eq(
230            triangle_twice_signed_area(triangle.a(), triangle.b(), triangle.c()),
231            12.0
232        ));
233        assert!(approx_eq(
234            triangle_twice_area(triangle.a(), triangle.b(), triangle.c()),
235            12.0
236        ));
237        assert!(approx_eq(
238            triangle_area(triangle.a(), triangle.b(), triangle.c()),
239            6.0
240        ));
241    }
242
243    #[test]
244    fn signed_area_tracks_orientation() {
245        let counter_clockwise = Triangle::new(
246            Point2::new(0.0, 0.0),
247            Point2::new(4.0, 0.0),
248            Point2::new(0.0, 3.0),
249        );
250        let clockwise = Triangle::new(
251            Point2::new(0.0, 0.0),
252            Point2::new(0.0, 3.0),
253            Point2::new(4.0, 0.0),
254        );
255
256        assert!(approx_eq(counter_clockwise.twice_signed_area(), 12.0));
257        assert!(approx_eq(clockwise.twice_signed_area(), -12.0));
258        assert_eq!(
259            counter_clockwise.orientation(),
260            Orientation2::CounterClockwise
261        );
262        assert_eq!(clockwise.orientation(), Orientation2::Clockwise);
263    }
264
265    #[test]
266    fn computes_triangle_perimeter() {
267        let triangle = Triangle::new(
268            Point2::new(0.0, 0.0),
269            Point2::new(4.0, 0.0),
270            Point2::new(0.0, 3.0),
271        );
272
273        assert!(approx_eq_slice(triangle.sides(), [4.0, 5.0, 3.0]));
274        assert!(approx_eq(triangle.perimeter(), 12.0));
275        assert_eq!(
276            triangle.vertices(),
277            [triangle.a(), triangle.b(), triangle.c()]
278        );
279        assert_eq!(triangle.centroid(), Point2::new(4.0 / 3.0, 1.0));
280    }
281
282    #[test]
283    fn detects_degenerate_triangles() {
284        let triangle = Triangle::new(
285            Point2::new(0.0, 0.0),
286            Point2::new(1.0, 1.0),
287            Point2::new(2.0, 2.0),
288        );
289
290        assert!(triangle.is_degenerate());
291        assert_eq!(triangle.is_degenerate_with_tolerance(0.0), Ok(true));
292    }
293
294    #[test]
295    fn detects_near_degenerate_triangles_with_tolerance() {
296        let triangle = Triangle::new(
297            Point2::new(0.0, 0.0),
298            Point2::new(1.0, 1.0),
299            Point2::new(2.0, 2.0 + 1.0e-12),
300        );
301
302        assert!(!triangle.is_degenerate());
303        assert_eq!(triangle.is_degenerate_with_tolerance(1.0e-11), Ok(true));
304    }
305
306    #[test]
307    fn rejects_negative_degeneracy_tolerance() {
308        let triangle = Triangle::new(
309            Point2::new(0.0, 0.0),
310            Point2::new(1.0, 1.0),
311            Point2::new(2.0, 2.0),
312        );
313
314        assert_eq!(
315            triangle.is_degenerate_with_tolerance(-1.0),
316            Err(GeometryError::NegativeTolerance(-1.0))
317        );
318    }
319
320    #[test]
321    fn computes_triangle_bounds() {
322        let triangle = Triangle::new(
323            Point2::new(4.0, 1.0),
324            Point2::new(1.0, 3.0),
325            Point2::new(2.0, -1.0),
326        );
327
328        assert_eq!(triangle.aabb().min(), Point2::new(1.0, -1.0));
329        assert_eq!(triangle.aabb().max(), Point2::new(4.0, 3.0));
330    }
331}