#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use use_coordinate::GeometryError;
use use_orientation::signed_twice_area_2d;
use use_point::Point2;
use use_vector::Vector2;
fn validate_vector(direction: Vector2) -> Result<Vector2, GeometryError> {
if !direction.x().is_finite() {
return Err(GeometryError::non_finite_component(
"Vector2",
"x",
direction.x(),
));
}
if !direction.y().is_finite() {
return Err(GeometryError::non_finite_component(
"Vector2",
"y",
direction.y(),
));
}
Ok(direction)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Line2 {
a: Point2,
b: Point2,
}
impl Line2 {
#[must_use]
pub const fn new(a: Point2, b: Point2) -> Self {
Self { a, b }
}
pub fn try_new(a: Point2, b: Point2) -> Result<Self, GeometryError> {
Self::try_from_points(a, b)
}
pub fn try_from_points(a: Point2, b: Point2) -> Result<Self, GeometryError> {
let a = a.validate()?;
let b = b.validate()?;
if a == b {
return Err(GeometryError::IdenticalPoints);
}
Ok(Self::new(a, b))
}
pub fn try_from_point_direction(
point: Point2,
direction: Vector2,
) -> Result<Self, GeometryError> {
let point = point.validate()?;
let direction = validate_vector(direction)?;
if direction.magnitude_squared() == 0.0 {
return Err(GeometryError::ZeroDirectionVector);
}
Ok(Self::new(point, point + direction))
}
#[must_use]
pub const fn a(self) -> Point2 {
self.a
}
#[must_use]
pub const fn b(self) -> Point2 {
self.b
}
#[must_use]
pub const fn point(self) -> Point2 {
self.a()
}
#[must_use]
pub fn direction(self) -> Vector2 {
self.b() - self.a()
}
#[must_use]
pub fn contains_point(self, point: Point2) -> bool {
signed_twice_area_2d(self.a(), self.b(), point) == 0.0
}
pub fn contains_point_with_tolerance(
self,
point: Point2,
tolerance: f64,
) -> Result<bool, GeometryError> {
let tolerance = GeometryError::validate_tolerance(tolerance)?;
let direction_length = self.direction().magnitude();
if direction_length == 0.0 {
return Ok(self.a().distance_to(point) <= tolerance);
}
Ok(signed_twice_area_2d(self.a(), self.b(), point).abs() <= tolerance * direction_length)
}
#[must_use]
pub fn slope(self) -> Option<f64> {
slope(self.a(), self.b())
}
pub fn try_slope(self) -> Result<Option<f64>, GeometryError> {
try_slope(self.a(), self.b())
}
}
#[must_use]
pub fn slope(left: Point2, right: Point2) -> Option<f64> {
let delta_x = right.x() - left.x();
if delta_x == 0.0 {
None
} else {
Some((right.y() - left.y()) / delta_x)
}
}
pub fn try_slope(left: Point2, right: Point2) -> Result<Option<f64>, GeometryError> {
let left = left.validate()?;
let right = right.validate()?;
Ok(slope(left, right))
}
#[cfg(test)]
mod tests {
use super::{Line2, slope, try_slope};
use use_coordinate::GeometryError;
use use_point::Point2;
use use_vector::Vector2;
#[test]
fn constructs_lines() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 1.0);
assert_eq!(Line2::new(a, b).a(), a);
assert_eq!(Line2::new(a, b).b(), b);
}
#[test]
fn constructs_lines_with_try_new() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 1.0);
assert_eq!(Line2::try_new(a, b), Ok(Line2::new(a, b)));
assert_eq!(Line2::try_from_points(a, b), Ok(Line2::new(a, b)));
}
#[test]
fn computes_direction() {
let line = Line2::new(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
assert_eq!(line.direction(), Vector2::new(3.0, 4.0));
assert_eq!(line.point(), Point2::new(0.0, 0.0));
}
#[test]
fn computes_slope() {
let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
assert_eq!(line.slope(), Some(2.0));
assert_eq!(slope(line.a(), line.b()), Some(2.0));
}
#[test]
fn vertical_lines_have_no_slope() {
let line = Line2::new(Point2::new(2.0, 1.0), Point2::new(2.0, 5.0));
assert_eq!(line.slope(), None);
}
#[test]
fn computes_try_slope_for_finite_lines() {
let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
assert_eq!(line.try_slope(), Ok(Some(2.0)));
assert_eq!(try_slope(line.a(), line.b()), Ok(Some(2.0)));
}
#[test]
fn rejects_try_slope_for_non_finite_points() {
assert!(matches!(
try_slope(Point2::new(f64::NAN, 1.0), Point2::new(3.0, 5.0)),
Err(GeometryError::NonFiniteComponent {
type_name: "Point2",
component: "x",
value,
}) if value.is_nan()
));
}
#[test]
fn rejects_identical_points_for_validated_lines() {
assert_eq!(
Line2::try_new(Point2::new(1.0, 1.0), Point2::new(1.0, 1.0)),
Err(GeometryError::IdenticalPoints)
);
}
#[test]
fn constructs_lines_from_point_and_direction() {
let line = Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::new(3.0, 4.0))
.expect("valid line");
assert_eq!(
line,
Line2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0))
);
}
#[test]
fn rejects_zero_direction_vectors() {
assert_eq!(
Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::ZERO),
Err(GeometryError::ZeroDirectionVector)
);
}
#[test]
fn checks_line_containment() {
let line =
Line2::try_new(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0)).expect("valid line");
assert!(line.contains_point(Point2::new(4.0, 4.0)));
assert!(!line.contains_point(Point2::new(4.0, 4.1)));
assert_eq!(
line.contains_point_with_tolerance(Point2::new(4.0, 4.1), 0.1),
Ok(true)
);
}
}