fj_math/
line.rs

1use crate::{Point, Scalar, Triangle, Vector};
2
3/// An n-dimensional line, defined by an origin and a direction
4///
5/// The dimensionality of the line is defined by the const generic `D`
6/// parameter.
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
8#[repr(C)]
9pub struct Line<const D: usize> {
10    origin: Point<D>,
11    direction: Vector<D>,
12}
13
14impl<const D: usize> Line<D> {
15    /// Create a line from a point and a vector
16    ///
17    /// # Panics
18    ///
19    /// Panics, if `direction` has a length of zero.
20    pub fn from_origin_and_direction(
21        origin: Point<D>,
22        direction: Vector<D>,
23    ) -> Self {
24        assert!(
25            direction.magnitude() != Scalar::ZERO,
26            "Can't construct `Line`. Direction is zero: {direction:?}"
27        );
28
29        Self { origin, direction }
30    }
31
32    /// Create a line from two points
33    ///
34    /// Also returns the lines coordinates of the provided points on the new
35    /// line.
36    ///
37    /// # Panics
38    ///
39    /// Panics, if the points are coincident.
40    pub fn from_points(
41        points: [impl Into<Point<D>>; 2],
42    ) -> (Self, [Point<1>; 2]) {
43        let [a, b] = points.map(Into::into);
44
45        let line = Self::from_origin_and_direction(a, b - a);
46        let coords = [[0.], [1.]].map(Point::from);
47
48        (line, coords)
49    }
50
51    /// Create a line from two points that include line coordinates
52    ///
53    /// # Panics
54    ///
55    /// Panics, if the points are coincident.
56    pub fn from_points_with_line_coords(
57        points: [(impl Into<Point<1>>, impl Into<Point<D>>); 2],
58    ) -> Self {
59        let [(a_line, a_global), (b_line, b_global)] =
60            points.map(|(point_line, point_global)| {
61                (point_line.into(), point_global.into())
62            });
63
64        let direction = (b_global - a_global) / (b_line - a_line).t;
65        let origin = a_global + direction * -a_line.t;
66
67        Self::from_origin_and_direction(origin, direction)
68    }
69
70    /// Access the origin of the line
71    ///
72    /// The origin is a point on the line which, together with the `direction`
73    /// field, defines the line fully. The origin also defines the origin of the
74    /// line's 1-dimensional coordinate system.
75    pub fn origin(&self) -> Point<D> {
76        self.origin
77    }
78
79    /// Access the direction of the line
80    ///
81    /// The length of this vector defines the unit of the line's curve
82    /// coordinate system. The coordinate `1.` is always where the direction
83    /// vector points, from `origin`.
84    pub fn direction(&self) -> Vector<D> {
85        self.direction
86    }
87
88    /// Determine if this line is coincident with another line
89    ///
90    /// # Implementation Note
91    ///
92    /// This method only returns `true`, if the lines are precisely coincident.
93    /// This will probably not be enough going forward, but it'll do for now.
94    pub fn is_coincident_with(&self, other: &Self) -> bool {
95        let other_origin_is_not_on_self = {
96            let a = other.origin;
97            let b = self.origin;
98            let c = self.origin + self.direction;
99
100            // The triangle is valid only, if the three points are not on the
101            // same line.
102            Triangle::from_points([a, b, c]).is_ok()
103        };
104
105        if other_origin_is_not_on_self {
106            return false;
107        }
108
109        let d1 = self.direction.normalize();
110        let d2 = other.direction.normalize();
111
112        d1 == d2 || d1 == -d2
113    }
114
115    /// Create a new instance that is reversed
116    #[must_use]
117    pub fn reverse(mut self) -> Self {
118        self.origin += self.direction;
119        self.direction = -self.direction;
120        self
121    }
122
123    /// Convert a `D`-dimensional point to line coordinates
124    ///
125    /// Projects the point onto the line before the conversion. This is done to
126    /// make this method robust against floating point accuracy issues.
127    ///
128    /// Callers are advised to be careful about the points they pass, as the
129    /// point not being on the line, intentional or not, will never result in an
130    /// error.
131    pub fn point_to_line_coords(&self, point: impl Into<Point<D>>) -> Point<1> {
132        Point {
133            coords: self.vector_to_line_coords(point.into() - self.origin),
134        }
135    }
136
137    /// Convert a `D`-dimensional vector to line coordinates
138    pub fn vector_to_line_coords(
139        &self,
140        vector: impl Into<Vector<D>>,
141    ) -> Vector<1> {
142        let t = vector.into().scalar_projection_onto(&self.direction)
143            / self.direction.magnitude();
144        Vector::from([t])
145    }
146
147    /// Convert a point in line coordinates into a `D`-dimensional point
148    pub fn point_from_line_coords(
149        &self,
150        point: impl Into<Point<1>>,
151    ) -> Point<D> {
152        self.origin + self.vector_from_line_coords(point.into().coords)
153    }
154
155    /// Convert a vector in line coordinates into a `D`-dimensional vector
156    pub fn vector_from_line_coords(
157        &self,
158        vector: impl Into<Vector<1>>,
159    ) -> Vector<D> {
160        self.direction * vector.into().t
161    }
162}
163
164impl<const D: usize> approx::AbsDiffEq for Line<D> {
165    type Epsilon = <Scalar as approx::AbsDiffEq>::Epsilon;
166
167    fn default_epsilon() -> Self::Epsilon {
168        Scalar::default_epsilon()
169    }
170
171    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
172        self.origin.abs_diff_eq(&other.origin, epsilon)
173            && self.direction.abs_diff_eq(&other.direction, epsilon)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use approx::assert_abs_diff_eq;
180
181    use crate::{Point, Scalar, Vector};
182
183    use super::Line;
184
185    #[test]
186    fn from_points_with_line_coords() {
187        let line = Line::from_points_with_line_coords([
188            ([0.], [0., 0.]),
189            ([1.], [1., 0.]),
190        ]);
191        assert_eq!(line.origin(), Point::from([0., 0.]));
192        assert_eq!(line.direction(), Vector::from([1., 0.]));
193
194        let line = Line::from_points_with_line_coords([
195            ([1.], [0., 1.]),
196            ([0.], [1., 1.]),
197        ]);
198        assert_eq!(line.origin(), Point::from([1., 1.]));
199        assert_eq!(line.direction(), Vector::from([-1., 0.]));
200
201        let line = Line::from_points_with_line_coords([
202            ([-1.], [0., 2.]),
203            ([0.], [1., 2.]),
204        ]);
205        assert_eq!(line.origin(), Point::from([1., 2.]));
206        assert_eq!(line.direction(), Vector::from([1., 0.]));
207    }
208
209    #[test]
210    fn is_coincident_with() {
211        let (line, _) = Line::from_points([[0., 0.], [1., 0.]]);
212
213        let (a, _) = Line::from_points([[0., 0.], [1., 0.]]);
214        let (b, _) = Line::from_points([[0., 0.], [-1., 0.]]);
215        let (c, _) = Line::from_points([[0., 1.], [1., 1.]]);
216
217        assert!(line.is_coincident_with(&a));
218        assert!(line.is_coincident_with(&b));
219        assert!(!line.is_coincident_with(&c));
220    }
221
222    #[test]
223    fn convert_point_to_line_coords() {
224        let line = Line {
225            origin: Point::from([1., 2., 3.]),
226            direction: Vector::from([2., 3., 5.]),
227        };
228
229        verify(line, -1.);
230        verify(line, 0.);
231        verify(line, 1.);
232        verify(line, 2.);
233
234        fn verify(line: Line<3>, t: f64) {
235            let point = line.point_from_line_coords([t]);
236            let t_result = line.point_to_line_coords(point);
237
238            assert_abs_diff_eq!(
239                Point::from([t]),
240                t_result,
241                epsilon = Scalar::from(1e-8)
242            );
243        }
244    }
245}