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#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct Triangle {
36 a: Point2,
38 b: Point2,
40 c: Point2,
42}
43
44impl Triangle {
45 #[must_use]
47 pub const fn new(a: Point2, b: Point2, c: Point2) -> Self {
48 Self { a, b, c }
49 }
50
51 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 #[must_use]
82 pub const fn a(self) -> Point2 {
83 self.a
84 }
85
86 #[must_use]
88 pub const fn b(self) -> Point2 {
89 self.b
90 }
91
92 #[must_use]
94 pub const fn c(self) -> Point2 {
95 self.c
96 }
97
98 #[must_use]
100 pub const fn vertices(self) -> [Point2; 3] {
101 [self.a(), self.b(), self.c()]
102 }
103
104 #[must_use]
108 pub fn twice_signed_area(self) -> f64 {
109 triangle_twice_signed_area(self.a(), self.b(), self.c())
110 }
111
112 #[must_use]
114 pub fn twice_area(self) -> f64 {
115 triangle_twice_area(self.a(), self.b(), self.c())
116 }
117
118 #[must_use]
120 pub fn orientation(self) -> Orientation2 {
121 orientation_2d(self.a(), self.b(), self.c())
122 }
123
124 #[must_use]
126 pub fn area(self) -> f64 {
127 self.twice_area() * 0.5
128 }
129
130 #[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 #[must_use]
142 pub fn perimeter(self) -> f64 {
143 self.sides().into_iter().sum()
144 }
145
146 #[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 #[must_use]
159 pub fn is_degenerate(self) -> bool {
160 self.twice_signed_area() == 0.0
161 }
162
163 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 #[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#[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#[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#[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}