src/cylinder.rs

changeset 38
63318d1b4f00
parent 37
d7cd14b8ccc0
child 39
3d5c8ea1522c
--- a/src/cylinder.rs	Wed Dec 04 23:19:46 2024 -0500
+++ b/src/cylinder.rs	Thu Dec 05 16:50:08 2024 -0500
@@ -2,7 +2,7 @@
 Implementation of the surface of a 3D cylinder as a [`ManifoldPoint`].
 */
 
-use alg_tools::euclidean::Euclidean;
+use alg_tools::euclidean::{Euclidean, Dot};
 use serde_repr::*;
 use serde::{Serialize, Deserialize};
 use alg_tools::loc::Loc;
@@ -46,6 +46,7 @@
     pub angle : Angle
 }
 
+/// Rotate input vector by angle φ.
 #[inline]
 fn rotate(φ : f64, Loc([x, y]) : Loc<f64, 2>) -> Loc<f64, 2> {
     let sin_φ = φ.sin();
@@ -53,6 +54,13 @@
     [cos_φ * x - sin_φ * y, sin_φ * x + cos_φ * y].into()
 }
 
+/// Mirror y coordinate of input vector.
+#[inline]
+fn ymirror(Loc([x, y]) : Loc<f64, 2>) -> Loc<f64, 2> {
+    [x, -y].into()
+}
+
+
 impl CapPoint {
     #[inline]
     /// Convert to cylindrical coordinates given z coordinate
@@ -63,7 +71,7 @@
 
     #[inline]
     /// Convert to cylindrical coordinates given z coordinate
-    fn cartesian_coords(&self) -> Loc<f64, 2> {
+    fn to_cartesian(&self) -> Loc<f64, 2> {
         let CapPoint { r, angle } = *self;
         [r * angle.cos(), r * angle.sin()].into()
     }
@@ -74,14 +82,14 @@
     fn from_cartesian(coords : Loc<f64, 2>) -> Self {
         let [x, y] = coords.into();
         let r = (x*x + y*y).sqrt();
-        let angle = y.atan2(x);
+        let angle = normalise_angle(y.atan2(x));
         CapPoint { r, angle }
     }
 
     #[inline]
     /// Calculate the vector between two points on the cap
     fn log(&self, other : &CapPoint) -> Loc<f64, 2> {
-        other.cartesian_coords() - self.cartesian_coords()
+        other.to_cartesian() - self.to_cartesian()
     }
 
     #[inline]
@@ -89,13 +97,19 @@
     /// Returns the final point within the cap as well as a remaining tangent on
     /// the side of the cylinder, if `t` wasn't fully used.
     fn partial_exp(&self, r : f64, t : &Tangent) -> (CapPoint, Option<Tangent>) {
-        let q = self.cartesian_coords() + t;
-        let n = q.norm2();
-        if n <= r {
-            (Self::from_cartesian(q), None)
+        // |p + s t| <= r
+        // |p|^2 + s^2 |t|^2 + 2s<p,t> = r^2
+        let p = self.to_cartesian();
+        let np2 = p.norm2_squared();
+        let nt2 = t.norm2_squared();
+        let r2 = r * r;
+        let d = p.dot(t);
+        assert!(np2 <= r2);
+        let s = (-d + (d*d + nt2 * (r2 - np2)).sqrt()) / nt2;
+        if s < 1.0 {
+            (Self::from_cartesian(p + s * t), Some((1.0-s) * t))
         } else {
-            let p = q * r / n;
-            (Self::from_cartesian(p), Some(q - p))
+            (Self::from_cartesian(p + t), None)
         }
     }
 
@@ -108,7 +122,7 @@
             rotate(self.angle+f64::PI/2.0, t)
         } else {
             // The angle is such that up would be rotated to self.angle, clockwise
-            rotate(self.angle-f64::PI/2.0, t)
+            rotate(self.angle+f64::PI/2.0, ymirror(t))
         }
     }
 
@@ -120,7 +134,7 @@
             rotate(-self.angle-f64::PI/2.0, t)
         } else {
             // The angle is such that self.angle would be rotated to up, counterclockwise
-            rotate(f64::PI/2.0-self.angle, t)
+            ymirror(rotate(-self.angle-f64::PI/2.0, t))
         }
     }
 }
@@ -709,9 +723,11 @@
                         if side_new.z >= self.top_z() - f64::EPSILON {
                             let cap_new = CapPoint { angle : side_new.angle, r : self.radius };
                             (Point::Top(cap_new), Some(cap_new.tangent_from_side(true, t_new)))
-                        } else {
+                        } else if side_new.z <= self.bottom_z() + f64::EPSILON  {
                             let cap_new = CapPoint { angle : side_new.angle, r : self.radius };
                             (Point::Bottom(cap_new), Some(cap_new.tangent_from_side(false, t_new)))
+                        } else {
+                            (Point::Side(side_new), None)
                         }
                     }
                 }
