Skip to content

Commit 75b23c0

Browse files
committed
simplify unit vector impl
1 parent 2a58bdd commit 75b23c0

5 files changed

Lines changed: 54 additions & 32 deletions

File tree

‎src/int/angle/angle.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ impl Angle {
8080
/// opposite rays return exactly `2^31`. A nonzero cross product is never
8181
/// rounded to an empty arc, even arbitrarily close to a full turn.
8282
/// Products fit in the associated wide type, including `i128` for `i64`:
83-
/// each component is at most S = 2^(I::BITS-2), each product at most S²,
84-
/// and sums/differences at most 2S². No products of cross/dot are formed.
83+
/// components are approximately bounded by S = 2^(I::BITS-2), so products
84+
/// and sums/differences fit with room for float rounding near unit length.
85+
/// No products of cross/dot are formed.
8586
pub fn between<I: IntNumber>(from: UnitIntVector<I>, to: UnitIntVector<I>) -> Self {
8687
let (ax, ay) = (from.x().to_wide(), from.y().to_wide());
8788
let (bx, by) = (to.x().to_wide(), to.y().to_wide());

‎src/int/angle/rotation.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,14 @@ impl<I: IntNumber> Rotation<I> {
109109
}
110110

111111
/// Applies the matrix without CORDIC, square roots, or normalization.
112-
/// Preserves the length-at-most-one invariant of `UnitIntVector`.
112+
/// Does not increase the input length; an approximate float input may
113+
/// remain slightly longer than one.
113114
#[inline]
114115
pub fn apply(self, vector: UnitIntVector<I>) -> UnitIntVector<I> {
115116
let (sin, cos) = (self.sin.to_wide(), self.cos.to_wide());
116117
let (x, y) = (vector.x().to_wide(), vector.y().to_wide());
117118
let scale = FixedScale::<I>::DENOMINATOR;
118-
// Each product is at most scale^2; even their sum fits I::Wide.
119+
// Products and sums fit I::Wide for approximately unit inputs.
119120
UnitIntVector::from_components(
120121
I::from_wide((cos * x - sin * y) / scale),
121122
I::from_wide((sin * x + cos * y) / scale),

‎src/int/unit_vector.rs‎

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ use core::ops::Mul;
99
/// An approximate unit direction stored as fixed-scale integer components.
1010
///
1111
/// Obtain one with [`IntVector::fast_normalize`] or [`Self::normalize_with_float`].
12-
/// The represented components are
13-
/// `x() / DENOMINATOR` and `y() / DENOMINATOR`, each in `[-1, 1]`. The length is
14-
/// approximately one and never exceeds it.
12+
/// The represented components are `x() / DENOMINATOR` and `y() / DENOMINATOR`.
13+
/// Floating-point construction is approximate: rounding may make the length
14+
/// slightly greater than one. Integer normalization via `fast_normalize` and
15+
/// checked conversion via [`Self::try_from_float`] keep the length at most one.
1516
///
1617
/// Integer normalization via `fast_normalize` favors speed over precision:
1718
/// it keeps about 6, 14, or 30 bits
1819
/// of direction precision for `i16`, `i32`, or `i64`, respectively.
1920
/// The storage scale does not imply that all stored bits are accurate.
20-
/// [`Rotation::apply`](crate::int::angle::Rotation::apply) preserves the upper
21-
/// length bound, but repeated rotations accumulate contraction and angular
21+
/// [`Rotation::apply`](crate::int::angle::Rotation::apply) does not increase
22+
/// length, but repeated rotations accumulate contraction and angular
2223
/// error; the normalization precision above is not a bound on that accumulation.
2324
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2425
pub struct UnitIntVector<T: IntNumber = i32> {
@@ -27,7 +28,7 @@ pub struct UnitIntVector<T: IntNumber = i32> {
2728
}
2829

2930
impl<T: IntNumber> UnitIntVector<T> {
30-
// Only for arithmetic that proves the resulting norm is at most one.
31+
// The caller supplies an approximately unit or shorter nonzero vector.
3132
#[inline(always)]
3233
pub(crate) fn from_components(x: T, y: T) -> Self {
3334
Self { x, y }
@@ -47,23 +48,37 @@ impl<T: IntNumber> UnitIntVector<T> {
4748
if x.abs() > 1.0 || y.abs() > 1.0 {
4849
return None;
4950
}
50-
let scale = Self::DENOMINATOR.to_f64();
51-
let x = T::from_rounded_float(libm::trunc(x * scale));
52-
let y = T::from_rounded_float(libm::trunc(y * scale));
53-
let (wx, wy) = (x.to_wide(), y.to_wide());
51+
let unit = Self::from_float_unchecked(x, y);
52+
let (wx, wy) = (unit.x.to_wide(), unit.y.to_wide());
5453
let squared = wx * wx + wy * wy;
5554
if squared == T::Wide::ZERO || squared > Self::DENOMINATOR * Self::DENOMINATOR {
5655
return None;
5756
}
58-
Some(Self { x, y })
57+
Some(unit)
58+
}
59+
60+
/// Converts already prepared floating-point components without validation
61+
/// or normalization. Components are scaled and truncated toward zero.
62+
///
63+
/// The caller supplies finite components of an approximately unit or shorter
64+
/// vector that remains nonzero after quantization. Small floating-point
65+
/// overshoots above unit length are accepted. Use [`Self::try_from_float`]
66+
/// when the fixed-scale length must be checked against one.
67+
#[inline]
68+
pub fn from_float_unchecked<F: FloatNumber>(x: F, y: F) -> Self {
69+
let scale = Self::DENOMINATOR.to_f64();
70+
Self {
71+
x: T::from_float(x.to_f64() * scale),
72+
y: T::from_float(y.to_f64() * scale),
73+
}
5974
}
6075

6176
/// Normalizes an integer vector using f64 arithmetic, returning `None` only
6277
/// for the zero vector. Supports the full built-in wide component range,
6378
/// including i128::MIN; no integer products of the input are formed.
6479
///
65-
/// Axis directions are exact. Other directions are contracted slightly
66-
/// before conversion so their fixed-scale length never exceeds one.
80+
/// This is an approximate, unchecked normalization: rounding may make the
81+
/// output length slightly greater than one, including for axis directions.
6782
/// Precision is limited by f64 arithmetic and the output component scale.
6883
/// When normalizing a point difference, subtract the integer points first
6984
/// to retain small differences between large coordinates.
@@ -74,16 +89,9 @@ impl<T: IntNumber> UnitIntVector<T> {
7489
}
7590
let x = vector.x.to_f64();
7691
let y = vector.y.to_f64();
77-
if x == 0.0 {
78-
return Self::try_from_float(0.0, y.signum());
79-
}
80-
if y == 0.0 {
81-
return Self::try_from_float(x.signum(), 0.0);
82-
}
8392
// Squaring and adding even two i128 components fits in f64.
84-
let length = FloatNumber::sqrt(x * x + y * y);
85-
let inward = 1.0 - 4.0 * f64::EPSILON;
86-
Self::try_from_float(x / length * inward, y / length * inward)
93+
let inv_length = 1.0 / FloatNumber::sqrt(x * x + y * y);
94+
Some(Self::from_float_unchecked(x * inv_length, y * inv_length))
8795
}
8896

8997
/// The stored integer value representing one: 2^14, 2^30, or 2^62.

‎tests/float_normalize_tests.rs‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@ macro_rules! check_normalize {
1414
}
1515
let unit = result.expect("every nonzero wide vector can be normalized");
1616
let (a, b) = (unit.x() as $wide, unit.y() as $wide);
17-
assert!(a * a + b * b <= scale * scale);
1817
assert!(a != 0 || b != 0);
19-
if y == 0 {
20-
assert_eq!((a, b), (x.signum() * scale, 0));
21-
} else if x == 0 {
22-
assert_eq!((a, b), (0, y.signum() * scale));
23-
}
2418
let length = (x as f64).hypot(y as f64);
2519
let tolerance = 1.0 / scale as f64 + 8.0 * f64::EPSILON;
20+
let norm = (a as f64 / scale as f64).hypot(b as f64 / scale as f64);
21+
assert!((norm - 1.0).abs() <= 2.0 * tolerance);
2622
assert!((a as f64 / scale as f64 - x as f64 / length).abs() <= tolerance);
2723
assert!((b as f64 / scale as f64 - y as f64 / length).abs() <= tolerance);
2824
if x != <$wide>::MIN && y != <$wide>::MIN {
@@ -63,3 +59,14 @@ macro_rules! check_normalize {
6359
check_normalize!(normalize_i16, i16, i32);
6460
check_normalize!(normalize_i32, i32, i64);
6561
check_normalize!(normalize_i64, i64, i128);
62+
63+
#[test]
64+
fn near_axis_roundoff_does_not_reject_nonzero_vector() {
65+
let unit = UnitIntVector::<i64>::normalize_with_float(IntVector::new(1 << 30, 1)).unwrap();
66+
let scale = UnitIntVector::<i64>::DENOMINATOR;
67+
let (x, y) = (unit.x() as i128, unit.y() as i128);
68+
// f64 loses the small squared component. A slight overshoot is intentional.
69+
assert!(x * x + y * y > scale * scale);
70+
assert!((x as f64 / scale as f64 - 1.0).abs() <= f64::EPSILON);
71+
assert!(y > 0);
72+
}

‎tests/float_unit_vector_tests.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ macro_rules! float_conversion {
1414
(0.5, 0.5),
1515
] {
1616
let unit = UnitIntVector::<$int>::try_from_float(x, y).unwrap();
17+
assert_eq!(unit, UnitIntVector::<$int>::from_float_unchecked(x, y));
1718
let (a, b) = (unit.x() as $wide, unit.y() as $wide);
1819
assert!(a * a + b * b <= s * s);
1920
assert!((a as f64 / s as f64 - x).abs() <= 1.0 / s as f64);
@@ -26,6 +27,10 @@ macro_rules! float_conversion {
2627
let (sin, cos) = libm::sincos(angle);
2728
let inward = 1.0 - 8.0 * f64::EPSILON;
2829
let unit = UnitIntVector::<$int>::try_from_float(cos * inward, sin * inward).unwrap();
30+
assert_eq!(
31+
unit,
32+
UnitIntVector::<$int>::from_float_unchecked(cos * inward, sin * inward)
33+
);
2934
let (a, b) = (unit.x() as $wide, unit.y() as $wide);
3035
assert!(a * a + b * b <= s * s);
3136
}

0 commit comments

Comments
 (0)