#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use use_bounds::Aabb2;
use use_coordinate::GeometryError;
use use_point::Point2;
use use_vector::Vector2;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Segment2 {
start: Point2,
end: Point2,
}
impl Segment2 {
#[must_use]
pub const fn new(start: Point2, end: Point2) -> Self {
Self { start, end }
}
pub fn try_new(start: Point2, end: Point2) -> Result<Self, GeometryError> {
Ok(Self::new(start.validate()?, end.validate()?))
}
#[must_use]
pub const fn start(self) -> Point2 {
self.start
}
#[must_use]
pub const fn end(self) -> Point2 {
self.end
}
#[must_use]
pub fn length(self) -> f64 {
self.start.distance_to(self.end)
}
#[must_use]
pub fn length_squared(self) -> f64 {
self.start.distance_squared_to(self.end)
}
#[must_use]
pub const fn midpoint(self) -> Point2 {
self.start.midpoint(self.end)
}
#[must_use]
pub fn vector(self) -> Vector2 {
self.end - self.start
}
#[must_use]
pub const fn point_at(self, t: f64) -> Point2 {
self.start.lerp(self.end, t)
}
#[must_use]
pub const fn reversed(self) -> Self {
Self::new(self.end, self.start)
}
#[must_use]
pub fn is_degenerate(self) -> bool {
self.length_squared() == 0.0
}
pub fn is_degenerate_with_tolerance(self, tolerance: f64) -> Result<bool, GeometryError> {
let tolerance = GeometryError::validate_tolerance(tolerance)?;
Ok(self.length_squared() <= tolerance * tolerance)
}
#[must_use]
pub const fn aabb(self) -> Aabb2 {
Aabb2::from_points(self.start, self.end)
}
}
#[cfg(test)]
mod tests {
use super::Segment2;
use use_coordinate::GeometryError;
use use_point::Point2;
use use_vector::Vector2;
fn approx_eq(left: f64, right: f64) -> bool {
(left - right).abs() < 1.0e-10
}
#[test]
fn constructs_segments() {
let start = Point2::new(0.0, 0.0);
let end = Point2::new(1.0, 1.0);
assert_eq!(Segment2::new(start, end).start(), start);
assert_eq!(Segment2::new(start, end).end(), end);
}
#[test]
fn constructs_segments_with_try_new() {
let start = Point2::new(0.0, 0.0);
let end = Point2::new(1.0, 1.0);
assert_eq!(Segment2::try_new(start, end), Ok(Segment2::new(start, end)));
}
#[test]
fn rejects_non_finite_segment_points() {
assert_eq!(
Segment2::try_new(Point2::new(0.0, 0.0), Point2::new(1.0, f64::INFINITY)),
Err(GeometryError::NonFiniteComponent {
type_name: "Point2",
component: "y",
value: f64::INFINITY,
})
);
}
#[test]
fn computes_length() {
let segment = Segment2::new(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
assert!(approx_eq(segment.length(), 5.0));
assert!(approx_eq(segment.length_squared(), 25.0));
}
#[test]
fn computes_midpoint() {
let segment = Segment2::new(Point2::new(0.0, 0.0), Point2::new(4.0, 2.0));
assert_eq!(segment.midpoint(), Point2::new(2.0, 1.0));
assert_eq!(segment.point_at(0.25), Point2::new(1.0, 0.5));
}
#[test]
fn computes_vector() {
let segment = Segment2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0));
assert_eq!(segment.vector(), Vector2::new(3.0, 4.0));
assert_eq!(segment.start(), Point2::new(1.0, 2.0));
assert_eq!(segment.end(), Point2::new(4.0, 6.0));
assert_eq!(
segment.reversed(),
Segment2::new(Point2::new(4.0, 6.0), Point2::new(1.0, 2.0))
);
}
#[test]
fn detects_degenerate_segments() {
let segment = Segment2::new(Point2::new(2.0, 2.0), Point2::new(2.0, 2.0));
assert!(segment.is_degenerate());
assert_eq!(segment.is_degenerate_with_tolerance(0.0), Ok(true));
assert_eq!(
segment.is_degenerate_with_tolerance(-1.0),
Err(GeometryError::NegativeTolerance(-1.0))
);
}
#[test]
fn computes_segment_bounds() {
let segment = Segment2::new(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0));
assert_eq!(segment.aabb().min(), Point2::new(1.0, 1.0));
assert_eq!(segment.aabb().max(), Point2::new(4.0, 3.0));
}
}