@@ -850,28 +866,55 @@
 mod tests {
     use super::*;
 
+    static TOL : f64 = 1e-8;
+
+    macro_rules! check_distance {
+        ($distance : expr, $expected : expr) => {
+            assert!(
+                ($distance-$expected).abs() < TOL,
+                "{} = {}, expected = {}",
+                stringify!($distance),
+                $distance,
+                $expected,
+            )
+        }
+    }
+
+    macro_rules! check_vec_eq {
+        ($left : expr, $right : expr) => {
+            let err = ($left-$right).norm(L2);
+            if err > TOL {
+                panic!("{:?} != {:?} [error {err}]", $left, $right);
+            }
+        }
+    }
+
+    macro_rules! check_point_eq {
+        ($left : expr, $right : expr) => {
+            match ($left, $right) {
+                (Point::Top(a), Point::Top(b)) | (Point::Bottom(a), Point::Bottom(b))=> {
+                    if (a.angle-b.angle).abs() > TOL || (a.r-b.r).abs() > TOL {
+                        panic!("{:?} != {:?}", a, b)
+                    }
+                },
+                (Point::Side(a), Point::Side(b)) => {
+                    if (a.angle-b.angle).abs() > TOL || (a.z-b.z).abs() > TOL {
+                        panic!("{:?} != {:?}", a, b)
+                    }
+                },
+                (a, b) => {
+                    panic!("{:?} != {:?}", a, b)
+                }
+            }
+        }
+    }
+
     static CYL : Cylinder = Cylinder {
         radius : 1.0,
         height : 1.0,
         config : CylinderConfig { newton_iters : 20 },
     };
 
-    fn check_distance(distance : f64, expected : f64) {
-        let tol = 1e-10;
-        assert!(
-            (distance-expected).abs() < tol,
-            "distance = {distance}, expected = {expected}"
-        );
-    }
-
-    // fn check_distance_less(distance : f64, expected : f64) {
-    //     let tol = 1e-10;
-    //     assert!(
-    //         distance < expected + tol,
-    //         "distance = {distance}, expected = {expected}"
-    //     );
-    // }
-
     #[test]
     fn intra_cap_log_dist() {
         let π = f64::PI;
@@ -879,9 +922,9 @@
         let p2 = CYL.on_top(π, 0.5);
         let p3 = CYL.on_top(π/2.0, 0.5);
 
-        check_distance(p1.dist_to(&p2), 1.0);
-        check_distance(p2.dist_to(&p3), 0.5_f64.sqrt());
-        check_distance(p3.dist_to(&p1), 0.5_f64.sqrt());
+        check_distance!(p1.dist_to(&p2), 1.0);
+        check_distance!(p2.dist_to(&p3), 0.5_f64.sqrt());
+        check_distance!(p3.dist_to(&p1), 0.5_f64.sqrt());
     }
 
     #[test]
@@ -891,8 +934,8 @@
         let p2 = CYL.on_side(0.0, 0.4);
         let p3 = CYL.on_side(π/2.0, 0.0);
 
-        check_distance(p1.dist_to(&p2), 0.4);
-        check_distance(p1.dist_to(&p3), π/2.0*CYL.radius);
+        check_distance!(p1.dist_to(&p2), 0.4);
+        check_distance!(p1.dist_to(&p3), π/2.0*CYL.radius);
     }
 
     #[test]
@@ -903,7 +946,7 @@
         let p1 = CYL.on_side(0.0, z);
         let p2 = CYL.on_side(π, z);
 
-        check_distance(p1.dist_to(&p2), 2.0 * (CYL.radius + off));
+        check_distance!(p1.dist_to(&p2), 2.0 * (CYL.radius + off));
     }
 
     #[test]
@@ -912,14 +955,14 @@
         let p1 = CYL.on_top(0.0, 0.0);
         let p2 = CYL.on_bottom(0.0, 0.0);
 
-        check_distance(p1.dist_to(&p2), 2.0 * CYL.radius + CYL.height);
+        check_distance!(p1.dist_to(&p2), 2.0 * CYL.radius + CYL.height);
 
         let p1 = CYL.on_top(0.0, CYL.radius / 2.0);
         let p2 = CYL.on_bottom(0.0, CYL.radius / 2.0);
         let p3 = CYL.on_bottom(π, CYL.radius / 2.0);
 
-        check_distance(p1.dist_to(&p2), CYL.radius + CYL.height);
-        check_distance(p1.dist_to(&p3), 2.0 * CYL.radius + CYL.height);
+        check_distance!(p1.dist_to(&p2), CYL.radius + CYL.height);
+        check_distance!(p1.dist_to(&p3), 2.0 * CYL.radius + CYL.height);
     }
 
     #[test]
