1use crate::{
2 error::GeometryError, orientation::signed_twice_area_2d, point::Point2, vector::Vector2,
3};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Line2 {
8 a: Point2,
10 b: Point2,
12}
13
14impl Line2 {
15 #[must_use]
17 pub const fn new(a: Point2, b: Point2) -> Self {
18 Self { a, b }
19 }
20
21 pub fn try_new(a: Point2, b: Point2) -> Result<Self, GeometryError> {
30 Self::try_from_points(a, b)
31 }
32
33 pub fn try_from_points(a: Point2, b: Point2) -> Result<Self, GeometryError> {
57 let a = a.validate()?;
58 let b = b.validate()?;
59
60 if a == b {
61 return Err(GeometryError::IdenticalPoints);
62 }
63
64 Ok(Self::new(a, b))
65 }
66
67 pub fn try_from_point_direction(
86 point: Point2,
87 direction: Vector2,
88 ) -> Result<Self, GeometryError> {
89 let point = point.validate()?;
90 let direction = direction.validate()?;
91
92 if direction.length_squared() == 0.0 {
93 return Err(GeometryError::ZeroDirectionVector);
94 }
95
96 Ok(Self::new(point, point + direction))
97 }
98
99 #[must_use]
101 pub const fn a(self) -> Point2 {
102 self.a
103 }
104
105 #[must_use]
107 pub const fn b(self) -> Point2 {
108 self.b
109 }
110
111 #[must_use]
113 pub const fn point(self) -> Point2 {
114 self.a()
115 }
116
117 #[must_use]
119 pub const fn direction(self) -> Vector2 {
120 Vector2::from_points(self.a(), self.b())
121 }
122
123 #[must_use]
125 pub fn contains_point(self, point: Point2) -> bool {
126 signed_twice_area_2d(self.a(), self.b(), point) == 0.0
127 }
128
129 pub fn contains_point_with_tolerance(
138 self,
139 point: Point2,
140 tolerance: f64,
141 ) -> Result<bool, GeometryError> {
142 let tolerance = GeometryError::validate_tolerance(tolerance)?;
143 let direction_length = self.direction().length();
144
145 if direction_length == 0.0 {
146 return Ok(self.a().distance_to(point) <= tolerance);
147 }
148
149 Ok(signed_twice_area_2d(self.a(), self.b(), point).abs() <= tolerance * direction_length)
150 }
151
152 #[must_use]
154 pub fn slope(self) -> Option<f64> {
155 slope(self.a(), self.b())
156 }
157
158 pub fn try_slope(self) -> Result<Option<f64>, GeometryError> {
178 try_slope(self.a(), self.b())
179 }
180}
181
182#[must_use]
184pub fn slope(left: Point2, right: Point2) -> Option<f64> {
185 let delta_x = right.x() - left.x();
186 if delta_x == 0.0 {
187 None
188 } else {
189 Some((right.y() - left.y()) / delta_x)
190 }
191}
192
193pub fn try_slope(left: Point2, right: Point2) -> Result<Option<f64>, GeometryError> {
200 let left = left.validate()?;
201 let right = right.validate()?;
202
203 Ok(slope(left, right))
204}
205
206#[cfg(test)]
207mod tests {
208 use super::{Line2, slope, try_slope};
209 use crate::{error::GeometryError, point::Point2, vector::Vector2};
210
211 #[test]
212 fn constructs_lines() {
213 let a = Point2::new(0.0, 0.0);
214 let b = Point2::new(1.0, 1.0);
215
216 assert_eq!(Line2::new(a, b).a(), a);
217 assert_eq!(Line2::new(a, b).b(), b);
218 }
219
220 #[test]
221 fn constructs_lines_with_try_new() {
222 let a = Point2::new(0.0, 0.0);
223 let b = Point2::new(1.0, 1.0);
224
225 assert_eq!(Line2::try_new(a, b), Ok(Line2::new(a, b)));
226 assert_eq!(Line2::try_from_points(a, b), Ok(Line2::new(a, b)));
227 }
228
229 #[test]
230 fn computes_direction() {
231 let line = Line2::new(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
232
233 assert_eq!(line.direction(), Vector2::new(3.0, 4.0));
234 assert_eq!(line.point(), Point2::new(0.0, 0.0));
235 }
236
237 #[test]
238 fn computes_slope() {
239 let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
240
241 assert_eq!(line.slope(), Some(2.0));
242 assert_eq!(slope(line.a(), line.b()), Some(2.0));
243 }
244
245 #[test]
246 fn vertical_lines_have_no_slope() {
247 let line = Line2::new(Point2::new(2.0, 1.0), Point2::new(2.0, 5.0));
248
249 assert_eq!(line.slope(), None);
250 }
251
252 #[test]
253 fn computes_try_slope_for_finite_lines() {
254 let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
255
256 assert_eq!(line.try_slope(), Ok(Some(2.0)));
257 assert_eq!(try_slope(line.a(), line.b()), Ok(Some(2.0)));
258 }
259
260 #[test]
261 fn rejects_try_slope_for_non_finite_points() {
262 assert!(matches!(
263 try_slope(Point2::new(f64::NAN, 1.0), Point2::new(3.0, 5.0)),
264 Err(GeometryError::NonFiniteComponent {
265 type_name: "Point2",
266 component: "x",
267 value,
268 }) if value.is_nan()
269 ));
270 }
271
272 #[test]
273 fn rejects_identical_points_for_validated_lines() {
274 assert_eq!(
275 Line2::try_new(Point2::new(1.0, 1.0), Point2::new(1.0, 1.0)),
276 Err(GeometryError::IdenticalPoints)
277 );
278 }
279
280 #[test]
281 fn constructs_lines_from_point_and_direction() {
282 let line = Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::new(3.0, 4.0))
283 .expect("valid line");
284
285 assert_eq!(
286 line,
287 Line2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0))
288 );
289 }
290
291 #[test]
292 fn rejects_zero_direction_vectors() {
293 assert_eq!(
294 Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::zero()),
295 Err(GeometryError::ZeroDirectionVector)
296 );
297 }
298
299 #[test]
300 fn checks_line_containment() {
301 let line =
302 Line2::try_new(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0)).expect("valid line");
303
304 assert!(line.contains_point(Point2::new(4.0, 4.0)));
305 assert!(!line.contains_point(Point2::new(4.0, 4.1)));
306 assert_eq!(
307 line.contains_point_with_tolerance(Point2::new(4.0, 4.1), 0.1),
308 Ok(true)
309 );
310 }
311}