Skip to main content

sightloom_core/
line.rs

1//! Finite line-segment geometry.
2
3use core::cmp::Ordering;
4
5use crate::{GeometryError, Point, orientation::orientation_sign};
6
7/// A finite, directed line segment with distinct endpoints.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct LineSegment {
10    start: Point,
11    end: Point,
12}
13
14impl LineSegment {
15    /// Creates a line segment with distinct endpoints.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`GeometryError::DegenerateSegment`] when both endpoints are
20    /// equal.
21    pub fn new(start: Point, end: Point) -> Result<Self, GeometryError> {
22        if start == end {
23            return Err(GeometryError::DegenerateSegment);
24        }
25
26        Ok(Self { start, end })
27    }
28
29    /// Returns the directed segment's starting endpoint.
30    #[must_use]
31    pub const fn start(self) -> Point {
32        self.start
33    }
34
35    /// Returns the directed segment's ending endpoint.
36    #[must_use]
37    pub const fn end(self) -> Point {
38        self.end
39    }
40}
41
42/// The algebraic side of a point relative to a directed line.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum LineSide {
45    /// The point has positive orientation relative to the line.
46    Left,
47    /// The point is exactly collinear with the line.
48    On,
49    /// The point has negative orientation relative to the line.
50    Right,
51}
52
53/// Returns the algebraic side of a point relative to a directed segment.
54#[must_use]
55pub fn line_side(segment: LineSegment, point: Point) -> LineSide {
56    match orientation_sign(segment.start, segment.end, point) {
57        Ordering::Greater => LineSide::Left,
58        Ordering::Equal => LineSide::On,
59        Ordering::Less => LineSide::Right,
60    }
61}
62
63/// Returns whether two closed finite segments intersect.
64#[must_use]
65pub fn crosses_segment(first: LineSegment, second: LineSegment) -> bool {
66    let first_start_side = line_side(first, second.start);
67    let first_end_side = line_side(first, second.end);
68    let second_start_side = line_side(second, first.start);
69    let second_end_side = line_side(second, first.end);
70
71    if opposite(first_start_side, first_end_side) && opposite(second_start_side, second_end_side) {
72        return true;
73    }
74
75    (first_start_side == LineSide::On && point_on_segment(first.start, first.end, second.start))
76        || (first_end_side == LineSide::On && point_on_segment(first.start, first.end, second.end))
77        || (second_start_side == LineSide::On
78            && point_on_segment(second.start, second.end, first.start))
79        || (second_end_side == LineSide::On
80            && point_on_segment(second.start, second.end, first.end))
81}
82
83/// Returns whether a point belongs to the closed segment between two points.
84#[must_use]
85pub(crate) fn point_on_segment(start: Point, end: Point, point: Point) -> bool {
86    orientation_sign(start, end, point) == Ordering::Equal
87        && point.x() >= start.x().min(end.x())
88        && point.x() <= start.x().max(end.x())
89        && point.y() >= start.y().min(end.y())
90        && point.y() <= start.y().max(end.y())
91}
92
93fn opposite(first: LineSide, second: LineSide) -> bool {
94    matches!(
95        (first, second),
96        (LineSide::Left, LineSide::Right) | (LineSide::Right, LineSide::Left)
97    )
98}