diff --git a/crates/bermuda/tests/intersection.rs b/crates/bermuda/tests/intersection.rs new file mode 100644 index 0000000..bac1f2d --- /dev/null +++ b/crates/bermuda/tests/intersection.rs @@ -0,0 +1,217 @@ +use rstest::rstest; +use std::collections::HashSet; +use triangulation::intersection; +use triangulation::point::{Point, Segment}; + +fn seg(x1: i32, y1: i32, x2: i32, y2: i32) -> Segment { + Segment::new( + Point::new(x1 as f32, y1 as f32), + Point::new(x2 as f32, y2 as f32), + ) +} + +fn seg_f(x1: f32, y1: f32, x2: f32, y2: f32) -> Segment { + Segment::new(Point::new(x1, y1), Point::new(x2, y2)) +} + +#[rstest] +#[case(seg(0, 0, 2, 2), Point::new(1.0, 1.0), true)] +#[case(seg(0, 0, 0, 2), Point::new(0.0, 1.0), true)] +#[case(seg(0, 0, 2, 0), Point::new(1.0, 0.0), true)] +#[case(seg(0, 0, 1, 1), Point::new(2.0, 2.0), false)] +#[case(seg(0, 0, 0, 1), Point::new(0.0, 2.0), false)] +#[case(seg(0, 0, 1, 0), Point::new(2.0, 0.0), false)] +#[case(seg_f(1e6, 1e6, 3e6, 3e6), Point::new(2e6, 2e6), true)] +#[case(seg_f(0.0, 0.0, 0.0001, 0.0001), Point::new(0.00005, 0.00005), true)] +#[case(seg(0, 0, -2, -2), Point::new(-1.0, -1.0), true)] +fn test_on_segment_if_collinear(#[case] s: Segment, #[case] q: Point, #[case] expected: bool) { + assert_eq!(intersection::on_segment_if_collinear(&s, q), expected); +} + +#[rstest] +fn test_do_intersect_crossing_segments() { + assert!(intersection::do_intersect( + &Segment::new(Point::new_i(0, -1), Point::new_i(0, 1)), + &Segment::new(Point::new_i(-1, 0), Point::new_i(1, 0)) + )); +} + +#[rstest] +#[case(Segment::new_i((0, 0), (1, 1)), Segment::new_i((1, 0), (0, 1)))] +#[case(Segment::new_i((1, 0), (0, 1)), Segment::new_i((0, 0), (1, 1)))] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_i((0, 1), (1, 1)))] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_i((1, 1), (0, 1)))] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_i((0, 0), (1, 1)))] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_i((1, 1), (0, 0)))] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_f((0.0, 0.5), (1.0, 1.0)))] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_f((1.0, 1.0), (0.0, 0.5)))] +fn test_do_intersect(#[case] s1: Segment, #[case] s2: Segment) { + assert!(intersection::do_intersect(&s1, &s2)); +} + +#[rstest] +#[case(Segment::new_i((0, 0), (0, 1)), Segment::new_i((1, 2), (-1, 2)))] +#[case(Segment::new_i((0, 0), (1, 0)), Segment::new_i((2, 1), (2, -1)))] +#[case(Segment::new_i((0, 0), (1, 1)), Segment::new_i((1, 2), (0, 1)))] +fn test_do_intersect_ne(#[case] s1: Segment, #[case] s2: Segment) { + assert!(!intersection::do_intersect(&s1, &s2)); +} + +#[rstest] +fn test_do_intersect_parallel_segments() { + assert_ne!( + intersection::do_intersect( + &Segment::new(Point::new_i(0, -1), Point::new_i(0, 1)), + &Segment::new(Point::new_i(1, -2), Point::new_i(1, 1)) + ), + true + ) +} + +#[rstest] +#[case(Segment::new_i((0, 0), (2, 2)), Segment::new_i((2, 0), (0, 2)), Point::new_i(1, 1))] +#[case(Segment::new_i((0, 0), (1, 0)), Segment::new_i((0, 1), (0, 0)), Point::new_i(0, 0))] +#[case(Segment::new_i((0, 0), (2, 0)), Segment::new_i((1, 0), (1, 2)), Point::new_i(1, 0))] +#[case(Segment::new_f((0.0, 0.0), (2.0, 2.0)), Segment::new_f((2.0, 0.0), (0.0, 2.0)), Point::new(1.0, 1.0))] +#[case(Segment::new_f((0.0, 0.0), (1.0, 1.0)), Segment::new_f((0.99, 0.0), (0.0, 0.99)), Point::new(0.495, 0.495))] +#[case(Segment::new_f((1e6, 1e6), (2e6, 2e6)), Segment::new_f((2e6, 1e6), (1e6, 2e6)), Point::new(1.5e6, 1.5e6))] +fn test_find_intersection_point(#[case] s1: Segment, #[case] s2: Segment, #[case] expected: Point) { + assert_eq!( + intersection::find_intersection(&s1, &s2), + intersection::Intersection::PointIntersection(expected) + ); + assert_eq!( + intersection::find_intersection(&s2, &s1), + intersection::Intersection::PointIntersection(expected) + ); +} + +#[rstest] +fn test_find_intersection_collinear_segments() { + assert_eq!( + intersection::find_intersection( + &Segment::new_i((0, 0), (2, 0)), + &Segment::new_i((1, 0), (3, 0)) + ), + intersection::Intersection::CollinearWithOverlap(vec![ + Point::new_i(1, 0), + Point::new_i(2, 0) + ]) + ); + assert_eq!( + intersection::find_intersection( + &Segment::new_i((0, 0), (2, 0)), + &Segment::new_i((1, 0), (3, 0)) + ), + intersection::Intersection::CollinearWithOverlap(vec![ + Point::new_i(1, 0), + Point::new_i(2, 0) + ]) + ); +} + +/// Tests a simple square configuration with no intersections. +/// Each segment connects to the next at endpoints, but there +/// are no true intersections between non-adjacent segments. +/// (1, 0) --- (1, 1) +/// | | +/// (0, 0) --- (0, 1) +#[rstest] +fn test_find_intersections_1() { + let segments = vec![ + Segment::new(Point::new(0.0, 0.0), Point::new(0.0, 1.0)), + Segment::new(Point::new(0.0, 1.0), Point::new(1.0, 1.0)), + Segment::new(Point::new(1.0, 1.0), Point::new(1.0, 0.0)), + Segment::new(Point::new(1.0, 0.0), Point::new(0.0, 0.0)), + ]; + let intersections = intersection::find_intersections(&segments); + assert!(intersections.is_empty()); +} + +/// Tests a configuration with two intersecting diagonals. +/// Expected behavior: +/// - Only one intersection is recorded between segments 1 and 3 +/// - The intersection occurs at (0.5, 0.5) +/// (1, 0) --- (1, 1) +/// \ / +/// \ / +/// \ / +/// X +/// / \ +/// / \ +/// / \ +/// (0, 0) --- (0, 1) +#[rstest] +fn test_find_intersections_2() { + let segments = vec![ + Segment::new(Point::new(0.0, 0.0), Point::new(0.0, 1.0)), + Segment::new(Point::new(0.0, 1.0), Point::new(1.0, 0.0)), + Segment::new(Point::new(1.0, 0.0), Point::new(1.0, 1.0)), + Segment::new(Point::new(1.0, 1.0), Point::new(0.0, 0.0)), + Segment::new(Point::new(0.0, 0.0), Point::new(0.0, 1.0)), + ]; + let intersections = intersection::find_intersections(&segments); + let expected = [(1, 3)] + .iter() + .map(|&(a, b)| intersection::OrderedPair::new(a, b)) + .collect(); + assert_eq!(intersections, expected); +} + +#[rstest] +#[case::no_intersections_simple_square( + vec![ + Segment::new_i((0, 0), (0, 1)), + Segment::new_i((0, 1), (1, 1)), + Segment::new_i((1, 1), (1, 0)), + Segment::new_i((1, 0), (0, 0)), + ], + HashSet::new() +)] +#[case::one_intersection_crossing_diagonals( + vec![ + Segment::new_i((0, 0), (2, 2)), + Segment::new_i((2, 0), (0, 2)), + ], + [(0, 1)].iter().map(|&(a, b)| intersection::OrderedPair::new(a, b)).collect() +)] +#[case::multiple_intersections_complex_shape( + vec![ + Segment::new_i((0, 0), (2, 2)), + Segment::new_i((2, 0), (0, 2)), + Segment::new_i((1, 0), (1, 2)), + Segment::new_i((0, 1), (2, 1)), + ], + [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)].iter().map(|&(a, b)| intersection::OrderedPair::new(a, b)).collect() +)] +#[case::no_intersections_non_intersecting_lines( + vec![ + Segment::new_i((0, 0), (1, 1)), + Segment::new_i((2, 2), (3, 3)), + ], + HashSet::new() +)] +#[case::one_intersection_t_shaped_intersection( + vec![ + Segment::new_i((0, 0), (2, 0)), + Segment::new_i((1, -1), (1, 1)), + ], + [(0, 1)].iter().map(|&(a, b)| intersection::OrderedPair::new(a, b)).collect() +)] +#[case::multiple_intersections_grid_shape( + vec![ + Segment::new_i((0, 0), (2, 0)), + Segment::new_i((0, 1), (2, 1)), + Segment::new_i((0, 2), (2, 2)), + Segment::new_i((0, 0), (0, 2)), + Segment::new_i((1, 0), (1, 2)), + Segment::new_i((2, 0), (2, 2)), + ], + [(0, 4), (1, 3), (1, 4), (1, 5), (2, 4)].iter().map(|&(a, b)| intersection::OrderedPair::new(a, b)).collect() +)] +fn test_find_intersections_param( + #[case] segments: Vec, + #[case] expected: HashSet, +) { + assert_eq!(intersection::find_intersections(&segments), expected); +} diff --git a/crates/triangulation/src/intersection.rs b/crates/triangulation/src/intersection.rs new file mode 100644 index 0000000..d8fa027 --- /dev/null +++ b/crates/triangulation/src/intersection.rs @@ -0,0 +1,394 @@ +use crate::point; +use std::cmp::Ordering; +use std::collections::{BTreeMap, HashSet}; +use std::hash::Hash; + +const EPSILON: f32 = 1e-6; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Event { + pub p: point::Point, + pub index: point::Index, + pub is_top: bool, +} + +impl Event { + pub fn new(p: point::Point, index: point::Index, is_top: bool) -> Self { + Self { p, index, is_top } + } +} + +impl PartialOrd for Event { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Event { + fn cmp(&self, other: &Self) -> Ordering { + if self.p == other.p { + if self.is_top == other.is_top { + self.index.cmp(&other.index) + } else { + // Note the reversed comparison for is_top + other.is_top.cmp(&self.is_top) + } + } else { + // Assuming Point implements PartialOrd + self.p.cmp(&other.p) + } + } +} + +#[derive(Default, Clone)] +struct EventData { + tops: Vec, + bottoms: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OrderedPair(point::Index, point::Index); + +impl PartialOrd for OrderedPair { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for OrderedPair { + fn cmp(&self, other: &Self) -> Ordering { + (self.0.min(self.1), self.0.max(self.1)).cmp(&(other.0.min(other.1), other.0.max(other.1))) + } +} + +impl OrderedPair { + pub fn new(i1: point::Index, i2: point::Index) -> Self { + OrderedPair(i1.min(i2), i1.max(i2)) + } + + pub fn first(&self) -> point::Index { + self.0 + } + + pub fn second(&self) -> point::Index { + self.1 + } +} + +/// Checks if point `q` lies on the segment defined by points `p` and `r`, assuming all three points are collinear. +/// +/// # Arguments +/// +/// * `p` - A [`Point`](point::Point) representing one endpoint of the segment. +/// * `q` - A [`Point`](point::Point) to check if it lies on the segment. +/// * `r` - A [`Point`](point::Point) representing the other endpoint of the segment. +/// +/// # Returns +/// +/// +/// * `true` - If `q` lies on the segment defined by `p` and `r`. +/// * `false` - If `q` does not lie on the segment. +/// +/// # Example +/// +/// ```rust +/// use triangulation::point::{Point, Segment}; +/// use triangulation::intersection::on_segment_if_collinear; +/// +/// let p = Point::new(0.0, 0.0); +/// let r = Point::new(4.0, 4.0); +/// let q = Point::new(2.0, 2.0); +/// let s = Segment::new(p, r); +/// +/// assert!(on_segment_if_collinear(&s, q)); // `q` lies on the segment +/// +/// let q_outside = Point::new(5.0, 5.0); +/// assert!(!on_segment_if_collinear(&s, q_outside)); // `q_outside` does not lie on the segment +/// ``` +pub fn on_segment_if_collinear(s: &point::Segment, q: point::Point) -> bool { + // TODO We know that point is collinear, so we may use faster code. + s.point_on_line(q) +} + +/// Determines if two segments intersect. +/// +/// This function checks whether two line segments, `s1` and `s2`, intersect with each other. +/// +/// # Arguments +/// +/// * `s1` - A reference to the first [`Segment`](point::Segment). +/// * `s2` - A reference to the second [`Segment`](point::Segment). +/// +/// # Returns +/// +/// * `true` - If the segments intersect. +/// * `false` - If the segments do not intersect. +/// +/// # Examples +/// +/// ```rust +/// use triangulation::point::{Point, Segment}; +/// use triangulation::intersection::do_intersect; +/// +/// let seg1 = Segment::new(Point::new(0.0, 0.0), Point::new(4.0, 4.0)); +/// let seg2 = Segment::new(Point::new(0.0, 4.0), Point::new(4.0, 0.0)); +/// +/// assert!(do_intersect(&seg1, &seg2)); // The segments intersect +/// +/// let seg3 = Segment::new(Point::new(0.0, 0.0), Point::new(2.0, 2.0)); +/// let seg4 = Segment::new(Point::new(3.0, 3.0), Point::new(4.0, 4.0)); +/// +/// assert!(!do_intersect(&seg3, &seg4)); // The segments do not intersect +/// ``` +pub fn do_intersect(s1: &point::Segment, s2: &point::Segment) -> bool { + let p1 = s1.bottom; + let q1 = s1.top; + let p2 = s2.bottom; + let q2 = s2.top; + + let o1 = point::orientation(p1, q1, p2); + let o2 = point::orientation(p1, q1, q2); + let o3 = point::orientation(p2, q2, p1); + let o4 = point::orientation(p2, q2, q1); + + if o1 != o2 && o3 != o4 { + return true; + } + + if o1 == point::Orientation::Collinear && on_segment_if_collinear(s1, p2) { + return true; + } + if o2 == point::Orientation::Collinear && on_segment_if_collinear(s1, q2) { + return true; + } + if o3 == point::Orientation::Collinear && on_segment_if_collinear(s2, p1) { + return true; + } + if o4 == point::Orientation::Collinear && on_segment_if_collinear(s2, q1) { + return true; + } + + false +} + +/// Checks if two segments share an endpoint. +/// +/// This function determines whether two segments, each defined by +/// two endpoints, share any endpoint. Specifically, it checks if +/// the bottom or top endpoint of the first segment is equal to the +/// bottom or top endpoint of the second segment. +/// +/// # Arguments +/// +/// * `s1` - The first segment. +/// * `s2` - The second segment. +/// +/// # Returns +/// +/// `true` if the segments share at least one endpoint, `false` otherwise. +/// +/// # Example +/// +/// ``` +/// use triangulation::point::{Point, Segment}; +/// use triangulation::intersection::do_share_endpoint; +/// +/// let s1 = Segment::new(Point::new(0.0, 0.0), Point::new(1.0, 1.0)); +/// let s2 = Segment::new(Point::new(1.0, 1.0), Point::new(2.0, 2.0)); +/// assert!(do_share_endpoint(&s1, &s2)); // Shared endpoint +/// +/// let s3 = Segment::new(Point::new(0.0, 0.0), Point::new(1.0, 1.0)); +/// let s4 = Segment::new(Point::new(2.0, 2.0), Point::new(3.0, 3.0)); +/// assert!(!do_share_endpoint(&s3, &s4)); // No shared endpoint +/// ``` +pub fn do_share_endpoint(s1: &point::Segment, s2: &point::Segment) -> bool { + s1.bottom == s2.bottom || s1.bottom == s2.top || s1.top == s2.bottom || s1.top == s2.top +} + +#[derive(Debug, PartialEq)] +pub enum Intersection { + NoIntersection, + PointIntersection(point::Point), + CollinearNoOverlap, + CollinearWithOverlap(Vec), +} + +/// Finds the intersection point of two line segments, if it exists. +/// +/// This function calculates the intersection point of two given line segments. +/// Each segment is defined by two endpoints. If the segments do not intersect, +/// or are collinear and overlapping, the function returns a vector of the shared points. +/// If they are collinear and don't overlap, an empty vector is returned. +/// If they intersect at a single point, the function returns a vector containing that single point. +/// If the segments are not collinear but intersect, the function returns a vector containing the intersection point. +/// +/// # Arguments +/// +/// * `s1` - The first line segment. +/// * `s2` - The second line segment. +/// +/// # Returns +/// +/// An element of Intersection enum with intersection points +/// +/// # Example +/// +/// ``` +/// use triangulation::point::{Point, Segment}; +/// use triangulation::intersection::{find_intersection, Intersection}; +/// +/// let s1 = Segment::new(Point::new(0.0, 0.0), Point::new(2.0, 2.0)); +/// let s2 = Segment::new(Point::new(0.0, 2.0), Point::new(2.0, 0.0)); +/// let intersection = find_intersection(&s1, &s2); +/// assert_eq!(intersection, Intersection::PointIntersection(Point::new(1.0, 1.0))); // Intersecting segments +/// +/// let s3 = Segment::new(Point::new(0.0, 0.0), Point::new(1.0, 1.0)); +/// let s4 = Segment::new(Point::new(2.0, 2.0), Point::new(3.0, 3.0)); +/// let intersection = find_intersection(&s3, &s4); +/// assert!(matches!(intersection, Intersection::CollinearNoOverlap)); // Non-intersecting segments +/// +/// let s5 = Segment::new(Point::new(0.0, 0.0), Point::new(2.0, 0.0)); +/// let s6 = Segment::new(Point::new(1.0, 0.0), Point::new(3.0, 0.0)); +/// let intersection = find_intersection(&s5, &s6); +/// assert!(matches!(intersection, Intersection::CollinearWithOverlap(_))); // Overlapping collinear segments +/// +/// +/// ``` +pub fn find_intersection(s1: &point::Segment, s2: &point::Segment) -> Intersection { + let a1 = s1.top.y - s1.bottom.y; + let b1 = s1.bottom.x - s1.top.x; + let a2 = s2.top.y - s2.bottom.y; + let b2 = s2.bottom.x - s2.top.x; + let det = a1 * b2 - a2 * b1; + + if det == 0.0 { + // collinear case + let mut res = Vec::new(); + if s1.point_on_line(s2.bottom) { + res.push(s2.bottom); + } + if s1.point_on_line(s2.top) { + res.push(s2.top); + } + if s2.point_on_line(s1.bottom) { + res.push(s1.bottom); + } + if s2.point_on_line(s1.top) { + res.push(s1.top); + } + + // remove duplicates from the collinear intersection case + res.sort(); + res.dedup(); + if res.len() == 0 { + return Intersection::CollinearNoOverlap; + } + if res.len() == 1 { + return Intersection::PointIntersection(res[0]); + } + return Intersection::CollinearWithOverlap(res); + } + + let t = ((s2.top.x - s1.top.x) * (s2.bottom.y - s2.top.y) + - (s2.top.y - s1.top.y) * (s2.bottom.x - s2.top.x)) + / det; + + // clip to handle problems with floating point precision + if t < 0.0 { + return if t > -EPSILON { + Intersection::PointIntersection(s1.top) + } else { + Intersection::NoIntersection + }; + } + if t > 1.0 { + return if t < 1.0 + EPSILON { + Intersection::PointIntersection(s1.bottom) + } else { + Intersection::NoIntersection + }; + } + + let x = s1.top.x + t * b1; + let y = s1.top.y + t * (-a1); + Intersection::PointIntersection(point::Point { x, y }) +} + +/// Finds intersections among a set of line segments. +/// +/// This function takes a vector of line segments and returns a set of pairs of +/// segment indices that intersect. The pairs are ordered to ensure uniqueness +/// regardless of the order of segments in the input vector. +/// +/// # Arguments +/// +/// * `segments` - A vector of [`Segment`](point::Segment) representing the line segments. +/// +/// # Returns +/// +/// A [`HashSet`](HashSet) of [`OrderedPair`], where each `OrderedPair` contains the indices of two intersecting segments. +/// +/// # Example +/// +/// ``` +/// use triangulation::point::{Point, Segment}; +/// use triangulation::intersection::{find_intersections, OrderedPair}; +/// use std::collections::HashSet; +/// +/// let segments = vec![ +/// Segment::new(Point::new(0.0, 0.0), Point::new(2.0, 2.0)), +/// Segment::new(Point::new(0.0, 2.0), Point::new(2.0, 0.0)), +/// ]; +/// let intersections = find_intersections(&segments); +/// +/// let expected_intersections: HashSet = [(0, 1)].iter().map(|&(a, b)| OrderedPair::new(a, b)).collect(); +/// assert_eq!(intersections, expected_intersections); +/// ``` +pub fn find_intersections(segments: &[point::Segment]) -> HashSet { + let mut intersections = HashSet::new(); + let mut intersection_events: BTreeMap = BTreeMap::new(); + for (i, segment) in segments.iter().enumerate() { + intersection_events + .entry(segment.top) + .or_default() + .tops + .push(i); + intersection_events + .entry(segment.bottom) + .or_default() + .bottoms + .push(i); + } + + let mut active: BTreeMap> = BTreeMap::new(); + + while let Some((&point, event_data)) = intersection_events.iter().next_back() { + for &event_index in &event_data.tops { + for active_el in active.iter() { + for &index in active_el.1 { + if do_intersect(&segments[event_index], &segments[index]) + && !do_share_endpoint(&segments[event_index], &segments[index]) + { + intersections.insert(OrderedPair::new(event_index, index)); + } + } + } + } + active + .entry(point) + .or_default() + .extend(event_data.tops.iter()); + + for &event_index in &event_data.bottoms { + if let Some(entry) = active.get_mut(&segments[event_index].top) { + entry.remove(&event_index); + if entry.is_empty() { + active.remove(&segments[event_index].top); + } + } + } + + intersection_events.remove(&point); + } + + intersections +} diff --git a/crates/triangulation/src/lib.rs b/crates/triangulation/src/lib.rs index 04b3e88..144e664 100644 --- a/crates/triangulation/src/lib.rs +++ b/crates/triangulation/src/lib.rs @@ -1,3 +1,4 @@ +pub mod intersection; pub mod path_triangulation; pub mod point; diff --git a/crates/triangulation/src/point.rs b/crates/triangulation/src/point.rs index a9d96a8..366ffca 100644 --- a/crates/triangulation/src/point.rs +++ b/crates/triangulation/src/point.rs @@ -4,7 +4,7 @@ use std::hash::{Hash, Hasher}; pub(crate) type Coord = f32; pub(crate) type Index = usize; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Point { pub x: Coord, pub y: Coord, @@ -14,6 +14,9 @@ impl Point { pub fn new(x: Coord, y: Coord) -> Self { Self { x, y } } + pub fn new_i(x: i32, y: i32) -> Self { + Self::new(x as f32, y as f32) + } pub fn add(&self, other: &Point) -> Vector { Vector { @@ -37,12 +40,6 @@ impl Point { } } -impl PartialEq for Point { - fn eq(&self, other: &Self) -> bool { - self.x == other.x && self.y == other.y - } -} - impl Eq for Point {} impl PartialOrd for Point { @@ -192,6 +189,14 @@ impl Segment { } } + pub fn new_i(p1: (i32, i32), p2: (i32, i32)) -> Self { + Self::new(Point::new_i(p1.0, p1.1), Point::new_i(p2.0, p2.1)) + } + + pub fn new_f(p1: (f32, f32), p2: (f32, f32)) -> Self { + Self::new(Point::new(p1.0, p1.1), Point::new(p2.0, p2.1)) + } + pub fn is_horizontal(&self) -> bool { self.bottom.y == self.top.y }