Files
shinespark/src/math/segment.rs
T

180 lines
5.8 KiB
Rust

use bevy::math::{Vec2, primitives::Segment2d};
use bevy::prelude::*;
use fixed::prelude::*;
use crate::math::{
int::{FGWide, FGi32},
vec::FGVec2,
};
#[derive(Clone, Copy, PartialEq, Reflect)]
pub struct FGSegment2d {
point1: FGVec2,
point2: FGVec2
}
impl FGSegment2d {
#[inline]
pub fn new(point1: FGVec2, point2: FGVec2) -> Self {
Self {
point1, point2
}
}
#[inline]
pub fn point1(&self) -> FGVec2 {
self.point1
}
#[inline]
pub fn point2(&self) -> FGVec2 {
self.point2
}
/// Compute the normalized direction pointing from the first endpoint to the second endpoint.
///
/// For the non-panicking version, see [`FGVec2::try_direction`].
///
/// # Panics
///
/// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite.
#[inline]
pub fn direction(&self) -> FGVec2 {
self.try_direction().expect("Failed to compute the direction of a line segment")
}
/// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.
///
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed,
/// for example when the endpoints are coincident, NaN, or infinite.
#[inline]
pub fn try_direction(&self) -> Option<FGVec2> {
self.point1.direction_to(self.point2)
}
/// Returns the point on the [`Segment2d`] that is closest to the specified `point`.
#[inline]
pub fn closest_point(&self, point: FGVec2) -> FGVec2 {
// `point`
// x
// ^|
// / |
//`offset`/ |
// / | `segment_vector`
// x----.-------------->x
// 0 t 1
let segment_vector = self.point2 - self.point1;
let offset = point - self.point1;
// The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment.
let projection_scaled = segment_vector.dot(offset);
// `point` is too far "left" in the picture
if projection_scaled <= 0.0 {
return self.point1;
}
let length_squared = segment_vector.length_squared();
// `point` is too far "right" in the picture
if projection_scaled >= length_squared {
return self.point2;
}
// Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line.
let t = projection_scaled / length_squared;
FGVec2::new(
FGi32::from_num(
FGWide::from_num(self.point1.x) + FGWide::from_num(segment_vector.x) * t,
),
FGi32::from_num(
FGWide::from_num(self.point1.y) + FGWide::from_num(segment_vector.y) * t,
),
)
}
/// Compute the normalized counterclockwise normal on the left-hand side of the line segment.
///
/// For the non-panicking version, see [`Segment2d::try_left_normal`].
///
/// # Panics
///
/// Panics if a valid normal could not be computed, for example when the endpoints are coincident
pub fn left_normal(&self) -> FGVec2 {
self.point1.direction_to(self.point2).map(|o| { FGVec2::new(-o.y, o.x)}).expect("a valid normal could not be computed")
}
pub fn segment_intersection(&self, rhs: &FGSegment2d) -> Option<FGVec2> {
let p0 = self.point1();
let p1 = self.point2();
let p2 = rhs.point1();
let p3 = rhs.point2();
let p0_x = FGWide::from_num(p0.x);
let p0_y = FGWide::from_num(p0.y);
let p1_x = FGWide::from_num(p1.x);
let p1_y = FGWide::from_num(p1.y);
let p2_x = FGWide::from_num(p2.x);
let p2_y = FGWide::from_num(p2.y);
let p3_x = FGWide::from_num(p3.x);
let p3_y = FGWide::from_num(p3.y);
let s1_x = p1_x - p0_x;
let s1_y = p1_y - p0_y;
let s2_x = p3_x - p2_x;
let s2_y = p3_y - p2_y;
let denom = -s2_x * s1_y + s1_x * s2_y;
if denom == FGWide::ZERO {
return None; // Parallel or collinear
}
info!("{p0}, {p1}, {p2}, {p3}");
let s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / denom;
let t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / denom;
if s >= FGWide::ZERO && s <= FGWide::ONE && t >= FGWide::ZERO && t <= FGWide::ONE {
Some(FGVec2::new(
FGi32::from_num(p0_x + t * s1_x),
FGi32::from_num(p0_y + t * s1_y),
))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closest_point_projects_and_clamps_to_segment() {
let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0"));
assert_eq!(segment.closest_point(FGVec2::lit("2", "1")), FGVec2::lit("2", "0"));
assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1());
assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2());
}
#[test]
fn segment_intersection_avoids_cross_product_overflow() {
let segment = FGSegment2d::new(FGVec2::lit("56", "-3.5"), FGVec2::lit("56", "-200"));
let rhs = FGSegment2d::new(
FGVec2::lit("-111.26334", "-130.22586"),
FGVec2::lit("-112.36334", "-133.02586"),
);
assert_eq!(segment.segment_intersection(&rhs), None);
}
#[test]
fn segment_intersection_handles_large_crossing_segments() {
let horizontal = FGSegment2d::new(FGVec2::lit("-200", "0"), FGVec2::lit("200", "0"));
let vertical = FGSegment2d::new(FGVec2::lit("0", "-200"), FGVec2::lit("0", "200"));
assert_eq!(
horizontal.segment_intersection(&vertical),
Some(FGVec2::ZERO)
);
}
}