44 pub struct CapPoint { |
44 pub struct CapPoint { |
45 pub r : f64, |
45 pub r : f64, |
46 pub angle : Angle |
46 pub angle : Angle |
47 } |
47 } |
48 |
48 |
|
49 /// Rotate input vector by angle φ. |
49 #[inline] |
50 #[inline] |
50 fn rotate(φ : f64, Loc([x, y]) : Loc<f64, 2>) -> Loc<f64, 2> { |
51 fn rotate(φ : f64, Loc([x, y]) : Loc<f64, 2>) -> Loc<f64, 2> { |
51 let sin_φ = φ.sin(); |
52 let sin_φ = φ.sin(); |
52 let cos_φ = φ.cos(); |
53 let cos_φ = φ.cos(); |
53 [cos_φ * x - sin_φ * y, sin_φ * x + cos_φ * y].into() |
54 [cos_φ * x - sin_φ * y, sin_φ * x + cos_φ * y].into() |
54 } |
55 } |
55 |
56 |
|
57 /// Mirror y coordinate of input vector. |
|
58 #[inline] |
|
59 fn ymirror(Loc([x, y]) : Loc<f64, 2>) -> Loc<f64, 2> { |
|
60 [x, -y].into() |
|
61 } |
|
62 |
|
63 |
56 impl CapPoint { |
64 impl CapPoint { |
57 #[inline] |
65 #[inline] |
58 /// Convert to cylindrical coordinates given z coordinate |
66 /// Convert to cylindrical coordinates given z coordinate |
59 fn cyl_coords(&self, z : f64) -> CylCoords { |
67 fn cyl_coords(&self, z : f64) -> CylCoords { |
60 let CapPoint { r, angle } = *self; |
68 let CapPoint { r, angle } = *self; |
61 CylCoords { r, angle, z } |
69 CylCoords { r, angle, z } |
62 } |
70 } |
63 |
71 |
64 #[inline] |
72 #[inline] |
65 /// Convert to cylindrical coordinates given z coordinate |
73 /// Convert to cylindrical coordinates given z coordinate |
66 fn cartesian_coords(&self) -> Loc<f64, 2> { |
74 fn to_cartesian(&self) -> Loc<f64, 2> { |
67 let CapPoint { r, angle } = *self; |
75 let CapPoint { r, angle } = *self; |
68 [r * angle.cos(), r * angle.sin()].into() |
76 [r * angle.cos(), r * angle.sin()].into() |
69 } |
77 } |
70 |
78 |
71 #[inline] |
79 #[inline] |
72 #[allow(dead_code)] |
80 #[allow(dead_code)] |
73 /// Convert to cylindrical coordinates given z coordinate |
81 /// Convert to cylindrical coordinates given z coordinate |
74 fn from_cartesian(coords : Loc<f64, 2>) -> Self { |
82 fn from_cartesian(coords : Loc<f64, 2>) -> Self { |
75 let [x, y] = coords.into(); |
83 let [x, y] = coords.into(); |
76 let r = (x*x + y*y).sqrt(); |
84 let r = (x*x + y*y).sqrt(); |
77 let angle = y.atan2(x); |
85 let angle = normalise_angle(y.atan2(x)); |
78 CapPoint { r, angle } |
86 CapPoint { r, angle } |
79 } |
87 } |
80 |
88 |
81 #[inline] |
89 #[inline] |
82 /// Calculate the vector between two points on the cap |
90 /// Calculate the vector between two points on the cap |
83 fn log(&self, other : &CapPoint) -> Loc<f64, 2> { |
91 fn log(&self, other : &CapPoint) -> Loc<f64, 2> { |
84 other.cartesian_coords() - self.cartesian_coords() |
92 other.to_cartesian() - self.to_cartesian() |
85 } |
93 } |
86 |
94 |
87 #[inline] |
95 #[inline] |
88 /// Calculate partial exponential map until boundary. |
96 /// Calculate partial exponential map until boundary. |
89 /// Returns the final point within the cap as well as a remaining tangent on |
97 /// Returns the final point within the cap as well as a remaining tangent on |
90 /// the side of the cylinder, if `t` wasn't fully used. |
98 /// the side of the cylinder, if `t` wasn't fully used. |
91 fn partial_exp(&self, r : f64, t : &Tangent) -> (CapPoint, Option<Tangent>) { |
99 fn partial_exp(&self, r : f64, t : &Tangent) -> (CapPoint, Option<Tangent>) { |
92 let q = self.cartesian_coords() + t; |
100 // |p + s t| <= r |
93 let n = q.norm2(); |
101 // |p|^2 + s^2 |t|^2 + 2s<p,t> = r^2 |
94 if n <= r { |
102 let p = self.to_cartesian(); |
95 (Self::from_cartesian(q), None) |
103 let np2 = p.norm2_squared(); |
96 } else { |
104 let nt2 = t.norm2_squared(); |
97 let p = q * r / n; |
105 let r2 = r * r; |
98 (Self::from_cartesian(p), Some(q - p)) |
106 let d = p.dot(t); |
|
107 assert!(np2 <= r2); |
|
108 let s = (-d + (d*d + nt2 * (r2 - np2)).sqrt()) / nt2; |
|
109 if s < 1.0 { |
|
110 (Self::from_cartesian(p + s * t), Some((1.0-s) * t)) |
|
111 } else { |
|
112 (Self::from_cartesian(p + t), None) |
99 } |
113 } |
100 } |
114 } |
101 |
115 |
102 #[inline] |
116 #[inline] |
103 /// Convert tangent from side tangent to cap tangent |
117 /// Convert tangent from side tangent to cap tangent |
848 |
864 |
849 #[cfg(test)] |
865 #[cfg(test)] |
850 mod tests { |
866 mod tests { |
851 use super::*; |
867 use super::*; |
852 |
868 |
|
869 static TOL : f64 = 1e-8; |
|
870 |
|
871 macro_rules! check_distance { |
|
872 ($distance : expr, $expected : expr) => { |
|
873 assert!( |
|
874 ($distance-$expected).abs() < TOL, |
|
875 "{} = {}, expected = {}", |
|
876 stringify!($distance), |
|
877 $distance, |
|
878 $expected, |
|
879 ) |
|
880 } |
|
881 } |
|
882 |
|
883 macro_rules! check_vec_eq { |
|
884 ($left : expr, $right : expr) => { |
|
885 let err = ($left-$right).norm(L2); |
|
886 if err > TOL { |
|
887 panic!("{:?} != {:?} [error {err}]", $left, $right); |
|
888 } |
|
889 } |
|
890 } |
|
891 |
|
892 macro_rules! check_point_eq { |
|
893 ($left : expr, $right : expr) => { |
|
894 match ($left, $right) { |
|
895 (Point::Top(a), Point::Top(b)) | (Point::Bottom(a), Point::Bottom(b))=> { |
|
896 if (a.angle-b.angle).abs() > TOL || (a.r-b.r).abs() > TOL { |
|
897 panic!("{:?} != {:?}", a, b) |
|
898 } |
|
899 }, |
|
900 (Point::Side(a), Point::Side(b)) => { |
|
901 if (a.angle-b.angle).abs() > TOL || (a.z-b.z).abs() > TOL { |
|
902 panic!("{:?} != {:?}", a, b) |
|
903 } |
|
904 }, |
|
905 (a, b) => { |
|
906 panic!("{:?} != {:?}", a, b) |
|
907 } |
|
908 } |
|
909 } |
|
910 } |
|
911 |
853 static CYL : Cylinder = Cylinder { |
912 static CYL : Cylinder = Cylinder { |
854 radius : 1.0, |
913 radius : 1.0, |
855 height : 1.0, |
914 height : 1.0, |
856 config : CylinderConfig { newton_iters : 20 }, |
915 config : CylinderConfig { newton_iters : 20 }, |
857 }; |
916 }; |
858 |
|
859 fn check_distance(distance : f64, expected : f64) { |
|
860 let tol = 1e-10; |
|
861 assert!( |
|
862 (distance-expected).abs() < tol, |
|
863 "distance = {distance}, expected = {expected}" |
|
864 ); |
|
865 } |
|
866 |
|
867 // fn check_distance_less(distance : f64, expected : f64) { |
|
868 // let tol = 1e-10; |
|
869 // assert!( |
|
870 // distance < expected + tol, |
|
871 // "distance = {distance}, expected = {expected}" |
|
872 // ); |
|
873 // } |
|
874 |
917 |
875 #[test] |
918 #[test] |
876 fn intra_cap_log_dist() { |
919 fn intra_cap_log_dist() { |
877 let π = f64::PI; |
920 let π = f64::PI; |
878 let p1 = CYL.on_top(0.0, 0.5); |
921 let p1 = CYL.on_top(0.0, 0.5); |
879 let p2 = CYL.on_top(π, 0.5); |
922 let p2 = CYL.on_top(π, 0.5); |
880 let p3 = CYL.on_top(π/2.0, 0.5); |
923 let p3 = CYL.on_top(π/2.0, 0.5); |
881 |
924 |
882 check_distance(p1.dist_to(&p2), 1.0); |
925 check_distance!(p1.dist_to(&p2), 1.0); |
883 check_distance(p2.dist_to(&p3), 0.5_f64.sqrt()); |
926 check_distance!(p2.dist_to(&p3), 0.5_f64.sqrt()); |
884 check_distance(p3.dist_to(&p1), 0.5_f64.sqrt()); |
927 check_distance!(p3.dist_to(&p1), 0.5_f64.sqrt()); |
885 } |
928 } |
886 |
929 |
887 #[test] |
930 #[test] |
888 fn intra_side_log_dist() { |
931 fn intra_side_log_dist() { |
889 let π = f64::PI; |
932 let π = f64::PI; |
890 let p1 = CYL.on_side(0.0, 0.0); |
933 let p1 = CYL.on_side(0.0, 0.0); |
891 let p2 = CYL.on_side(0.0, 0.4); |
934 let p2 = CYL.on_side(0.0, 0.4); |
892 let p3 = CYL.on_side(π/2.0, 0.0); |
935 let p3 = CYL.on_side(π/2.0, 0.0); |
893 |
936 |
894 check_distance(p1.dist_to(&p2), 0.4); |
937 check_distance!(p1.dist_to(&p2), 0.4); |
895 check_distance(p1.dist_to(&p3), π/2.0*CYL.radius); |
938 check_distance!(p1.dist_to(&p3), π/2.0*CYL.radius); |
896 } |
939 } |
897 |
940 |
898 #[test] |
941 #[test] |
899 fn intra_side_over_cap_log_dist() { |
942 fn intra_side_over_cap_log_dist() { |
900 let π = f64::PI; |
943 let π = f64::PI; |
901 let off = 0.05; |
944 let off = 0.05; |
902 let z = CYL.top_z() - off; |
945 let z = CYL.top_z() - off; |
903 let p1 = CYL.on_side(0.0, z); |
946 let p1 = CYL.on_side(0.0, z); |
904 let p2 = CYL.on_side(π, z); |
947 let p2 = CYL.on_side(π, z); |
905 |
948 |
906 check_distance(p1.dist_to(&p2), 2.0 * (CYL.radius + off)); |
949 check_distance!(p1.dist_to(&p2), 2.0 * (CYL.radius + off)); |
907 } |
950 } |
908 |
951 |
909 #[test] |
952 #[test] |
910 fn top_bottom_log_dist() { |
953 fn top_bottom_log_dist() { |
911 let π = f64::PI; |
954 let π = f64::PI; |
912 let p1 = CYL.on_top(0.0, 0.0); |
955 let p1 = CYL.on_top(0.0, 0.0); |
913 let p2 = CYL.on_bottom(0.0, 0.0); |
956 let p2 = CYL.on_bottom(0.0, 0.0); |
914 |
957 |
915 check_distance(p1.dist_to(&p2), 2.0 * CYL.radius + CYL.height); |
958 check_distance!(p1.dist_to(&p2), 2.0 * CYL.radius + CYL.height); |
916 |
959 |
917 let p1 = CYL.on_top(0.0, CYL.radius / 2.0); |
960 let p1 = CYL.on_top(0.0, CYL.radius / 2.0); |
918 let p2 = CYL.on_bottom(0.0, CYL.radius / 2.0); |
961 let p2 = CYL.on_bottom(0.0, CYL.radius / 2.0); |
919 let p3 = CYL.on_bottom(π, CYL.radius / 2.0); |
962 let p3 = CYL.on_bottom(π, CYL.radius / 2.0); |
920 |
963 |
921 check_distance(p1.dist_to(&p2), CYL.radius + CYL.height); |
964 check_distance!(p1.dist_to(&p2), CYL.radius + CYL.height); |
922 check_distance(p1.dist_to(&p3), 2.0 * CYL.radius + CYL.height); |
965 check_distance!(p1.dist_to(&p3), 2.0 * CYL.radius + CYL.height); |
923 } |
966 } |
924 |
967 |
925 #[test] |
968 #[test] |
926 fn top_side_log_dist() { |
969 fn top_side_log_dist() { |
927 let p1 = CYL.on_top(0.0, 0.0); |
970 let p1 = CYL.on_top(0.0, 0.0); |
928 let p2 = CYL.on_side(0.0, 0.0); |
971 let p2 = CYL.on_side(0.0, 0.0); |
929 |
972 |
930 check_distance(p1.dist_to(&p2), CYL.radius + CYL.height / 2.0); |
973 check_distance!(p1.dist_to(&p2), CYL.radius + CYL.height / 2.0); |
931 } |
974 } |
932 } |
975 |
|
976 #[test] |
|
977 fn cap_side_partial_exp() { |
|
978 let π = f64::PI; |
|
979 let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0]; |
|
980 |
|
981 for φ in angles { |
|
982 let p = CYL.on_top(φ, CYL.radius / 2.0); |
|
983 let q_target = CYL.on_side(φ, CYL.height / 2.0); |
|
984 let t = rotate(φ, [CYL.radius, 0.0].into()); |
|
985 let t_target = Loc([0.0, -CYL.radius / 2.0]); |
|
986 let (q, t_left) = CYL.partial_exp(p.point, t); |
|
987 check_point_eq!(q, q_target.point); |
|
988 if let Some(t_left_) = t_left { |
|
989 check_vec_eq!(t_left_, t_target); |
|
990 } else { |
|
991 panic!("No tangent left"); |
|
992 } |
|
993 } |
|
994 |
|
995 for φ in angles { |
|
996 let p = CYL.on_top(φ + π/2.0, CYL.radius); |
|
997 let q_target = CYL.on_side(φ, CYL.height / 2.0); |
|
998 let t = 2.0 * rotate(φ, Loc([CYL.radius, -CYL.radius])); |
|
999 let t_target = Loc([-CYL.radius, -CYL.radius]); |
|
1000 let (q, t_left) = CYL.partial_exp(p.point, t); |
|
1001 check_point_eq!(q, q_target.point); |
|
1002 if let Some(t_left_) = t_left { |
|
1003 check_vec_eq!(t_left_, t_target); |
|
1004 } else { |
|
1005 panic!("No tangent left"); |
|
1006 } |
|
1007 } |
|
1008 } |
|
1009 |
|
1010 #[test] |
|
1011 fn side_top_exp() { |
|
1012 let π = f64::PI; |
|
1013 let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0]; |
|
1014 |
|
1015 for φ in angles { |
|
1016 let pt = CYL.on_top(φ, CYL.radius); |
|
1017 let t = rotate(φ, Loc([0.1, 0.3])); |
|
1018 let b = pt.clone().exp(&t); |
|
1019 let a = pt.exp(&(-t)); |
|
1020 check_point_eq!(a.exp(&(2.0*t)).point, b.point); |
|
1021 } |
|
1022 } |
|
1023 |
|
1024 /// Tests that tangent “returned” on edge from cap to top, correctly |
|
1025 /// sums with a top tangent. |
|
1026 #[test] |
|
1027 fn cap_side_log_exp() { |
|
1028 let π = f64::PI; |
|
1029 let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0]; |
|
1030 |
|
1031 for top in [true, false] { |
|
1032 for (&φ, &ψ) in angles.iter().zip(angles.iter().rev()) { |
|
1033 let a = if top { |
|
1034 CYL.on_top(φ, CYL.radius/2.0) |
|
1035 } else { |
|
1036 CYL.on_bottom(φ, CYL.radius/2.0) |
|
1037 }; |
|
1038 let b = CYL.on_side(ψ, 0.0); |
|
1039 let t = a.log(&b); |
|
1040 let (p@Point::Side(side), Some(tpb)) = CYL.partial_exp(a.point, t) else { |
|
1041 panic!("No tangent or point not on side"); |
|
1042 }; |
|
1043 let pp = CYL.point_on(p); |
|
1044 let tpb2 = pp.log(&b); |
|
1045 let tap = a.log(&pp); |
|
1046 check_vec_eq!(tpb, tpb2); |
|
1047 if top { |
|
1048 assert!(indistinguishable(side.z, CYL.top_z())); |
|
1049 } else { |
|
1050 assert!(indistinguishable(side.z, CYL.bottom_z())); |
|
1051 } |
|
1052 let cap = CapPoint{ angle : side.angle, r : CYL.radius }; |
|
1053 let tbpcap = cap.tangent_from_side(top, tpb); |
|
1054 check_vec_eq!(tap + tbpcap, t); |
|
1055 } |
|
1056 } |
|
1057 } |
|
1058 |
|
1059 |
|
1060 /// Tests that tangent “returned” on edge from top to side, correctly |
|
1061 /// sums with a side tangent. |
|
1062 #[test] |
|
1063 fn side_cap_log_exp() { |
|
1064 let π = f64::PI; |
|
1065 let angles = [0.0, π/2.0, π*5.0/6.0, π*7.0/5.0]; |
|
1066 |
|
1067 for top in [true, false] { |
|
1068 for (&φ, &ψ) in angles.iter().zip(angles.iter().rev()) { |
|
1069 let a = CYL.on_side(ψ, 0.0); |
|
1070 let b = if top { |
|
1071 CYL.on_top(φ, CYL.radius/2.0) |
|
1072 } else { |
|
1073 CYL.on_bottom(φ, CYL.radius/2.0) |
|
1074 }; |
|
1075 let t = a.log(&b); |
|
1076 let (p, cap, tpb) = match CYL.partial_exp(a.point, t) { |
|
1077 (p@Point::Top(cap), Some(tpb)) if top => (p, cap, tpb), |
|
1078 (p@Point::Bottom(cap), Some(tpb)) if !top => (p, cap, tpb), |
|
1079 _ => panic!("No tangent or point not on correct cap"), |
|
1080 }; |
|
1081 let pp = CYL.point_on(p); |
|
1082 let tpb2 = pp.log(&b); |
|
1083 let tap = a.log(&pp); |
|
1084 check_vec_eq!(tpb, tpb2); |
|
1085 let tbpside = cap.tangent_to_side(top, tpb); |
|
1086 check_vec_eq!(tap + tbpside, t); |
|
1087 } |
|
1088 } |
|
1089 } |
|
1090 } |