1use super::{fixed, interval, rational};
13use super::{DropAxis, ImplicitPoint, Sign};
14
15pub fn orient3d(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, d: &ImplicitPoint) -> Sign {
22 use ImplicitPoint::{Explicit, Lpi, Tpi};
23 match (a, b, c, d) {
24 (Explicit(a), Explicit(b), Explicit(c), Explicit(d)) => {
25 Sign::from_f64(geometry_predicates::orient3d(*a, *b, *c, *d))
26 }
27 (Lpi(l), Explicit(b), Explicit(c), Explicit(d)) => interval::lpi_orient3d(l, *b, *c, *d)
28 .or_else(|| {
29 crate::kernel::budget::note_escalation();
30 fixed::indirect_orient3d(a, *b, *c, *d)
31 })
32 .unwrap_or_else(|| rational::lpi_orient3d(l, *b, *c, *d)),
33 (Tpi(t), Explicit(b), Explicit(c), Explicit(d)) => interval::tpi_orient3d(t, *b, *c, *d)
34 .or_else(|| {
35 crate::kernel::budget::note_escalation();
36 fixed::indirect_orient3d(a, *b, *c, *d)
37 })
38 .unwrap_or_else(|| rational::tpi_orient3d(t, *b, *c, *d)),
39 _ => unimplemented!(
41 "kernel::orient3d: implicit-point configuration never produced by the arrangement pipeline"
42 ),
43 }
44}
45
46pub fn orient2d(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
50 use ImplicitPoint::{Explicit, Lpi, Tpi};
51 let (i, j) = match axis {
52 DropAxis::X => (1, 2),
53 DropAxis::Y => (0, 2),
54 DropAxis::Z => (0, 1),
55 };
56 match (a, b, c) {
57 (Explicit(a), Explicit(b), Explicit(c)) => {
58 Sign::from_f64(geometry_predicates::orient2d([a[i], a[j]], [b[i], b[j]], [c[i], c[j]]))
59 }
60 (Lpi(l), Explicit(b), Explicit(c)) => interval::lpi_orient2d(l, *b, *c, axis)
61 .or_else(|| {
62 crate::kernel::budget::note_escalation();
63 fixed::indirect_orient2d(a, *b, *c, axis)
64 })
65 .unwrap_or_else(|| rational::lpi_orient2d(l, *b, *c, axis)),
66 (Tpi(t), Explicit(b), Explicit(c)) => interval::tpi_orient2d(t, *b, *c, axis)
67 .or_else(|| {
68 crate::kernel::budget::note_escalation();
69 fixed::indirect_orient2d(a, *b, *c, axis)
70 })
71 .unwrap_or_else(|| rational::tpi_orient2d(t, *b, *c, axis)),
72 _ => unimplemented!(
74 "kernel::orient2d: implicit-point configuration never produced by the arrangement pipeline"
75 ),
76 }
77}
78
79pub fn orient2d_2i(a: &ImplicitPoint, b: &ImplicitPoint, c: [f64; 3], axis: DropAxis) -> Sign {
81 interval::orient2d_2i(a, b, c, axis)
83 .or_else(|| {
84 crate::kernel::budget::note_escalation();
85 fixed::orient2d_2i(a, b, c, axis)
86 })
87 .unwrap_or_else(|| rational::orient2d_2i(a, b, c, axis))
88}
89
90pub fn orient2d_3i(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
92 interval::orient2d_3i(a, b, c, axis)
93 .or_else(|| {
94 crate::kernel::budget::note_escalation();
95 fixed::orient2d_3i(a, b, c, axis)
96 })
97 .unwrap_or_else(|| rational::orient2d_3i(a, b, c, axis))
98}
99
100pub fn cmp_lex(a: &ImplicitPoint, b: &ImplicitPoint) -> Sign {
102 interval::cmp_lex(a, b)
103 .or_else(|| {
104 crate::kernel::budget::note_escalation();
105 fixed::cmp_lex(a, b)
106 })
107 .unwrap_or_else(|| rational::cmp_lex(a, b))
108}
109
110#[inline]
111fn explicit_coord(p: &ImplicitPoint) -> [f64; 3] {
112 match p {
113 ImplicitPoint::Explicit(c) => *c,
114 _ => unreachable!("explicit_coord on an implicit point"),
115 }
116}
117
118pub fn orient2d_any(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
125 let pts = [a, b, c];
126 let key = |p: &ImplicitPoint| u8::from(matches!(p, ImplicitPoint::Explicit(_))); let keys = [key(a), key(b), key(c)];
128 let mut perm = [0usize, 1, 2];
129 perm.sort_by_key(|&i| keys[i]); let inversions = u8::from(perm[0] > perm[1]) + u8::from(perm[0] > perm[2]) + u8::from(perm[1] > perm[2]);
131 let rp = [pts[perm[0]], pts[perm[1]], pts[perm[2]]];
132 let n_implicit = 3 - (keys[0] + keys[1] + keys[2]) as usize;
133 let canonical = match n_implicit {
134 0 | 1 => orient2d(rp[0], rp[1], rp[2], axis),
136 2 => orient2d_2i(rp[0], rp[1], explicit_coord(rp[2]), axis),
137 _ => orient2d_3i(rp[0], rp[1], rp[2], axis),
138 };
139 if inversions % 2 == 1 {
140 canonical.flip()
141 } else {
142 canonical
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::super::{rational, DropAxis, Lpi, Tpi};
149 use super::{
150 cmp_lex, orient2d, orient2d_2i, orient2d_3i, orient2d_any, orient3d, ImplicitPoint, Sign,
151 };
152
153 fn e(p: [f64; 3]) -> ImplicitPoint {
154 ImplicitPoint::Explicit(p)
155 }
156
157 fn battery() -> Vec<[[f64; 3]; 4]> {
160 vec![
161 [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [1., 1., 0.]], [[0., 0., 12.3456789], [10., 0., 12.3456789], [0., 7., 12.3456789], [3.3, 2.1, 12.3456789 + 1e-9]],
163 [[0., 0., 12.3456789], [10., 0., 12.3456789], [0., 7., 12.3456789], [3.3, 2.1, 12.3456789 - 1e-9]],
164 [[1e7, 1e7, 0.], [1e7 + 1., 1e7, 0.], [1e7, 1e7 + 1., 0.], [1e7 + 0.5, 1e7 + 0.5, 1e-7]],
165 [[0., 0., 0.], [1., 2., 3.], [-2., 1., 0.5], [0.5, 0.5, 0.5]],
166 [[0., 0., 0.], [1., 1., 1.], [2., 2., 2.], [5., 1., 9.]], [[0., 0., 0.], [1e-4, 0., 0.], [0., 1e-4, 0.], [0., 0., 1e-4]],
168 [[0., 0., 0.], [0., 1e-4, 0.], [1e-4, 0., 0.], [0., 0., 1e-4]],
169 [[-3., 2., 5.], [7., -1., 2.], [4., 4., -6.], [1.5, 0.0, 0.25]],
170 ]
171 }
172
173 fn lpi_cases() -> Vec<(Lpi, [f64; 3], [f64; 3], [f64; 3])> {
175 vec![
176 (
178 Lpi { p: [0.3, 0.3, -1.], q: [0.3, 0.3, 1.], r: [0., 0., 0.], s: [2., 0., 0.], t: [0., 2., 0.] },
179 [0., 0., 1.], [1., 0., 1.], [0., 1., 1.],
180 ),
181 (
183 Lpi { p: [0.3, 0.3, -1.], q: [0.3, 0.3, 1.], r: [0., 0., 0.], s: [2., 0., 0.], t: [0., 2., 0.] },
184 [0., 0., -1.], [1., 0., -1.], [0., 1., -1.],
185 ),
186 (
188 Lpi { p: [1., 1., 0.], q: [2., 3., 4.], r: [0., 0., 1.], s: [3., 0., 2.], t: [0., 3., 2.] },
189 [5., -2., 0.], [-1., 4., 3.], [2., 2., -3.],
190 ),
191 (
193 Lpi { p: [12.3, 4.5, -2.], q: [12.3, 4.5, 6.], r: [0., 0., 3.1], s: [20., 0., 3.1], t: [0., 9., 3.1] },
194 [10., 10., 10.], [-5., 0., 0.], [0., -5., 8.],
195 ),
196 ]
197 }
198
199 #[test]
200 fn explicit_orient3d_matches_rational_oracle() {
201 for cfg in battery() {
202 let [a, b, c, d] = cfg;
203 let fast = orient3d(&e(a), &e(b), &e(c), &e(d));
204 let oracle = rational::orient3d_exact(a, b, c, d);
205 assert_eq!(fast, oracle, "explicit orient3d != rational oracle on {cfg:?}");
206 }
207 }
208
209 #[test]
210 fn lpi_orient3d_matches_materialised_point() {
211 for (l, p2, p3, p4) in lpi_cases() {
214 let homog = rational::lpi_orient3d(&l, p2, p3, p4);
215 let direct = rational::orient3d_exact_pt(&rational::lpi_point(&l), p2, p3, p4);
216 assert_eq!(homog, direct, "LPI homogenisation/flip wrong for {l:?}");
217 assert_ne!(homog, Sign::Zero, "test LPI case should be off-plane: {l:?}");
219 }
220 }
221
222 #[test]
223 fn lpi_orient3d_sign_invariant_to_plane_winding() {
224 for (l, p2, p3, p4) in lpi_cases() {
228 let l_rewound = Lpi { s: l.t, t: l.s, ..l };
229 assert_eq!(
230 rational::lpi_orient3d(&l, p2, p3, p4),
231 rational::lpi_orient3d(&l_rewound, p2, p3, p4),
232 "LPI-orient3d sign changed under plane re-winding — the sign(d) flip is wrong/missing"
233 );
234 }
235 }
236
237 #[test]
238 fn assemble_sign_per_config_flip() {
239 use super::super::assemble_sign;
240 assert_eq!(assemble_sign(Sign::Positive, &[Sign::Negative]), Sign::Negative);
242 assert_eq!(assemble_sign(Sign::Positive, &[Sign::Negative, Sign::Negative]), Sign::Positive);
243 assert_eq!(assemble_sign(Sign::Negative, &[Sign::Positive]), Sign::Negative);
244 assert_eq!(assemble_sign(Sign::Positive, &[Sign::Zero]), Sign::Zero);
245 assert_eq!(assemble_sign(Sign::Positive, &[]), Sign::Positive);
246 }
247
248 #[test]
249 fn next_up_down_are_adjacent() {
250 use super::super::interval::{next_down, next_up};
251 for &x in &[1.0, -1.0, 0.0, 1e7, -1e-9, 12.3456789, f64::MIN_POSITIVE] {
252 assert!(next_up(x) > x, "next_up({x}) not strictly greater");
253 assert!(next_down(x) < x, "next_down({x}) not strictly less");
254 assert_eq!(next_down(next_up(x)), x, "next_up/next_down not adjacent at {x}");
256 }
257 }
258
259 struct Lcg(u64);
261 impl Lcg {
262 fn u(&mut self) -> u64 {
263 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
264 self.0
265 }
266 fn f(&mut self, lo: f64, hi: f64) -> f64 {
267 let unit = (self.u() >> 11) as f64 / (1u64 << 53) as f64; lo + (hi - lo) * unit
269 }
270 fn p(&mut self) -> [f64; 3] {
271 [self.f(-10., 10.), self.f(-10., 10.), self.f(-10., 10.)]
272 }
273 }
274
275 #[test]
276 fn interval_tier_is_sound_and_the_cascade_equals_exact() {
277 use super::super::{interval, rational, Lpi};
278 let mut rng = Lcg(0x1234_5678_9abc_def0);
279 let (mut definite, mut escalated) = (0u32, 0u32);
280 for _ in 0..3000 {
281 let l = Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
282 let (p2, p3, p4) = (rng.p(), rng.p(), rng.p());
283 let exact = rational::lpi_orient3d(&l, p2, p3, p4);
284 match interval::lpi_orient3d(&l, p2, p3, p4) {
286 Some(s) => {
287 assert_eq!(s, exact, "interval returned a WRONG definite sign for {l:?}");
288 definite += 1;
289 }
290 None => escalated += 1,
291 }
292 let cascade = orient3d(&ImplicitPoint::Lpi(l), &e(p2), &e(p3), &e(p4));
294 assert_eq!(cascade, exact, "cascade != exact for {l:?}");
295 }
296 assert!(
298 definite as f64 / (definite + escalated) as f64 > 0.95,
299 "interval resolved only {definite}/{} — fast path too cold",
300 definite + escalated
301 );
302 eprintln!("interval tier: {definite} definite, {escalated} escalated to exact");
303 }
304
305
306 fn tpi_cases() -> Vec<(Tpi, [f64; 3], [f64; 3], [f64; 3])> {
308 let axis_aligned = Tpi {
310 planes: [
311 [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]], [[0.3, 0., 0.], [0.3, 1., 0.], [0.3, 0., 1.]], [[0., 0.4, 0.], [1., 0.4, 0.], [0., 0.4, 1.]], ],
315 };
316 let tilted = Tpi {
318 planes: [
319 [[0., 0., 1.], [3., 0., 2.], [0., 3., 2.]],
320 [[1., 0., 0.], [1., 2., 1.], [2., 0., 3.]],
321 [[-1., -1., 0.], [2., -1., 1.], [-1., 2., 2.]],
322 ],
323 };
324 vec![
325 (axis_aligned, [0., 0., 1.], [1., 0., 1.], [0., 1., 1.]), (axis_aligned, [0., 0., -1.], [1., 0., -1.], [0., 1., -1.]), (tilted, [5., -2., 0.], [-1., 4., 3.], [2., 2., -3.]),
328 (tilted, [10., 10., 10.], [-5., 0., 0.], [0., -5., 8.]),
329 ]
330 }
331
332 #[test]
333 fn tpi_orient3d_matches_materialised_point() {
334 for (t, p2, p3, p4) in tpi_cases() {
337 let homog = rational::tpi_orient3d(&t, p2, p3, p4);
338 let direct = rational::orient3d_exact_pt(&rational::tpi_point(&t), p2, p3, p4);
339 assert_eq!(homog, direct, "TPI homogenisation/flip wrong for {t:?}");
340 assert_ne!(homog, Sign::Zero, "test TPI case should be off-plane: {t:?}");
341 }
342 }
343
344 #[test]
345 fn tpi_orient3d_sign_invariant_to_plane_winding() {
346 for (t, p2, p3, p4) in tpi_cases() {
350 let mut rewound = t;
351 rewound.planes[0].swap(1, 2);
352 assert_eq!(
353 rational::tpi_orient3d(&t, p2, p3, p4),
354 rational::tpi_orient3d(&rewound, p2, p3, p4),
355 "TPI-orient3d sign changed under plane re-winding — the sign(d) flip is wrong/missing"
356 );
357 }
358 }
359
360 #[test]
361 fn tpi_interval_is_sound_and_the_cascade_equals_exact() {
362 use super::super::interval;
363 let mut rng = Lcg(0xfeed_face_cafe_d00d);
364 let (mut definite, mut escalated) = (0u32, 0u32);
365 for _ in 0..2000 {
366 let plane = |rng: &mut Lcg| [rng.p(), rng.p(), rng.p()];
368 let t = Tpi { planes: [plane(&mut rng), plane(&mut rng), plane(&mut rng)] };
369 let (p2, p3, p4) = (rng.p(), rng.p(), rng.p());
370 let exact = rational::tpi_orient3d(&t, p2, p3, p4);
371 match interval::tpi_orient3d(&t, p2, p3, p4) {
372 Some(s) => {
373 assert_eq!(s, exact, "TPI interval returned a WRONG definite sign for {t:?}");
374 definite += 1;
375 }
376 None => escalated += 1,
377 }
378 let cascade = orient3d(&ImplicitPoint::Tpi(t), &e(p2), &e(p3), &e(p4));
379 assert_eq!(cascade, exact, "TPI cascade != exact for {t:?}");
380 }
381 assert!(
384 definite as f64 / (definite + escalated) as f64 > 0.80,
385 "TPI interval resolved only {definite}/{}",
386 definite + escalated
387 );
388 eprintln!("TPI interval tier: {definite} definite, {escalated} escalated");
389 }
390
391 const AXES: [DropAxis; 3] = [DropAxis::X, DropAxis::Y, DropAxis::Z];
392
393 #[test]
394 fn explicit_orient2d_matches_oracle() {
395 for axis in AXES {
396 for cfg in battery() {
397 let [a, b, c, _d] = cfg;
398 let fast = orient2d(&e(a), &e(b), &e(c), axis);
399 let oracle = rational::orient2d_exact(a, b, c, axis);
400 assert_eq!(fast, oracle, "explicit orient2d != oracle on {cfg:?} axis {axis:?}");
401 }
402 }
403 }
404
405 #[test]
406 fn indirect_orient2d_matches_materialised_point() {
407 for axis in AXES {
410 for (l, p2, p3, _p4) in lpi_cases() {
411 let homog = rational::lpi_orient2d(&l, p2, p3, axis);
412 let direct = rational::orient2d_exact_pt(&rational::lpi_point(&l), p2, p3, axis);
413 assert_eq!(homog, direct, "LPI orient2d homog/flip wrong, axis {axis:?}");
414 let cascade = orient2d(&ImplicitPoint::Lpi(l), &e(p2), &e(p3), axis);
415 assert_eq!(cascade, direct, "LPI orient2d cascade != exact, axis {axis:?}");
416 }
417 for (t, p2, p3, _p4) in tpi_cases() {
418 let homog = rational::tpi_orient2d(&t, p2, p3, axis);
419 let direct = rational::orient2d_exact_pt(&rational::tpi_point(&t), p2, p3, axis);
420 assert_eq!(homog, direct, "TPI orient2d homog/flip wrong, axis {axis:?}");
421 let cascade = orient2d(&ImplicitPoint::Tpi(t), &e(p2), &e(p3), axis);
422 assert_eq!(cascade, direct, "TPI orient2d cascade != exact, axis {axis:?}");
423 }
424 }
425 }
426
427 #[test]
428 fn orient2d_interval_is_sound_vs_oracle() {
429 use super::super::interval;
430 let mut rng = Lcg(0xabcd_1234_5678_9999);
431 for _ in 0..2000 {
432 let l = Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
433 let (b, c) = (rng.p(), rng.p());
434 let axis = AXES[(rng.u() % 3) as usize];
435 let exact = rational::lpi_orient2d(&l, b, c, axis);
436 if let Some(s) = interval::lpi_orient2d(&l, b, c, axis) {
437 assert_eq!(s, exact, "orient2d interval returned a WRONG definite sign for {l:?}");
438 }
439 assert_eq!(orient2d(&ImplicitPoint::Lpi(l), &e(b), &e(c), axis), exact);
441 }
442 }
443
444 #[test]
445 fn lpi_point_lies_on_its_defining_plane() {
446 for (l, _, _, _) in lpi_cases() {
450 assert_eq!(
451 orient3d(&ImplicitPoint::Lpi(l), &e(l.r), &e(l.s), &e(l.t)),
452 Sign::Zero,
453 "LPI point is not on its defining plane R,S,T: {l:?}"
454 );
455 }
456 }
457
458 #[test]
459 fn tpi_point_lies_on_all_three_defining_planes() {
460 for (t, _, _, _) in tpi_cases() {
461 for plane in &t.planes {
462 assert_eq!(
463 orient3d(&ImplicitPoint::Tpi(t), &e(plane[0]), &e(plane[1]), &e(plane[2])),
464 Sign::Zero,
465 "TPI point is not on one of its defining planes: {t:?}"
466 );
467 }
468 }
469 }
470
471 #[test]
472 fn orient2d_1i_sign_invariant_to_plane_winding() {
473 for axis in AXES {
480 for (l, p2, p3, _p4) in lpi_cases() {
481 let rewound = Lpi { s: l.t, t: l.s, ..l };
482 assert_eq!(
483 rational::lpi_orient2d(&l, p2, p3, axis),
484 rational::lpi_orient2d(&rewound, p2, p3, axis),
485 "orient2d 1I sign changed under plane re-winding (axis {axis:?})"
486 );
487 }
488 }
489 }
490
491 #[test]
492 fn multi_implicit_orient2d_matches_materialised_oracle() {
493 let mut pts: Vec<ImplicitPoint> =
496 lpi_cases().into_iter().map(|(l, ..)| ImplicitPoint::Lpi(l)).collect();
497 pts.extend(tpi_cases().into_iter().map(|(t, ..)| ImplicitPoint::Tpi(t)));
498 let c = [1.3, -0.7, 2.1];
499 let cpt = rational::point_of(&ImplicitPoint::Explicit(c));
500 for axis in AXES {
501 for a in &pts {
502 for b in &pts {
503 let oracle = rational::orient2d_pts(
504 &rational::point_of(a), &rational::point_of(b), &cpt, axis);
505 assert_eq!(rational::orient2d_2i(a, b, c, axis), oracle, "orient2d_2i (axis {axis:?})");
506 for cc in &pts {
507 let oracle3 = rational::orient2d_pts(
508 &rational::point_of(a), &rational::point_of(b), &rational::point_of(cc), axis);
509 assert_eq!(rational::orient2d_3i(a, b, cc, axis), oracle3, "orient2d_3i (axis {axis:?})");
510 }
511 }
512 }
513 }
514 }
515
516 #[test]
517 fn multi_implicit_orient2d_winding_invariant() {
518 let l = lpi_cases()[2].0;
520 let a = ImplicitPoint::Lpi(l);
521 let a_rw = ImplicitPoint::Lpi(Lpi { s: l.t, t: l.s, ..l });
522 let b = ImplicitPoint::Tpi(tpi_cases()[0].0);
523 let cc = ImplicitPoint::Lpi(lpi_cases()[0].0);
524 let c = [0.4, 1.1, -0.3];
525 for axis in AXES {
526 assert_eq!(
527 rational::orient2d_2i(&a, &b, c, axis),
528 rational::orient2d_2i(&a_rw, &b, c, axis),
529 "2I sign changed under plane re-winding"
530 );
531 assert_eq!(
532 rational::orient2d_3i(&a, &b, &cc, axis),
533 rational::orient2d_3i(&a_rw, &b, &cc, axis),
534 "3I sign changed under plane re-winding"
535 );
536 }
537 }
538
539 #[test]
540 fn orient2d_any_matches_oracle_for_every_permutation_and_mix() {
541 let mut pts: Vec<ImplicitPoint> = vec![e([0.0, 0.0, 0.0]), e([3.0, 1.0, -2.0])];
545 pts.extend(lpi_cases().into_iter().take(2).map(|(l, ..)| ImplicitPoint::Lpi(l)));
546 pts.extend(tpi_cases().into_iter().take(2).map(|(t, ..)| ImplicitPoint::Tpi(t)));
547 for axis in AXES {
548 for a in &pts {
549 for b in &pts {
550 for c in &pts {
551 let got = orient2d_any(a, b, c, axis);
552 let want = rational::orient2d_pts(
553 &rational::point_of(a),
554 &rational::point_of(b),
555 &rational::point_of(c),
556 axis,
557 );
558 assert_eq!(got, want, "orient2d_any mismatch (axis {axis:?})");
559 }
560 }
561 }
562 }
563 }
564
565 #[test]
566 fn cmp_lex_matches_materialised_order_and_is_a_total_order() {
567 use std::cmp::Ordering;
568 let mut pts: Vec<ImplicitPoint> =
569 lpi_cases().into_iter().map(|(l, ..)| ImplicitPoint::Lpi(l)).collect();
570 pts.extend(tpi_cases().into_iter().map(|(t, ..)| ImplicitPoint::Tpi(t)));
571 pts.push(e([1.5, -2.0, 0.25]));
572 pts.push(e([0.0, 0.0, 0.0]));
573 let oracle = |a: &ImplicitPoint, b: &ImplicitPoint| -> Sign {
574 let (pa, pb) = (rational::point_of(a), rational::point_of(b));
575 for k in 0..3 {
576 match pa[k].cmp(&pb[k]) {
577 Ordering::Less => return Sign::Negative,
578 Ordering::Greater => return Sign::Positive,
579 Ordering::Equal => {}
580 }
581 }
582 Sign::Zero
583 };
584 for a in &pts {
585 assert_eq!(rational::cmp_lex(a, a), Sign::Zero, "cmp_lex not reflexive-zero");
586 for b in &pts {
587 assert_eq!(rational::cmp_lex(a, b), oracle(a, b), "cmp_lex != materialised lex");
588 assert_eq!(
589 rational::cmp_lex(a, b),
590 rational::cmp_lex(b, a).flip(),
591 "cmp_lex not antisymmetric"
592 );
593 }
594 }
595 for a in &pts {
597 for b in &pts {
598 for c in &pts {
599 if rational::cmp_lex(a, b) == Sign::Negative
600 && rational::cmp_lex(b, c) == Sign::Negative
601 {
602 assert_eq!(rational::cmp_lex(a, c), Sign::Negative, "cmp_lex not transitive");
603 }
604 }
605 }
606 }
607 }
608
609 #[test]
610 fn new_tier_interval_is_sound_and_cascade_equals_exact() {
611 use super::super::interval;
614 let mut rng = Lcg(0x0bad_c0de_1234_5678);
615 for _ in 0..500 {
616 let mk = |rng: &mut Lcg| Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
617 let a = ImplicitPoint::Lpi(mk(&mut rng));
618 let b = ImplicitPoint::Lpi(mk(&mut rng));
619 let cc = ImplicitPoint::Lpi(mk(&mut rng));
620 let c = rng.p();
621 for axis in AXES {
622 let ex2 = rational::orient2d_2i(&a, &b, c, axis);
623 if let Some(s) = interval::orient2d_2i(&a, &b, c, axis) {
624 assert_eq!(s, ex2, "orient2d_2i interval wrong sign");
625 }
626 assert_eq!(orient2d_2i(&a, &b, c, axis), ex2, "2i cascade != exact");
627 let ex3 = rational::orient2d_3i(&a, &b, &cc, axis);
628 if let Some(s) = interval::orient2d_3i(&a, &b, &cc, axis) {
629 assert_eq!(s, ex3, "orient2d_3i interval wrong sign");
630 }
631 assert_eq!(orient2d_3i(&a, &b, &cc, axis), ex3, "3i cascade != exact");
632 }
633 let exl = rational::cmp_lex(&a, &b);
634 if let Some(s) = interval::cmp_lex(&a, &b) {
635 assert_eq!(s, exl, "cmp_lex interval wrong sign");
636 }
637 assert_eq!(cmp_lex(&a, &b), exl, "cmp_lex cascade != exact");
638 }
639 }
640
641 #[test]
642 fn cmp_lex_welds_coincident_lpi_and_tpi() {
643 let lpi = ImplicitPoint::Lpi(Lpi {
646 p: [0.3, 0.4, -1.],
647 q: [0.3, 0.4, 1.],
648 r: [0., 0., 0.],
649 s: [1., 0., 0.],
650 t: [0., 1., 0.],
651 });
652 let tpi = ImplicitPoint::Tpi(Tpi {
653 planes: [
654 [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]], [[0.3, 0., 0.], [0.3, 1., 0.], [0.3, 0., 1.]], [[0., 0.4, 0.], [1., 0.4, 0.], [0., 0.4, 1.]], ],
658 });
659 assert_eq!(rational::point_of(&lpi), rational::point_of(&tpi), "test points not coincident");
660 assert_eq!(
661 rational::cmp_lex(&lpi, &tpi),
662 Sign::Zero,
663 "coincident LPI/TPI not welded by cmp_lex"
664 );
665 }
666
667 #[test]
668 fn cmp_along_matches_materialised_oracle() {
669 use num_rational::BigRational;
670 use num_traits::Signed;
671 let mut pts: Vec<ImplicitPoint> =
672 vec![e([0., 0., 0.]), e([3., 1., -2.]), e([1.5, -2., 0.25])];
673 pts.extend(lpi_cases().into_iter().take(2).map(|(l, ..)| ImplicitPoint::Lpi(l)));
674 pts.extend(tpi_cases().into_iter().take(2).map(|(t, ..)| ImplicitPoint::Tpi(t)));
675 let dirs = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [1., 2., -1.], [-0.3, 1.7, 0.5]];
676 for u in dirs {
677 let ur = [
678 BigRational::from_float(u[0]).unwrap(),
679 BigRational::from_float(u[1]).unwrap(),
680 BigRational::from_float(u[2]).unwrap(),
681 ];
682 for a in &pts {
683 for b in &pts {
684 let pa = rational::point_of(a);
685 let pb = rational::point_of(b);
686 let dot = (&pa[0] - &pb[0]) * &ur[0]
687 + (&pa[1] - &pb[1]) * &ur[1]
688 + (&pa[2] - &pb[2]) * &ur[2];
689 let want = if dot.is_negative() {
690 Sign::Negative
691 } else if dot.is_positive() {
692 Sign::Positive
693 } else {
694 Sign::Zero
695 };
696 assert_eq!(rational::cmp_along(a, b, u), want, "cmp_along != oracle (u={u:?})");
697 }
698 }
699 }
700 }
701}