graphics_shapes/
line.rs

1use crate::prelude::*;
2use crate::shape_box::ShapeBox;
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5use std::mem::swap;
6
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)]
9pub enum LineType {
10    /// Single pixel
11    Point,
12    Horizontal,
13    Vertical,
14    /// Anything other vertical or horizontal, see [angle][Line::angle]
15    Angled,
16}
17
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19#[derive(Debug, Clone, Eq, PartialEq)]
20pub struct Line {
21    start: Coord,
22    end: Coord,
23    len: usize,
24    line_type: LineType,
25    angle: isize,
26}
27
28impl IntersectsContains for Line {}
29
30impl Line {
31    #[must_use]
32    pub fn new<P1: Into<Coord>, P2: Into<Coord>>(start: P1, end: P2) -> Self {
33        let start = start.into();
34        let end = end.into();
35        let line_type = if start == end {
36            LineType::Point
37        } else if start.x == end.x {
38            LineType::Vertical
39        } else if start.y == end.y {
40            LineType::Horizontal
41        } else {
42            LineType::Angled
43        };
44        let len = start.distance(end);
45        let angle = start.angle_to(end);
46        Self {
47            start,
48            end,
49            len,
50            line_type,
51            angle,
52        }
53    }
54}
55
56impl Line {
57    #[allow(clippy::len_without_is_empty)] //use start()==end() to check that
58    #[inline]
59    #[must_use]
60    pub fn len(&self) -> usize {
61        self.len
62    }
63
64    /// Angle from `start` point to `end` point
65    #[inline]
66    #[must_use]
67    pub fn angle(&self) -> isize {
68        self.angle
69    }
70
71    #[inline]
72    #[must_use]
73    pub fn start(&self) -> Coord {
74        self.start
75    }
76
77    #[inline]
78    #[must_use]
79    pub fn end(&self) -> Coord {
80        self.end
81    }
82
83    #[inline]
84    #[must_use]
85    pub fn line_type(&self) -> LineType {
86        self.line_type
87    }
88
89    pub fn nearest_point<P: Into<Coord>>(&self, other: P) -> Coord {
90        if self.line_type == LineType::Point {
91            return self.start;
92        }
93        let other = other.into();
94        let ba_x = (self.end.x - self.start.x) as f64;
95        let ba_y = (self.end.y - self.start.y) as f64;
96
97        let len = ba_x.powi(2) + ba_y.powi(2);
98        let t =
99            ((other.x - self.start.x) as f64 * ba_x + (other.y - self.start.y) as f64 * ba_y) / len;
100        let t = t.clamp(0.0, 1.0);
101        coord!(
102            self.start.x as f64 + t * ba_x,
103            self.start.y as f64 + t * ba_y
104        )
105    }
106}
107
108impl Shape for Line {
109    fn from_points(points: &[Coord]) -> Self
110    where
111        Self: Sized,
112    {
113        debug_assert!(points.len() >= 2);
114        Line::new(points[0], points[1])
115    }
116
117    fn rebuild(&self, points: &[Coord]) -> Self
118    where
119        Self: Sized,
120    {
121        Line::from_points(points)
122    }
123
124    fn contains(&self, point: Coord) -> bool {
125        match self.line_type {
126            LineType::Point => self.start == point,
127            LineType::Horizontal => {
128                self.start.y == point.y && (self.left()..=self.right()).contains(&point.x)
129            }
130            LineType::Vertical => {
131                self.start.x == point.x && (self.top()..=self.bottom()).contains(&point.y)
132            }
133            LineType::Angled => {
134                self.start.are_collinear(self.end, point) && point.is_between(self.start, self.end)
135            }
136        }
137    }
138
139    fn points(&self) -> Vec<Coord> {
140        vec![self.start, self.end]
141    }
142
143    fn center(&self) -> Coord {
144        self.start.mid_point(self.end)
145    }
146
147    #[inline]
148    fn left(&self) -> isize {
149        self.start.x.min(self.end.x)
150    }
151
152    #[inline]
153    fn right(&self) -> isize {
154        self.start.x.max(self.end.x)
155    }
156
157    #[inline]
158    fn top(&self) -> isize {
159        self.start.y.min(self.end.y)
160    }
161
162    #[inline]
163    fn bottom(&self) -> isize {
164        self.start.y.max(self.end.y)
165    }
166
167    /// Returns left most point x and lowest y  (could be from `start` or `end` for both)
168    /// This doesn't really make sense for line as it may return (end.x, start.y) for example
169    fn top_left(&self) -> Coord {
170        coord!(self.left(), self.top())
171    }
172
173    /// Returns right most point x and lowest y  (could be from `start` or `end` for both)
174    /// This doesn't really make sense for line as it may return (start.x, end.y) for example
175    fn top_right(&self) -> Coord {
176        coord!(self.right(), self.top())
177    }
178
179    /// Returns left most point x and highest y  (could be from `start` or `end` for both)
180    /// This doesn't really make sense for line as it may return (start.x, end.y) for example
181    fn bottom_left(&self) -> Coord {
182        coord!(self.left(), self.bottom())
183    }
184
185    /// Returns right most point x and highest y  (could be from `start` or `end` for both)
186    /// This doesn't really make sense for line as it may return (start.x, end.y) for example
187    fn bottom_right(&self) -> Coord {
188        coord!(self.right(), self.bottom())
189    }
190
191    fn outline_pixels(&self) -> Vec<Coord> {
192        let mut start = self.start;
193        let mut end = self.end;
194        if start.x > end.x || start.y > end.y {
195            swap(&mut start, &mut end);
196        }
197        let mut output = vec![];
198        if start.x == end.x {
199            for y in start.y..=end.y {
200                output.push(coord!(start.x, y));
201            }
202        } else if start.y == end.y {
203            for x in start.x..=end.x {
204                output.push(coord!(x, start.y));
205            }
206        } else {
207            let mut delta = 0;
208            let x1 = start.x;
209            let y1 = start.y;
210            let x2 = end.x;
211            let y2 = end.y;
212            let dx = isize::abs(x2 - x1);
213            let dy = isize::abs(y2 - y1);
214            let dx2 = dx * 2;
215            let dy2 = dy * 2;
216            let ix: isize = if x1 < x2 { 1 } else { -1 };
217            let iy: isize = if y1 < y2 { 1 } else { -1 };
218            let mut x = x1;
219            let mut y = y1;
220            if dx >= dy {
221                loop {
222                    output.push(coord!(x, y));
223                    if x == x2 {
224                        break;
225                    }
226                    x += ix;
227                    delta += dy2;
228                    if delta > dx {
229                        y += iy;
230                        delta -= dx2;
231                    }
232                }
233            } else {
234                loop {
235                    output.push(coord!(x, y));
236                    if y == y2 {
237                        break;
238                    }
239                    y += iy;
240                    delta += dx2;
241                    if delta > dy {
242                        x += ix;
243                        delta -= dy2;
244                    }
245                }
246            }
247        }
248        output
249    }
250
251    fn filled_pixels(&self) -> Vec<Coord> {
252        self.outline_pixels()
253    }
254
255    fn to_shape_box(&self) -> ShapeBox {
256        ShapeBox::Line(self.clone())
257    }
258}
259
260impl Line {
261    #[must_use]
262    pub fn as_rect(&self) -> Rect {
263        Rect::new(self.start, self.end)
264    }
265
266    #[must_use]
267    pub fn as_circle(&self) -> Circle {
268        Circle::new(self.start, self.start.distance(self.end))
269    }
270}
271
272#[cfg(test)]
273mod test {
274    use crate::line::Line;
275    use crate::Shape;
276
277    #[test]
278    fn len() {
279        assert_eq!(Line::new((10, 10), (20, 10)).len, 10);
280        assert_eq!(Line::new((10, 10), (10, 20)).len, 10);
281        assert_eq!(Line::new((10, 10), (0, 10)).len, 10);
282        assert_eq!(Line::new((10, 10), (10, 0)).len, 10);
283        assert_eq!(Line::new((10, 10), (0, 0)).len, 14);
284        assert_eq!(Line::new((10, 10), (20, 20)).len, 14);
285        assert_eq!(Line::new((10, 10), (0, 20)).len, 14);
286        assert_eq!(Line::new((10, 10), (20, 0)).len, 14);
287    }
288
289    #[test]
290    fn rotate_center() {
291        assert_eq!(
292            Line::new((10, 10), (20, 10)).rotate(90),
293            Line::new((15, 5), (15, 15))
294        );
295        assert_eq!(
296            Line::new((10, 10), (20, 10)).rotate(25),
297            Line::new((10, 8), (20, 12))
298        );
299    }
300
301    #[test]
302    fn nearest() {
303        let line = Line::new((10, 10), (20, 20));
304        let point = line.nearest_point((17, 12));
305        assert!(line.contains(coord!(14, 14)));
306        assert_eq!(point, coord!(14, 14));
307    }
308
309    #[test]
310    fn nearest_line_reversed() {
311        let line = Line::new((110, 100), (40, 30));
312        let point = line.nearest_point((55, 85));
313        assert!(line.contains(coord!(75, 65)));
314        assert_eq!(point, coord!(75, 65));
315    }
316
317    mod contains {
318        use crate::line::Line;
319        use crate::Shape;
320
321        #[test]
322        fn point() {
323            let line = Line::new((10, 10), (10, 10));
324            assert!(line.contains(coord!(10, 10)));
325            assert!(!line.contains(coord!(11, 10)));
326        }
327
328        #[test]
329        fn vert() {
330            let line = Line::new((10, 10), (10, 20));
331            assert!(line.contains(coord!(10, 14)));
332            assert!(!line.contains(coord!(10, 24)));
333            assert!(!line.contains(coord!(11, 14)));
334        }
335
336        #[test]
337        fn horz() {
338            let line = Line::new((10, 10), (0, 10));
339            assert!(line.contains(coord!(5, 10)));
340            assert!(!line.contains(coord!(-1, 10)));
341            assert!(!line.contains(coord!(5, 11)));
342        }
343
344        #[test]
345        fn angle() {
346            let line = Line::new((0, 0), (10, 10));
347            assert!(line.contains(coord!(5, 5)));
348            assert!(!line.contains(coord!(8, 7)));
349            assert!(!line.contains(coord!(11, 11)));
350        }
351    }
352
353    mod outline {
354        use crate::line::Line;
355        use crate::Shape;
356
357        #[test]
358        fn flat_horz_right() {
359            let points = Line::new((0, 0), (6, 0)).outline_pixels();
360            assert_eq!(
361                points,
362                coord_vec![(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0)]
363            );
364        }
365
366        #[test]
367        fn flat_vert_down() {
368            let points = Line::new((0, 0), (0, 6)).outline_pixels();
369            assert_eq!(
370                points,
371                coord_vec![(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6)]
372            );
373        }
374
375        #[test]
376        fn flat_horz_left() {
377            let points = Line::new((0, 0), (-6, 0)).outline_pixels();
378            assert_eq!(
379                points,
380                coord_vec![(-6, 0), (-5, 0), (-4, 0), (-3, 0), (-2, 0), (-1, 0), (0, 0)]
381            );
382        }
383
384        #[test]
385        fn flat_vert_up() {
386            let points = Line::new((0, 0), (0, -6)).outline_pixels();
387            assert_eq!(
388                points,
389                coord_vec![(0, -6), (0, -5), (0, -4), (0, -3), (0, -2), (0, -1), (0, 0)]
390            );
391        }
392    }
393}