Coverage Report

Created: 2025-12-31 07:38

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}