Skip to main content

use_geometry/
triangle.rs

1use crate::{
2    aabb::Aabb2,
3    distance::distance_2d,
4    error::GeometryError,
5    orientation::{Orientation2, orientation_2d, signed_twice_area_2d},
6    point::Point2,
7};
8
9/// A constructed 2D triangle represented by three vertices.
10///
11/// Use [`Triangle::try_new`] when the vertices come from user input, files,
12/// or other external sources. Use [`Triangle::new`] when the points are
13/// already trusted.
14///
15/// # Examples
16///
17/// ```rust
18/// use use_geometry::{
19///     Orientation2, Point2, Triangle, triangle_area,
20///     triangle_twice_signed_area,
21/// };
22///
23/// let a = Point2::try_new(0.0, 0.0)?;
24/// let b = Point2::try_new(4.0, 0.0)?;
25/// let c = Point2::try_new(0.0, 3.0)?;
26/// let triangle = Triangle::try_new(a, b, c)?;
27///
28/// assert_eq!(triangle.orientation(), Orientation2::CounterClockwise);
29/// assert_eq!(triangle.twice_signed_area(), triangle_twice_signed_area(a, b, c));
30/// assert_eq!(triangle.area(), triangle_area(a, b, c));
31/// assert_eq!(triangle.sides(), [4.0, 5.0, 3.0]);
32/// # Ok::<(), use_geometry::GeometryError>(())
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct Triangle {
36    /// The first vertex.
37    a: Point2,
38    /// The second vertex.
39    b: Point2,
40    /// The third vertex.
41    c: Point2,
42}
43
44impl Triangle {
45    /// Creates a triangle from three points.
46    #[must_use]
47    pub const fn new(a: Point2, b: Point2, c: Point2) -> Self {
48        Self { a, b, c }
49    }
50
51    /// Creates a triangle from three points with finite coordinates.
52    ///
53    /// Use this constructor at API boundaries where coordinates may still need
54    /// validation. [`Triangle::new`] remains available for already-validated
55    /// points.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`GeometryError::NonFiniteComponent`] when any vertex contains a
60    /// non-finite coordinate.
61    ///
62    /// # Examples
63    ///
64    /// ```rust
65    /// use use_geometry::{Point2, Triangle};
66    ///
67    /// let triangle = Triangle::try_new(
68    ///     Point2::try_new(0.0, 0.0)?,
69    ///     Point2::try_new(4.0, 0.0)?,
70    ///     Point2::try_new(0.0, 3.0)?,
71    /// )?;
72    ///
73    /// assert_eq!(triangle.area(), 6.0);
74    /// # Ok::<(), use_geometry::GeometryError>(())
75    /// ```
76    pub fn try_new(a: Point2, b: Point2, c: Point2) -> Result<Self, GeometryError> {
77        Ok(Self::new(a.validate()?, b.validate()?, c.validate()?))
78    }
79
80    /// Returns the first vertex.
81    #[must_use]
82    pub const fn a(self) -> Point2 {
83        self.a
84    }
85
86    /// Returns the second vertex.
87    #[must_use]
88    pub const fn b(self) -> Point2 {
89        self.b
90    }
91
92    /// Returns the third vertex.
93    #[must_use]
94    pub const fn c(self) -> Point2 {
95        self.c
96    }
97
98    /// Returns the triangle vertices in `[a, b, c]` order.
99    #[must_use]
100    pub const fn vertices(self) -> [Point2; 3] {
101        [self.a(), self.b(), self.c()]
102    }
103
104    /// Returns twice the signed area of the triangle.
105    ///
106    /// The sign depends on the vertex winding order.
107    #[must_use]
108    pub fn twice_signed_area(self) -> f64 {
109        triangle_twice_signed_area(self.a(), self.b(), self.c())
110    }
111
112    /// Returns twice the unsigned area of the triangle.
113    #[must_use]
114    pub fn twice_area(self) -> f64 {
115        triangle_twice_area(self.a(), self.b(), self.c())
116    }
117
118    /// Returns the triangle orientation implied by the vertex winding order.
119    #[must_use]
120    pub fn orientation(self) -> Orientation2 {
121        orientation_2d(self.a(), self.b(), self.c())
122    }
123
124    /// Returns the triangle area.
125    #[must_use]
126    pub fn area(self) -> f64 {
127        self.twice_area() * 0.5
128    }
129
130    /// Returns the triangle side lengths in `[ab, bc, ca]` order.
131    #[must_use]
132    pub fn sides(self) -> [f64; 3] {
133        [
134            distance_2d(self.a(), self.b()),
135            distance_2d(self.b(), self.c()),
136            distance_2d(self.c(), self.a()),
137        ]
138    }
139
140    /// Returns the triangle perimeter.
141    #[must_use]
142    pub fn perimeter(self) -> f64 {
143        self.sides().into_iter().sum()
144    }
145
146    /// Returns the triangle centroid.
147    #[must_use]
148    pub fn centroid(self) -> Point2 {
149        let [a, b, c] = self.vertices();
150
151        Point2::new((a.x() + b.x() + c.x()) / 3.0, (a.y() + b.y() + c.y()) / 3.0)
152    }
153
154    /// Returns `true` when the triangle is exactly degenerate.
155    ///
156    /// Exact degeneracy means the signed twice-area is exactly zero, which in
157    /// turn means the vertices are collinear.
158    #[must_use]
159    pub fn is_degenerate(self) -> bool {
160        self.twice_signed_area() == 0.0
161    }
162
163    /// Returns `true` when the triangle's unsigned twice-area is within `tolerance` of zero.
164    ///
165    /// Use this when you care about practical collapse in measured or generated
166    /// geometry rather than exact arithmetic collapse.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
171    /// or infinite.
172    ///
173    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
174    pub fn is_degenerate_with_tolerance(self, tolerance: f64) -> Result<bool, GeometryError> {
175        let tolerance = GeometryError::validate_tolerance(tolerance)?;
176
177        Ok(self.twice_signed_area().abs() <= tolerance)
178    }
179
180    /// Returns the triangle bounding box.
181    #[must_use]
182    pub const fn aabb(self) -> Aabb2 {
183        let [a, b, c] = self.vertices();
184        let min_x = a.x().min(b.x()).min(c.x());
185        let min_y = a.y().min(b.y()).min(c.y());
186        let max_x = a.x().max(b.x()).max(c.x());
187        let max_y = a.y().max(b.y()).max(c.y());
188
189        Aabb2::from_points(Point2::new(min_x, min_y), Point2::new(max_x, max_y))
190    }
191}
192
193/// Returns twice the signed 2D triangle area using the shoelace formula.
194#[must_use]
195pub fn triangle_twice_signed_area(a: Point2, b: Point2, c: Point2) -> f64 {
196    signed_twice_area_2d(a, b, c)
197}
198
199/// Returns twice the unsigned 2D triangle area.
200#[must_use]
201pub fn triangle_twice_area(a: Point2, b: Point2, c: Point2) -> f64 {
202    triangle_twice_signed_area(a, b, c).abs()
203}
204
205/// Returns the 2D triangle area.
206#[must_use]
207pub fn triangle_area(a: Point2, b: Point2, c: Point2) -> f64 {
208    triangle_twice_area(a, b, c) * 0.5
209}
210
211#[cfg(test)]
212mod tests {
213    use super::{Triangle, triangle_area, triangle_twice_area, triangle_twice_signed_area};
214    use crate::{error::GeometryError, orientation::Orientation2, point::Point2};
215
216    fn approx_eq(left: f64, right: f64) -> bool {
217        (left - right).abs() < 1.0e-10
218    }
219
220    fn approx_eq_slice(left: [f64; 3], right: [f64; 3]) -> bool {
221        left.into_iter()
222            .zip(right)
223            .all(|(left_value, right_value)| approx_eq(left_value, right_value))
224    }
225
226    #[test]
227    fn constructs_triangles() {
228        let triangle = Triangle::new(
229            Point2::new(0.0, 0.0),
230            Point2::new(4.0, 0.0),
231            Point2::new(0.0, 3.0),
232        );
233
234        assert_eq!(triangle.a(), Point2::new(0.0, 0.0));
235    }
236
237    #[test]
238    fn constructs_triangles_with_try_new() {
239        assert_eq!(
240            Triangle::try_new(
241                Point2::new(0.0, 0.0),
242                Point2::new(4.0, 0.0),
243                Point2::new(0.0, 3.0),
244            ),
245            Ok(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        );
251    }
252
253    #[test]
254    fn rejects_non_finite_triangle_vertices() {
255        assert!(matches!(
256            Triangle::try_new(
257                Point2::new(0.0, 0.0),
258                Point2::new(4.0, 0.0),
259                Point2::new(0.0, f64::NAN),
260            ),
261            Err(GeometryError::NonFiniteComponent {
262                type_name: "Point2",
263                component: "y",
264                value,
265            }) if value.is_nan()
266        ));
267    }
268
269    #[test]
270    fn computes_triangle_area() {
271        let triangle = Triangle::new(
272            Point2::new(0.0, 0.0),
273            Point2::new(4.0, 0.0),
274            Point2::new(0.0, 3.0),
275        );
276
277        assert!(approx_eq(triangle.twice_signed_area(), 12.0));
278        assert!(approx_eq(triangle.twice_area(), 12.0));
279        assert!(approx_eq(triangle.area(), 6.0));
280        assert!(approx_eq(
281            triangle_twice_signed_area(triangle.a(), triangle.b(), triangle.c()),
282            12.0
283        ));
284        assert!(approx_eq(
285            triangle_twice_area(triangle.a(), triangle.b(), triangle.c()),
286            12.0
287        ));
288        assert!(approx_eq(
289            triangle_area(triangle.a(), triangle.b(), triangle.c()),
290            6.0
291        ));
292    }
293
294    #[test]
295    fn signed_area_tracks_orientation() {
296        let counter_clockwise = Triangle::new(
297            Point2::new(0.0, 0.0),
298            Point2::new(4.0, 0.0),
299            Point2::new(0.0, 3.0),
300        );
301        let clockwise = Triangle::new(
302            Point2::new(0.0, 0.0),
303            Point2::new(0.0, 3.0),
304            Point2::new(4.0, 0.0),
305        );
306
307        assert!(approx_eq(counter_clockwise.twice_signed_area(), 12.0));
308        assert!(approx_eq(clockwise.twice_signed_area(), -12.0));
309        assert_eq!(
310            counter_clockwise.orientation(),
311            Orientation2::CounterClockwise
312        );
313        assert_eq!(clockwise.orientation(), Orientation2::Clockwise);
314        assert_eq!(
315            crate::orientation::orientation_2d(
316                Point2::new(0.0, 0.0),
317                Point2::new(1.0, 1.0),
318                Point2::new(2.0, 2.0)
319            ),
320            Orientation2::Collinear
321        );
322    }
323
324    #[test]
325    fn computes_triangle_perimeter() {
326        let triangle = Triangle::new(
327            Point2::new(0.0, 0.0),
328            Point2::new(4.0, 0.0),
329            Point2::new(0.0, 3.0),
330        );
331
332        assert!(approx_eq_slice(triangle.sides(), [4.0, 5.0, 3.0]));
333        assert!(approx_eq(triangle.perimeter(), 12.0));
334        assert_eq!(
335            triangle.vertices(),
336            [triangle.a(), triangle.b(), triangle.c()]
337        );
338        assert_eq!(triangle.centroid(), Point2::new(4.0 / 3.0, 1.0));
339    }
340
341    #[test]
342    fn detects_degenerate_triangles() {
343        let triangle = Triangle::new(
344            Point2::new(0.0, 0.0),
345            Point2::new(1.0, 1.0),
346            Point2::new(2.0, 2.0),
347        );
348
349        assert!(triangle.is_degenerate());
350        assert_eq!(triangle.is_degenerate_with_tolerance(0.0), Ok(true));
351    }
352
353    #[test]
354    fn detects_near_degenerate_triangles_with_tolerance() {
355        let triangle = Triangle::new(
356            Point2::new(0.0, 0.0),
357            Point2::new(1.0, 1.0),
358            Point2::new(2.0, 2.0 + 1.0e-12),
359        );
360
361        assert!(!triangle.is_degenerate());
362        assert_eq!(triangle.is_degenerate_with_tolerance(1.0e-11), Ok(true));
363    }
364
365    #[test]
366    fn rejects_negative_degeneracy_tolerance() {
367        let triangle = Triangle::new(
368            Point2::new(0.0, 0.0),
369            Point2::new(1.0, 1.0),
370            Point2::new(2.0, 2.0),
371        );
372
373        assert_eq!(
374            triangle.is_degenerate_with_tolerance(-1.0),
375            Err(GeometryError::NegativeTolerance(-1.0))
376        );
377    }
378
379    #[test]
380    fn computes_triangle_bounds() {
381        let triangle = Triangle::new(
382            Point2::new(4.0, 1.0),
383            Point2::new(1.0, 3.0),
384            Point2::new(2.0, -1.0),
385        );
386
387        assert_eq!(triangle.aabb().min(), Point2::new(1.0, -1.0));
388        assert_eq!(triangle.aabb().max(), Point2::new(4.0, 3.0));
389    }
390}