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 {
124 let pts = [a, b, c];
125 let key = |p: &ImplicitPoint| u8::from(matches!(p, ImplicitPoint::Explicit(_))); let keys = [key(a), key(b), key(c)];
127 let mut perm = [0usize, 1, 2];
128 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]);
130 let rp = [pts[perm[0]], pts[perm[1]], pts[perm[2]]];
131 let n_implicit = 3 - (keys[0] + keys[1] + keys[2]) as usize;
132 let canonical = match n_implicit {
133 0 | 1 => orient2d(rp[0], rp[1], rp[2], axis),
135 2 => orient2d_2i(rp[0], rp[1], explicit_coord(rp[2]), axis),
136 _ => orient2d_3i(rp[0], rp[1], rp[2], axis),
137 };
138 if inversions % 2 == 1 {
139 canonical.flip()
140 } else {
141 canonical
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::super::{rational, DropAxis, Lpi, Tpi};
148 use super::{
149 cmp_lex, orient2d, orient2d_2i, orient2d_3i, orient2d_any, orient3d, ImplicitPoint, Sign,
150 };
151
152 fn e(p: [f64; 3]) -> ImplicitPoint {
153 ImplicitPoint::Explicit(p)
154 }
155
156 fn battery() -> Vec<[[f64; 3]; 4]> {
159 vec![
160 [[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]],
162 [[0., 0., 12.3456789], [10., 0., 12.3456789], [0., 7., 12.3456789], [3.3, 2.1, 12.3456789 - 1e-9]],
163 [[1e7, 1e7, 0.], [1e7 + 1., 1e7, 0.], [1e7, 1e7 + 1., 0.], [1e7 + 0.5, 1e7 + 0.5, 1e-7]],
164 [[0., 0., 0.], [1., 2., 3.], [-2., 1., 0.5], [0.5, 0.5, 0.5]],
165 [[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]],
167 [[0., 0., 0.], [0., 1e-4, 0.], [1e-4, 0., 0.], [0., 0., 1e-4]],
168 [[-3., 2., 5.], [7., -1., 2.], [4., 4., -6.], [1.5, 0.0, 0.25]],
169 ]
170 }
171
172 fn lpi_cases() -> Vec<(Lpi, [f64; 3], [f64; 3], [f64; 3])> {
174 vec![
175 (
177 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.] },
178 [0., 0., 1.], [1., 0., 1.], [0., 1., 1.],
179 ),
180 (
182 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.] },
183 [0., 0., -1.], [1., 0., -1.], [0., 1., -1.],
184 ),
185 (
187 Lpi { p: [1., 1., 0.], q: [2., 3., 4.], r: [0., 0., 1.], s: [3., 0., 2.], t: [0., 3., 2.] },
188 [5., -2., 0.], [-1., 4., 3.], [2., 2., -3.],
189 ),
190 (
192 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] },
193 [10., 10., 10.], [-5., 0., 0.], [0., -5., 8.],
194 ),
195 ]
196 }
197
198 #[test]
199 fn explicit_orient3d_matches_rational_oracle() {
200 for cfg in battery() {
201 let [a, b, c, d] = cfg;
202 let fast = orient3d(&e(a), &e(b), &e(c), &e(d));
203 let oracle = rational::orient3d_exact(a, b, c, d);
204 assert_eq!(fast, oracle, "explicit orient3d != rational oracle on {cfg:?}");
205 }
206 }
207
208 #[test]
209 fn lpi_orient3d_matches_materialised_point() {
210 for (l, p2, p3, p4) in lpi_cases() {
213 let homog = rational::lpi_orient3d(&l, p2, p3, p4);
214 let direct = rational::orient3d_exact_pt(&rational::lpi_point(&l), p2, p3, p4);
215 assert_eq!(homog, direct, "LPI homogenisation/flip wrong for {l:?}");
216 assert_ne!(homog, Sign::Zero, "test LPI case should be off-plane: {l:?}");
218 }
219 }
220
221 #[test]
222 fn lpi_orient3d_sign_invariant_to_plane_winding() {
223 for (l, p2, p3, p4) in lpi_cases() {
227 let l_rewound = Lpi { s: l.t, t: l.s, ..l };
228 assert_eq!(
229 rational::lpi_orient3d(&l, p2, p3, p4),
230 rational::lpi_orient3d(&l_rewound, p2, p3, p4),
231 "LPI-orient3d sign changed under plane re-winding — the sign(d) flip is wrong/missing"
232 );
233 }
234 }
235
236 #[test]
237 fn assemble_sign_per_config_flip() {
238 use super::super::assemble_sign;
239 assert_eq!(assemble_sign(Sign::Positive, &[Sign::Negative]), Sign::Negative);
241 assert_eq!(assemble_sign(Sign::Positive, &[Sign::Negative, Sign::Negative]), Sign::Positive);
242 assert_eq!(assemble_sign(Sign::Negative, &[Sign::Positive]), Sign::Negative);
243 assert_eq!(assemble_sign(Sign::Positive, &[Sign::Zero]), Sign::Zero);
244 assert_eq!(assemble_sign(Sign::Positive, &[]), Sign::Positive);
245 }
246
247 #[test]
248 fn next_up_down_are_adjacent() {
249 use super::super::interval::{next_down, next_up};
250 for &x in &[1.0, -1.0, 0.0, 1e7, -1e-9, 12.3456789, f64::MIN_POSITIVE] {
251 assert!(next_up(x) > x, "next_up({x}) not strictly greater");
252 assert!(next_down(x) < x, "next_down({x}) not strictly less");
253 assert_eq!(next_down(next_up(x)), x, "next_up/next_down not adjacent at {x}");
255 }
256 }
257
258 struct Lcg(u64);
260 impl Lcg {
261 fn u(&mut self) -> u64 {
262 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
263 self.0
264 }
265 fn f(&mut self, lo: f64, hi: f64) -> f64 {
266 let unit = (self.u() >> 11) as f64 / (1u64 << 53) as f64; lo + (hi - lo) * unit
268 }
269 fn p(&mut self) -> [f64; 3] {
270 [self.f(-10., 10.), self.f(-10., 10.), self.f(-10., 10.)]
271 }
272 }
273
274 #[test]
275 fn interval_tier_is_sound_and_the_cascade_equals_exact() {
276 use super::super::{interval, rational, Lpi};
277 let mut rng = Lcg(0x1234_5678_9abc_def0);
278 let (mut definite, mut escalated) = (0u32, 0u32);
279 for _ in 0..3000 {
280 let l = Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
281 let (p2, p3, p4) = (rng.p(), rng.p(), rng.p());
282 let exact = rational::lpi_orient3d(&l, p2, p3, p4);
283 match interval::lpi_orient3d(&l, p2, p3, p4) {
285 Some(s) => {
286 assert_eq!(s, exact, "interval returned a WRONG definite sign for {l:?}");
287 definite += 1;
288 }
289 None => escalated += 1,
290 }
291 let cascade = orient3d(&ImplicitPoint::Lpi(l), &e(p2), &e(p3), &e(p4));
293 assert_eq!(cascade, exact, "cascade != exact for {l:?}");
294 }
295 assert!(
297 definite as f64 / (definite + escalated) as f64 > 0.95,
298 "interval resolved only {definite}/{} — fast path too cold",
299 definite + escalated
300 );
301 eprintln!("interval tier: {definite} definite, {escalated} escalated to exact");
302 }
303
304
305 fn tpi_cases() -> Vec<(Tpi, [f64; 3], [f64; 3], [f64; 3])> {
307 let axis_aligned = Tpi {
309 planes: [
310 [[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.]], ],
314 };
315 let tilted = Tpi {
317 planes: [
318 [[0., 0., 1.], [3., 0., 2.], [0., 3., 2.]],
319 [[1., 0., 0.], [1., 2., 1.], [2., 0., 3.]],
320 [[-1., -1., 0.], [2., -1., 1.], [-1., 2., 2.]],
321 ],
322 };
323 vec![
324 (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.]),
327 (tilted, [10., 10., 10.], [-5., 0., 0.], [0., -5., 8.]),
328 ]
329 }
330
331 #[test]
332 fn tpi_orient3d_matches_materialised_point() {
333 for (t, p2, p3, p4) in tpi_cases() {
336 let homog = rational::tpi_orient3d(&t, p2, p3, p4);
337 let direct = rational::orient3d_exact_pt(&rational::tpi_point(&t), p2, p3, p4);
338 assert_eq!(homog, direct, "TPI homogenisation/flip wrong for {t:?}");
339 assert_ne!(homog, Sign::Zero, "test TPI case should be off-plane: {t:?}");
340 }
341 }
342
343 #[test]
344 fn tpi_orient3d_sign_invariant_to_plane_winding() {
345 for (t, p2, p3, p4) in tpi_cases() {
349 let mut rewound = t;
350 rewound.planes[0].swap(1, 2);
351 assert_eq!(
352 rational::tpi_orient3d(&t, p2, p3, p4),
353 rational::tpi_orient3d(&rewound, p2, p3, p4),
354 "TPI-orient3d sign changed under plane re-winding — the sign(d) flip is wrong/missing"
355 );
356 }
357 }
358
359 #[test]
360 fn tpi_interval_is_sound_and_the_cascade_equals_exact() {
361 use super::super::interval;
362 let mut rng = Lcg(0xfeed_face_cafe_d00d);
363 let (mut definite, mut escalated) = (0u32, 0u32);
364 for _ in 0..2000 {
365 let plane = |rng: &mut Lcg| [rng.p(), rng.p(), rng.p()];
367 let t = Tpi { planes: [plane(&mut rng), plane(&mut rng), plane(&mut rng)] };
368 let (p2, p3, p4) = (rng.p(), rng.p(), rng.p());
369 let exact = rational::tpi_orient3d(&t, p2, p3, p4);
370 match interval::tpi_orient3d(&t, p2, p3, p4) {
371 Some(s) => {
372 assert_eq!(s, exact, "TPI interval returned a WRONG definite sign for {t:?}");
373 definite += 1;
374 }
375 None => escalated += 1,
376 }
377 let cascade = orient3d(&ImplicitPoint::Tpi(t), &e(p2), &e(p3), &e(p4));
378 assert_eq!(cascade, exact, "TPI cascade != exact for {t:?}");
379 }
380 assert!(
383 definite as f64 / (definite + escalated) as f64 > 0.80,
384 "TPI interval resolved only {definite}/{}",
385 definite + escalated
386 );
387 eprintln!("TPI interval tier: {definite} definite, {escalated} escalated");
388 }
389
390 const AXES: [DropAxis; 3] = [DropAxis::X, DropAxis::Y, DropAxis::Z];
391
392 #[test]
393 fn explicit_orient2d_matches_oracle() {
394 for axis in AXES {
395 for cfg in battery() {
396 let [a, b, c, _d] = cfg;
397 let fast = orient2d(&e(a), &e(b), &e(c), axis);
398 let oracle = rational::orient2d_exact(a, b, c, axis);
399 assert_eq!(fast, oracle, "explicit orient2d != oracle on {cfg:?} axis {axis:?}");
400 }
401 }
402 }
403
404 #[test]
405 fn indirect_orient2d_matches_materialised_point() {
406 for axis in AXES {
409 for (l, p2, p3, _p4) in lpi_cases() {
410 let homog = rational::lpi_orient2d(&l, p2, p3, axis);
411 let direct = rational::orient2d_exact_pt(&rational::lpi_point(&l), p2, p3, axis);
412 assert_eq!(homog, direct, "LPI orient2d homog/flip wrong, axis {axis:?}");
413 let cascade = orient2d(&ImplicitPoint::Lpi(l), &e(p2), &e(p3), axis);
414 assert_eq!(cascade, direct, "LPI orient2d cascade != exact, axis {axis:?}");
415 }
416 for (t, p2, p3, _p4) in tpi_cases() {
417 let homog = rational::tpi_orient2d(&t, p2, p3, axis);
418 let direct = rational::orient2d_exact_pt(&rational::tpi_point(&t), p2, p3, axis);
419 assert_eq!(homog, direct, "TPI orient2d homog/flip wrong, axis {axis:?}");
420 let cascade = orient2d(&ImplicitPoint::Tpi(t), &e(p2), &e(p3), axis);
421 assert_eq!(cascade, direct, "TPI orient2d cascade != exact, axis {axis:?}");
422 }
423 }
424 }
425
426 #[test]
427 fn orient2d_interval_is_sound_vs_oracle() {
428 use super::super::interval;
429 let mut rng = Lcg(0xabcd_1234_5678_9999);
430 for _ in 0..2000 {
431 let l = Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
432 let (b, c) = (rng.p(), rng.p());
433 let axis = AXES[(rng.u() % 3) as usize];
434 let exact = rational::lpi_orient2d(&l, b, c, axis);
435 if let Some(s) = interval::lpi_orient2d(&l, b, c, axis) {
436 assert_eq!(s, exact, "orient2d interval returned a WRONG definite sign for {l:?}");
437 }
438 assert_eq!(orient2d(&ImplicitPoint::Lpi(l), &e(b), &e(c), axis), exact);
440 }
441 }
442
443 #[test]
444 fn lpi_point_lies_on_its_defining_plane() {
445 for (l, _, _, _) in lpi_cases() {
449 assert_eq!(
450 orient3d(&ImplicitPoint::Lpi(l), &e(l.r), &e(l.s), &e(l.t)),
451 Sign::Zero,
452 "LPI point is not on its defining plane R,S,T: {l:?}"
453 );
454 }
455 }
456
457 #[test]
458 fn tpi_point_lies_on_all_three_defining_planes() {
459 for (t, _, _, _) in tpi_cases() {
460 for plane in &t.planes {
461 assert_eq!(
462 orient3d(&ImplicitPoint::Tpi(t), &e(plane[0]), &e(plane[1]), &e(plane[2])),
463 Sign::Zero,
464 "TPI point is not on one of its defining planes: {t:?}"
465 );
466 }
467 }
468 }
469
470 #[test]
471 fn orient2d_1i_sign_invariant_to_plane_winding() {
472 for axis in AXES {
479 for (l, p2, p3, _p4) in lpi_cases() {
480 let rewound = Lpi { s: l.t, t: l.s, ..l };
481 assert_eq!(
482 rational::lpi_orient2d(&l, p2, p3, axis),
483 rational::lpi_orient2d(&rewound, p2, p3, axis),
484 "orient2d 1I sign changed under plane re-winding (axis {axis:?})"
485 );
486 }
487 }
488 }
489
490 #[test]
491 fn multi_implicit_orient2d_matches_materialised_oracle() {
492 let mut pts: Vec<ImplicitPoint> =
495 lpi_cases().into_iter().map(|(l, ..)| ImplicitPoint::Lpi(l)).collect();
496 pts.extend(tpi_cases().into_iter().map(|(t, ..)| ImplicitPoint::Tpi(t)));
497 let c = [1.3, -0.7, 2.1];
498 let cpt = rational::point_of(&ImplicitPoint::Explicit(c));
499 for axis in AXES {
500 for a in &pts {
501 for b in &pts {
502 let oracle = rational::orient2d_pts(
503 &rational::point_of(a), &rational::point_of(b), &cpt, axis);
504 assert_eq!(rational::orient2d_2i(a, b, c, axis), oracle, "orient2d_2i (axis {axis:?})");
505 for cc in &pts {
506 let oracle3 = rational::orient2d_pts(
507 &rational::point_of(a), &rational::point_of(b), &rational::point_of(cc), axis);
508 assert_eq!(rational::orient2d_3i(a, b, cc, axis), oracle3, "orient2d_3i (axis {axis:?})");
509 }
510 }
511 }
512 }
513 }
514
515 #[test]
516 fn multi_implicit_orient2d_winding_invariant() {
517 let l = lpi_cases()[2].0;
519 let a = ImplicitPoint::Lpi(l);
520 let a_rw = ImplicitPoint::Lpi(Lpi { s: l.t, t: l.s, ..l });
521 let b = ImplicitPoint::Tpi(tpi_cases()[0].0);
522 let cc = ImplicitPoint::Lpi(lpi_cases()[0].0);
523 let c = [0.4, 1.1, -0.3];
524 for axis in AXES {
525 assert_eq!(
526 rational::orient2d_2i(&a, &b, c, axis),
527 rational::orient2d_2i(&a_rw, &b, c, axis),
528 "2I sign changed under plane re-winding"
529 );
530 assert_eq!(
531 rational::orient2d_3i(&a, &b, &cc, axis),
532 rational::orient2d_3i(&a_rw, &b, &cc, axis),
533 "3I sign changed under plane re-winding"
534 );
535 }
536 }
537
538 #[test]
539 fn orient2d_any_matches_oracle_for_every_permutation_and_mix() {
540 let mut pts: Vec<ImplicitPoint> = vec![e([0.0, 0.0, 0.0]), e([3.0, 1.0, -2.0])];
544 pts.extend(lpi_cases().into_iter().take(2).map(|(l, ..)| ImplicitPoint::Lpi(l)));
545 pts.extend(tpi_cases().into_iter().take(2).map(|(t, ..)| ImplicitPoint::Tpi(t)));
546 for axis in AXES {
547 for a in &pts {
548 for b in &pts {
549 for c in &pts {
550 let got = orient2d_any(a, b, c, axis);
551 let want = rational::orient2d_pts(
552 &rational::point_of(a),
553 &rational::point_of(b),
554 &rational::point_of(c),
555 axis,
556 );
557 assert_eq!(got, want, "orient2d_any mismatch (axis {axis:?})");
558 }
559 }
560 }
561 }
562 }
563
564 #[test]
565 fn cmp_lex_matches_materialised_order_and_is_a_total_order() {
566 use std::cmp::Ordering;
567 let mut pts: Vec<ImplicitPoint> =
568 lpi_cases().into_iter().map(|(l, ..)| ImplicitPoint::Lpi(l)).collect();
569 pts.extend(tpi_cases().into_iter().map(|(t, ..)| ImplicitPoint::Tpi(t)));
570 pts.push(e([1.5, -2.0, 0.25]));
571 pts.push(e([0.0, 0.0, 0.0]));
572 let oracle = |a: &ImplicitPoint, b: &ImplicitPoint| -> Sign {
573 let (pa, pb) = (rational::point_of(a), rational::point_of(b));
574 for k in 0..3 {
575 match pa[k].cmp(&pb[k]) {
576 Ordering::Less => return Sign::Negative,
577 Ordering::Greater => return Sign::Positive,
578 Ordering::Equal => {}
579 }
580 }
581 Sign::Zero
582 };
583 for a in &pts {
584 assert_eq!(rational::cmp_lex(a, a), Sign::Zero, "cmp_lex not reflexive-zero");
585 for b in &pts {
586 assert_eq!(rational::cmp_lex(a, b), oracle(a, b), "cmp_lex != materialised lex");
587 assert_eq!(
588 rational::cmp_lex(a, b),
589 rational::cmp_lex(b, a).flip(),
590 "cmp_lex not antisymmetric"
591 );
592 }
593 }
594 for a in &pts {
596 for b in &pts {
597 for c in &pts {
598 if rational::cmp_lex(a, b) == Sign::Negative
599 && rational::cmp_lex(b, c) == Sign::Negative
600 {
601 assert_eq!(rational::cmp_lex(a, c), Sign::Negative, "cmp_lex not transitive");
602 }
603 }
604 }
605 }
606 }
607
608 #[test]
609 fn new_tier_interval_is_sound_and_cascade_equals_exact() {
610 use super::super::interval;
613 let mut rng = Lcg(0x0bad_c0de_1234_5678);
614 for _ in 0..500 {
615 let mk = |rng: &mut Lcg| Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
616 let a = ImplicitPoint::Lpi(mk(&mut rng));
617 let b = ImplicitPoint::Lpi(mk(&mut rng));
618 let cc = ImplicitPoint::Lpi(mk(&mut rng));
619 let c = rng.p();
620 for axis in AXES {
621 let ex2 = rational::orient2d_2i(&a, &b, c, axis);
622 if let Some(s) = interval::orient2d_2i(&a, &b, c, axis) {
623 assert_eq!(s, ex2, "orient2d_2i interval wrong sign");
624 }
625 assert_eq!(orient2d_2i(&a, &b, c, axis), ex2, "2i cascade != exact");
626 let ex3 = rational::orient2d_3i(&a, &b, &cc, axis);
627 if let Some(s) = interval::orient2d_3i(&a, &b, &cc, axis) {
628 assert_eq!(s, ex3, "orient2d_3i interval wrong sign");
629 }
630 assert_eq!(orient2d_3i(&a, &b, &cc, axis), ex3, "3i cascade != exact");
631 }
632 let exl = rational::cmp_lex(&a, &b);
633 if let Some(s) = interval::cmp_lex(&a, &b) {
634 assert_eq!(s, exl, "cmp_lex interval wrong sign");
635 }
636 assert_eq!(cmp_lex(&a, &b), exl, "cmp_lex cascade != exact");
637 }
638 }
639
640 #[test]
641 fn cmp_lex_welds_coincident_lpi_and_tpi() {
642 let lpi = ImplicitPoint::Lpi(Lpi {
645 p: [0.3, 0.4, -1.],
646 q: [0.3, 0.4, 1.],
647 r: [0., 0., 0.],
648 s: [1., 0., 0.],
649 t: [0., 1., 0.],
650 });
651 let tpi = ImplicitPoint::Tpi(Tpi {
652 planes: [
653 [[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.]], ],
657 });
658 assert_eq!(rational::point_of(&lpi), rational::point_of(&tpi), "test points not coincident");
659 assert_eq!(
660 rational::cmp_lex(&lpi, &tpi),
661 Sign::Zero,
662 "coincident LPI/TPI not welded by cmp_lex"
663 );
664 }
665
666 #[test]
667 fn cmp_along_matches_materialised_oracle() {
668 use num_rational::BigRational;
669 use num_traits::Signed;
670 let mut pts: Vec<ImplicitPoint> =
671 vec![e([0., 0., 0.]), e([3., 1., -2.]), e([1.5, -2., 0.25])];
672 pts.extend(lpi_cases().into_iter().take(2).map(|(l, ..)| ImplicitPoint::Lpi(l)));
673 pts.extend(tpi_cases().into_iter().take(2).map(|(t, ..)| ImplicitPoint::Tpi(t)));
674 let dirs = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [1., 2., -1.], [-0.3, 1.7, 0.5]];
675 for u in dirs {
676 let ur = [
677 BigRational::from_float(u[0]).unwrap(),
678 BigRational::from_float(u[1]).unwrap(),
679 BigRational::from_float(u[2]).unwrap(),
680 ];
681 for a in &pts {
682 for b in &pts {
683 let pa = rational::point_of(a);
684 let pb = rational::point_of(b);
685 let dot = (&pa[0] - &pb[0]) * &ur[0]
686 + (&pa[1] - &pb[1]) * &ur[1]
687 + (&pa[2] - &pb[2]) * &ur[2];
688 let want = if dot.is_negative() {
689 Sign::Negative
690 } else if dot.is_positive() {
691 Sign::Positive
692 } else {
693 Sign::Zero
694 };
695 assert_eq!(rational::cmp_along(a, b, u), want, "cmp_along != oracle (u={u:?})");
696 }
697 }
698 }
699 }
700}