pixel_map/shapes/
line.rs

1#[cfg(feature = "serialize")]
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5use super::line_interval::LineInterval;
6use super::line_iterator::{plot_line, LinePixelIterator};
7use crate::{distance_squared_to_line, distance_to_line, irect_edges, Direction};
8use bevy_math::{ivec2, IRect, IVec2, Vec2};
9
10/// An alias for [ILine::new].
11#[inline]
12pub fn iline<P>(start: P, end: P) -> ILine
13where
14    P: Into<IVec2>,
15{
16    ILine::new(start, end)
17}
18
19/// A line segment represented by two points, in integer coordinates.
20#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
21#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
22pub struct ILine {
23    start: IVec2,
24    end: IVec2,
25}
26
27impl ILine {
28    pub const ZERO: Self = Self {
29        start: IVec2::ZERO,
30        end: IVec2::ZERO,
31    };
32
33    /// Creates a new line with the given start and end points.
34    #[inline]
35    #[must_use]
36    pub fn new<P>(start: P, end: P) -> Self
37    where
38        P: Into<IVec2>,
39    {
40        Self {
41            start: start.into(),
42            end: end.into(),
43        }
44    }
45
46    /// Get the start point.
47    #[inline]
48    #[must_use]
49    pub fn start(&self) -> IVec2 {
50        self.start
51    }
52
53    /// Get the end point.
54    #[inline]
55    #[must_use]
56    pub fn end(&self) -> IVec2 {
57        self.end
58    }
59
60    /// Get the line's length squared.
61    #[inline]
62    #[must_use]
63    pub fn length_squared(&self) -> f32 {
64        self.start.as_vec2().distance_squared(self.end.as_vec2())
65    }
66
67    /// Get the line's length.
68    #[inline]
69    #[must_use]
70    pub fn length(&self) -> f32 {
71        self.start.as_vec2().distance(self.end.as_vec2())
72    }
73
74    #[inline]
75    #[must_use]
76    pub fn distance_squared_to_point(&self, p: Vec2) -> f32 {
77        distance_squared_to_line(p, &self.into())
78    }
79
80    #[inline]
81    #[must_use]
82    pub fn distance_to_point(&self, p: Vec2) -> f32 {
83        distance_to_line(p, &self.into())
84    }
85
86    /// Create a new line that is the rotation of this line around its start point, by the given radians.
87    #[inline]
88    #[must_use]
89    pub fn rotate(&self, radians: f32) -> Self {
90        self.rotate_around(self.start, radians)
91    }
92
93    /// Create a new line that is the rotation of this line around the given point, by the given radians.
94    #[inline]
95    #[must_use]
96    pub fn rotate_around(&self, center: IVec2, radians: f32) -> Self {
97        let cos_theta = f32::cos(radians);
98        let sin_theta = f32::sin(radians);
99
100        let start_x_diff = self.start.x as f32 - center.x as f32;
101        let start_y_diff = self.start.y as f32 - center.y as f32;
102
103        let end_x_diff = self.end.x as f32 - center.x as f32;
104        let end_y_diff = self.end.y as f32 - center.y as f32;
105
106        let x0 = cos_theta * start_x_diff - sin_theta * start_y_diff + center.x as f32;
107        let y0 = sin_theta * start_x_diff + cos_theta * start_y_diff + center.y as f32;
108
109        let x1 = cos_theta * end_x_diff - sin_theta * end_y_diff + center.x as f32;
110        let y1 = sin_theta * end_x_diff + cos_theta * end_y_diff + center.y as f32;
111
112        Self::new((x0 as i32, y0 as i32), (x1 as i32, y1 as i32))
113    }
114
115    /// Flip the orientation of the line such that the start point
116    /// becomes the end point and vice versa.
117    #[inline]
118    #[must_use]
119    pub fn flip(&self) -> Self {
120        Self::new(self.end, self.start)
121    }
122
123    /// Determine if the given point lies on this line.
124    #[inline]
125    #[must_use]
126    pub fn contains<P>(&self, point: P) -> bool
127    where
128        P: Into<Vec2>,
129    {
130        let point = point.into();
131        let d_sp = self.start.as_vec2().distance(point);
132        let d_pe = point.distance(self.end.as_vec2());
133        let d = d_sp + d_pe - self.length();
134        -f32::EPSILON < d && d < f32::EPSILON
135    }
136
137    #[inline]
138    #[must_use]
139    pub fn is_vertical(&self) -> bool {
140        self.start.x == self.end.x
141    }
142
143    #[inline]
144    pub fn is_horizontal(&self) -> bool {
145        self.start.y == self.end.y
146    }
147
148    /// Determine if this line is axis-aligned.
149    #[inline]
150    #[must_use]
151    pub fn is_axis_aligned(&self) -> bool {
152        self.is_horizontal() || self.is_vertical()
153    }
154
155    /// Get the axis-aligned bounding box of this line.
156    #[inline]
157    #[must_use]
158    pub fn aabb(&self) -> IRect {
159        IRect::from_corners(self.start, self.end)
160    }
161
162    /// Get the axis-aligned direction of this line, if it is axis-aligned, `None` otherwise.
163    #[inline]
164    #[must_use]
165    pub fn axis_alignment(&self) -> Option<Direction> {
166        if self.start.x == self.end.x {
167            if self.start.y < self.end.y {
168                Some(Direction::North)
169            } else {
170                Some(Direction::South)
171            }
172        } else if self.start.y == self.end.y {
173            if self.start.x > self.end.x {
174                Some(Direction::West)
175            } else {
176                Some(Direction::East)
177            }
178        } else {
179            None
180        }
181    }
182
183    /// Get the diagonal axis-aligned direction of this line, if it is diagonally axis-aligned, `None` otherwise.
184    #[inline]
185    #[must_use]
186    pub fn diagonal_axis_alignment(&self) -> Option<Direction> {
187        let dx = self.end.x - self.start.x;
188        let dy = self.end.y - self.start.y;
189        if dx == dy {
190            if dx > 0 {
191                Some(Direction::NorthEast)
192            } else {
193                Some(Direction::SouthWest)
194            }
195        } else if dx == -dy {
196            if dx > 0 {
197                Some(Direction::SouthEast)
198            } else {
199                Some(Direction::NorthWest)
200            }
201        } else {
202            None
203        }
204    }
205
206    /// Determine if this line intersects the given line.
207    #[inline]
208    #[must_use]
209    pub fn intersects_line(&self, other: &ILine) -> Option<IVec2> {
210        let seg1 = LineInterval::line_segment(*self);
211        let seg2 = LineInterval::line_segment(*other);
212        seg1.relate(&seg2).unique_intersection()
213    }
214
215    /// Determine if this line intersects the edges of, or is contained within the given rectangle.
216    #[inline]
217    #[must_use]
218    pub fn intersects_rect(&self, rect: &IRect) -> bool {
219        if rect.contains(self.start) || rect.contains(self.end) {
220            return true;
221        }
222        for edge in irect_edges(rect) {
223            if self.intersects_line(&edge).is_some() {
224                return true;
225            }
226        }
227        false
228    }
229
230    /// Obtain the segment of this line that intersects the given rectangle, if any, otherwise `None`.
231    /// This line must be axis-aligned, otherwise `None` is returned.
232    #[inline]
233    #[must_use]
234    pub fn axis_aligned_intersect_rect(&self, rect: &IRect) -> Option<ILine> {
235        if self.is_vertical() {
236            // Ensure the smaller `y` value is at the bottom
237            let (start, end, flipped) = if self.start.y < self.end.y {
238                (self.start, self.end, false)
239            } else {
240                (self.end, self.start, true)
241            };
242
243            // Ensure the vertical line is within the horizontal bounds of the rect
244            if start.x < rect.min.x || start.x > rect.max.x {
245                return None;
246            }
247
248            // Restrain the `y` values to the rect
249            let start_y = start.y.max(rect.min.y).min(rect.max.y);
250            let end_y = end.y.max(rect.min.y).min(rect.max.y);
251
252            if start_y <= end_y {
253                let result = iline((start.x, start_y), (start.x, end_y));
254                if flipped {
255                    Some(result.flip())
256                } else {
257                    Some(result)
258                }
259            } else {
260                None
261            }
262        } else if self.is_horizontal() {
263            // Ensure the smaller `x` value is at the left
264            let (start, end, flipped) = if self.start.x < self.end.x {
265                (self.start, self.end, false)
266            } else {
267                (self.end, self.start, true)
268            };
269
270            // Ensure the horizontal line is within the vertical bounds of the rect
271            if start.y < rect.min.y || start.y > rect.max.y {
272                return None;
273            }
274
275            // Restrain the `x` values to the rect
276            let start_x = start.x.max(rect.min.x).min(rect.max.x);
277            let end_x = end.x.max(rect.min.x).min(rect.max.x);
278
279            if start_x <= end_x {
280                let result = iline((start_x, start.y), (end_x, start.y));
281                if flipped {
282                    Some(result.flip())
283                } else {
284                    Some(result)
285                }
286            } else {
287                None
288            }
289        } else {
290            None
291        }
292    }
293
294    /// If this and the given line segments overlap, return the overlapping segment.
295    /// Otherwise, return `None`.
296    #[inline]
297    #[must_use]
298    pub fn overlap(&self, other: &ILine) -> Option<ILine> {
299        // Check if the edges share a common point
300        if self.start == other.start
301            || self.start == other.end
302            || self.end == other.start
303            || self.end == other.end
304        {
305            // If they share a common point, return the shorter of the two edges
306            let self_length = self.length_squared();
307            let other_length = other.length_squared();
308            return if self_length < other_length {
309                Some(*self)
310            } else {
311                Some(*other)
312            };
313        }
314
315        // Check for overlapping segments
316        let min_x1 = self.start.x.min(self.end.x);
317        let max_x1 = self.start.x.max(self.end.x);
318        let min_y1 = self.start.y.min(self.end.y);
319        let max_y1 = self.start.y.max(self.end.y);
320
321        let min_x2 = other.start.x.min(other.end.x);
322        let max_x2 = other.start.x.max(other.end.x);
323        let min_y2 = other.start.y.min(other.end.y);
324        let max_y2 = other.start.y.max(other.end.y);
325
326        if max_x1 < min_x2 || min_x1 > max_x2 || max_y1 < min_y2 || min_y1 > max_y2 {
327            // No overlap
328            return None;
329        }
330
331        // Calculate the overlapping segment
332        let overlap_start_x = max_x1.min(max_x2);
333        let overlap_start_y = max_y1.min(max_y2);
334        let overlap_end_x = min_x1.max(min_x2);
335        let overlap_end_y = min_y1.max(min_y2);
336
337        let overlap_start = ivec2(overlap_start_x, overlap_start_y);
338        let overlap_end = ivec2(overlap_end_x, overlap_end_y);
339
340        Some(iline(overlap_start, overlap_end))
341    }
342
343    /// Use Bresenham's line algorithm to visit points on this line.
344    #[inline]
345    pub fn visit_points<F>(&self, visitor: F)
346    where
347        F: FnMut(i32, i32),
348    {
349        plot_line(self.start.x, self.start.y, self.end.x, self.end.y, visitor);
350    }
351
352    #[inline]
353    #[must_use]
354    pub fn pixels(&self) -> LinePixelIterator {
355        LinePixelIterator::new(self)
356    }
357}
358
359impl From<&ILine> for [Vec2; 2] {
360    fn from(value: &ILine) -> Self {
361        [value.start.as_vec2(), value.end.as_vec2()]
362    }
363}
364
365impl From<ILine> for [Vec2; 2] {
366    fn from(value: ILine) -> Self {
367        [value.start.as_vec2(), value.end.as_vec2()]
368    }
369}
370
371impl From<&ILine> for [IVec2; 2] {
372    fn from(value: &ILine) -> Self {
373        [value.start, value.end]
374    }
375}
376
377impl From<ILine> for [IVec2; 2] {
378    fn from(value: ILine) -> Self {
379        [value.start, value.end]
380    }
381}
382
383impl fmt::Display for ILine {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        write!(f, "[{}, {}]", self.start, self.end)
386    }
387}
388
389impl fmt::Debug for ILine {
390    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
391        fmt.debug_tuple(stringify!(ILine))
392            .field(&self.start)
393            .field(&self.end)
394            .finish()
395    }
396}
397
398#[cfg(test)]
399mod test {
400    use super::*;
401
402    #[test]
403    fn test_contains() {
404        let line = iline((0, 0), (10, 10));
405        assert!(line.contains((5., 5.)));
406        assert!(!line.contains((5., 6.)));
407        assert!(!line.contains((6., 5.)));
408    }
409
410    #[test]
411    fn test_aabb() {
412        let line = iline((0, 0), (10, 10));
413        let aabb = line.aabb();
414        assert_eq!(aabb.min.x, 0);
415        assert_eq!(aabb.min.y, 0);
416        assert_eq!(aabb.width(), 10);
417        assert_eq!(aabb.height(), 10);
418    }
419
420    #[test]
421    fn test_axis_alignment() {
422        let line = iline((0, 0), (10, 10));
423        assert_eq!(line.axis_alignment(), None);
424        let line = iline((0, 0), (10, 0));
425        assert_eq!(line.axis_alignment(), Some(Direction::East));
426        let line = iline((0, 0), (0, 10));
427        assert_eq!(line.axis_alignment(), Some(Direction::North));
428        let line = iline((10, 0), (0, 0));
429        assert_eq!(line.axis_alignment(), Some(Direction::West));
430        let line = iline((0, 10), (0, 0));
431        assert_eq!(line.axis_alignment(), Some(Direction::South));
432        let line = iline((0, 10), (1, 0));
433        assert_eq!(line.axis_alignment(), None);
434    }
435
436    #[test]
437    fn test_diag_axis_alignment() {
438        let line = iline((0, 0), (9, 10));
439        assert_eq!(line.diagonal_axis_alignment(), None);
440        let line = iline((0, 0), (10, 10));
441        assert_eq!(line.diagonal_axis_alignment(), Some(Direction::NorthEast));
442        let line = iline((0, 10), (10, 0));
443        assert_eq!(line.diagonal_axis_alignment(), Some(Direction::SouthEast));
444        let line = iline((10, 0), (0, 10));
445        assert_eq!(line.diagonal_axis_alignment(), Some(Direction::NorthWest));
446        let line = iline((10, 10), (0, 0));
447        assert_eq!(line.diagonal_axis_alignment(), Some(Direction::SouthWest));
448    }
449
450    #[test]
451    fn test_intersects_rect() {
452        let rect = IRect::from_corners((0, 0).into(), (4, 4).into());
453
454        let intersecting_lines = vec![
455            iline((-1, 2), (2, 2)), // Crossing left edge
456            iline((3, 3), (3, 5)),  // Crossing top edge
457            iline((3, 3), (5, 3)),  // Crossing right edge
458            iline((2, 2), (2, -2)), // Crossing bottom edge
459            iline((-2, 2), (6, 2)), // Crossing two edges
460            //
461            iline((-1, 2), (0, 2)), // Touching left edge
462            iline((2, 5), (2, 4)),  // Touching top edge
463            iline((5, 1), (4, 1)),  // Touching right edge
464            iline((2, -1), (2, 0)), // Touching bottom edge
465            //
466            iline((-1, 5), (1, 3)),  // Crossing top-left vertex
467            iline((3, 3), (5, 5)),   // Crossing top-right vertex
468            iline((3, 1), (5, -1)),  // Crossing bottom-right vertex
469            iline((-1, -1), (1, 1)), // Crossing bottom-left vertex
470            //
471            iline((0, 4), (-1, 5)),  // Touching top-left vertex
472            iline((4, 4), (5, 5)),   // Touching top-right vertex
473            iline((-1, -1), (0, 0)), // Touching bottom-left vertex
474            iline((4, -1), (4, 0)),  // Touching bottom-right vertex
475            //
476            iline((0, 0), (0, 4)), // Along left edge
477            iline((0, 4), (4, 4)), // Along top edge
478            iline((0, 0), (4, 0)), // Along bottom edge
479            iline((4, 0), (4, 4)), // Along right edge
480            //
481            iline((1, 1), (3, 3)), // Contained
482            iline((3, 3), (1, 3)), // Contained
483        ];
484
485        for line in intersecting_lines {
486            assert!(
487                line.intersects_rect(&rect),
488                "expected intersection: {:?}",
489                line
490            );
491        }
492
493        let outlying_lines = vec![
494            iline((-2, 1), (-1, 3)),
495            iline((2, 5), (2, 6)),
496            iline((5, 1), (7, 1)),
497            iline((3, -1), (4, -2)),
498        ];
499
500        for line in outlying_lines {
501            assert!(
502                !line.intersects_rect(&rect),
503                "expected no intersection: {:?}",
504                line
505            );
506        }
507    }
508}