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#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Triangle {
13 a: Point2,
14 b: Point2,
15 c: Point2,
16}
17
18impl Triangle {
19 #[must_use]
21 pub const fn new(a: Point2, b: Point2, c: Point2) -> Self {
22 Self { a, b, c }
23 }
24
25 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 #[must_use]
37 pub const fn a(self) -> Point2 {
38 self.a
39 }
40
41 #[must_use]
43 pub const fn b(self) -> Point2 {
44 self.b
45 }
46
47 #[must_use]
49 pub const fn c(self) -> Point2 {
50 self.c
51 }
52
53 #[must_use]
55 pub const fn vertices(self) -> [Point2; 3] {
56 [self.a(), self.b(), self.c()]
57 }
58
59 #[must_use]
61 pub fn twice_signed_area(self) -> f64 {
62 triangle_twice_signed_area(self.a(), self.b(), self.c())
63 }
64
65 #[must_use]
67 pub fn twice_area(self) -> f64 {
68 triangle_twice_area(self.a(), self.b(), self.c())
69 }
70
71 #[must_use]
73 pub fn orientation(self) -> Orientation2 {
74 orientation_2d(self.a(), self.b(), self.c())
75 }
76
77 #[must_use]
79 pub fn area(self) -> f64 {
80 self.twice_area() * 0.5
81 }
82
83 #[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 #[must_use]
95 pub fn perimeter(self) -> f64 {
96 self.sides().into_iter().sum()
97 }
98
99 #[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 #[must_use]
109 pub fn is_degenerate(self) -> bool {
110 self.twice_signed_area() == 0.0
111 }
112
113 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 #[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#[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#[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#[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}