Another project
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, ¶llel),
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}