Port to fixed point math
This commit is contained in:
+79
-34
@@ -1,12 +1,13 @@
|
||||
use bevy::math::{Vec2, primitives::Segment2d};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use fixed::prelude::*;
|
||||
|
||||
use crate::math::vec::FGVec2;
|
||||
|
||||
type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Reflect)]
|
||||
pub struct FGSegment2d {
|
||||
point1: FGVec2,
|
||||
point2: FGVec2
|
||||
@@ -29,14 +30,72 @@ impl FGSegment2d {
|
||||
pub fn point2(&self) -> FGVec2 {
|
||||
self.point2
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SegmentIntersection {
|
||||
fn segment_intersection(&self, rhs: &Segment2d) -> Option<FGVec2>;
|
||||
}
|
||||
/// 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")
|
||||
}
|
||||
|
||||
impl SegmentIntersection for Segment2d {
|
||||
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
|
||||
/// 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;
|
||||
self.point1 + segment_vector * 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();
|
||||
@@ -48,45 +107,31 @@ impl SegmentIntersection for Segment2d {
|
||||
let s2_y = p3.y - p2.y;
|
||||
|
||||
let denom = -s2_x * s1_y + s1_x * s2_y;
|
||||
if denom.abs() < f32::EPSILON {
|
||||
if denom == FGi32::ZERO {
|
||||
return None; // Parallel or collinear
|
||||
}
|
||||
|
||||
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 >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 {
|
||||
Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
|
||||
if s >= FGi32::ZERO && s <= FGi32::ONE && t >= FGi32::ZERO && t <= FGi32::ONE {
|
||||
Some(FGVec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SegmentIntersection for Segment2d {
|
||||
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
|
||||
let p0 = self.point1();
|
||||
let p1 = self.point2();
|
||||
let p2 = rhs.point1();
|
||||
let p3 = rhs.point2();
|
||||
|
||||
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;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
let denom = -s2_x * s1_y + s1_x * s2_y;
|
||||
if denom.abs() < f32::EPSILON {
|
||||
return None; // Parallel or collinear
|
||||
}
|
||||
#[test]
|
||||
fn closest_point_projects_and_clamps_to_segment() {
|
||||
let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0"));
|
||||
|
||||
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 >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 {
|
||||
Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+206
-8
@@ -1,14 +1,24 @@
|
||||
use bevy::math::Vec2;
|
||||
use std::ops::{Add, Div, Mul, Sub};
|
||||
use bevy::prelude::*;
|
||||
use fixed::types::I32F32;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{AddAssign, Add, Div, Mul, Sub};
|
||||
|
||||
use super::int::FGi32;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize)]
|
||||
#[reflect(opaque)]
|
||||
pub struct FGVec2 {
|
||||
pub x: FGi32,
|
||||
pub y: FGi32,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FGVec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{}, {}]", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl FGVec2 {
|
||||
pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO);
|
||||
|
||||
@@ -17,6 +27,11 @@ impl FGVec2 {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn lit(x: &str, y: &str) -> Self {
|
||||
Self { x: FGi32::lit(x), y: FGi32::lit(y) }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_i32(x: i32, y: i32) -> Self {
|
||||
Self {
|
||||
@@ -31,17 +46,78 @@ impl FGVec2 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn length(&self) -> FGi32 {
|
||||
pub fn length(self) -> FGi32 {
|
||||
(self.x * self.x + self.y * self.y).sqrt()
|
||||
}
|
||||
|
||||
/// Returns the vector projection of `self` onto `rhs`.
|
||||
///
|
||||
/// `rhs` must be normalized.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if `rhs` is not normalized when `glam_assert` is enabled.
|
||||
#[inline]
|
||||
pub fn normalized_or_zero(&self) -> Self {
|
||||
let len = self.length();
|
||||
if len.is_zero() {
|
||||
Self::ZERO
|
||||
#[must_use]
|
||||
pub fn project_onto_normalized(self, rhs: Self) -> Self {
|
||||
assert!(rhs.is_normalized());
|
||||
|
||||
rhs * self.dot(rhs)
|
||||
}
|
||||
|
||||
const EPS: FGi32 = FGi32::from_bits(6);
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_normalized(self) -> bool {
|
||||
let diff = self.length() - FGi32::ONE;
|
||||
diff.abs() <= Self::EPS
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn dot(self, rhs: Self) -> FGi32 {
|
||||
(self.x * rhs.x) + (self.y * rhs.y)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn project_onto(self, rhs: Self) -> Self {
|
||||
let other_len_sq = rhs.dot(rhs);
|
||||
assert!(other_len_sq != FGi32::ZERO);
|
||||
rhs * (self.dot(rhs) / other_len_sq)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn normalize_or_zero(&self) -> Self {
|
||||
self.normalize().unwrap_or(FGVec2::ZERO)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn direction_to(self, to: Self) -> Option<Self> {
|
||||
(to - self).normalize()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn distance(self, to: Self) -> FGi32 {
|
||||
(to - self).length()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn normalize(&self) -> Option<Self> {
|
||||
let x = I32F32::from_num(self.x);
|
||||
let y = I32F32::from_num(self.y);
|
||||
let length_squared = x * x + y * y;
|
||||
|
||||
if length_squared.is_zero() {
|
||||
None
|
||||
} else {
|
||||
self / len
|
||||
let length = length_squared.sqrt();
|
||||
Some(Self::new(
|
||||
FGi32::from_num(x / length),
|
||||
FGi32::from_num(y / length),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +154,14 @@ impl Sub for FGVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<&FGVec2> for &FGVec2 {
|
||||
type Output = FGVec2;
|
||||
#[inline]
|
||||
fn sub(self, rhs: &FGVec2) -> FGVec2 {
|
||||
(*self).sub(*rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for FGVec2 {
|
||||
type Output = Self;
|
||||
|
||||
@@ -113,3 +197,117 @@ impl Div<FGi32> for &FGVec2 {
|
||||
(*self).div(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for FGVec2 {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
*self = Self {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod is_normalized_tests {
|
||||
use super::*;
|
||||
use fixed::types::I16F16 as FGi32;
|
||||
|
||||
// Normalization is computed in Q32.32, so the Q16.16 error comes from the
|
||||
// two final component conversions and length_squared()'s multiplications.
|
||||
const EPS_FIXED_BITS: i32 = 4;
|
||||
|
||||
#[test]
|
||||
fn normalizes_small_axis_vector_without_losing_squared_precision() {
|
||||
let normalized = FGVec2::lit("0.01", "0").normalize().unwrap();
|
||||
|
||||
assert_eq!(normalized, FGVec2::lit("1", "0"));
|
||||
assert!(normalized.is_normalized());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measure_normalize_error_bound() {
|
||||
let mut max_abs_diff_bits: i32 = 0;
|
||||
let mut worst_case: Option<(f64, f64, i32)> = None;
|
||||
|
||||
let angle_steps = 3600; // 0.1 degree resolution
|
||||
let magnitudes: &[f64] = &[
|
||||
0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0, 1000.0, 30000.0,
|
||||
];
|
||||
|
||||
for &mag in magnitudes {
|
||||
for i in 0..angle_steps {
|
||||
let theta = (i as f64) * std::f64::consts::TAU / (angle_steps as f64);
|
||||
let x = mag * theta.cos();
|
||||
let y = mag * theta.sin();
|
||||
|
||||
let Some(fx) = FGi32::checked_from_num(x) else { continue };
|
||||
let Some(fy) = FGi32::checked_from_num(y) else { continue };
|
||||
|
||||
let v = FGVec2 { x: fx, y: fy };
|
||||
|
||||
if let Some(normalized) = v.normalize() {
|
||||
let ls = normalized.length_squared();
|
||||
let diff_bits = ls.to_bits() - FGi32::ONE.to_bits();
|
||||
let abs_diff_bits = diff_bits.abs();
|
||||
|
||||
if abs_diff_bits > max_abs_diff_bits {
|
||||
max_abs_diff_bits = abs_diff_bits;
|
||||
worst_case = Some((x, y, abs_diff_bits));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ulp_value = FGi32::DELTA.to_num::<f64>();
|
||||
println!(
|
||||
"Max |length_squared - 1.0| = {} raw bits ({:e} real), worst case: {:?}",
|
||||
max_abs_diff_bits,
|
||||
max_abs_diff_bits as f64 * ulp_value,
|
||||
worst_case
|
||||
);
|
||||
|
||||
assert!(
|
||||
max_abs_diff_bits <= EPS_FIXED_BITS,
|
||||
"measured error ({} bits) exceeds analytical bound ({} bits) — \
|
||||
revisit the epsilon derivation",
|
||||
max_abs_diff_bits,
|
||||
EPS_FIXED_BITS
|
||||
);
|
||||
|
||||
println!(
|
||||
"Bound: {} bits, measured: {} bits (headroom: {} bits)",
|
||||
EPS_FIXED_BITS,
|
||||
max_abs_diff_bits,
|
||||
EPS_FIXED_BITS - max_abs_diff_bits
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_normalized_behaves_correctly() {
|
||||
let angle_steps = 360;
|
||||
for i in 0..angle_steps {
|
||||
let theta = (i as f64) * std::f64::consts::TAU / (angle_steps as f64);
|
||||
for &mag in &[0.01, 1.0, 100.0, 10000.0] {
|
||||
let x = mag * theta.cos();
|
||||
let y = mag * theta.sin();
|
||||
let Some(fx) = FGi32::checked_from_num(x) else { continue };
|
||||
let Some(fy) = FGi32::checked_from_num(y) else { continue };
|
||||
let v = FGVec2 { x: fx, y: fy };
|
||||
|
||||
if let Some(normalized) = v.normalize() {
|
||||
assert!(
|
||||
normalized.is_normalized(),
|
||||
"normalized({}, {}) failed is_normalized(): length_squared = {:?}",
|
||||
x, y, normalized.length_squared()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let non_unit = FGVec2 { x: FGi32::from_num(2.0), y: FGi32::from_num(0.0) };
|
||||
assert!(!non_unit.is_normalized());
|
||||
|
||||
let non_unit_small = FGVec2 { x: FGi32::from_num(0.5), y: FGi32::from_num(0.0) };
|
||||
assert!(!non_unit_small.is_normalized());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user