Skip to main content

gizmo_math/
fixed.rs

1//! Fixed-Point (Sabit Noktalı) Matematik Kütüphanesi
2//!
3//! Kayan noktalı (Floating-Point / f32, f64) sayılar farklı CPU mimarilerinde (x86, ARM)
4//! veya farklı derleyici optimizasyonlarında farklı yuvarlama hataları verebilir.
5//! Bu kütüphane, Multiplayer, eSpor, Lock-step RTS oyunlarında %100 Bit-Exact
6//! Cross-Platform Determinism sağlamak için Q16.16 ve Q32.32 sabit noktalı yapıları içerir.
7
8use serde::{Deserialize, Serialize};
9use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
10
11/// Q16.16 Sabit Noktalı Sayı (32-bit)
12///
13/// 16 bit tam sayı kısmı, 16 bit ondalık kısmı temsil eder.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15pub struct Fp32(pub i32);
16
17impl Fp32 {
18    pub const SHIFT: usize = 16;
19    pub const ONE_RAW: i32 = 1 << Self::SHIFT;
20
21    pub const ZERO: Self = Self(0);
22    pub const ONE: Self = Self(Self::ONE_RAW);
23    pub const MINUS_ONE: Self = Self(-Self::ONE_RAW);
24    pub const MAX: Self = Self(i32::MAX);
25    pub const MIN: Self = Self(i32::MIN);
26
27    // Pi yaklaşımları
28    pub const PI: Self = Self(205887); // 3.1415926 * 65536
29    pub const TWO_PI: Self = Self(411774); // 2 * PI  (205887 * 2)
30    pub const HALF_PI: Self = Self(102943); // PI / 2  (205887 / 2)
31
32    #[inline]
33    pub const fn from_raw(raw: i32) -> Self {
34        Self(raw)
35    }
36
37    #[inline]
38    pub fn from_i32(val: i32) -> Self {
39        Self(val << Self::SHIFT)
40    }
41
42    #[inline]
43    pub fn from_f32(val: f32) -> Self {
44        Self((val * (Self::ONE_RAW as f32)) as i32)
45    }
46
47    #[inline]
48    pub fn to_f32(self) -> f32 {
49        self.0 as f32 / Self::ONE_RAW as f32
50    }
51
52    #[inline]
53    pub fn to_i32(self) -> i32 {
54        self.0 >> Self::SHIFT
55    }
56
57    #[inline]
58    pub fn abs(self) -> Self {
59        Self(self.0.abs())
60    }
61
62    /// Bit-exact Karekök (Sqrt) - Newton-Raphson veya Integer Sqrt algoritması
63    pub fn sqrt(self) -> Self {
64        if self.0 <= 0 {
65            return Self::ZERO;
66        }
67        let mut bit = 1u32 << 30; // Max pwr of 4
68        let mut x = self.0 as u32;
69
70        while bit > x {
71            bit >>= 2;
72        }
73
74        let mut res = 0u32;
75        while bit != 0 {
76            if x >= res + bit {
77                x -= res + bit;
78                res = (res >> 1) + bit;
79            } else {
80                res >>= 1;
81            }
82            bit >>= 2;
83        }
84        // Q16.16 formatına uyarlamak için (res << 8) yapıyoruz.
85        // Çünkü x aslında (val * 2^16), karekökü (val^0.5 * 2^8).
86        Self((res << 8) as i32)
87    }
88
89    /// Taylor Serisi veya Bhaskara Approximation ile Bit-exact Sinüs
90    pub fn sin(self) -> Self {
91        // Bhaskara I sine approximation formula
92        // sin(x) ≈ (16 * x * (PI - x)) / (5 * PI^2 - 4 * x * (PI - x))
93        // Not: Açının 0 ile PI arasında olması gerekir. Radian wrap yapılmalı.
94        let mut x = self % Self::TWO_PI;
95        if x < Self::ZERO {
96            x += Self::TWO_PI;
97        }
98        let sign = if x > Self::PI {
99            x -= Self::PI;
100            -1
101        } else {
102            1
103        };
104
105        let pi_minus_x = Self::PI - x;
106        let num = Self::from_i32(16) * x * pi_minus_x;
107        let den = Self::from_i32(5) * Self::PI * Self::PI - Self::from_i32(4) * x * pi_minus_x;
108
109        let res = num / den;
110        if sign < 0 {
111            -res
112        } else {
113            res
114        }
115    }
116
117    pub fn cos(self) -> Self {
118        (self + Self::HALF_PI).sin()
119    }
120}
121
122impl Add for Fp32 {
123    type Output = Self;
124    #[inline]
125    fn add(self, rhs: Self) -> Self {
126        Self(self.0 + rhs.0)
127    }
128}
129
130impl Sub for Fp32 {
131    type Output = Self;
132    #[inline]
133    fn sub(self, rhs: Self) -> Self {
134        Self(self.0 - rhs.0)
135    }
136}
137
138impl Mul for Fp32 {
139    type Output = Self;
140    #[inline]
141    fn mul(self, rhs: Self) -> Self {
142        let val = (self.0 as i64 * rhs.0 as i64) >> Fp32::SHIFT;
143        Self(val as i32)
144    }
145}
146
147impl Div for Fp32 {
148    type Output = Self;
149    #[inline]
150    fn div(self, rhs: Self) -> Self {
151        debug_assert!(rhs.0 != 0, "Fp32: division by zero");
152        let val = ((self.0 as i64) << Fp32::SHIFT) / (rhs.0 as i64);
153        Self(val as i32)
154    }
155}
156
157impl Neg for Fp32 {
158    type Output = Self;
159    #[inline]
160    fn neg(self) -> Self {
161        Self(-self.0)
162    }
163}
164
165impl std::ops::Rem for Fp32 {
166    type Output = Self;
167    #[inline]
168    fn rem(self, rhs: Self) -> Self {
169        Self(self.0 % rhs.0)
170    }
171}
172
173impl AddAssign for Fp32 {
174    #[inline]
175    fn add_assign(&mut self, rhs: Self) {
176        self.0 += rhs.0;
177    }
178}
179impl SubAssign for Fp32 {
180    #[inline]
181    fn sub_assign(&mut self, rhs: Self) {
182        self.0 -= rhs.0;
183    }
184}
185impl MulAssign for Fp32 {
186    #[inline]
187    fn mul_assign(&mut self, rhs: Self) {
188        *self = *self * rhs;
189    }
190}
191impl DivAssign for Fp32 {
192    #[inline]
193    fn div_assign(&mut self, rhs: Self) {
194        *self = *self / rhs;
195    }
196}
197
198// --- Vektör yapıları (Sabit noktalı 3D Fizik için) ---
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
201pub struct FpVec3 {
202    pub x: Fp32,
203    pub y: Fp32,
204    pub z: Fp32,
205}
206
207impl FpVec3 {
208    pub const ZERO: Self = Self {
209        x: Fp32::ZERO,
210        y: Fp32::ZERO,
211        z: Fp32::ZERO,
212    };
213
214    #[inline]
215    pub fn new(x: Fp32, y: Fp32, z: Fp32) -> Self {
216        Self { x, y, z }
217    }
218
219    pub fn dot(self, rhs: Self) -> Fp32 {
220        self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
221    }
222
223    pub fn cross(self, rhs: Self) -> Self {
224        Self {
225            x: self.y * rhs.z - self.z * rhs.y,
226            y: self.z * rhs.x - self.x * rhs.z,
227            z: self.x * rhs.y - self.y * rhs.x,
228        }
229    }
230
231    pub fn length_squared(self) -> Fp32 {
232        self.dot(self)
233    }
234
235    pub fn length(self) -> Fp32 {
236        self.length_squared().sqrt()
237    }
238
239    pub fn normalize(self) -> Self {
240        let len = self.length();
241        if len.0 > 0 {
242            self / len
243        } else {
244            Self::ZERO
245        }
246    }
247}
248
249impl Add for FpVec3 {
250    type Output = Self;
251    fn add(self, rhs: Self) -> Self {
252        Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
253    }
254}
255
256impl Sub for FpVec3 {
257    type Output = Self;
258    fn sub(self, rhs: Self) -> Self {
259        Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
260    }
261}
262
263impl Mul<Fp32> for FpVec3 {
264    type Output = Self;
265    fn mul(self, rhs: Fp32) -> Self {
266        Self::new(self.x * rhs, self.y * rhs, self.z * rhs)
267    }
268}
269
270impl Div<Fp32> for FpVec3 {
271    type Output = Self;
272    fn div(self, rhs: Fp32) -> Self {
273        Self::new(self.x / rhs, self.y / rhs, self.z / rhs)
274    }
275}
276
277// ---------------------------------------------------------------------------
278// Tests
279// ---------------------------------------------------------------------------
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    /// Fixed-point tolerans: Q16.16'da 1 LSB = 1/65536 ≈ 0.0000153
286    /// Trigonometrik yaklaşımlar için daha geniş tolerans kullanıyoruz.
287    const FP_EPS: f32 = 0.01; // ~%1 tolerans (Bhaskara max error ~%1.9)
288
289    fn fp_approx(a: Fp32, expected: f32) -> bool {
290        (a.to_f32() - expected).abs() < FP_EPS
291    }
292
293    // =======================================================================
294    // Fp32 — Dönüşümler (Conversions)
295    // =======================================================================
296
297    #[test]
298    fn fp32_from_i32_roundtrip() {
299        for val in [-100, -1, 0, 1, 42, 1000] {
300            let fp = Fp32::from_i32(val);
301            assert_eq!(fp.to_i32(), val, "from_i32({val}).to_i32() roundtrip failed");
302        }
303    }
304
305    #[test]
306    fn fp32_from_f32_roundtrip() {
307        for val in [0.0f32, 1.0, -1.0, 0.5, -0.25, 3.14, 100.75] {
308            let fp = Fp32::from_f32(val);
309            let back = fp.to_f32();
310            assert!(
311                (back - val).abs() < 0.001,
312                "from_f32({val}).to_f32() = {back}, expected ~{val}"
313            );
314        }
315    }
316
317    #[test]
318    fn fp32_from_raw() {
319        let fp = Fp32::from_raw(65536);
320        assert_eq!(fp, Fp32::ONE);
321        assert_eq!(fp.to_f32(), 1.0);
322    }
323
324    // =======================================================================
325    // Fp32 — Sabitler (Constants Consistency)
326    // =======================================================================
327
328    #[test]
329    fn fp32_constants_consistency() {
330        // TWO_PI = 2 * PI
331        assert_eq!(Fp32::TWO_PI.0, Fp32::PI.0 * 2, "TWO_PI != 2 * PI");
332
333        // HALF_PI = PI / 2 (integer division)
334        assert_eq!(Fp32::HALF_PI.0, Fp32::PI.0 / 2, "HALF_PI != PI / 2");
335
336        // ONE = from_i32(1)
337        assert_eq!(Fp32::ONE, Fp32::from_i32(1));
338
339        // MINUS_ONE = -ONE
340        assert_eq!(Fp32::MINUS_ONE, -Fp32::ONE);
341
342        // ZERO
343        assert_eq!(Fp32::ZERO.0, 0);
344    }
345
346    #[test]
347    fn fp32_pi_accuracy() {
348        let pi_f32 = Fp32::PI.to_f32();
349        assert!(
350            (pi_f32 - std::f32::consts::PI).abs() < 0.0001,
351            "PI as f32 = {pi_f32}, expected ~3.14159"
352        );
353    }
354
355    // =======================================================================
356    // Fp32 — Aritmetik Operatörler
357    // =======================================================================
358
359    #[test]
360    fn fp32_add() {
361        let a = Fp32::from_f32(1.5);
362        let b = Fp32::from_f32(2.25);
363        assert!(fp_approx(a + b, 3.75));
364    }
365
366    #[test]
367    fn fp32_sub() {
368        let a = Fp32::from_f32(5.0);
369        let b = Fp32::from_f32(3.5);
370        assert!(fp_approx(a - b, 1.5));
371    }
372
373    #[test]
374    fn fp32_mul() {
375        let a = Fp32::from_f32(3.0);
376        let b = Fp32::from_f32(4.0);
377        assert!(fp_approx(a * b, 12.0));
378
379        // Çarpım ile ondalık
380        let c = Fp32::from_f32(2.5);
381        let d = Fp32::from_f32(1.5);
382        assert!(fp_approx(c * d, 3.75));
383    }
384
385    #[test]
386    fn fp32_div() {
387        let a = Fp32::from_f32(10.0);
388        let b = Fp32::from_f32(4.0);
389        assert!(fp_approx(a / b, 2.5));
390    }
391
392    #[test]
393    fn fp32_neg() {
394        let a = Fp32::from_f32(3.14);
395        assert!(fp_approx(-a, -3.14));
396        assert!(fp_approx(-(-a), 3.14));
397    }
398
399    #[test]
400    fn fp32_rem() {
401        let a = Fp32::from_f32(7.0);
402        let b = Fp32::from_f32(3.0);
403        assert!(fp_approx(a % b, 1.0));
404    }
405
406    #[test]
407    fn fp32_abs() {
408        assert!(fp_approx(Fp32::from_f32(-5.0).abs(), 5.0));
409        assert!(fp_approx(Fp32::from_f32(5.0).abs(), 5.0));
410        assert!(fp_approx(Fp32::ZERO.abs(), 0.0));
411    }
412
413    // =======================================================================
414    // Fp32 — Compound Assign Operatörleri
415    // =======================================================================
416
417    #[test]
418    fn fp32_add_assign() {
419        let mut a = Fp32::from_f32(1.0);
420        a += Fp32::from_f32(2.0);
421        assert!(fp_approx(a, 3.0));
422    }
423
424    #[test]
425    fn fp32_sub_assign() {
426        let mut a = Fp32::from_f32(5.0);
427        a -= Fp32::from_f32(3.0);
428        assert!(fp_approx(a, 2.0));
429    }
430
431    #[test]
432    fn fp32_mul_assign() {
433        let mut a = Fp32::from_f32(3.0);
434        a *= Fp32::from_f32(4.0);
435        assert!(fp_approx(a, 12.0));
436    }
437
438    #[test]
439    fn fp32_div_assign() {
440        let mut a = Fp32::from_f32(10.0);
441        a /= Fp32::from_f32(2.0);
442        assert!(fp_approx(a, 5.0));
443    }
444
445    // =======================================================================
446    // Fp32 — Sqrt
447    // =======================================================================
448
449    #[test]
450    fn fp32_sqrt_perfect_squares() {
451        for val in [1.0f32, 4.0, 9.0, 16.0, 25.0, 100.0] {
452            let result = Fp32::from_f32(val).sqrt().to_f32();
453            let expected = val.sqrt();
454            assert!(
455                (result - expected).abs() < 0.05,
456                "sqrt({val}) = {result}, expected {expected}"
457            );
458        }
459    }
460
461    #[test]
462    fn fp32_sqrt_fractional() {
463        let result = Fp32::from_f32(2.0).sqrt().to_f32();
464        assert!(
465            (result - std::f32::consts::SQRT_2).abs() < 0.05,
466            "sqrt(2) = {result}, expected ~1.414"
467        );
468    }
469
470    #[test]
471    fn fp32_sqrt_zero() {
472        assert_eq!(Fp32::ZERO.sqrt(), Fp32::ZERO);
473    }
474
475    #[test]
476    fn fp32_sqrt_negative_returns_zero() {
477        assert_eq!(Fp32::from_f32(-4.0).sqrt(), Fp32::ZERO);
478        assert_eq!(Fp32::MINUS_ONE.sqrt(), Fp32::ZERO);
479    }
480
481    // =======================================================================
482    // Fp32 — Trigonometri (sin / cos)
483    // =======================================================================
484
485    #[test]
486    fn fp32_sin_known_values() {
487        // sin(0) = 0
488        assert!(fp_approx(Fp32::ZERO.sin(), 0.0));
489
490        // sin(PI) = 0
491        assert!(
492            Fp32::PI.sin().to_f32().abs() < 0.02,
493            "sin(PI) = {}, expected ~0",
494            Fp32::PI.sin().to_f32()
495        );
496
497        // sin(PI/2) = 1
498        assert!(
499            fp_approx(Fp32::HALF_PI.sin(), 1.0),
500            "sin(PI/2) = {}, expected ~1.0",
501            Fp32::HALF_PI.sin().to_f32()
502        );
503
504        // sin(3*PI/2) = -1
505        let three_half_pi = Fp32::PI + Fp32::HALF_PI;
506        assert!(
507            fp_approx(three_half_pi.sin(), -1.0),
508            "sin(3PI/2) = {}, expected ~-1.0",
509            three_half_pi.sin().to_f32()
510        );
511    }
512
513    #[test]
514    fn fp32_cos_known_values() {
515        // cos(0) = 1
516        assert!(
517            fp_approx(Fp32::ZERO.cos(), 1.0),
518            "cos(0) = {}, expected ~1.0",
519            Fp32::ZERO.cos().to_f32()
520        );
521
522        // cos(PI) = -1
523        assert!(
524            fp_approx(Fp32::PI.cos(), -1.0),
525            "cos(PI) = {}, expected ~-1.0",
526            Fp32::PI.cos().to_f32()
527        );
528
529        // cos(PI/2) ≈ 0
530        assert!(
531            Fp32::HALF_PI.cos().to_f32().abs() < 0.02,
532            "cos(PI/2) = {}, expected ~0",
533            Fp32::HALF_PI.cos().to_f32()
534        );
535    }
536
537    #[test]
538    fn fp32_sin_negative_angle() {
539        // sin(-x) = -sin(x)
540        let x = Fp32::from_f32(1.0);
541        let sin_x = x.sin().to_f32();
542        let sin_neg_x = (-x).sin().to_f32();
543        assert!(
544            (sin_x + sin_neg_x).abs() < 0.02,
545            "sin(x) + sin(-x) = {}, expected ~0",
546            sin_x + sin_neg_x
547        );
548    }
549
550    #[test]
551    fn fp32_sin_cos_pythagorean_identity() {
552        // sin²(x) + cos²(x) = 1 for various angles
553        for angle_f32 in [0.5f32, 1.0, 1.5, 2.0, 2.5, 3.0, 4.5, 5.5] {
554            let x = Fp32::from_f32(angle_f32);
555            let s = x.sin().to_f32();
556            let c = x.cos().to_f32();
557            let identity = s * s + c * c;
558            assert!(
559                (identity - 1.0).abs() < 0.05,
560                "sin²({angle_f32}) + cos²({angle_f32}) = {identity}, expected ~1.0"
561            );
562        }
563    }
564
565    #[test]
566    fn fp32_sin_wrap_around() {
567        // sin(x) = sin(x + 2*PI) — periodicity
568        let x = Fp32::from_f32(1.23);
569        let s1 = x.sin().to_f32();
570        let s2 = (x + Fp32::TWO_PI).sin().to_f32();
571        assert!(
572            (s1 - s2).abs() < 0.02,
573            "sin(x) = {s1}, sin(x + 2PI) = {s2}, expected equal"
574        );
575    }
576
577    // =======================================================================
578    // Fp32 — Ordering / Comparison
579    // =======================================================================
580
581    #[test]
582    fn fp32_ordering() {
583        assert!(Fp32::ZERO < Fp32::ONE);
584        assert!(Fp32::MINUS_ONE < Fp32::ZERO);
585        assert!(Fp32::ONE > Fp32::MINUS_ONE);
586        assert_eq!(Fp32::ONE, Fp32::ONE);
587    }
588
589    // =======================================================================
590    // FpVec3 — Temel Operasyonlar
591    // =======================================================================
592
593    #[test]
594    fn fpvec3_add_sub() {
595        let a = FpVec3::new(Fp32::from_f32(1.0), Fp32::from_f32(2.0), Fp32::from_f32(3.0));
596        let b = FpVec3::new(Fp32::from_f32(4.0), Fp32::from_f32(5.0), Fp32::from_f32(6.0));
597
598        let sum = a + b;
599        assert!(fp_approx(sum.x, 5.0));
600        assert!(fp_approx(sum.y, 7.0));
601        assert!(fp_approx(sum.z, 9.0));
602
603        let diff = b - a;
604        assert!(fp_approx(diff.x, 3.0));
605        assert!(fp_approx(diff.y, 3.0));
606        assert!(fp_approx(diff.z, 3.0));
607    }
608
609    #[test]
610    fn fpvec3_scalar_mul_div() {
611        let v = FpVec3::new(Fp32::from_f32(2.0), Fp32::from_f32(4.0), Fp32::from_f32(6.0));
612        let scaled = v * Fp32::from_f32(0.5);
613        assert!(fp_approx(scaled.x, 1.0));
614        assert!(fp_approx(scaled.y, 2.0));
615        assert!(fp_approx(scaled.z, 3.0));
616
617        let divided = v / Fp32::from_f32(2.0);
618        assert!(fp_approx(divided.x, 1.0));
619        assert!(fp_approx(divided.y, 2.0));
620        assert!(fp_approx(divided.z, 3.0));
621    }
622
623    // =======================================================================
624    // FpVec3 — Dot / Cross / Length
625    // =======================================================================
626
627    #[test]
628    fn fpvec3_dot() {
629        let a = FpVec3::new(Fp32::from_f32(1.0), Fp32::from_f32(2.0), Fp32::from_f32(3.0));
630        let b = FpVec3::new(Fp32::from_f32(4.0), Fp32::from_f32(5.0), Fp32::from_f32(6.0));
631        // 1*4 + 2*5 + 3*6 = 32
632        assert!(fp_approx(a.dot(b), 32.0));
633    }
634
635    #[test]
636    fn fpvec3_dot_perpendicular_is_zero() {
637        let x = FpVec3::new(Fp32::ONE, Fp32::ZERO, Fp32::ZERO);
638        let y = FpVec3::new(Fp32::ZERO, Fp32::ONE, Fp32::ZERO);
639        assert_eq!(x.dot(y), Fp32::ZERO);
640    }
641
642    #[test]
643    fn fpvec3_cross_basis_vectors() {
644        let x = FpVec3::new(Fp32::ONE, Fp32::ZERO, Fp32::ZERO);
645        let y = FpVec3::new(Fp32::ZERO, Fp32::ONE, Fp32::ZERO);
646        let z = FpVec3::new(Fp32::ZERO, Fp32::ZERO, Fp32::ONE);
647
648        // X × Y = Z
649        let xy = x.cross(y);
650        assert_eq!(xy.x, Fp32::ZERO);
651        assert_eq!(xy.y, Fp32::ZERO);
652        assert_eq!(xy.z, Fp32::ONE);
653
654        // Y × Z = X
655        let yz = y.cross(z);
656        assert_eq!(yz.x, Fp32::ONE);
657        assert_eq!(yz.y, Fp32::ZERO);
658        assert_eq!(yz.z, Fp32::ZERO);
659
660        // Z × X = Y
661        let zx = z.cross(x);
662        assert_eq!(zx.x, Fp32::ZERO);
663        assert_eq!(zx.y, Fp32::ONE);
664        assert_eq!(zx.z, Fp32::ZERO);
665    }
666
667    #[test]
668    fn fpvec3_cross_anti_commutative() {
669        // A × B = -(B × A)
670        let a = FpVec3::new(Fp32::from_f32(1.0), Fp32::from_f32(2.0), Fp32::from_f32(3.0));
671        let b = FpVec3::new(Fp32::from_f32(4.0), Fp32::from_f32(5.0), Fp32::from_f32(6.0));
672        let ab = a.cross(b);
673        let ba = b.cross(a);
674        assert_eq!(ab.x, Fp32::from_raw(-ba.x.0));
675        assert_eq!(ab.y, Fp32::from_raw(-ba.y.0));
676        assert_eq!(ab.z, Fp32::from_raw(-ba.z.0));
677    }
678
679    #[test]
680    fn fpvec3_length_squared() {
681        // (3, 4, 0) → length² = 25
682        let v = FpVec3::new(Fp32::from_f32(3.0), Fp32::from_f32(4.0), Fp32::ZERO);
683        assert!(fp_approx(v.length_squared(), 25.0));
684    }
685
686    #[test]
687    fn fpvec3_length() {
688        // (3, 4, 0) → length = 5
689        let v = FpVec3::new(Fp32::from_f32(3.0), Fp32::from_f32(4.0), Fp32::ZERO);
690        let len = v.length().to_f32();
691        assert!(
692            (len - 5.0).abs() < 0.1,
693            "length of (3,4,0) = {len}, expected ~5.0"
694        );
695    }
696
697    #[test]
698    fn fpvec3_normalize() {
699        let v = FpVec3::new(Fp32::from_f32(3.0), Fp32::from_f32(4.0), Fp32::ZERO);
700        let n = v.normalize();
701        let len = n.length().to_f32();
702        assert!(
703            (len - 1.0).abs() < 0.1,
704            "normalized length = {len}, expected ~1.0"
705        );
706    }
707
708    #[test]
709    fn fpvec3_normalize_zero_vector() {
710        let v = FpVec3::ZERO;
711        let n = v.normalize();
712        assert_eq!(n, FpVec3::ZERO, "normalizing zero vector should return zero");
713    }
714
715    #[test]
716    fn fpvec3_zero_constant() {
717        assert_eq!(FpVec3::ZERO.x, Fp32::ZERO);
718        assert_eq!(FpVec3::ZERO.y, Fp32::ZERO);
719        assert_eq!(FpVec3::ZERO.z, Fp32::ZERO);
720    }
721}