1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_coordinate::GeometryError;
5use use_orientation::signed_twice_area_2d;
6use use_point::Point2;
7use use_vector::Vector2;
8
9fn validate_vector(direction: Vector2) -> Result<Vector2, GeometryError> {
10 if !direction.x().is_finite() {
11 return Err(GeometryError::non_finite_component(
12 "Vector2",
13 "x",
14 direction.x(),
15 ));
16 }
17
18 if !direction.y().is_finite() {
19 return Err(GeometryError::non_finite_component(
20 "Vector2",
21 "y",
22 direction.y(),
23 ));
24 }
25
26 Ok(direction)
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct Line2 {
32 a: Point2,
33 b: Point2,
34}
35
36impl Line2 {
37 #[must_use]
39 pub const fn new(a: Point2, b: Point2) -> Self {
40 Self { a, b }
41 }
42
43 pub fn try_new(a: Point2, b: Point2) -> Result<Self, GeometryError> {
52 Self::try_from_points(a, b)
53 }
54
55 pub fn try_from_points(a: Point2, b: Point2) -> Result<Self, GeometryError> {
64 let a = a.validate()?;
65 let b = b.validate()?;
66
67 if a == b {
68 return Err(GeometryError::IdenticalPoints);
69 }
70
71 Ok(Self::new(a, b))
72 }
73
74 pub fn try_from_point_direction(
83 point: Point2,
84 direction: Vector2,
85 ) -> Result<Self, GeometryError> {
86 let point = point.validate()?;
87 let direction = validate_vector(direction)?;
88
89 if direction.magnitude_squared() == 0.0 {
90 return Err(GeometryError::ZeroDirectionVector);
91 }
92
93 Ok(Self::new(point, point + direction))
94 }
95
96 #[must_use]
98 pub const fn a(self) -> Point2 {
99 self.a
100 }
101
102 #[must_use]
104 pub const fn b(self) -> Point2 {
105 self.b
106 }
107
108 #[must_use]
110 pub const fn point(self) -> Point2 {
111 self.a()
112 }
113
114 #[must_use]
116 pub fn direction(self) -> Vector2 {
117 self.b() - self.a()
118 }
119
120 #[must_use]
122 pub fn contains_point(self, point: Point2) -> bool {
123 signed_twice_area_2d(self.a(), self.b(), point) == 0.0
124 }
125
126 pub fn contains_point_with_tolerance(
135 self,
136 point: Point2,
137 tolerance: f64,
138 ) -> Result<bool, GeometryError> {
139 let tolerance = GeometryError::validate_tolerance(tolerance)?;
140 let direction_length = self.direction().magnitude();
141
142 if direction_length == 0.0 {
143 return Ok(self.a().distance_to(point) <= tolerance);
144 }
145
146 Ok(signed_twice_area_2d(self.a(), self.b(), point).abs() <= tolerance * direction_length)
147 }
148
149 #[must_use]
151 pub fn slope(self) -> Option<f64> {
152 slope(self.a(), self.b())
153 }
154
155 pub fn try_slope(self) -> Result<Option<f64>, GeometryError> {
162 try_slope(self.a(), self.b())
163 }
164}
165
166#[must_use]
168pub fn slope(left: Point2, right: Point2) -> Option<f64> {
169 let delta_x = right.x() - left.x();
170 if delta_x == 0.0 {
171 None
172 } else {
173 Some((right.y() - left.y()) / delta_x)
174 }
175}
176
177pub fn try_slope(left: Point2, right: Point2) -> Result<Option<f64>, GeometryError> {
184 let left = left.validate()?;
185 let right = right.validate()?;
186
187 Ok(slope(left, right))
188}
189
190#[cfg(test)]
191mod tests {
192 use super::{Line2, slope, try_slope};
193 use use_coordinate::GeometryError;
194 use use_point::Point2;
195 use use_vector::Vector2;
196
197 #[test]
198 fn constructs_lines() {
199 let a = Point2::new(0.0, 0.0);
200 let b = Point2::new(1.0, 1.0);
201
202 assert_eq!(Line2::new(a, b).a(), a);
203 assert_eq!(Line2::new(a, b).b(), b);
204 }
205
206 #[test]
207 fn constructs_lines_with_try_new() {
208 let a = Point2::new(0.0, 0.0);
209 let b = Point2::new(1.0, 1.0);
210
211 assert_eq!(Line2::try_new(a, b), Ok(Line2::new(a, b)));
212 assert_eq!(Line2::try_from_points(a, b), Ok(Line2::new(a, b)));
213 }
214
215 #[test]
216 fn computes_direction() {
217 let line = Line2::new(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
218
219 assert_eq!(line.direction(), Vector2::new(3.0, 4.0));
220 assert_eq!(line.point(), Point2::new(0.0, 0.0));
221 }
222
223 #[test]
224 fn computes_slope() {
225 let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
226
227 assert_eq!(line.slope(), Some(2.0));
228 assert_eq!(slope(line.a(), line.b()), Some(2.0));
229 }
230
231 #[test]
232 fn vertical_lines_have_no_slope() {
233 let line = Line2::new(Point2::new(2.0, 1.0), Point2::new(2.0, 5.0));
234
235 assert_eq!(line.slope(), None);
236 }
237
238 #[test]
239 fn computes_try_slope_for_finite_lines() {
240 let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
241
242 assert_eq!(line.try_slope(), Ok(Some(2.0)));
243 assert_eq!(try_slope(line.a(), line.b()), Ok(Some(2.0)));
244 }
245
246 #[test]
247 fn rejects_try_slope_for_non_finite_points() {
248 assert!(matches!(
249 try_slope(Point2::new(f64::NAN, 1.0), Point2::new(3.0, 5.0)),
250 Err(GeometryError::NonFiniteComponent {
251 type_name: "Point2",
252 component: "x",
253 value,
254 }) if value.is_nan()
255 ));
256 }
257
258 #[test]
259 fn rejects_identical_points_for_validated_lines() {
260 assert_eq!(
261 Line2::try_new(Point2::new(1.0, 1.0), Point2::new(1.0, 1.0)),
262 Err(GeometryError::IdenticalPoints)
263 );
264 }
265
266 #[test]
267 fn constructs_lines_from_point_and_direction() {
268 let line = Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::new(3.0, 4.0))
269 .expect("valid line");
270
271 assert_eq!(
272 line,
273 Line2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0))
274 );
275 }
276
277 #[test]
278 fn rejects_zero_direction_vectors() {
279 assert_eq!(
280 Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::ZERO),
281 Err(GeometryError::ZeroDirectionVector)
282 );
283 }
284
285 #[test]
286 fn checks_line_containment() {
287 let line =
288 Line2::try_new(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0)).expect("valid line");
289
290 assert!(line.contains_point(Point2::new(4.0, 4.0)));
291 assert!(!line.contains_point(Point2::new(4.0, 4.1)));
292 assert_eq!(
293 line.contains_point_with_tolerance(Point2::new(4.0, 4.1), 0.1),
294 Ok(true)
295 );
296 }
297}