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
107impl Shape for Triangle {
108    fn from_points(points: &[Coord]) -> Self
109    where
110        Self: Sized,
111    {
112        Triangle::new(points[0], points[1], points[2])
113    }
114
115    fn rebuild(&self, points: &[Coord]) -> Self
116    where
117        Self: Sized,
118    {
119        Triangle::from_points(points)
120    }
121
122    fn contains(&self, point: Coord) -> bool {
123        let p1 = coord!(
124            self.points[1].x - self.points[0].x,
125            self.points[1].y - self.points[0].y,
126        );
127        let p2 = coord!(
128            self.points[2].x - self.points[0].x,
129            self.points[2].y - self.points[0].y,
130        );
131        let q = coord!(point.x - self.points[0].x, point.y - self.points[0].y);
132
133        let s = q.cross_product(p2) as f32 / p1.cross_product(p2) as f32;
134        let t = p1.cross_product(q) as f32 / p1.cross_product(p2) as f32;
135
136        s >= 0.0 && t >= 0.0 && (s + t) <= 1.0
137    }
138
139    fn points(&self) -> Vec<Coord> {
140        self.points.to_vec()
141    }
142
143    #[inline]
144    fn center(&self) -> Coord {
145        self.center
146    }
147
148    fn outline_pixels(&self) -> Vec<Coord> {
149        self.as_lines()
150            .iter()
151            .flat_map(|line| line.outline_pixels())
152            .collect()
153    }
154
155    fn filled_pixels(&self) -> Vec<Coord> {
156        let mut output = new_hash_set();
157        let mut sorted_points = self.points.to_vec();
158        sorted_points.sort_by_key(|c| c.y);
159        let points = [
160            (sorted_points[0].x as f32, sorted_points[0].y as f32),
161            (sorted_points[1].x as f32, sorted_points[1].y as f32),
162            (sorted_points[2].x as f32, sorted_points[2].y as f32),
163        ];
164        if points[1].1 == points[2].1 {
165            draw_flat_bottom(&mut output, points);
166        } else if points[0].1 == points[1].1 {
167            draw_flat_top(&mut output, points);
168        } else {
169            let p = (
170                points[0].0
171                    + ((points[1].1 - points[0].1) / (points[2].1 - points[0].1))
172                        * (points[2].0 - points[0].0),
173                points[1].1,
174            );
175            draw_flat_bottom(&mut output, [points[0], points[1], p]);
176            draw_flat_top(&mut output, [points[1], p, points[2]]);
177        }
178        output.into_iter().collect()
179    }
180
181    fn to_shape_box(&self) -> ShapeBox {
182        ShapeBox::Triangle(self.clone())
183    }
184}
185
186#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
187#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
188pub enum AnglePosition {
189    TopLeft,
190    TopRight,
191    BottomRight,
192    BottomLeft,
193    Top,
194    Right,
195    Bottom,
196    Left,
197}
198
199#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
200#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
201pub enum FlatSide {
202    Top,
203    Bottom,
204    Left,
205    Right,
206}
207
208impl Triangle {
209    #[must_use]
210    pub fn as_rect(&self) -> Rect {
211        Rect::new((self.left(), self.top()), (self.right(), self.bottom()))
212    }
213
214    #[must_use]
215    pub fn as_lines(&self) -> [Line; 3] {
216        let points = self.points();
217        [
218            Line::new(points[0], points[1]),
219            Line::new(points[1], points[2]),
220            Line::new(points[2], points[0]),
221        ]
222    }
223
224    /// Create an Isosceles Right Angle Triangle
225    #[must_use]
226    pub fn right_angle<P: Into<Coord>>(
227        angle_coord: P,
228        size: usize,
229        angle_position: AnglePosition,
230    ) -> Triangle {
231        let size = size as isize;
232        let point = angle_coord.into();
233        let left = point.x - size;
234        let right = point.x + size;
235        let top = point.y - size;
236        let bottom = point.y + size;
237        let half_size = size / 2;
238        match angle_position {
239            AnglePosition::TopLeft => Triangle::new(point, (right, point.y), (point.x, bottom)),
240            AnglePosition::BottomRight => Triangle::new(point, (left, point.y), (point.x, top)),
241            AnglePosition::BottomLeft => Triangle::new(point, (right, point.y), (point.x, top)),
242            AnglePosition::TopRight => Triangle::new(point, (left, point.y), (point.x, bottom)),
243            AnglePosition::Top => Triangle::new(
244                point,
245                point + (-half_size, half_size),
246                point + (half_size, half_size),
247            ),
248            AnglePosition::Right => Triangle::new(
249                point,
250                point - (half_size, half_size),
251                point + (-half_size, half_size),
252            ),
253            AnglePosition::Bottom => Triangle::new(
254                point,
255                point - (half_size, half_size),
256                point + (half_size, -half_size),
257            ),
258            AnglePosition::Left => Triangle::new(
259                point,
260                point + (half_size, -half_size),
261                point + (half_size, half_size),
262            ),
263        }
264    }
265
266    /// Create an equilateral triangle with width and height of [size] around [center]
267    /// The top left would be (center.x - size / 2, center.y + size / 2) and bottom right (center.x + size / 2, center.y + size / 2)
268    #[must_use]
269    pub fn equilateral<P: Into<Coord>>(center: P, size: usize, flat_side: FlatSide) -> Triangle {
270        let point = center.into();
271        let dist = (size / 2) as isize;
272        let left = point.x - dist;
273        let right = point.x + dist;
274        let top = point.y - dist;
275        let bottom = point.y + dist;
276        match flat_side {
277            FlatSide::Top => Triangle::new((left, top), (right, top), (point.x, bottom)),
278            FlatSide::Bottom => Triangle::new((left, bottom), (right, bottom), (point.x, top)),
279            FlatSide::Left => Triangle::new((left, top), (left, bottom), (right, point.y)),
280            FlatSide::Right => Triangle::new((right, top), (right, bottom), (left, point.y)),
281        }
282    }
283}
284
285fn draw_flat_bottom(output: &mut FnvHashSet<Coord>, points: [(f32, f32); 3]) {
286    let slope1 = (points[1].0 - points[0].0) / (points[1].1 - points[0].1);
287    let slope2 = (points[2].0 - points[0].0) / (points[2].1 - points[0].1);
288    let mut x1 = points[0].0;
289    let mut x2 = points[0].0;
290    for y in (points[0].1 as usize)..(points[1].1 as usize) {
291        let start = x1.min(x2) as isize;
292        let end = x1.max(x2) as isize + 1;
293        let line_points = Line::new((start, y as isize), (end, y as isize)).outline_pixels();
294        for point in line_points {
295            output.insert(point);
296        }
297        x1 += slope1;
298        x2 += slope2;
299    }
300}
301
302fn draw_flat_top(output: &mut FnvHashSet<Coord>, points: [(f32, f32); 3]) {
303    let slope1 = (points[2].0 - points[0].0) / (points[2].1 - points[0].1);
304    let slope2 = (points[2].0 - points[1].0) / (points[2].1 - points[1].1);
305    let mut x1 = points[2].0;
306    let mut x2 = points[2].0;
307    for y in ((points[0].1 as usize)..(points[2].1 as usize)).rev() {
308        let start = x1.min(x2) as usize;
309        let end = x1.max(x2) as usize + 1;
310        let line_points = Line::new((start, y), (end, y)).outline_pixels();
311        for point in line_points {
312            output.insert(point);
313        }
314        x1 -= slope1;
315        x2 -= slope2;
316    }
317}
318
319#[cfg(test)]
320mod test {
321    use crate::triangle::{AnglePosition, FlatSide, Triangle};
322    use crate::Shape;
323
324    #[test]
325    fn right_angle_triangles() {
326        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::TopLeft);
327        assert_eq!(
328            triangle.points[0],
329            (100, 100).into(),
330            "topleft - main angle"
331        );
332        assert_eq!(triangle.points[1], (200, 100).into(), "topleft - same y");
333        assert_eq!(triangle.points[2], (100, 200).into(), "topleft - same x");
334
335        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::BottomRight);
336        assert_eq!(
337            triangle.points[0],
338            (100, 100).into(),
339            "bottomright - main angle"
340        );
341        assert_eq!(triangle.points[1], (0, 100).into(), "bottomright - same y");
342        assert_eq!(triangle.points[2], (100, 0).into(), "bottomright - same x");
343
344        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::TopRight);
345        assert_eq!(
346            triangle.points[0],
347            (100, 100).into(),
348            "topright - main angle"
349        );
350        assert_eq!(triangle.points[1], (0, 100).into(), "topright - same y");
351        assert_eq!(triangle.points[2], (100, 200).into(), "topright - same x");
352
353        let triangle = Triangle::right_angle((100, 100), 100, AnglePosition::BottomLeft);
354        assert_eq!(
355            triangle.points[0],
356            (100, 100).into(),
357            "bottomleft - main angle"
358        );
359        assert_eq!(triangle.points[1], (200, 100).into(), "bottomleft - same y");
360        assert_eq!(triangle.points[2], (100, 0).into(), "bottomleft - same x");
361    }
362
363    #[test]
364    fn check_moving() {
365        let triangle = Triangle::equilateral((50, 50), 10, FlatSide::Left);
366        assert_eq!(triangle.center, coord!(50, 50));
367        assert_eq!(triangle.points[0], coord!(45, 45));
368        let moved = triangle.move_to(coord!(30, 30));
369        assert_eq!(moved.points[0], coord!(30, 30));
370
371        let moved = triangle.move_center_to(coord!(130, 230));
372        assert_eq!(moved.center, coord!(130, 230));
373        assert_eq!(moved.points[0], coord!(125, 225));
374    }
375}