Skip to main content

graphics_shapes/
triangle.rs

1use crate::new_hash_set;
2use crate::prelude::*;
3use crate::shape_box::ShapeBox;
4use fnv::FnvHashSet;
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)]
10pub enum TriangleAngleType {
11    Acute,
12    Right,
13    Obtuse,
14    Equiangular,
15    Other,
16}
17
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)]
20pub enum TriangleSideType {
21    Isosceles,
22    Scalene,
23    Equilateral,
24}
25
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27#[derive(Debug, Clone, Eq, PartialEq, Hash)]
28pub struct Triangle {
29    points: [Coord; 3],
30    angles: [isize; 3],
31    angle_type: TriangleAngleType,
32    side_type: TriangleSideType,
33    center: Coord,
34}
35
36impl IntersectsContains for Triangle {}
37
38impl Triangle {
39    #[must_use]
40    pub fn new<P1: Into<Coord>, P2: Into<Coord>, P3: Into<Coord>>(
41        point1: P1,
42        point2: P2,
43        point3: P3,
44    ) -> Self {
45        let points = [point1.into(), point2.into(), point3.into()];
46        let angles = [
47            points[0].angle_to(points[1]),
48            points[1].angle_to(points[2]),
49            points[2].angle_to(points[0]),
50        ];
51        let angle_type = if angles.iter().any(|a| a.abs() == 90) {
52            TriangleAngleType::Right
53        } else if angles.iter().all(|a| a.abs() < 90) {
54            TriangleAngleType::Acute
55        } else if angles[0].abs() == angles[1].abs() && angles[1].abs() == angles[2].abs() {
56            TriangleAngleType::Equiangular
57        } else if angles.iter().any(|a| a.abs() > 90) {
58            TriangleAngleType::Obtuse
59        } else {
60            TriangleAngleType::Other
61        };
62        let side_type = if points[0].distance(points[1]) == points[1].distance(points[2])
63            && points[2].distance(points[0]) == points[1].distance(points[2])
64        {
65            TriangleSideType::Equilateral
66        } else if points[0].distance(points[1]) == points[1].distance(points[2])
67            || points[2].distance(points[0]) == points[1].distance(points[2])
68            || points[2].distance(points[0]) == points[0].distance(points[1])
69        {
70            TriangleSideType::Isosceles
71        } else {
72            TriangleSideType::Scalene
73        };
74        let mut triangle = Self {
75            points,
76            angles,
77            angle_type,
78            side_type,
79            center: coord!(0, 0),
80        };
81        triangle.center = coord!(triangle.left(), triangle.top())
82            .mid_point(coord!(triangle.right(), triangle.bottom()));
83        triangle
84    }
85}
86
87impl Triangle {
88    #[inline]
89    #[must_use]
90    pub fn angles(&self) -> [isize; 3] {
91        self.angles
92    }
93
94    #[inline]
95    #[must_use]
96    pub fn angle_type(&self) -> &TriangleAngleType {
97        &self.angle_type
98    }
99
100    #[inline]
101    #[must_use]
102    pub fn side_type(&self) -> &TriangleSideType {
103        &self.side_type
104    }
105
106    /// Return center of triangle area
107    /// [center] returns midpoint of bounding area
108    pub fn centroid(&self) -> Coord {
109        (self.points[0] + self.points[1] + self.points[2]) / 3
110    }
111}
112
113impl Shape for Triangle {
114    fn from_points(points: &[Coord]) -> Self
115    where
116        Self: Sized,
117    {
118        Triangle::new(points[0], points[1], points[2])
119    }
120
121    fn rebuild(&self, points: &[Coord]) -> Self
122    where
123        Self: Sized,
124    {
125        Triangle::from_points(points)
126    }
127
128    fn contains(&self, point: Coord) -> bool {
129        let p1 = coord!(
130            self.points[1].x - self.points[0].x,
131            self.points[1].y - self.points[0].y,
132        );
133        let p2 = coord!(
134            self.points[2].x - self.points[0].x,
135            self.points[2].y - self.points[0].y,
136        );
137        let q = coord!(point.x - self.points[0].x, point.y - self.points[0].y);
138
139        let s = q.cross_product(p2) as f32 / p1.cross_product(p2) as f32;
140        let t = p1.cross_product(q) as f32 / p1.cross_product(p2) as f32;
141
142        s >= 0.0 && t >= 0.0 && (s + t) <= 1.0
143    }
144
145    fn points(&self) -> Vec<Coord> {
146        self.points.to_vec()
147    }
148
149    #[inline]
150    fn center(&self) -> Coord {
151        self.center
152    }
153
154    fn outline_pixels(&self) -> Vec<Coord> {
155        self.as_lines()
156            .iter()
157            .flat_map(|line| line.outline_pixels())
158            .collect()
159    }
160
161    fn filled_pixels(&self) -> Vec<Coord> {
162        let mut output = new_hash_set();
163        let mut sorted_points = self.points.to_vec();
164        sorted_points.sort_by_key(|c| c.y);
165        let points = [
166            (sorted_points[0].x as f32, sorted_points[0].y as f32),
167            (sorted_points[1].x as f32, sorted_points[1].y as f32),
168            (sorted_points[2].x as f32, sorted_points[2].y as f32),
169        ];
170        if points[1].1 == points[2].1 {
171            draw_flat_bottom(&mut output, points);
172        } else if points[0].1 == points[1].1 {
173            draw_flat_top(&mut output, points);
174        } else {
175            let p = (
176                points[0].0
177                    + ((points[1].1 - points[0].1) / (points[2].1 - points[0].1))
178                        * (points[2].0 - points[0].0),
179                points[1].1,
180            );
181            draw_flat_bottom(&mut output, [points[0], points[1], p]);
182            draw_flat_top(&mut output, [points[1], p, points[2]]);
183        }
184        output.into_iter().collect()
185    }
186
187    fn to_shape_box(&self) -> ShapeBox {
188        ShapeBox::Triangle(self.clone())
189    }
190}
191
192#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
193#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
194pub enum AnglePosition {
195    TopLeft,
196    TopRight,
197    BottomRight,
198    BottomLeft,
199    Top,
200    Right,
201    Bottom,
202    Left,
203}
204
205#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
206#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
207pub enum FlatSide {
208    Top,
209    Bottom,
210    Left,
211    Right,
212}
213
214impl Triangle {
215    #[must_use]
216    pub fn as_rect(&self) -> Rect {
217        Rect::new((self.left(), self.top()), (self.right(), self.bottom()))
218    }
219
220    #[must_use]
221    pub fn as_lines(&self) -> [Line; 3] {
222        let points = self.points();
223        [
224            Line::new(points[0], points[1]),
225            Line::new(points[1], points[2]),
226            Line::new(points[2], points[0]),
227        ]
228    }
229
230    /// Create an Isosceles Right Angle Triangle
231    #[must_use]
232    pub fn right_angle<P: Into<Coord>>(
233        angle_coord: P,
234        size: usize,
235        angle_position: AnglePosition,
236    ) -> Triangle {
237        let size = size as isize;
238        let point = angle_coord.into();
239        let left = point.x - size;
240        let right = point.x + size;
241        let top = point.y - size;
242        let bottom = point.y + size;
243        let half_size = size / 2;
244        match angle_position {
245            AnglePosition::TopLeft => Triangle::new(point, (right, point.y), (point.x, bottom)),
246            AnglePosition::BottomRight => Triangle::new(point, (left, point.y), (point.x, top)),
247            AnglePosition::BottomLeft => Triangle::new(point, (right, point.y), (point.x, top)),
248            AnglePosition::TopRight => Triangle::new(point, (left, point.y), (point.x, bottom)),
249            AnglePosition::Top => Triangle::new(
250                point,
251                point + (-half_size, half_size),
252                point + (half_size, half_size),
253            ),
254            AnglePosition::Right => Triangle::new(
255                point,
256                point - (half_size, half_size),
257                point + (-half_size, half_size),
258            ),
259            AnglePosition::Bottom => Triangle::new(
260                point,
261                point - (half_size, half_size),
262                point + (half_size, -half_size),
263            ),
264            AnglePosition::Left => Triangle::new(
265                point,
266                point + (half_size, -half_size),
267                point + (half_size, half_size),
268            ),
269        }
270    }
271
272    /// Create an equilateral triangle with width and height of [size] around [center]
273    /// The top left would be (center.x - size / 2, center.y + size / 2) and bottom right (center.x + size / 2, center.y + size / 2)
274    #[must_use]
275    pub fn equilateral<P: Into<Coord>>(center: P, size: usize, flat_side: FlatSide) -> Triangle {
276        let point = center.into();
277        let dist = (size / 2) as isize;
278        let left = point.x - dist;
279        let right = point.x + dist;
280        let top = point.y - dist;
281        let bottom = point.y + dist;
282        match flat_side {
283            FlatSide::Top => Triangle::new((left, top), (right, top), (point.x, bottom)),
284            FlatSide::Bottom => Triangle::new((left, bottom), (right, bottom), (point.x, top)),
285            FlatSide::Left => Triangle::new((left, top), (left, bottom), (right, point.y)),
286            FlatSide::Right => Triangle::new((right, top), (right, bottom), (left, point.y)),
287        }
288    }
289}
290
291fn draw_flat_bottom(output: &mut FnvHashSet<Coord>, points: [(f32, f32); 3]) {
292    let slope1 = (points[1].0 - points[0].0) / (points[1].1 - points[0].1);
293    let slope2 = (points[2].0 - points[0].0) / (points[2].1 - points[0].1);
294    let mut x1 = points[0].0;
295    let mut x2 = points[0].0;
296    for y in (points[0].1 as usize)..(points[1].1 as usize) {
297        let start = x1.min(x2) as isize;
298        let end = x1.max(x2) as isize + 1;
299        let line_points = Line::new((start, y as isize), (end, y as isize)).outline_pixels();
300        for point in line_points {
301            output.insert(point);
302        }
303        x1 += slope1;
304        x2 += slope2;
305    }
306}
307
308fn draw_flat_top(output: &mut FnvHashSet<Coord>, points: [(f32, f32); 3]) {
309    let slope1 = (points[2].0 - points[0].0) / (points[2].1 - points[0].1);
310    let slope2 = (points[2].0 - points[1].0) / (points[2].1 - points[1].1);
311    let mut x1 = points[2].0;
312    let mut x2 = points[2].0;
313    for y in ((points[0].1 as usize)..(points[2].1 as usize)).rev() {
314        let start = x1.min(x2) as usize;
315        let end = x1.max(x2) as usize + 1;
316        let line_points = Line::new((start, y), (end, y)).outline_pixels();
317        for point in line_points {
318            output.insert(point);
319        }
320        x1 -= slope1;
321        x2 -= slope2;
322    }
323}
324
325#[cfg(test)]
326mod test {
327    use crate::triangle::{AnglePosition, FlatSide, Triangle};
328    use crate::Shape;
329
330    #[test]
331    fn right_angle_triangles() {
332        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::TopLeft);
333        assert_eq!(
334            triangle.points[0],
335            (100, 100).into(),
336            "topleft - main angle"
337        );
338        assert_eq!(triangle.points[1], (200, 100).into(), "topleft - same y");
339        assert_eq!(triangle.points[2], (100, 200).into(), "topleft - same x");
340
341        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::BottomRight);
342        assert_eq!(
343            triangle.points[0],
344            (100, 100).into(),
345            "bottomright - main angle"
346        );
347        assert_eq!(triangle.points[1], (0, 100).into(), "bottomright - same y");
348        assert_eq!(triangle.points[2], (100, 0).into(), "bottomright - same x");
349
350        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::TopRight);
351        assert_eq!(
352            triangle.points[0],
353            (100, 100).into(),
354            "topright - main angle"
355        );
356        assert_eq!(triangle.points[1], (0, 100).into(), "topright - same y");
357        assert_eq!(triangle.points[2], (100, 200).into(), "topright - same x");
358
359        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::BottomLeft);
360        assert_eq!(
361            triangle.points[0],
362            (100, 100).into(),
363            "bottomleft - main angle"
364        );
365        assert_eq!(triangle.points[1], (200, 100).into(), "bottomleft - same y");
366        assert_eq!(triangle.points[2], (100, 0).into(), "bottomleft - same x");
367    }
368
369    #[test]
370    fn check_moving() {
371        let triangle = Triangle::equilateral((50, 50), 10, FlatSide::Left);
372        assert_eq!(triangle.center, coord!(50, 50));
373        assert_eq!(triangle.points[0], coord!(45, 45));
374        let moved = triangle.move_to(coord!(30, 30));
375        assert_eq!(moved.points[0], coord!(30, 30));
376
377        let moved = triangle.move_center_to(coord!(130, 230));
378        assert_eq!(moved.center, coord!(130, 230));
379        assert_eq!(moved.points[0], coord!(125, 225));
380    }
381}