Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

1use bone_kernel::{Arc3, Circle3, Curve3Kind, IntersectionSet3, Line3, intersect_curves_3}; 2use bone_types::{Angle, Length, Plane3, Point3, Tolerance, UnitVec3}; 3use core::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI}; 4use uom::si::angle::radian; 5use uom::si::length::millimeter; 6 7const TOL: Tolerance = Tolerance::new(1e-9); 8 9fn mm(value: f64) -> Length { 10 Length::new::<millimeter>(value) 11} 12 13fn xy_plane(origin: Point3) -> Plane3 { 14 Plane3::new_unchecked(origin, UnitVec3::x_axis(), UnitVec3::y_axis()) 15} 16 17fn xz_plane(origin: Point3) -> Plane3 { 18 Plane3::new_unchecked(origin, UnitVec3::x_axis(), UnitVec3::z_axis()) 19} 20 21fn line(start: Point3, end: Point3) -> Curve3Kind { 22 let Ok(seg) = Line3::new(start, end, TOL) else { 23 panic!("line endpoints distinct"); 24 }; 25 seg.as_kind() 26} 27 28fn circle(plane: Plane3, radius: f64) -> Curve3Kind { 29 let Ok(disc) = Circle3::new(plane, mm(radius), TOL) else { 30 panic!("radius positive"); 31 }; 32 disc.as_kind() 33} 34 35fn arc(plane: Plane3, radius: f64, start: f64, sweep: f64) -> Curve3Kind { 36 let Ok(span) = Arc3::new( 37 plane, 38 mm(radius), 39 Angle::new::<radian>(start), 40 Angle::new::<radian>(sweep), 41 TOL, 42 ) else { 43 panic!("sweep nonzero"); 44 }; 45 span.as_kind() 46} 47 48fn row(label: &str, first: &Curve3Kind, second: &Curve3Kind) -> String { 49 format!("{label} = {}", intersect_curves_3(first, second, TOL)) 50} 51 52fn approx(point: Point3, x: f64, y: f64, z: f64) -> bool { 53 let (px, py, pz) = point.coords_mm(); 54 (px - x).abs() < 1e-9 && (py - y).abs() < 1e-9 && (pz - z).abs() < 1e-9 55} 56 57#[test] 58fn intersection_matrix_surface() { 59 let diag_up = line( 60 Point3::from_mm(-5.0, -5.0, 0.0), 61 Point3::from_mm(5.0, 5.0, 0.0), 62 ); 63 let diag_down = line( 64 Point3::from_mm(-5.0, 5.0, 0.0), 65 Point3::from_mm(5.0, -5.0, 0.0), 66 ); 67 let skew = line( 68 Point3::from_mm(-5.0, 0.0, 3.0), 69 Point3::from_mm(5.0, 0.0, 3.0), 70 ); 71 let collinear_overlap = line( 72 Point3::from_mm(-2.0, -2.0, 0.0), 73 Point3::from_mm(2.0, 2.0, 0.0), 74 ); 75 let collinear_touch = line( 76 Point3::from_mm(5.0, 5.0, 0.0), 77 Point3::from_mm(9.0, 9.0, 0.0), 78 ); 79 let parallel = line( 80 Point3::from_mm(-5.0, -4.0, 0.0), 81 Point3::from_mm(5.0, 6.0, 0.0), 82 ); 83 let upright_segment = line( 84 Point3::from_mm(3.0, 0.0, -5.0), 85 Point3::from_mm(3.0, 0.0, 5.0), 86 ); 87 let in_plane = line( 88 Point3::from_mm(-6.0, 0.0, 0.0), 89 Point3::from_mm(6.0, 0.0, 0.0), 90 ); 91 92 let flat_circle = circle(xy_plane(Point3::origin()), 3.0); 93 let shifted_circle = circle(xy_plane(Point3::from_mm(4.0, 0.0, 0.0)), 3.0); 94 let twin_circle = circle(xy_plane(Point3::origin()), 3.0); 95 let upright_circle = circle(xz_plane(Point3::origin()), 3.0); 96 97 let flat_arc = arc(xy_plane(Point3::origin()), 3.0, -FRAC_PI_2, PI); 98 let distant_arc = arc( 99 xy_plane(Point3::origin()), 100 3.0, 101 FRAC_PI_2 + FRAC_PI_4, 102 FRAC_PI_4, 103 ); 104 let upright_arc = arc(xz_plane(Point3::origin()), 3.0, -FRAC_PI_2, PI); 105 106 let rows = [ 107 row("line_cross_line", &diag_up, &diag_down), 108 row("line_skew_line", &diag_up, &skew), 109 row("line_collinear_overlap", &diag_up, &collinear_overlap), 110 row("line_collinear_touch", &diag_up, &collinear_touch), 111 row("line_parallel", &diag_up, &parallel), 112 row("line_pierces_circle", &upright_segment, &flat_circle), 113 row("line_in_plane_circle", &in_plane, &flat_circle), 114 row("line_in_plane_arc", &in_plane, &flat_arc), 115 row("circle_circle_coplanar", &flat_circle, &shifted_circle), 116 row("circle_circle_same", &flat_circle, &twin_circle), 117 row("circle_circle_perpendicular", &flat_circle, &upright_circle), 118 row("circle_arc_coplanar", &shifted_circle, &flat_arc), 119 row("arc_arc_hit", &flat_arc, &upright_arc), 120 row("arc_arc_miss", &flat_arc, &distant_arc), 121 ] 122 .join("\n"); 123 insta::assert_snapshot!(rows); 124} 125 126#[test] 127fn skew_lines_do_not_meet() { 128 let horizontal = line( 129 Point3::from_mm(-5.0, 0.0, 0.0), 130 Point3::from_mm(5.0, 0.0, 0.0), 131 ); 132 let lifted = line( 133 Point3::from_mm(0.0, -5.0, 2.0), 134 Point3::from_mm(0.0, 5.0, 2.0), 135 ); 136 assert_eq!( 137 intersect_curves_3(&horizontal, &lifted, TOL), 138 IntersectionSet3::Empty 139 ); 140} 141 142#[test] 143fn crossing_lines_meet_at_origin() { 144 let diag_up = line( 145 Point3::from_mm(-5.0, -5.0, 1.0), 146 Point3::from_mm(5.0, 5.0, 1.0), 147 ); 148 let diag_down = line( 149 Point3::from_mm(-5.0, 5.0, 1.0), 150 Point3::from_mm(5.0, -5.0, 1.0), 151 ); 152 let IntersectionSet3::One(meet) = intersect_curves_3(&diag_up, &diag_down, TOL) else { 153 panic!("crossing lines meet once"); 154 }; 155 assert!(approx(meet, 0.0, 0.0, 1.0)); 156} 157 158#[test] 159fn line_pierces_circle_plane_on_curve() { 160 let upright_segment = line( 161 Point3::from_mm(3.0, 0.0, -5.0), 162 Point3::from_mm(3.0, 0.0, 5.0), 163 ); 164 let disc = circle(xy_plane(Point3::origin()), 3.0); 165 let IntersectionSet3::One(hit) = intersect_curves_3(&upright_segment, &disc, TOL) else { 166 panic!("line through (3,0,0) hits the circle once"); 167 }; 168 assert!(approx(hit, 3.0, 0.0, 0.0)); 169} 170 171#[test] 172fn perpendicular_circles_meet_at_two_points() { 173 let flat_disc = circle(xy_plane(Point3::origin()), 5.0); 174 let upright_disc = circle(xz_plane(Point3::origin()), 5.0); 175 let IntersectionSet3::Two(first, second) = intersect_curves_3(&flat_disc, &upright_disc, TOL) 176 else { 177 panic!("circles in perpendicular planes share two points"); 178 }; 179 let hit_plus = approx(first, 5.0, 0.0, 0.0) || approx(second, 5.0, 0.0, 0.0); 180 let hit_minus = approx(first, -5.0, 0.0, 0.0) || approx(second, -5.0, 0.0, 0.0); 181 assert!( 182 hit_plus && hit_minus, 183 "expected (±5, 0, 0), got {first} and {second}" 184 ); 185} 186 187#[test] 188fn near_parallel_circles_keep_their_two_hits() { 189 let center = Point3::from_mm(10.0, 20.0, 30.0); 190 let flat = circle(xy_plane(center), 5.0); 191 let theta: f64 = 1e-8; 192 let tilted = circle( 193 Plane3::new_unchecked( 194 center, 195 UnitVec3::x_axis(), 196 UnitVec3::new_unchecked(0.0, theta.cos(), theta.sin()), 197 ), 198 5.0, 199 ); 200 let IntersectionSet3::Two(p, q) = intersect_curves_3(&flat, &tilted, TOL) else { 201 panic!("circles tilted by 1e-8 rad still cross twice on the shared x-axis"); 202 }; 203 let near = |pt: Point3, x: f64| { 204 let (px, py, pz) = pt.coords_mm(); 205 (px - x).abs() < 1e-6 && (py - 20.0).abs() < 1e-6 && (pz - 30.0).abs() < 1e-6 206 }; 207 assert!( 208 (near(p, 15.0) && near(q, 5.0)) || (near(p, 5.0) && near(q, 15.0)), 209 "expected (15,20,30) and (5,20,30), got {p} and {q}" 210 ); 211} 212 213#[test] 214fn coplanar_same_circle_is_coincident() { 215 let original = circle(xy_plane(Point3::origin()), 3.0); 216 let duplicate = circle(xy_plane(Point3::origin()), 3.0); 217 assert_eq!( 218 intersect_curves_3(&original, &duplicate, TOL), 219 IntersectionSet3::Coincident 220 ); 221} 222 223#[test] 224fn coincident_arcs_with_opposite_normals_share_both_endpoints() { 225 let upper = arc(xy_plane(Point3::origin()), 3.0, 0.0, PI); 226 let lower = arc( 227 Plane3::new_unchecked( 228 Point3::origin(), 229 UnitVec3::x_axis(), 230 UnitVec3::new_unchecked(0.0, -1.0, 0.0), 231 ), 232 3.0, 233 0.0, 234 PI, 235 ); 236 let IntersectionSet3::Two(p, q) = intersect_curves_3(&upper, &lower, TOL) else { 237 panic!("two half-circles on the same circle meet at both shared endpoints"); 238 }; 239 let hit_plus = approx(p, 3.0, 0.0, 0.0) || approx(q, 3.0, 0.0, 0.0); 240 let hit_minus = approx(p, -3.0, 0.0, 0.0) || approx(q, -3.0, 0.0, 0.0); 241 assert!( 242 hit_plus && hit_minus, 243 "expected (±3, 0, 0), got {p} and {q}" 244 ); 245} 246 247#[test] 248fn coincident_arcs_with_one_dimensional_overlap_are_coincident() { 249 let lead = arc(xy_plane(Point3::origin()), 3.0, 0.0, PI); 250 let trail = arc(xy_plane(Point3::origin()), 3.0, FRAC_PI_2, PI); 251 assert_eq!( 252 intersect_curves_3(&lead, &trail, TOL), 253 IntersectionSet3::Coincident 254 ); 255} 256 257#[test] 258fn coincident_arcs_abutting_at_one_endpoint_meet_once() { 259 let lead = arc(xy_plane(Point3::origin()), 3.0, 0.0, PI); 260 let trail = arc(xy_plane(Point3::origin()), 3.0, PI, FRAC_PI_2); 261 let IntersectionSet3::One(touch) = intersect_curves_3(&lead, &trail, TOL) else { 262 panic!("arcs abutting only at angle pi meet exactly once"); 263 }; 264 assert!( 265 approx(touch, -3.0, 0.0, 0.0), 266 "expected (-3, 0, 0), got {touch}" 267 ); 268}