/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.13.0/src/triangle.rs
Line | Count | Source |
1 | | // Copyright 2024 the Kurbo Authors |
2 | | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | | |
4 | | //! Triangle shape |
5 | | use crate::{Circle, PathEl, Point, Rect, Shape, Vec2}; |
6 | | |
7 | | use core::cmp::*; |
8 | | use core::f64::consts::FRAC_PI_4; |
9 | | use core::ops::{Add, Sub}; |
10 | | |
11 | | #[cfg(not(feature = "std"))] |
12 | | use crate::common::FloatFuncs; |
13 | | |
14 | | /// Triangle |
15 | | // A |
16 | | // * |
17 | | // / \ |
18 | | // / \ |
19 | | // *-----* |
20 | | // B C |
21 | | #[derive(Clone, Copy, PartialEq, Debug)] |
22 | | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
23 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
24 | | pub struct Triangle { |
25 | | /// vertex a. |
26 | | pub a: Point, |
27 | | /// vertex b. |
28 | | pub b: Point, |
29 | | /// vertex c. |
30 | | pub c: Point, |
31 | | } |
32 | | |
33 | | impl Triangle { |
34 | | /// The empty [`Triangle`] at the origin. |
35 | | pub const ZERO: Self = Self::from_coords((0., 0.), (0., 0.), (0., 0.)); |
36 | | |
37 | | /// Equilateral [`Triangle`] with the x-axis unit vector as its base. |
38 | | pub const EQUILATERAL: Self = Self::from_coords( |
39 | | ( |
40 | | 1.0 / 2.0, |
41 | | 1.732050807568877293527446341505872367_f64 / 2.0, /* (sqrt 3)/2 */ |
42 | | ), |
43 | | (0.0, 0.0), |
44 | | (1.0, 0.0), |
45 | | ); |
46 | | |
47 | | /// A new [`Triangle`] from three vertices ([`Point`]s). |
48 | | #[inline(always)] |
49 | 0 | pub fn new(a: impl Into<Point>, b: impl Into<Point>, c: impl Into<Point>) -> Self { |
50 | 0 | Self { |
51 | 0 | a: a.into(), |
52 | 0 | b: b.into(), |
53 | 0 | c: c.into(), |
54 | 0 | } |
55 | 0 | } |
56 | | |
57 | | /// A new [`Triangle`] from three float vertex coordinates. |
58 | | /// |
59 | | /// Works as a constant [`Triangle::new`]. |
60 | | #[inline(always)] |
61 | 0 | pub const fn from_coords(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> Self { |
62 | 0 | Self { |
63 | 0 | a: Point::new(a.0, a.1), |
64 | 0 | b: Point::new(b.0, b.1), |
65 | 0 | c: Point::new(c.0, c.1), |
66 | 0 | } |
67 | 0 | } |
68 | | |
69 | | /// The centroid of the [`Triangle`]. |
70 | | #[inline] |
71 | 0 | pub fn centroid(&self) -> Point { |
72 | 0 | (1.0 / 3.0 * (self.a.to_vec2() + self.b.to_vec2() + self.c.to_vec2())).to_point() |
73 | 0 | } |
74 | | |
75 | | /// The offset of each vertex from the centroid. |
76 | | #[inline] |
77 | 0 | pub fn offsets(&self) -> [Vec2; 3] { |
78 | 0 | let centroid = self.centroid().to_vec2(); |
79 | | |
80 | 0 | [ |
81 | 0 | (self.a.to_vec2() - centroid), |
82 | 0 | (self.b.to_vec2() - centroid), |
83 | 0 | (self.c.to_vec2() - centroid), |
84 | 0 | ] |
85 | 0 | } |
86 | | |
87 | | /// The area of the [`Triangle`]. |
88 | | #[inline] |
89 | 0 | pub fn area(&self) -> f64 { |
90 | 0 | 0.5 * (self.b - self.a).cross(self.c - self.a) |
91 | 0 | } |
92 | | |
93 | | /// Whether this [`Triangle`] has zero area. |
94 | | #[doc(alias = "is_empty")] |
95 | | #[inline] |
96 | 0 | pub fn is_zero_area(&self) -> bool { |
97 | 0 | self.area() == 0.0 |
98 | 0 | } |
99 | | |
100 | | /// The inscribed circle of [`Triangle`]. |
101 | | /// |
102 | | /// This is defined as the greatest [`Circle`] that lies within the [`Triangle`]. |
103 | | #[doc(alias = "incircle")] |
104 | | #[inline] |
105 | 0 | pub fn inscribed_circle(&self) -> Circle { |
106 | 0 | let ab = self.a.distance(self.b); |
107 | 0 | let bc = self.b.distance(self.c); |
108 | 0 | let ac = self.a.distance(self.c); |
109 | | |
110 | | // [`Vec2::div`] also multiplies by the reciprocal of the argument, but as this reciprocal |
111 | | // is reused below to calculate the radius, there's explicitly only one division needed. |
112 | 0 | let perimeter_recip = 1. / (ab + bc + ac); |
113 | 0 | let incenter = (self.a.to_vec2() * bc + self.b.to_vec2() * ac + self.c.to_vec2() * ab) |
114 | 0 | * perimeter_recip; |
115 | | |
116 | 0 | Circle::new(incenter.to_point(), 2.0 * self.area() * perimeter_recip) |
117 | 0 | } |
118 | | |
119 | | /// The circumscribed circle of [`Triangle`]. |
120 | | /// |
121 | | /// This is defined as the smallest [`Circle`] which intercepts each vertex of the [`Triangle`]. |
122 | | #[doc(alias = "circumcircle")] |
123 | | #[inline] |
124 | 0 | pub fn circumscribed_circle(&self) -> Circle { |
125 | 0 | let b = self.b - self.a; |
126 | 0 | let c = self.c - self.a; |
127 | 0 | let b_len2 = b.hypot2(); |
128 | 0 | let c_len2 = c.hypot2(); |
129 | 0 | let d_recip = 0.5 / b.cross(c); |
130 | | |
131 | 0 | let x = (c.y * b_len2 - b.y * c_len2) * d_recip; |
132 | 0 | let y = (b.x * c_len2 - c.x * b_len2) * d_recip; |
133 | 0 | let r = (b_len2 * c_len2).sqrt() * (c - b).hypot() * d_recip; |
134 | | |
135 | 0 | Circle::new(self.a + Vec2::new(x, y), r) |
136 | 0 | } |
137 | | |
138 | | /// Expand the triangle by a constant amount (`scalar`) in all directions. |
139 | | #[doc(alias = "offset")] |
140 | 0 | pub fn inflate(&self, scalar: f64) -> Self { |
141 | 0 | let centroid = self.centroid(); |
142 | | |
143 | 0 | Self::new( |
144 | 0 | centroid + (0.0, scalar), |
145 | 0 | centroid + scalar * Vec2::from_angle(5.0 * FRAC_PI_4), |
146 | 0 | centroid + scalar * Vec2::from_angle(7.0 * FRAC_PI_4), |
147 | | ) |
148 | 0 | } |
149 | | |
150 | | /// Is this [`Triangle`] [finite]? |
151 | | /// |
152 | | /// [finite]: f64::is_finite |
153 | | #[inline] |
154 | 0 | pub const fn is_finite(&self) -> bool { |
155 | 0 | self.a.is_finite() && self.b.is_finite() && self.c.is_finite() |
156 | 0 | } |
157 | | |
158 | | /// Is this [`Triangle`] [NaN]? |
159 | | /// |
160 | | /// [NaN]: f64::is_nan |
161 | | #[inline] |
162 | 0 | pub const fn is_nan(&self) -> bool { |
163 | 0 | self.a.is_nan() || self.b.is_nan() || self.c.is_nan() |
164 | 0 | } |
165 | | } |
166 | | |
167 | | impl From<(Point, Point, Point)> for Triangle { |
168 | 0 | fn from(points: (Point, Point, Point)) -> Triangle { |
169 | 0 | Triangle::new(points.0, points.1, points.2) |
170 | 0 | } |
171 | | } |
172 | | |
173 | | impl Add<Vec2> for Triangle { |
174 | | type Output = Triangle; |
175 | | |
176 | | #[inline] |
177 | 0 | fn add(self, v: Vec2) -> Triangle { |
178 | 0 | Triangle::new(self.a + v, self.b + v, self.c + v) |
179 | 0 | } |
180 | | } |
181 | | |
182 | | impl Sub<Vec2> for Triangle { |
183 | | type Output = Triangle; |
184 | | |
185 | | #[inline] |
186 | 0 | fn sub(self, v: Vec2) -> Triangle { |
187 | 0 | Triangle::new(self.a - v, self.b - v, self.c - v) |
188 | 0 | } |
189 | | } |
190 | | |
191 | | #[doc(hidden)] |
192 | | pub struct TrianglePathIter { |
193 | | triangle: Triangle, |
194 | | ix: usize, |
195 | | } |
196 | | |
197 | | impl Shape for Triangle { |
198 | | type PathElementsIter<'iter> = TrianglePathIter; |
199 | | |
200 | 0 | fn path_elements(&self, _tolerance: f64) -> TrianglePathIter { |
201 | 0 | TrianglePathIter { |
202 | 0 | triangle: *self, |
203 | 0 | ix: 0, |
204 | 0 | } |
205 | 0 | } |
206 | | |
207 | | #[inline] |
208 | 0 | fn area(&self) -> f64 { |
209 | 0 | Triangle::area(self) |
210 | 0 | } |
211 | | |
212 | | #[inline] |
213 | 0 | fn perimeter(&self, _accuracy: f64) -> f64 { |
214 | 0 | self.a.distance(self.b) + self.b.distance(self.c) + self.c.distance(self.a) |
215 | 0 | } |
216 | | |
217 | | #[inline] |
218 | 0 | fn winding(&self, pt: Point) -> i32 { |
219 | 0 | let s0 = (self.b - self.a).cross(pt - self.a).signum(); |
220 | 0 | let s1 = (self.c - self.b).cross(pt - self.b).signum(); |
221 | 0 | let s2 = (self.a - self.c).cross(pt - self.c).signum(); |
222 | | |
223 | 0 | if s0 == s1 && s1 == s2 { |
224 | 0 | s0 as i32 |
225 | | } else { |
226 | 0 | 0 |
227 | | } |
228 | 0 | } |
229 | | |
230 | | #[inline] |
231 | 0 | fn bounding_box(&self) -> Rect { |
232 | 0 | Rect::new( |
233 | 0 | self.a.x.min(self.b.x.min(self.c.x)), |
234 | 0 | self.a.y.min(self.b.y.min(self.c.y)), |
235 | 0 | self.a.x.max(self.b.x.max(self.c.x)), |
236 | 0 | self.a.y.max(self.b.y.max(self.c.y)), |
237 | | ) |
238 | 0 | } |
239 | | } |
240 | | |
241 | | // Note: vertices a, b and c are not guaranteed to be in order as described in the struct comments |
242 | | // (i.e. as "vertex a is topmost, vertex b is leftmost, and vertex c is rightmost") |
243 | | impl Iterator for TrianglePathIter { |
244 | | type Item = PathEl; |
245 | | |
246 | 0 | fn next(&mut self) -> Option<PathEl> { |
247 | 0 | self.ix += 1; |
248 | 0 | match self.ix { |
249 | 0 | 1 => Some(PathEl::MoveTo(self.triangle.a)), |
250 | 0 | 2 => Some(PathEl::LineTo(self.triangle.b)), |
251 | 0 | 3 => Some(PathEl::LineTo(self.triangle.c)), |
252 | 0 | 4 => Some(PathEl::ClosePath), |
253 | 0 | _ => None, |
254 | | } |
255 | 0 | } |
256 | | } |
257 | | |
258 | | // TODO: better and more tests |
259 | | #[cfg(test)] |
260 | | mod tests { |
261 | | use crate::{Point, Triangle, Vec2}; |
262 | | |
263 | | fn assert_approx_eq(x: f64, y: f64, max_relative_error: f64) { |
264 | | assert!( |
265 | | (x - y).abs() <= f64::max(x.abs(), y.abs()) * max_relative_error, |
266 | | "{x} != {y}" |
267 | | ); |
268 | | } |
269 | | |
270 | | fn assert_approx_eq_point(x: Point, y: Point, max_relative_error: f64) { |
271 | | assert_approx_eq(x.x, y.x, max_relative_error); |
272 | | assert_approx_eq(x.y, y.y, max_relative_error); |
273 | | } |
274 | | |
275 | | #[test] |
276 | | fn centroid() { |
277 | | let test = Triangle::from_coords((-90.02, 3.5), (7.2, -9.3), (8.0, 9.1)).centroid(); |
278 | | let expected = Point::new(-24.94, 1.1); |
279 | | |
280 | | assert_approx_eq_point(test, expected, f64::EPSILON * 100.); |
281 | | } |
282 | | |
283 | | #[test] |
284 | | fn offsets() { |
285 | | let test = Triangle::from_coords((-20.0, 180.2), (1.2, 0.0), (290.0, 100.0)).offsets(); |
286 | | let expected = [ |
287 | | Vec2::new(-110.4, 86.8), |
288 | | Vec2::new(-89.2, -93.4), |
289 | | Vec2::new(199.6, 6.6), |
290 | | ]; |
291 | | |
292 | | test.iter().zip(expected.iter()).for_each(|(t, e)| { |
293 | | assert_approx_eq_point(t.to_point(), e.to_point(), f64::EPSILON * 100.); |
294 | | }); |
295 | | } |
296 | | |
297 | | #[test] |
298 | | fn area() { |
299 | | let test = Triangle::new( |
300 | | (12123.423, 2382.7834), |
301 | | (7892.729, 238.459), |
302 | | (7820.2, 712.23), |
303 | | ); |
304 | | let expected = 1079952.9157407999; |
305 | | |
306 | | // initial |
307 | | assert_approx_eq(test.area(), -expected, f64::EPSILON * 100.); |
308 | | // permutate vertex |
309 | | let test = Triangle::new(test.b, test.a, test.c); |
310 | | assert_approx_eq(test.area(), expected, f64::EPSILON * 100.); |
311 | | } |
312 | | |
313 | | #[test] |
314 | | fn circumcenter() { |
315 | | let test = Triangle::EQUILATERAL.circumscribed_circle().center; |
316 | | let expected = Point::new(0.5, 0.28867513459481288); |
317 | | |
318 | | assert_approx_eq_point(test, expected, f64::EPSILON * 100.); |
319 | | } |
320 | | |
321 | | #[test] |
322 | | fn inradius() { |
323 | | let test = Triangle::EQUILATERAL.inscribed_circle().radius; |
324 | | let expected = 0.28867513459481287; |
325 | | |
326 | | assert_approx_eq(test, expected, f64::EPSILON * 100.); |
327 | | } |
328 | | |
329 | | #[test] |
330 | | fn circumradius() { |
331 | | let test = Triangle::EQUILATERAL; |
332 | | let expected = 0.57735026918962576; |
333 | | |
334 | | assert_approx_eq( |
335 | | test.circumscribed_circle().radius, |
336 | | expected, |
337 | | f64::EPSILON * 100., |
338 | | ); |
339 | | // permute vertex |
340 | | let test = Triangle::new(test.b, test.a, test.c); |
341 | | assert_approx_eq( |
342 | | test.circumscribed_circle().radius, |
343 | | -expected, |
344 | | f64::EPSILON * 100., |
345 | | ); |
346 | | } |
347 | | |
348 | | #[test] |
349 | | fn inscribed_circle() { |
350 | | let test = Triangle::new((-4., 1.), (-4., -1.), (10., 3.)); |
351 | | |
352 | | let inscribed = test.inscribed_circle(); |
353 | | assert_approx_eq_point( |
354 | | inscribed.center, |
355 | | (-3.0880178529263671, 0.20904207741504303).into(), |
356 | | f64::EPSILON * 100., |
357 | | ); |
358 | | assert_approx_eq(inscribed.radius, 0.91198214707363295, f64::EPSILON * 100.); |
359 | | } |
360 | | |
361 | | #[test] |
362 | | fn circumscribed_circle() { |
363 | | let test = Triangle::new((-4., 1.), (-4., -1.), (10., 3.)); |
364 | | |
365 | | let circumscribed = test.circumscribed_circle(); |
366 | | assert_approx_eq_point( |
367 | | circumscribed.center, |
368 | | (3.2857142857142857, 0.).into(), |
369 | | f64::EPSILON * 100., |
370 | | ); |
371 | | assert_approx_eq( |
372 | | circumscribed.radius, |
373 | | 7.3540215292764288, |
374 | | f64::EPSILON * 100., |
375 | | ); |
376 | | } |
377 | | } |