@@ -927,6 +970,121 @@
         let p1 = CYL.on_top(0.0, 0.0);
         let p2 = CYL.on_side(0.0, 0.0);
 
-        check_distance(p1.dist_to(&p2), CYL.radius + CYL.height / 2.0);
+        check_distance!(p1.dist_to(&p2), CYL.radius + CYL.height / 2.0);
+    }
+
+    #[test]
+    fn cap_side_partial_exp() {
+        let π = f64::PI;
+        let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0];
+
+        for φ in angles {
+            let p = CYL.on_top(φ, CYL.radius / 2.0);
+            let q_target = CYL.on_side(φ, CYL.height / 2.0);
+            let t = rotate(φ, [CYL.radius, 0.0].into());
+            let t_target = Loc([0.0, -CYL.radius / 2.0]);
+            let (q, t_left) = CYL.partial_exp(p.point, t);
+            check_point_eq!(q, q_target.point);
+            if let Some(t_left_) = t_left {
+                check_vec_eq!(t_left_, t_target);
+            } else {
+                panic!("No tangent left");
+            }
+        }
+
+        for φ in angles {
+            let p = CYL.on_top(φ + π/2.0, CYL.radius);
+            let q_target = CYL.on_side(φ, CYL.height / 2.0);
+            let t = 2.0 * rotate(φ, Loc([CYL.radius, -CYL.radius]));
+            let t_target = Loc([-CYL.radius, -CYL.radius]);
+            let (q, t_left) = CYL.partial_exp(p.point, t);
+            check_point_eq!(q, q_target.point);
+            if let Some(t_left_) = t_left {
+                check_vec_eq!(t_left_, t_target);
+            } else {
+                panic!("No tangent left");
+            }
+        }
+    }
+
+    #[test]
+    fn side_top_exp() {
+        let π = f64::PI;
+        let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0];
+
+        for φ in angles {
+            let pt = CYL.on_top(φ, CYL.radius);
+            let t = rotate(φ, Loc([0.1, 0.3]));
+            let b = pt.clone().exp(&t);
+            let a = pt.exp(&(-t));
+            check_point_eq!(a.exp(&(2.0*t)).point, b.point);
+        }
+    }
+
+    /// Tests that tangent “returned” on edge from cap to top, correctly
+    /// sums with a top tangent.
+    #[test]
+    fn cap_side_log_exp() {
+        let π = f64::PI;
+        let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0];
+
+        for top in [true, false] {
+            for (&φ, &ψ) in angles.iter().zip(angles.iter().rev()) {
+                let a = if top {
+                    CYL.on_top(φ, CYL.radius/2.0)
+                } else {
+                    CYL.on_bottom(φ, CYL.radius/2.0)
+                };
+                let b = CYL.on_side(ψ, 0.0);
+                let t = a.log(&b);
+                let (p@Point::Side(side), Some(tpb)) = CYL.partial_exp(a.point, t) else {
+                    panic!("No tangent or point not on side");
+                };
+                let pp = CYL.point_on(p);
+                let tpb2 = pp.log(&b);
+                let tap = a.log(&pp);
+                check_vec_eq!(tpb, tpb2);
+                if top {
+                    assert!(indistinguishable(side.z, CYL.top_z()));
+                } else {
+                    assert!(indistinguishable(side.z, CYL.bottom_z()));
+                }
+                let cap = CapPoint{ angle : side.angle, r : CYL.radius };
+                let tbpcap = cap.tangent_from_side(top, tpb);
+                check_vec_eq!(tap + tbpcap, t);
+            }
+        }
+    }
+
+
+    /// Tests that tangent “returned” on edge from top to side, correctly
+    /// sums with a side tangent.
+    #[test]
+    fn side_cap_log_exp() {
+        let π = f64::PI;
+        let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0];
+
+        for top in [true, false] {
+            for (&φ, &ψ) in angles.iter().zip(angles.iter().rev()) {
+                let a = CYL.on_side(ψ, 0.0);
+                let b = if top {
+                    CYL.on_top(φ, CYL.radius/2.0)
+                } else {
+                    CYL.on_bottom(φ, CYL.radius/2.0)
+                };
+                let t = a.log(&b);
+                let (p, cap, tpb) = match CYL.partial_exp(a.point, t) {
+                    (p@Point::Top(cap), Some(tpb)) if top => (p, cap, tpb),
+                    (p@Point::Bottom(cap), Some(tpb)) if !top => (p, cap, tpb),
+                    _ =>  panic!("No tangent or point not on correct cap"),
+                };
+                let pp = CYL.point_on(p);
+                let tpb2 = pp.log(&b);
+                let tap = a.log(&pp);
+                check_vec_eq!(tpb, tpb2);
+                let tbpside = cap.tangent_to_side(top, tpb);
+                check_vec_eq!(tap + tbpside, t);
+            }
+        }
     }
 }

mercurial