renew_fixed/vector.rs
1//! Vectors over [`Fixed`].
2//!
3//! Two dimensions and three, as separate concrete types rather than one
4//! generic over dimension. The physics contract makes the same choice for the
5//! same reason: dimension-generic vocabularies produce signatures nobody can
6//! read, and the cost of writing `dot` twice is smaller than the cost of every
7//! caller reading a bound.
8
9use core::ops::{Add, Mul, Neg, Sub};
10
11use crate::Fixed;
12
13/// How far to shift a direction left so normalising it keeps its precision.
14///
15/// Normalising is unchanged by scaling, and **shifting a fixed-point value left is
16/// exact** — no rounding, no loss. So a short direction is scaled up before
17/// its length is taken, which is the difference between a normal that is
18/// unit to a thousandth of a percent and one that is forty per cent wrong.
19///
20/// The target is 2³⁸ for the largest component: big enough that squaring
21/// keeps every significant bit, small enough that three squared components
22/// summed stay inside what the type holds (3 × 2⁶⁰ < 2⁶²).
23fn normalising_shift(largest: u64) -> u32 {
24 // A value with `k` significant bits has `64 - k` leading zeros, so
25 // shifting by `64 - k - 26` leaves it with 38. The 26 was 25 in the
26 // first version, which targets 2^39 rather than 2^38 — and three
27 // squared 2^39 components summed overflow an i64, so 3D normalisation
28 // saturated and returned a normal a quarter of a per cent off unit.
29 // Caught by a property test whose generator had just been widened to
30 // reach short vectors; the arithmetic was one bit out and the comment
31 // above was right all along.
32 largest.leading_zeros().saturating_sub(26)
33}
34
35/// A two-dimensional vector.
36///
37/// # Contract
38///
39/// - **Every operation is deterministic**, because every operation is
40/// [`Fixed`] arithmetic and nothing else.
41/// - **Saturating throughout**, inheriting the scalar's behaviour: a component
42/// that overflows clamps and is counted rather than wrapping.
43/// - **`Eq` and `Hash`**, so a vector can be a map key or enter a state hash
44/// directly — which is the thing a float vector cannot offer.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46pub struct Vec2 {
47 pub x: Fixed,
48 pub y: Fixed,
49}
50
51/// A three-dimensional vector. See [`Vec2`] for the contract; it is the same.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
53pub struct Vec3 {
54 pub x: Fixed,
55 pub y: Fixed,
56 pub z: Fixed,
57}
58
59impl Vec2 {
60 /// The origin.
61 pub const ZERO: Self = Self {
62 x: Fixed::ZERO,
63 y: Fixed::ZERO,
64 };
65 /// One unit along x.
66 pub const X: Self = Self {
67 x: Fixed::ONE,
68 y: Fixed::ZERO,
69 };
70 /// One unit along y.
71 pub const Y: Self = Self {
72 x: Fixed::ZERO,
73 y: Fixed::ONE,
74 };
75
76 /// From components.
77 #[must_use]
78 pub const fn new(x: Fixed, y: Fixed) -> Self {
79 Self { x, y }
80 }
81
82 /// From whole numbers, which is how most call sites write a constant.
83 #[must_use]
84 pub const fn from_ints(x: i32, y: i32) -> Self {
85 Self {
86 x: Fixed::from_int(x),
87 y: Fixed::from_int(y),
88 }
89 }
90
91 /// The dot product.
92 #[must_use]
93 pub fn dot(self, other: Self) -> Fixed {
94 self.x.saturating_mul(other.x) + self.y.saturating_mul(other.y)
95 }
96
97 /// The 2D cross product: a scalar, the z of the 3D cross of these vectors
98 /// lifted into the plane. Positive when `other` is counter-clockwise of
99 /// `self`, which is what a winding test reads.
100 #[must_use]
101 pub fn cross(self, other: Self) -> Fixed {
102 self.x.saturating_mul(other.y) - self.y.saturating_mul(other.x)
103 }
104
105 /// The squared length.
106 ///
107 /// Preferred over [`Vec2::length`] wherever a comparison will do, and not
108 /// only for speed: this is exact where the length is rounded, so two
109 /// vectors that compare equal by squared length may compare unequal by
110 /// length.
111 #[must_use]
112 pub fn length_squared(self) -> Fixed {
113 self.dot(self)
114 }
115
116 /// The squared length at full width, which cannot overflow and cannot
117 /// round.
118 ///
119 /// The form to compare with. [`Vec2::length_squared`] narrows to a
120 /// `Fixed` and therefore has a floor: a vector whose components are all
121 /// below 182 raw units squares to *zero* there, which is how a direction
122 /// can appear to have no length at all. Nothing is lost here.
123 #[must_use]
124 pub fn length_squared_wide(self) -> crate::Wide {
125 self.x.wide_mul(self.x) + self.y.wide_mul(self.y)
126 }
127
128 /// The length, floored to the representable value below the exact one.
129 ///
130 /// Computed through the full-width square, so it is exact for short
131 /// vectors where narrowing first would have lost them entirely — a
132 /// one-raw-unit vector has length one here and had length zero before.
133 #[must_use]
134 pub fn length(self) -> Fixed {
135 self.length_squared_wide().sqrt()
136 }
137
138 /// The distance to another point.
139 #[must_use]
140 pub fn distance(self, other: Self) -> Fixed {
141 (self - other).length()
142 }
143
144 /// A unit vector in the same direction, or `None` for the zero vector.
145 ///
146 /// Fallible rather than asserting, because the zero vector is a value a
147 /// simulation legitimately produces — a body at rest, a contact between
148 /// coincident points — and refusing it would put an assertion on a path
149 /// that runs every frame.
150 ///
151 /// **The result is unit-length to within four parts in 65536**, which is
152 /// asserted by a property test over every magnitude including the
153 /// shortest. Callers wanting an exact equality should compare squared
154 /// lengths against a tolerance rather than expecting [`Fixed::ONE`].
155 ///
156 /// The direction is scaled up before its length is taken, and that is
157 /// not an optimisation. Shifting a fixed-point value left is exact, and
158 /// without it a short direction is divided by a length that rounded to
159 /// something far too coarse: before this, a direction of 41 raw units
160 /// came back as a normal forty-one per cent too long, and anything whose
161 /// components were all below 181 raw had no direction at all.
162 #[must_use]
163 pub fn normalize(self) -> Option<Self> {
164 let largest = self
165 .x
166 .to_bits()
167 .unsigned_abs()
168 .max(self.y.to_bits().unsigned_abs());
169 if largest == 0 {
170 return None;
171 }
172 let shift = normalising_shift(largest);
173 let scaled = Self::new(
174 Fixed::from_bits(self.x.to_bits() << shift),
175 Fixed::from_bits(self.y.to_bits() << shift),
176 );
177 // Non-zero after the check above, and rescaling is what makes that
178 // true: the largest component carries 38 significant bits, so its
179 // square alone exceeds 2^60 and the length cannot round to zero.
180 // Before rescaling this needed a second zero check, and that check
181 // was the bug — it turned a short direction into no direction.
182 let length = scaled.length();
183 Some(Self::new(
184 scaled.x.saturating_div(length),
185 scaled.y.saturating_div(length),
186 ))
187 }
188
189 /// Linear interpolation, `t` clamped to `[0, 1]`.
190 ///
191 /// Written as `a + (b - a) * t` rather than `a*(1-t) + b*t`: the second is
192 /// the numerically better form in floating point and the worse one here,
193 /// because it rounds twice as often and neither form gains anything from
194 /// exactness at the endpoints — this one is exact at both by construction.
195 #[must_use]
196 pub fn lerp(self, other: Self, t: Fixed) -> Self {
197 let t = t.clamp(Fixed::ZERO, Fixed::ONE);
198 self + (other - self) * t
199 }
200
201 /// The component of `self` along `direction`, which must be unit-length.
202 ///
203 /// The building block of move-and-slide: removing this from a
204 /// displacement is what makes a body slide along a wall rather than stop
205 /// at it.
206 #[must_use]
207 pub fn project_onto_unit(self, direction: Self) -> Self {
208 direction * self.dot(direction)
209 }
210
211 /// `self` with its component along `normal` removed.
212 ///
213 /// `normal` must be unit-length. This is the slide operation itself, named
214 /// so the physics implementation does not spell it out at each call site
215 /// and get the sign wrong at one of them.
216 #[must_use]
217 pub fn slide_along(self, normal: Self) -> Self {
218 self - self.project_onto_unit(normal)
219 }
220
221 /// Rotated counter-clockwise by `angle`.
222 ///
223 /// The standard rotation, in fixed point: `(x cos − y sin, x sin + y
224 /// cos)`. Each component is two rounded products, so a rotated vector
225 /// keeps its length to a few parts in 65536 rather than exactly — the
226 /// tests state the bound.
227 ///
228 /// [`Vec2::perpendicular`] remains for the quarter turn, and is not the
229 /// same thing: it is exact, where this rounds.
230 #[must_use]
231 pub fn rotate(self, angle: crate::Angle) -> Self {
232 let (sin, cos) = angle.sin_cos();
233 Self::new(
234 self.x.saturating_mul(cos) - self.y.saturating_mul(sin),
235 self.x.saturating_mul(sin) + self.y.saturating_mul(cos),
236 )
237 }
238
239 /// Perpendicular, rotated a quarter turn counter-clockwise.
240 ///
241 /// Exact — a quarter turn is a swap and a negation, needing no
242 /// trigonometry, which is why this is available when general rotation is
243 /// not.
244 #[must_use]
245 pub fn perpendicular(self) -> Self {
246 Self::new(-self.y, self.x)
247 }
248}
249
250impl Vec3 {
251 /// The origin.
252 pub const ZERO: Self = Self {
253 x: Fixed::ZERO,
254 y: Fixed::ZERO,
255 z: Fixed::ZERO,
256 };
257
258 /// From components.
259 #[must_use]
260 pub const fn new(x: Fixed, y: Fixed, z: Fixed) -> Self {
261 Self { x, y, z }
262 }
263
264 /// From whole numbers.
265 #[must_use]
266 pub const fn from_ints(x: i32, y: i32, z: i32) -> Self {
267 Self {
268 x: Fixed::from_int(x),
269 y: Fixed::from_int(y),
270 z: Fixed::from_int(z),
271 }
272 }
273
274 /// The dot product.
275 #[must_use]
276 pub fn dot(self, other: Self) -> Fixed {
277 self.x.saturating_mul(other.x)
278 + self.y.saturating_mul(other.y)
279 + self.z.saturating_mul(other.z)
280 }
281
282 /// The cross product: a vector perpendicular to both.
283 #[must_use]
284 pub fn cross(self, other: Self) -> Self {
285 Self::new(
286 self.y.saturating_mul(other.z) - self.z.saturating_mul(other.y),
287 self.z.saturating_mul(other.x) - self.x.saturating_mul(other.z),
288 self.x.saturating_mul(other.y) - self.y.saturating_mul(other.x),
289 )
290 }
291
292 /// The squared length. See [`Vec2::length_squared`] on why to prefer it.
293 #[must_use]
294 pub fn length_squared(self) -> Fixed {
295 self.dot(self)
296 }
297
298 /// The squared length at full width. See [`Vec2::length_squared_wide`].
299 #[must_use]
300 pub fn length_squared_wide(self) -> crate::Wide {
301 self.x.wide_mul(self.x) + self.y.wide_mul(self.y) + self.z.wide_mul(self.z)
302 }
303
304 /// The length, floored. Computed through the full-width square.
305 #[must_use]
306 pub fn length(self) -> Fixed {
307 self.length_squared_wide().sqrt()
308 }
309
310 /// The distance to another point.
311 #[must_use]
312 pub fn distance(self, other: Self) -> Fixed {
313 (self - other).length()
314 }
315
316 /// A unit vector in the same direction, or `None` for the zero vector.
317 /// See [`Vec2::normalize`] on how close to unit the result is.
318 #[must_use]
319 pub fn normalize(self) -> Option<Self> {
320 let largest = self
321 .x
322 .to_bits()
323 .unsigned_abs()
324 .max(self.y.to_bits().unsigned_abs())
325 .max(self.z.to_bits().unsigned_abs());
326 if largest == 0 {
327 return None;
328 }
329 let shift = normalising_shift(largest);
330 let scaled = Self::new(
331 Fixed::from_bits(self.x.to_bits() << shift),
332 Fixed::from_bits(self.y.to_bits() << shift),
333 Fixed::from_bits(self.z.to_bits() << shift),
334 );
335 // Non-zero after the check above, and rescaling is what makes that
336 // true: the largest component carries 38 significant bits, so its
337 // square alone exceeds 2^60 and the length cannot round to zero.
338 // Before rescaling this needed a second zero check, and that check
339 // was the bug — it turned a short direction into no direction.
340 let length = scaled.length();
341 Some(Self::new(
342 scaled.x.saturating_div(length),
343 scaled.y.saturating_div(length),
344 scaled.z.saturating_div(length),
345 ))
346 }
347
348 /// Linear interpolation, `t` clamped to `[0, 1]`.
349 #[must_use]
350 pub fn lerp(self, other: Self, t: Fixed) -> Self {
351 let t = t.clamp(Fixed::ZERO, Fixed::ONE);
352 self + (other - self) * t
353 }
354
355 /// `self` with its component along a unit `normal` removed.
356 #[must_use]
357 pub fn slide_along(self, normal: Self) -> Self {
358 self - normal * self.dot(normal)
359 }
360}
361
362// The operators, rather than inherent `add`/`sub`/`neg`/`scale`. For a vector
363// these read the way the maths does, and inherent methods by those names
364// shadow the traits confusingly enough that the linter says so.
365
366impl Add for Vec2 {
367 type Output = Self;
368 fn add(self, other: Self) -> Self {
369 Self::new(self.x + other.x, self.y + other.y)
370 }
371}
372
373impl Sub for Vec2 {
374 type Output = Self;
375 fn sub(self, other: Self) -> Self {
376 Self::new(self.x - other.x, self.y - other.y)
377 }
378}
379
380impl Neg for Vec2 {
381 type Output = Self;
382 fn neg(self) -> Self {
383 Self::new(-self.x, -self.y)
384 }
385}
386
387/// Scaled by a scalar. Saturating componentwise, like everything else here.
388impl Mul<Fixed> for Vec2 {
389 type Output = Self;
390 fn mul(self, factor: Fixed) -> Self {
391 Self::new(self.x.saturating_mul(factor), self.y.saturating_mul(factor))
392 }
393}
394
395impl Add for Vec3 {
396 type Output = Self;
397 fn add(self, other: Self) -> Self {
398 Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
399 }
400}
401
402impl Sub for Vec3 {
403 type Output = Self;
404 fn sub(self, other: Self) -> Self {
405 Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
406 }
407}
408
409impl Neg for Vec3 {
410 type Output = Self;
411 fn neg(self) -> Self {
412 Self::new(-self.x, -self.y, -self.z)
413 }
414}
415
416impl Mul<Fixed> for Vec3 {
417 type Output = Self;
418 fn mul(self, factor: Fixed) -> Self {
419 Self::new(
420 self.x.saturating_mul(factor),
421 self.y.saturating_mul(factor),
422 self.z.saturating_mul(factor),
423 )
424 }
425}