1use core::ops::{Add, AddAssign, Mul, Sub};
24
25#[derive(Debug, Clone, Copy, Default, PartialEq)]
39pub struct Point {
40 pub x: f64,
42 pub y: f64,
44 pub z: f64,
46}
47
48impl Point {
49 #[inline]
51 pub const fn new(x: f64, y: f64, z: f64) -> Self {
52 Self { x, y, z }
53 }
54
55 #[inline]
57 pub const fn new_2d(x: f64, y: f64) -> Self {
58 Self { x, y, z: 0.0 }
59 }
60
61 #[inline]
63 pub const fn origin() -> Self {
64 Self {
65 x: 0.0,
66 y: 0.0,
67 z: 0.0,
68 }
69 }
70}
71
72impl Add<Vector> for Point {
73 type Output = Point;
74
75 #[inline]
76 fn add(self, v: Vector) -> Point {
77 Point {
78 x: self.x + v.x,
79 y: self.y + v.y,
80 z: self.z + v.z,
81 }
82 }
83}
84
85impl AddAssign<Vector> for Point {
86 #[inline]
87 fn add_assign(&mut self, v: Vector) {
88 self.x += v.x;
89 self.y += v.y;
90 self.z += v.z;
91 }
92}
93
94impl Sub for Point {
95 type Output = Vector;
96
97 #[inline]
98 fn sub(self, other: Point) -> Vector {
99 Vector {
100 x: self.x - other.x,
101 y: self.y - other.y,
102 z: self.z - other.z,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, Default, PartialEq)]
123pub struct Vector {
124 pub x: f64,
126 pub y: f64,
128 pub z: f64,
130}
131
132impl Vector {
133 #[inline]
135 pub const fn new(x: f64, y: f64, z: f64) -> Self {
136 Self { x, y, z }
137 }
138
139 #[inline]
141 pub const fn new_2d(x: f64, y: f64) -> Self {
142 Self { x, y, z: 0.0 }
143 }
144
145 #[inline]
147 pub const fn zero() -> Self {
148 Self {
149 x: 0.0,
150 y: 0.0,
151 z: 0.0,
152 }
153 }
154
155 #[inline]
157 pub fn magnitude(&self) -> f64 {
158 sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
159 }
160
161 #[inline]
163 pub fn normalized(&self) -> Self {
164 let mag = self.magnitude();
165 if mag == 0.0 {
166 return *self;
167 }
168 Self {
169 x: self.x / mag,
170 y: self.y / mag,
171 z: self.z / mag,
172 }
173 }
174}
175
176#[cfg(feature = "std")]
177#[inline]
178fn sqrt(x: f64) -> f64 {
179 x.sqrt()
180}
181
182#[cfg(not(feature = "std"))]
183#[inline]
184fn sqrt(x: f64) -> f64 {
185 libm::sqrt(x)
186}
187
188impl Add for Vector {
189 type Output = Vector;
190
191 #[inline]
192 fn add(self, other: Vector) -> Vector {
193 Vector {
194 x: self.x + other.x,
195 y: self.y + other.y,
196 z: self.z + other.z,
197 }
198 }
199}
200
201impl AddAssign for Vector {
202 #[inline]
203 fn add_assign(&mut self, other: Vector) {
204 self.x += other.x;
205 self.y += other.y;
206 self.z += other.z;
207 }
208}
209
210impl Sub for Vector {
211 type Output = Vector;
212
213 #[inline]
214 fn sub(self, other: Vector) -> Vector {
215 Vector {
216 x: self.x - other.x,
217 y: self.y - other.y,
218 z: self.z - other.z,
219 }
220 }
221}
222
223impl Mul<f64> for Vector {
224 type Output = Vector;
225
226 #[inline]
227 fn mul(self, scalar: f64) -> Vector {
228 Vector {
229 x: self.x * scalar,
230 y: self.y * scalar,
231 z: self.z * scalar,
232 }
233 }
234}
235
236impl Mul<Vector> for f64 {
237 type Output = Vector;
238
239 #[inline]
240 fn mul(self, v: Vector) -> Vector {
241 v * self
242 }
243}
244
245pub const GRAVITY: Vector = Vector {
259 x: 0.0,
260 y: -9.81,
261 z: 0.0,
262};
263
264pub const TERMINAL_GRAVITY: Vector = Vector {
271 x: 0.0,
272 y: 9.81,
273 z: 0.0,
274};
275
276#[derive(Debug, Clone, Copy, PartialEq)]
301pub struct Projectile {
302 pos: Point,
303 vel: Vector,
304 acc: Vector,
305 delta_time: f64,
306}
307
308impl Projectile {
309 #[inline]
331 pub const fn new(
332 delta_time: f64,
333 position: Point,
334 velocity: Vector,
335 acceleration: Vector,
336 ) -> Self {
337 Self {
338 pos: position,
339 vel: velocity,
340 acc: acceleration,
341 delta_time,
342 }
343 }
344
345 #[inline]
365 pub fn update(&mut self) -> Point {
366 self.pos.x += self.vel.x * self.delta_time;
370 self.pos.y += self.vel.y * self.delta_time;
371 self.pos.z += self.vel.z * self.delta_time;
372
373 self.vel.x += self.acc.x * self.delta_time;
375 self.vel.y += self.acc.y * self.delta_time;
376 self.vel.z += self.acc.z * self.delta_time;
377
378 self.pos
379 }
380
381 #[inline]
383 pub const fn position(&self) -> Point {
384 self.pos
385 }
386
387 #[inline]
389 pub const fn velocity(&self) -> Vector {
390 self.vel
391 }
392
393 #[inline]
395 pub const fn acceleration(&self) -> Vector {
396 self.acc
397 }
398
399 #[inline]
401 pub fn set_position(&mut self, pos: Point) {
402 self.pos = pos;
403 }
404
405 #[inline]
407 pub fn set_velocity(&mut self, vel: Vector) {
408 self.vel = vel;
409 }
410
411 #[inline]
413 pub fn set_acceleration(&mut self, acc: Vector) {
414 self.acc = acc;
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::fps;
422
423 const TOLERANCE: f64 = 1e-10;
424
425 fn approx_eq(a: f64, b: f64) -> bool {
426 (a - b).abs() < TOLERANCE
427 }
428
429 #[test]
430 fn test_point_new() {
431 let p = Point::new(1.0, 2.0, 3.0);
432 assert!(approx_eq(p.x, 1.0));
433 assert!(approx_eq(p.y, 2.0));
434 assert!(approx_eq(p.z, 3.0));
435 }
436
437 #[test]
438 fn test_point_2d() {
439 let p = Point::new_2d(1.0, 2.0);
440 assert!(approx_eq(p.z, 0.0));
441 }
442
443 #[test]
444 fn test_point_add_vector() {
445 let p = Point::new(1.0, 2.0, 3.0);
446 let v = Vector::new(4.0, 5.0, 6.0);
447 let result = p + v;
448
449 assert!(approx_eq(result.x, 5.0));
450 assert!(approx_eq(result.y, 7.0));
451 assert!(approx_eq(result.z, 9.0));
452 }
453
454 #[test]
455 fn test_point_sub_point() {
456 let p1 = Point::new(5.0, 7.0, 9.0);
457 let p2 = Point::new(1.0, 2.0, 3.0);
458 let v = p1 - p2;
459
460 assert!(approx_eq(v.x, 4.0));
461 assert!(approx_eq(v.y, 5.0));
462 assert!(approx_eq(v.z, 6.0));
463 }
464
465 #[test]
466 fn test_vector_mul_scalar() {
467 let v = Vector::new(1.0, 2.0, 3.0);
468 let scaled = v * 2.0;
469
470 assert!(approx_eq(scaled.x, 2.0));
471 assert!(approx_eq(scaled.y, 4.0));
472 assert!(approx_eq(scaled.z, 6.0));
473 }
474
475 #[test]
476 fn test_scalar_mul_vector() {
477 let v = Vector::new(1.0, 2.0, 3.0);
478 let scaled = 2.0 * v;
479
480 assert!(approx_eq(scaled.x, 2.0));
481 assert!(approx_eq(scaled.y, 4.0));
482 assert!(approx_eq(scaled.z, 6.0));
483 }
484
485 #[test]
486 fn test_vector_magnitude() {
487 let v = Vector::new(3.0, 4.0, 0.0);
488 assert!(approx_eq(v.magnitude(), 5.0));
489 }
490
491 #[test]
492 fn test_vector_normalized() {
493 let v = Vector::new(3.0, 4.0, 0.0);
494 let n = v.normalized();
495
496 assert!(approx_eq(n.magnitude(), 1.0));
497 assert!(approx_eq(n.x, 0.6));
498 assert!(approx_eq(n.y, 0.8));
499 }
500
501 #[test]
502 fn test_gravity_constants() {
503 assert!(approx_eq(GRAVITY.y, -9.81));
504 assert!(approx_eq(TERMINAL_GRAVITY.y, 9.81));
505 }
506
507 #[test]
508 fn test_projectile_constant_velocity() {
509 let dt = fps(60);
510 let mut p = Projectile::new(
511 dt,
512 Point::origin(),
513 Vector::new(60.0, 0.0, 0.0), Vector::zero(),
515 );
516
517 for _ in 0..60 {
519 p.update();
520 }
521
522 assert!(
524 (p.position().x - 60.0).abs() < 0.1,
525 "Expected x ≈ 60, got {}",
526 p.position().x
527 );
528 }
529
530 #[test]
531 fn test_projectile_with_gravity() {
532 let dt = fps(60);
533 let mut p = Projectile::new(
534 dt,
535 Point::new(0.0, 100.0, 0.0),
536 Vector::zero(),
537 TERMINAL_GRAVITY,
538 );
539
540 for _ in 0..60 {
542 p.update();
543 }
544
545 assert!(p.position().y > 100.0, "Should have fallen (y increased)");
547 }
548
549 #[test]
550 fn test_projectile_parabolic_motion() {
551 let dt = fps(60);
552 let mut p = Projectile::new(
553 dt,
554 Point::origin(),
555 Vector::new(10.0, -10.0, 0.0), TERMINAL_GRAVITY,
557 );
558
559 let mut max_height = f64::MAX;
560
561 for _ in 0..120 {
563 p.update();
564 if p.position().y < max_height {
565 max_height = p.position().y;
566 }
567 }
568
569 assert!(
571 max_height < 0.0,
572 "Should have gone up (negative y in terminal coords)"
573 );
574 }
575
576 #[test]
577 fn test_projectile_accessors() {
578 let p = Projectile::new(
579 fps(60),
580 Point::new(1.0, 2.0, 3.0),
581 Vector::new(4.0, 5.0, 6.0),
582 Vector::new(7.0, 8.0, 9.0),
583 );
584
585 assert_eq!(p.position(), Point::new(1.0, 2.0, 3.0));
586 assert_eq!(p.velocity(), Vector::new(4.0, 5.0, 6.0));
587 assert_eq!(p.acceleration(), Vector::new(7.0, 8.0, 9.0));
588 }
589
590 #[test]
595 fn test_projectile_setters() {
596 let mut p = Projectile::new(fps(60), Point::origin(), Vector::zero(), Vector::zero());
597
598 p.set_position(Point::new(10.0, 20.0, 30.0));
599 assert_eq!(p.position(), Point::new(10.0, 20.0, 30.0));
600
601 p.set_velocity(Vector::new(1.0, 2.0, 3.0));
602 assert_eq!(p.velocity(), Vector::new(1.0, 2.0, 3.0));
603
604 p.set_acceleration(Vector::new(0.0, -9.81, 0.0));
605 assert_eq!(p.acceleration(), Vector::new(0.0, -9.81, 0.0));
606 }
607
608 #[test]
609 fn test_vector_add() {
610 let v1 = Vector::new(1.0, 2.0, 3.0);
611 let v2 = Vector::new(4.0, 5.0, 6.0);
612 let result = v1 + v2;
613
614 assert!(approx_eq(result.x, 5.0));
615 assert!(approx_eq(result.y, 7.0));
616 assert!(approx_eq(result.z, 9.0));
617 }
618
619 #[test]
620 fn test_vector_sub() {
621 let v1 = Vector::new(5.0, 7.0, 9.0);
622 let v2 = Vector::new(1.0, 2.0, 3.0);
623 let result = v1 - v2;
624
625 assert!(approx_eq(result.x, 4.0));
626 assert!(approx_eq(result.y, 5.0));
627 assert!(approx_eq(result.z, 6.0));
628 }
629
630 #[test]
631 fn test_vector_zero() {
632 let v = Vector::zero();
633 assert!(approx_eq(v.x, 0.0));
634 assert!(approx_eq(v.y, 0.0));
635 assert!(approx_eq(v.z, 0.0));
636 }
637
638 #[test]
639 fn test_point_origin() {
640 let p = Point::origin();
641 assert!(approx_eq(p.x, 0.0));
642 assert!(approx_eq(p.y, 0.0));
643 assert!(approx_eq(p.z, 0.0));
644 }
645
646 #[test]
647 fn test_vector_add_assign() {
648 let mut v1 = Vector::new(1.0, 2.0, 3.0);
649 v1 += Vector::new(4.0, 5.0, 6.0);
650
651 assert!(approx_eq(v1.x, 5.0));
652 assert!(approx_eq(v1.y, 7.0));
653 assert!(approx_eq(v1.z, 9.0));
654 }
655
656 #[test]
657 fn test_point_add_assign() {
658 let mut p = Point::new(1.0, 2.0, 3.0);
659 p += Vector::new(4.0, 5.0, 6.0);
660
661 assert!(approx_eq(p.x, 5.0));
662 assert!(approx_eq(p.y, 7.0));
663 assert!(approx_eq(p.z, 9.0));
664 }
665
666 #[test]
667 fn test_vector_normalized_zero() {
668 let v = Vector::zero();
670 let n = v.normalized();
671
672 assert!(approx_eq(n.x, 0.0));
673 assert!(approx_eq(n.y, 0.0));
674 assert!(approx_eq(n.z, 0.0));
675 }
676
677 #[test]
678 fn test_vector_2d() {
679 let v = Vector::new_2d(3.0, 4.0);
680 assert!(approx_eq(v.z, 0.0));
681 assert!(approx_eq(v.magnitude(), 5.0));
682 }
683
684 #[test]
685 fn test_point_default() {
686 let p = Point::default();
687 assert!(approx_eq(p.x, 0.0));
688 assert!(approx_eq(p.y, 0.0));
689 assert!(approx_eq(p.z, 0.0));
690 }
691
692 #[test]
693 fn test_vector_default() {
694 let v = Vector::default();
695 assert!(approx_eq(v.x, 0.0));
696 assert!(approx_eq(v.y, 0.0));
697 assert!(approx_eq(v.z, 0.0));
698 }
699
700 #[test]
701 fn test_projectile_3d_motion() {
702 let dt = fps(60);
704 let mut p = Projectile::new(
705 dt,
706 Point::origin(),
707 Vector::new(10.0, 10.0, 10.0),
708 Vector::new(0.0, 0.0, 0.0),
709 );
710
711 for _ in 0..60 {
713 p.update();
714 }
715
716 let pos = p.position();
717 assert!((pos.x - 10.0).abs() < 0.2);
718 assert!((pos.y - 10.0).abs() < 0.2);
719 assert!((pos.z - 10.0).abs() < 0.2);
720 }
721
722 #[test]
723 fn test_projectile_zero_delta_time() {
724 let mut p = Projectile::new(
726 0.0,
727 Point::new(1.0, 2.0, 3.0),
728 Vector::new(100.0, 100.0, 100.0),
729 GRAVITY,
730 );
731
732 p.update();
733
734 assert!(approx_eq(p.position().x, 1.0));
736 assert!(approx_eq(p.position().y, 2.0));
737 assert!(approx_eq(p.position().z, 3.0));
738 }
739
740 #[test]
741 fn test_projectile_is_copy() {
742 let p1 = Projectile::new(
743 fps(60),
744 Point::origin(),
745 Vector::new(1.0, 0.0, 0.0),
746 Vector::zero(),
747 );
748 let mut p2 = p1; p2.update();
752 assert!(approx_eq(p1.position().x, 0.0));
753 assert!(p2.position().x > 0.0);
754 }
755
756 #[test]
757 fn test_gravity_acceleration() {
758 let dt = fps(60);
759 let mut p = Projectile::new(dt, Point::new(0.0, 100.0, 0.0), Vector::zero(), GRAVITY);
760
761 for _ in 0..60 {
763 p.update();
764 }
765
766 assert!(
768 (p.velocity().y - GRAVITY.y).abs() < 0.2,
769 "Velocity should be ~-9.81, got {}",
770 p.velocity().y
771 );
772 }
773
774 #[test]
775 #[allow(clippy::assertions_on_constants)]
776 fn test_terminal_gravity_direction() {
777 assert!(TERMINAL_GRAVITY.y > 0.0);
779 assert!(TERMINAL_GRAVITY.x == 0.0);
780 assert!(TERMINAL_GRAVITY.z == 0.0);
781 }
782
783 #[test]
784 #[allow(clippy::assertions_on_constants)]
785 fn test_standard_gravity_direction() {
786 assert!(GRAVITY.y < 0.0);
788 assert!(GRAVITY.x == 0.0);
789 assert!(GRAVITY.z == 0.0);
790 }
791}