simplicity/
lib.rs

1//! Implementation of [Simulation of Simplicity by Edelsbrunner and Mücke](https://arxiv.org/pdf/math/9410209.pdf)
2//!
3//! Simulation of simplicity is a technique for ignoring
4//! degeneracies when calculating geometric predicates,
5//! such as the orientation of one point with respect to a list of points.
6//! Each point **p**\_ *i* is perturbed by some polynomial
7//! in ε, a sufficiently small positive number.
8//! Specifically, coordinate *p\_(i,j)* is perturbed by ε^(3^(*d*\**i* - *j*)),
9//! where *d* is more than the number of dimensions.
10//!
11//! # Predicates
12//!
13//! ## Orientation
14//!
15//! The orientation of 2 points **p**\_0, **p**\_1 in 1-dimensional space is
16//! positive if **p**\_0 is to the right of **p**\_1 and negative otherwise.
17//! We don't consider the case where **p**\_0 = **p**\_1 because of the perturbations.
18//!
19//! The orientation of *n* points **p**\_0, ..., **p**\_(n - 1) in (n - 1)-dimensional space is
20//! the same as the orientation of **p**\_1, ..., **p**\_(n - 1) when looked at
21//! from **p**_0. In particular, the orientation of 3 points in 2-dimensional space
22//! is positive iff they form a left turn.
23//!
24//! Orientation predicates for 1, 2, and 3 dimensions are implemented.
25//! They return whether the orientation is positive.
26//!
27//! ## In Hypersphere
28//!
29//! The in-circle of 4 points measures whether the last point is inside
30//! the circle that goes through the first 3 points. Those 3 points
31//! are not collinear because of the perturbations.
32//!
33//! The in-sphere of 5 points measures whether the last point is inside
34//! the sphere that goes through the first 4 points. Those 4 points
35//! are not coplanar because of the perturbations.
36//!
37//! # Usage
38//!
39//! ```rust
40//! use simplicity::{nalgebra, orient_2d};
41//! use nalgebra::Vector2;
42//!
43//! let points = vec![
44//!     Vector2::new(0.0, 0.0),
45//!     Vector2::new(1.0, 0.0),
46//!     Vector2::new(1.0, 1.0),
47//!     Vector2::new(0.0, 1.0),
48//!     Vector2::new(2.0, 0.0),
49//! ];
50//!
51//! // Positive orientation
52//! let result = orient_2d(&points, |l, i| l[i], 0, 1, 2);
53//! assert!(result);
54//!
55//! // Negative orientation
56//! let result = orient_2d(&points, |l, i| l[i], 0, 3, 2);
57//! assert!(!result);
58//!
59//! // Degenerate orientation, tie broken by perturbance
60//! let result = orient_2d(&points, |l, i| l[i], 0, 1, 4);
61//! assert!(result);
62//! let result = orient_2d(&points, |l, i| l[i], 4, 1, 0);
63//! assert!(!result);
64//! ```
65//!
66//! Because the predicates take an indexing function, this can be
67//! used for arbitrary lists without having to implement `Index` for them:
68//!
69//! ```rust
70//! # use simplicity::{nalgebra, orient_2d};
71//! # use nalgebra::Vector2;
72//! let points = vec![
73//!     (Vector2::new(0.0, 0.0), 0.8),
74//!     (Vector2::new(1.0, 0.0), 0.4),
75//!     (Vector2::new(2.0, 0.0), 0.6),
76//! ];
77//!
78//! let result = orient_2d(&points, |l, i| l[i].0, 0, 1, 2);
79//! ```
80
81use robust_geo as rg;
82pub use nalgebra;
83
84use nalgebra::{Vector1, Vector2, Vector3};
85type Vec1 = Vector1<f64>;
86type Vec2 = Vector2<f64>;
87type Vec3 = Vector3<f64>;
88
89macro_rules! sorted_fn {
90    ($name:ident, $n:expr) => {
91        /// Sorts an array of $n elements
92        /// and returns the sorted array,
93        /// along with the parity of the permutation;
94        /// `false` if even and `true` if odd.
95        fn $name<Idx: Ord + Copy>(mut arr: [Idx; $n]) -> ([Idx; $n], bool) {
96            let mut num_swaps = 0;
97
98            for i in 1..$n {
99                for j in (0..i).rev() {
100                    if arr[j] > arr[j + 1] {
101                        arr.swap(j, j + 1);
102                        num_swaps += 1;
103                    } else {
104                        break;
105                    }
106                }
107            }
108            (arr, num_swaps % 2 != 0)
109        }
110    };
111}
112
113sorted_fn!(sorted_3, 3);
114sorted_fn!(sorted_4, 4);
115sorted_fn!(sorted_5, 5);
116
117/// Returns whether the orientation of 2 points in 1-dimensional space
118/// is positive after perturbing them; that is, if the 1st one is
119/// to the right of the 2nd one.
120///
121/// Takes a list of all the points in consideration, an indexing function,
122/// and 2 indexes to the points to calculate the orientation of.
123///
124/// # Example
125///
126/// ```
127/// # use simplicity::{nalgebra, orient_1d};
128/// # use nalgebra::Vector1;
129/// let points = vec![0.0, 1.0, 2.0, 1.0];
130/// let positive = orient_1d(&points, |l, i| Vector1::new(l[i]), 1, 3);
131/// // points[1] gets perturbed farther to the right than points[3]
132/// assert!(positive);
133/// ```
134pub fn orient_1d<T: ?Sized, Idx: Ord + Copy>(
135    list: &T,
136    index_fn: impl Fn(&T, Idx) -> Vec1,
137    i: Idx,
138    j: Idx,
139) -> bool {
140    let pi = index_fn(list, i);
141    let pj = index_fn(list, j);
142    pi > pj || (pi == pj && i < j)
143}
144
145macro_rules! case {
146    (2: $pi:ident, $pj:ident, @ m2, != $odd:expr) => {
147        let val = rg::magnitude_cmp_2d($pi, $pj);
148        if val != 0.0 {
149            return (val > 0.0) != $odd;
150        }
151    };
152
153    (2: $pi:ident, $pj:ident, @ m3, != $odd:expr) => {
154        let val = rg::magnitude_cmp_3d($pi, $pj);
155        if val != 0.0 {
156            return (val > 0.0) != $odd;
157        }
158    };
159
160    (2: $pi:ident, $pj:ident, $(@ $swiz:ident,)? != $odd:expr) => {
161        if $pi$(.$swiz)? != $pj$(.$swiz)? {
162            return ($pi$(.$swiz)? > $pj$(.$swiz)?) != $odd;
163        }
164    };
165
166    (3: $pi:ident, $pj:ident, $pk:ident, @ $swiz:ident m2, != $odd:expr) => {
167        let val = rg::sign_det_x_x2y2($pi.$swiz(), $pj.$swiz(), $pk.$swiz());
168        if val != 0.0 {
169            return (val > 0.0) != $odd;
170        }
171    };
172
173    (3: $pi:ident, $pj:ident, $pk:ident, @ $swiz:ident m3, != $odd:expr) => {
174        let val = rg::sign_det_x_x2y2z2($pi.$swiz(), $pj.$swiz(), $pk.$swiz());
175        if val != 0.0 {
176            return (val > 0.0) != $odd;
177        }
178    };
179
180    (3: $pi:ident, $pj:ident, $pk:ident, $(@ $swiz:ident,)? != $odd:expr) => {
181        let val = rg::orient_2d($pi$(.$swiz())?, $pj$(.$swiz())?, $pk$(.$swiz())?);
182        if val != 0.0 {
183            return (val > 0.0) != $odd;
184        }
185    };
186
187    (4: $pi:ident, $pj:ident, $pk:ident, $pl:ident, @ xy m2, != $odd:expr) => {
188        let val = rg::in_circle($pi, $pj, $pk, $pl);
189        if val != 0.0 {
190            return (val > 0.0) != $odd;
191        }
192    };
193
194    (4: $pi:ident, $pj:ident, $pk:ident, $pl:ident, @ $swiz:ident m3, != $odd:expr) => {
195        let val = rg::sign_det_x_y_x2y2z2($pi.$swiz(), $pj.$swiz(), $pk.$swiz(), $pl.$swiz());
196        if val != 0.0 {
197            return (val > 0.0) != $odd;
198        }
199    };
200
201    (4: $pi:ident, $pj:ident, $pk:ident, $pl:ident, $(@ $swiz:ident,)? != $odd:expr) => {
202        let val = rg::orient_3d($pi$(.$swiz())?, $pj$(.$swiz())?, $pk$(.$swiz())?, $pl$(.$swiz())?);
203        if val != 0.0 {
204            return (val > 0.0) != $odd;
205        }
206    };
207
208    (5: $pi:ident, $pj:ident, $pk:ident, $pl:ident, $pm:ident, @ xyz m3, != $odd:expr) => {
209        let val = rg::in_sphere($pi, $pj, $pk, $pl, $pm);
210        if val != 0.0 {
211            return (val > 0.0) != $odd;
212        }
213    };
214
215}
216
217/// Returns whether the orientation of 3 points in 2-dimensional space
218/// is positive after perturbing them; that is, if the 3 points
219/// form a left turn when visited in order.
220///
221/// Takes a list of all the points in consideration, an indexing function,
222/// and 3 indexes to the points to calculate the orientation of.
223///
224/// # Example
225///
226/// ```
227/// # use simplicity::{nalgebra, orient_2d};
228/// # use nalgebra::Vector2;
229/// let points = vec![
230///     Vector2::new(0.0, 0.0),
231///     Vector2::new(1.0, 0.0),
232///     Vector2::new(1.0, 1.0),
233///     Vector2::new(2.0, 2.0),
234/// ];
235/// let positive = orient_2d(&points, |l, i| l[i], 0, 1, 2);
236/// assert!(positive);
237/// let positive = orient_2d(&points, |l, i| l[i], 0, 3, 2);
238/// assert!(!positive);
239/// ```
240pub fn orient_2d<T: ?Sized, Idx: Ord + Copy>(
241    list: &T,
242    index_fn: impl Fn(&T, Idx) -> Vec2,
243    i: Idx,
244    j: Idx,
245    k: Idx,
246) -> bool {
247    let ([i, j, k], odd) = sorted_3([i, j, k]);
248    let pi = index_fn(list, i);
249    let pj = index_fn(list, j);
250    let pk = index_fn(list, k);
251
252    case!(3: pi, pj, pk, != odd);
253    case!(2: pk, pj, @ x, != odd);
254    case!(2: pj, pk, @ y, != odd);
255    case!(2: pi, pk, @ x, != odd);
256    !odd
257}
258
259/// Returns whether the orientation of 4 points in 3-dimensional space
260/// is positive after perturbing them; that is, if the last 3 points
261/// form a left turn when visited in order, looking from the first point.
262///
263/// Takes a list of all the points in consideration, an indexing function,
264/// and 4 indexes to the points to calculate the orientation of.
265///
266/// # Example
267///
268/// ```
269/// # use simplicity::{nalgebra, orient_3d};
270/// # use nalgebra::Vector3;
271/// let points = vec![
272///     Vector3::new(0.0, 0.0, 0.0),
273///     Vector3::new(1.0, 0.0, 0.0),
274///     Vector3::new(1.0, 1.0, 1.0),
275///     Vector3::new(2.0, -2.0, 0.0),
276///     Vector3::new(2.0, 3.0, 4.0),
277///     Vector3::new(0.0, 0.0, 1.0),
278///     Vector3::new(0.0, 1.0, 0.0),
279///     Vector3::new(3.0, 4.0, 5.0),
280/// ];
281/// let positive = orient_3d(&points, |l, i| l[i], 0, 1, 6, 5);
282/// assert!(!positive);
283/// let positive = orient_3d(&points, |l, i| l[i], 7, 4, 0, 2);
284/// assert!(positive);
285/// ```
286pub fn orient_3d<T: ?Sized, Idx: Ord + Copy>(
287    list: &T,
288    index_fn: impl Fn(&T, Idx) -> Vec3,
289    i: Idx,
290    j: Idx,
291    k: Idx,
292    l: Idx,
293) -> bool {
294    let ([i, j, k, l], odd) = sorted_4([i, j, k, l]);
295    let pi = index_fn(list, i);
296    let pj = index_fn(list, j);
297    let pk = index_fn(list, k);
298    let pl = index_fn(list, l);
299
300    case!(4: pi, pj, pk, pl, != odd);
301    case!(3: pj, pk, pl, @ xy, != odd);
302    case!(3: pj, pk, pl, @ zx, != odd);
303    case!(3: pj, pk, pl, @ yz, != odd);
304    case!(3: pi, pk, pl, @ yx, != odd);
305    case!(2: pk, pl, @ x, != odd);
306    case!(2: pl, pk, @ y, != odd);
307    case!(3: pi, pk, pl, @ xz, != odd);
308    case!(2: pk, pl, @ z, != odd);
309    // case!(3: pi, pk, pl, @ zy, != odd); Impossible
310    case!(3: pi, pj, pl, @ xy, != odd);
311    case!(2: pl, pj, @ x, != odd);
312    case!(2: pj, pl, @ y, != odd);
313    case!(2: pi, pl, @ x, != odd);
314    !odd
315}
316
317/// Returns whether the last point is inside the oriented circle that goes through
318/// the first 3 points after perturbing them.
319/// The first 3 points should be oriented positive or the result will be flipped.
320///
321/// Takes a list of all the points in consideration, an indexing function,
322/// and 4 indexes to the points to calculate the in-circle of.
323///
324/// # Example
325///
326/// ```
327/// # use simplicity::{nalgebra, in_circle};
328/// # use nalgebra::Vector2;
329/// let points = vec![
330///     Vector2::new(0.0, 2.0),
331///     Vector2::new(1.0, 1.0),
332///     Vector2::new(2.0, 1.0),
333///     Vector2::new(0.0, 0.0),
334///     Vector2::new(2.0, 3.0),
335/// ];
336/// let inside = in_circle(&points, |l, i| l[i], 0, 3, 2, 1);
337/// assert!(inside);
338/// let inside = in_circle(&points, |l, i| l[i], 2, 1, 3, 4);
339/// assert!(!inside);
340/// ```
341pub fn in_circle<T: ?Sized, Idx: Ord + Copy>(
342    list: &T,
343    index_fn: impl Fn(&T, Idx) -> Vec2 + Clone,
344    i: Idx,
345    j: Idx,
346    k: Idx,
347    l: Idx,
348) -> bool {
349    simplicity_derive::generate_in_hypersphere!{list, index_fn, i, j, k, l}
350    // let flip = !orient_2d(list, index_fn.clone(), i, j, k);
351    // let ([i, j, k, l], odd) = sorted_4([i, j, k, l]);
352    // let odd = odd != flip;
353
354    // let pi = index_fn(list, i);
355    // let pj = index_fn(list, j);
356    // let pk = index_fn(list, k);
357    // let pl = index_fn(list, l);
358
359    // case!(4: pi, pj, pk, pl, @ xy m2, != odd);
360    // case!(3: pj, pk, pl, @ xy, != odd);
361    // case!(3: pj, pl, pk, @ xy m2, != odd);
362    // case!(3: pj, pk, pl, @ yx m2, != odd);
363    // case!(3: pi, pk, pl, @ yx, != odd);
364    // case!(2: pk, pl, @ x, != odd);
365    // case!(2: pl, pk, @ y, != odd);
366    // // case!(3: pi, pk, pl, @ xy m2, != odd); Impossible
367    // // case!(2: pk, pl, @ m2, != odd); Impossible
368    // // case!(3: pi, pk, pl, @ zy, != odd); Impossible
369    // case!(3: pi, pj, pl, @ xy, != odd);
370    // case!(2: pl, pj, @ x, != odd);
371    // case!(2: pj, pl, @ y, != odd);
372    // case!(2: pi, pl, @ x, != odd);
373    // !odd
374}
375
376/// Returns whether the last point is inside the circle that goes through
377/// the first 3 points after perturbing them.
378///
379/// Takes a list of all the points in consideration, an indexing function,
380/// and 4 indexes to the points to calculate the in-circle of.
381///
382/// # Example
383///
384/// ```
385/// # use simplicity::{nalgebra, in_circle_unoriented};
386/// # use nalgebra::Vector2;
387/// let points = vec![
388///     Vector2::new(0.0, 2.0),
389///     Vector2::new(1.0, 1.0),
390///     Vector2::new(2.0, 1.0),
391///     Vector2::new(0.0, 0.0),
392///     Vector2::new(2.0, 3.0),
393/// ];
394/// let inside = in_circle_unoriented(&points, |l, i| l[i], 0, 2, 3, 1);
395/// assert!(inside);
396/// let inside = in_circle_unoriented(&points, |l, i| l[i], 2, 3, 1, 4);
397/// assert!(!inside);
398/// ```
399pub fn in_circle_unoriented<T: ?Sized, Idx: Ord + Copy>(
400    list: &T,
401    index_fn: impl Fn(&T, Idx) -> Vec2 + Clone,
402    i: Idx,
403    j: Idx,
404    k: Idx,
405    l: Idx,
406) -> bool {
407    orient_2d(list, index_fn.clone(), i, j, k) == in_circle(list, index_fn, i, j, k, l)
408}
409
410/// Returns whether the last point is inside the sphere that goes through
411/// the first 4 points after perturbing them.
412///
413/// Takes a list of all the points in consideration, an indexing function,
414/// and 5 indexes to the points to calculate the in-sphere of.
415///
416/// # Example
417///
418/// ```
419/// # use simplicity::{nalgebra, in_sphere};
420/// # use nalgebra::Vector3;
421/// let points = vec![
422///     Vector3::new(0.0, 0.0, 0.0),
423///     Vector3::new(4.0, 0.0, 0.0),
424///     Vector3::new(0.0, 4.0, 0.0),
425///     Vector3::new(0.0, 0.0, 4.0),
426///     Vector3::new(1.0, 1.0, 1.0),
427/// ];
428/// let inside = in_sphere(&points, |l, i| l[i], 0, 2, 1, 3, 4);
429/// assert!(inside);
430/// let inside = in_sphere(&points, |l, i| l[i], 2, 3, 1, 4, 0);
431/// assert!(!inside);
432/// ```
433pub fn in_sphere<T: ?Sized, Idx: Ord + Copy>(
434    list: &T,
435    index_fn: impl Fn(&T, Idx) -> Vec3 + Clone,
436    i: Idx,
437    j: Idx,
438    k: Idx,
439    l: Idx,
440    m: Idx,
441) -> bool {
442    simplicity_derive::generate_in_hypersphere!{list, index_fn, i, j, k, l, m}
443    // let flip = !orient_3d(list, index_fn.clone(), i, j, k, l);
444    // let ([i, j, k, l, m], odd) = sorted_5([i, j, k, l, m]);
445    // let odd = odd != flip;
446
447    // let pi = index_fn(list, i);
448    // let pj = index_fn(list, j);
449    // let pk = index_fn(list, k);
450    // let pl = index_fn(list, l);
451    // let pm = index_fn(list, m);
452
453    // case!(5: pi, pj, pk, pl, pm, @ xyz m3, != odd);
454    // case!(4: pj, pk, pm, pl, != odd);
455    // case!(4: pj, pk, pl, pm, @ xyz m3, != odd);
456    // case!(4: pj, pk, pl, pm, @ zxy m3, != odd);
457    // case!(4: pj, pk, pl, pm, @ yzx m3, != odd);
458    // case!(4: pi, pk, pl, pm, != odd);
459    // case!(3: pk, pl, pm, @ xy, != odd);
460    // case!(3: pk, pl, pm, @ zx, != odd);
461    // case!(3: pk, pl, pm, @ yz, != odd);
462    // case!(4: pi, pk, pl, pm, @ yxz m3, != odd);
463    // case!(3: pk, pl, pm, @ xyz m3, != odd);
464    // case!(3: pk, pm, pl, @ yzx m3, != odd);
465    // case!(4: pi, pk, pl, pm, @ xzy m3, != odd);
466    // case!(3: pk, pl, pm, @ zxy m3, != odd);
467    // case!(4: pi, pk, pl, pm, @ zyx m3, != odd);
468    // case!(4: pi, pj, pm, pl, != odd);
469    // case!(3: pj, pl, pm, @ yx, != odd);
470    // case!(3: pj, pl, pm, @ xz, != odd);
471    // case!(3: pj, pl, pm, @ zy, != odd);
472    // case!(3: pi, pl, pm, @ xy, != odd);
473    // case!(2: pm, pl, @ x, != odd);
474    // case!(2: pl, pm, @ y, != odd);
475    // case!(3: pi, pl, pm, @ zx, != odd);
476    // case!(2: pm, pl, @ z, != odd);
477    // case!(3: pi, pl, pm, @ yz, != odd);
478    // case!(4: pi, pj, pl, pm, @ xyz m3, != odd);
479    // case!(3: pj, pm, pl, @ xyz m3, != odd);
480    // case!(3: pj, pl, pm, @ yzx m3, != odd);
481    // case!(3: pi, pl, pm, @ xyz m3, != odd);
482    // case!(2: pl, pm, @ m3, != odd);
483    // case!(3: pi, pm, pl, @ yzx m3, != odd);
484    // case!(4: pi, pj, pl, pm, @ zxy m3, != odd);
485    // case!(3: pj, pm, pl, @ zxy m3, != odd);
486    // case!(3: pi, pl, pm, @ zxy m3, != odd);
487    // case!(4: pi, pj, pl, pm, @ yzx m3, != odd);
488    // case!(4: pi, pj, pk, pm, != odd);
489    // case!(3: pj, pk, pm, @ xy, != odd);
490    // case!(3: pj, pk, pm, @ zx, != odd);
491    // case!(3: pj, pk, pm, @ yz, != odd);
492    // case!(3: pi, pk, pm, @ yx, != odd);
493    // case!(2: pk, pm, @ x, != odd);
494    // case!(2: pm, pk, @ y, != odd);
495    // case!(3: pi, pk, pm, @ xz, != odd);
496    // case!(2: pk, pm, @ z, != odd);
497    // // case!(3: pi, pk, pm, @ zy, != odd); Impossible
498    // case!(3: pi, pj, pm, @ xy, != odd);
499    // case!(2: pm, pj, @ x, != odd);
500    // case!(2: pj, pm, @ y, != odd);
501    // case!(2: pi, pm, @ x, != odd);
502    // !odd
503}
504
505/// Returns whether the last point is inside the sphere that goes through
506/// the first 4 points after perturbing them.
507/// The first 4 points must be oriented positive or the result will be flipped.
508///
509/// Takes a list of all the points in consideration, an indexing function,
510/// and 5 indexes to the points to calculate the in-sphere of.
511///
512/// # Example
513///
514/// ```
515/// # use simplicity::{nalgebra, in_sphere_unoriented};
516/// # use nalgebra::Vector3;
517/// let points = vec![
518///     Vector3::new(0.0, 0.0, 0.0),
519///     Vector3::new(4.0, 0.0, 0.0),
520///     Vector3::new(0.0, 4.0, 0.0),
521///     Vector3::new(0.0, 0.0, 4.0),
522///     Vector3::new(1.0, 1.0, 1.0),
523/// ];
524/// let inside = in_sphere_unoriented(&points, |l, i| l[i], 0, 2, 3, 1, 4);
525/// assert!(inside);
526/// let inside = in_sphere_unoriented(&points, |l, i| l[i], 2, 3, 1, 4, 0);
527/// assert!(!inside);
528/// ```
529pub fn in_sphere_unoriented<T: ?Sized, Idx: Ord + Copy>(
530    list: &T,
531    index_fn: impl Fn(&T, Idx) -> Vec3 + Clone,
532    i: Idx,
533    j: Idx,
534    k: Idx,
535    l: Idx,
536    m: Idx,
537) -> bool {
538    orient_3d(list, index_fn.clone(), i, j, k, l) == in_sphere(list, index_fn, i, j, k, l, m)
539}
540
541///// Returns whether the last point is closer to the second point
542///// than it is to the first point.
543/////
544///// Takes a list of all the points in consideration, an indexing function,
545///// and 3 indexes to the points to calculate the distance-compare-3d of.
546//pub fn distance_cmp_3d<T: ?Sized>(
547//    list: &T,
548//    index_fn: impl Fn(&T, usize) -> Vec3 + Clone,
549//    i: usize,
550//    j: usize,
551//    k: usize,
552//) -> bool {
553//    let pi = index_fn(list, i);
554//    let pj = index_fn(list, j);
555//    let pk = index_fn(list, k);
556//
557//    let val = rg::distance_cmp_3d(pi, pj, pk);
558//    if val != 0.0 {
559//        return val > 0.0;
560//    }
561//
562//    const DUMMY: bool = false;
563//    if k < i && k < j {
564//        case!(2: pj, pi, @ z, != DUMMY);
565//        case!(2: pj, pi, @ y, != DUMMY);
566//        case!(2: pj, pi, @ x, != DUMMY);
567//    }
568//
569//    return i < j
570//}
571
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use test_case::test_case;
577
578    // Test-specific to determine case reached
579    macro_rules! case {
580        ($arr:expr => $pi:ident, $pj:ident, @ m2) => {
581            let val = rg::magnitude_cmp_2d($pi, $pj);
582            if val != 0.0 {
583                return $arr;
584            }
585        };
586
587        ($arr:expr => $pi:ident, $pj:ident, @ m3) => {
588            let val = rg::magnitude_cmp_3d($pi, $pj);
589            if val != 0.0 {
590                return $arr;
591            }
592        };
593
594        ($arr:expr => $pi:ident, $pj:ident $(, @ $swiz:ident)?) => {
595            if $pi$(.$swiz)? != $pj$(.$swiz)? {
596                return $arr;
597            }
598        };
599
600        ($arr:expr => $pi:ident, $pj:ident, $pk:ident, @ $swiz:ident m2) => {
601            let val = rg::sign_det_x_x2y2($pi.$swiz(), $pj.$swiz(), $pk.$swiz());
602            if val != 0.0 {
603                return $arr;
604            }
605        };
606
607        ($arr:expr => $pi:ident, $pj:ident, $pk:ident, @ $swiz:ident m3) => {
608            let val = rg::sign_det_x_x2y2z2($pi.$swiz(), $pj.$swiz(), $pk.$swiz());
609            if val != 0.0 {
610                return $arr;
611            }
612        };
613
614        ($arr:expr => $pi:ident, $pj:ident, $pk:ident $(, @ $swiz:ident)?) => {
615            let val = rg::orient_2d($pi$(.$swiz())?, $pj$(.$swiz())?, $pk$(.$swiz())?);
616            if val != 0.0 {
617                return $arr;
618            }
619        };
620
621        ($arr:expr => $pi:ident, $pj:ident, $pk:ident, $pl:ident, @ xy m2) => {
622            let val = rg::in_circle($pi, $pj, $pk, $pl);
623            if val != 0.0 {
624                return $arr;
625            }
626        };
627
628        ($arr:expr => $pi:ident, $pj:ident, $pk:ident, $pl:ident, @ $swiz:ident m3) => {
629            let val = rg::sign_det_x_y_x2y2z2($pi.$swiz(), $pj.$swiz(), $pk.$swiz(), $pl.$swiz());
630            if val != 0.0 {
631                return $arr;
632            }
633        };
634
635        ($arr:expr => $pi:ident, $pj:ident, $pk:ident, $pl:ident $(, @ $swiz:ident)?) => {
636            let val = rg::orient_3d($pi$(.$swiz())?, $pj$(.$swiz())?, $pk$(.$swiz())?, $pl$(.$swiz())?);
637            if val != 0.0 {
638                return $arr;
639            }
640        };
641
642        ($arr:expr => $pi:ident, $pj:ident, $pk:ident, $pl:ident, $pm:ident, @ xyz m3) => {
643            let val = rg::in_sphere($pi, $pj, $pk, $pl, $pm);
644            if val != 0.0 {
645                return $arr;
646            }
647        };
648    }
649
650    // Copied from orient_2d
651    pub fn orient_2d_case<T: ?Sized>(
652        list: &T,
653        index_fn: impl Fn(&T, usize) -> Vec2,
654        i: usize,
655        j: usize,
656        k: usize,
657    ) -> [usize; 3] {
658        let ([i, j, k], _) = sorted_3([i, j, k]);
659        let pi = index_fn(list, i);
660        let pj = index_fn(list, j);
661        let pk = index_fn(list, k);
662
663        case!([3, 3, 3] => pi, pj, pk);
664        case!([2, 3, 3] => pk, pj, @ x);
665        case!([1, 3, 3] => pj, pk, @ y);
666        case!([2, 2, 3] => pi, pk, @ x);
667        [1, 2, 3]
668    }
669
670    // Copied from orient_3d
671    pub fn orient_3d_case<T: ?Sized>(
672        list: &T,
673        index_fn: impl Fn(&T, usize) -> Vec3,
674        i: usize,
675        j: usize,
676        k: usize,
677        l: usize,
678    ) -> [usize; 4] {
679        let ([i, j, k, l], _) = sorted_4([i, j, k, l]);
680        let pi = index_fn(list, i);
681        let pj = index_fn(list, j);
682        let pk = index_fn(list, k);
683        let pl = index_fn(list, l);
684
685        case!([4, 4, 4, 4] => pi, pj, pk, pl);
686        case!([3, 4, 4, 4] => pj, pk, pl, @ xy);
687        case!([2, 4, 4, 4] => pj, pk, pl, @ zx);
688        case!([1, 4, 4, 4] => pj, pk, pl, @ yz);
689        case!([3, 3, 4, 4] => pi, pk, pl, @ yx);
690        case!([2, 3, 4, 4] => pk, pl, @ x);
691        case!([1, 3, 4, 4] => pl, pk, @ y);
692        case!([2, 2, 4, 4] => pi, pk, pl, @ xz);
693        case!([1, 2, 4, 4] => pk, pl, @ z);
694        //case!([1, 1, 4, 4] => pi, pk, pl, @ zy); Impossible
695        case!([3, 3, 3, 4] => pi, pj, pl, @ xy);
696        case!([2, 3, 3, 4] => pl, pj, @ x);
697        case!([1, 3, 3, 4] => pj, pl, @ y);
698        case!([2, 2, 3, 4] => pi, pl, @ x);
699        [1, 2, 3, 4]
700    }
701
702    #[test]
703    fn orient_1d_positive() {
704        let points = vec![0.0, 1.0];
705        assert!(orient_1d(&points, |l, i| Vector1::new(l[i]), 1, 0))
706    }
707
708    #[test]
709    fn orient_1d_negative() {
710        let points = vec![0.0, 1.0];
711        assert!(!orient_1d(&points, |l, i| Vector1::new(l[i]), 0, 1))
712    }
713
714    #[test]
715    fn orient_1d_positive_degenerate() {
716        let points = vec![0.0, 0.0];
717        assert!(orient_1d(&points, |l, i| Vector1::new(l[i]), 0, 1))
718    }
719
720    #[test]
721    fn orient_1d_negative_degenerate() {
722        let points = vec![0.0, 0.0];
723        assert!(!orient_1d(&points, |l, i| Vector1::new(l[i]), 1, 0))
724    }
725
726    #[test_case([[0.0, 0.0], [1.0, 0.0], [2.0, 1.0]], [3,3,3] ; "General")]
727    #[test_case([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]], [2,3,3] ; "Collinear")]
728    #[test_case([[0.0, 0.0], [0.0, 2.0], [0.0, 1.0]], [1,3,3] ; "Collinear, pj.x = pk.x")]
729    #[test_case([[1.0, 0.0], [0.0, 2.0], [0.0, 2.0]], [2,2,3] ; "pj = pk")]
730    #[test_case([[0.0, 0.0], [0.0, 2.0], [0.0, 2.0]], [1,2,3] ; "pj = pk, pi.x = pk.x")]
731    fn test_orient_2d(points: [[f64; 2]; 3], case: [usize; 3]) {
732        let points = points
733            .iter()
734            .copied()
735            .map(Vector2::from)
736            .collect::<Vec<_>>();
737        assert!(orient_2d(&points, |l, i| l[i], 0, 1, 2));
738        assert!(!orient_2d(&points, |l, i| l[i], 0, 2, 1));
739        assert!(!orient_2d(&points, |l, i| l[i], 1, 0, 2));
740        assert!(orient_2d(&points, |l, i| l[i], 1, 2, 0));
741        assert!(orient_2d(&points, |l, i| l[i], 2, 0, 1));
742        assert!(!orient_2d(&points, |l, i| l[i], 2, 1, 0));
743        assert_eq!(orient_2d_case(&points, |l, i| l[i], 0, 1, 2), case);
744    }
745
746    #[test_case([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], [4,4,4,4] ; "General")]
747    #[test_case([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [3.0, 4.0, 5.0], [2.0, 3.0, 4.0]], [3,4,4,4] ; "Coplanar")]
748    #[test_case([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 2.0, 4.0], [3.0, 3.0, 5.0]], [2,4,4,4] ; "Coplanar, pj pk pl @ xy collinear")]
749    #[test_case([[1.0, 0.0, 0.0], [1.0, 1.0, 1.0], [1.0, 4.0, 2.0], [1.0, 5.0, 3.0]], [1,4,4,4] ; "Coplanar, pj.x = pk.x = pl.x or pj pk pl collinear")]
750    #[test_case([[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [2.0, 3.0, 4.0], [3.0, 4.0, 5.0]], [3,3,4,4] ; "pj pk pl collinear")]
751    #[test_case([[0.0, 0.0, 0.0], [1.0, 1.0, 3.0], [3.0, 3.0, 5.0], [2.0, 2.0, 4.0]], [2,3,4,4] ; "pj pk pl collinear, pi pk pl @ xy collinear")]
752    #[test_case([[0.0, 0.0, 0.0], [0.0, 1.0, 3.0], [0.0, 2.0, 4.0], [0.0, 3.0, 5.0]], [1,3,4,4] ; "pj pk pl collinear, pi pk pl @ xy collinear, pk.x = pl.x")]
753    #[test_case([[1.0, 0.0, 0.0], [0.0, 2.0, 3.0], [0.0, 2.0, 5.0], [0.0, 2.0, 4.0]], [2,2,4,4] ; "pj pk pl collinear, pi pk pl @ xy collinear, pk.xy = pl.xy")]
754    #[test_case([[0.0, 0.0, 0.0], [0.0, 2.0, 3.0], [0.0, 2.0, 4.0], [0.0, 2.0, 3.0]], [1,2,4,4] ; "pj pk pl collinear, pi.x = pk.x = pl.x or pi pk pl collinear, pk.xy = pl.xy")]
755    //                                                                              , [1,1,4,4] ; "pk = pl and pi pk pl @ yz not collinear is impossible
756    #[test_case([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 1.0, 0.0], [2.0, 1.0, 0.0]], [3,3,3,4] ; "pk = pl")]
757    #[test_case([[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 2.0, 0.0], [2.0, 2.0, 0.0]], [2,3,3,4] ; "pk = pl, pi pj pk @ xy collinear")]
758    #[test_case([[0.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], [1,3,3,4] ; "pk = pl, pi pj pk @ xy collinear, pj.x = pk.x")]
759    #[test_case([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 2.0, 0.0], [0.0, 2.0, 0.0]], [2,2,3,4] ; "pk = pl, pi pj pk @ xy collinear, pj.xy = pk.xy")]
760    #[test_case([[0.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 2.0, 0.0], [0.0, 2.0, 0.0]], [1,2,3,4] ; "pk = pl, pi pj pk @ xy collinear, pj.xy = pk.xy, pi.x = pk.x")]
761    fn test_orient_3d(points: [[f64; 3]; 4], case: [usize; 4]) {
762        let points = points
763            .iter()
764            .copied()
765            .map(Vector3::from)
766            .collect::<Vec<_>>();
767        // Trusting the insertion sort now
768        assert!(orient_3d(&points, |l, i| l[i], 0, 1, 2, 3));
769        assert!(!orient_3d(&points, |l, i| l[i], 3, 2, 0, 1));
770        assert_eq!(orient_3d_case(&points, |l, i| l[i], 0, 1, 2, 3), case);
771    }
772
773    #[test]
774    fn test_in_circle_unoriented_general() {
775        let points = [[0.0, 0.0], [0.0, 2.0], [2.0, 2.0], [1.0, 1.0]];
776        let points = points
777            .iter()
778            .copied()
779            .map(Vector2::from)
780            .collect::<Vec<_>>();
781        // Trusting the insertion sort now
782        assert!(in_circle_unoriented(&points, |l, i| l[i], 0, 1, 2, 3));
783        assert!(in_circle_unoriented(&points, |l, i| l[i], 0, 2, 1, 3));
784        assert!(in_circle_unoriented(&points, |l, i| l[i], 1, 2, 0, 3));
785        assert!(in_circle_unoriented(&points, |l, i| l[i], 1, 0, 2, 3));
786        assert!(in_circle_unoriented(&points, |l, i| l[i], 2, 0, 1, 3));
787        assert!(in_circle_unoriented(&points, |l, i| l[i], 2, 1, 0, 3));
788        assert!(
789            (in_circle_unoriented(&points, |l, i| l[i], 0, 1, 2, 3)
790                == in_circle_unoriented(&points, |l, i| l[i], 0, 1, 3, 2))
791                == (orient_2d(&points, |l, i| l[i], 0, 1, 3)
792                    != orient_2d(&points, |l, i| l[i], 0, 1, 2))
793        );
794    }
795
796    // Not sure how to test this properly in a non-tedious way.
797    // Let's just test the first degenerate expansion for now.
798    #[test]
799    fn test_in_circle_unoriented_cocircular() {
800        let points = [[0.0, 0.0], [0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
801        let points = points
802            .iter()
803            .copied()
804            .map(Vector2::from)
805            .collect::<Vec<_>>();
806        // Trusting the insertion sort now
807        assert!(in_circle_unoriented(&points, |l, i| l[i], 1, 2, 3, 0));
808        assert!(in_circle_unoriented(&points, |l, i| l[i], 1, 3, 2, 0));
809        assert!(in_circle_unoriented(&points, |l, i| l[i], 2, 3, 1, 0));
810        assert!(in_circle_unoriented(&points, |l, i| l[i], 1, 2, 3, 0));
811        assert!(in_circle_unoriented(&points, |l, i| l[i], 3, 1, 2, 0));
812        assert!(in_circle_unoriented(&points, |l, i| l[i], 3, 2, 1, 0));
813        assert!(
814            (in_circle_unoriented(&points, |l, i| l[i], 0, 1, 2, 3)
815                == in_circle_unoriented(&points, |l, i| l[i], 0, 1, 3, 2))
816                == (orient_2d(&points, |l, i| l[i], 0, 1, 3)
817                    != orient_2d(&points, |l, i| l[i], 0, 1, 2))
818        );
819    }
820
821    #[test]
822    fn test_in_sphere_unoriented_general() {
823        // Taking integers to shorten things
824        let points = [[0,0,0], [4,0,0], [0,4,0], [0,0,4], [1,1,1]];
825        let points = points
826            .iter()
827            .copied()
828            .map(|[x, y, z]| Vector3::new(x as f64, y as f64, z as f64))
829            .collect::<Vec<_>>();
830        // Trusting the insertion sort now
831        assert!(in_sphere_unoriented(&points, |l, i| l[i], 0, 1, 2, 3, 4));
832        assert!(in_sphere_unoriented(&points, |l, i| l[i], 0, 2, 1, 3, 4));
833        assert!(in_sphere_unoriented(&points, |l, i| l[i], 1, 2, 0, 3, 4));
834        assert!(in_sphere_unoriented(&points, |l, i| l[i], 1, 3, 0, 2, 4));
835        assert!(in_sphere_unoriented(&points, |l, i| l[i], 2, 3, 0, 1, 4));
836        assert!(in_sphere_unoriented(&points, |l, i| l[i], 2, 3, 1, 0, 4));
837        assert!(
838            (in_sphere_unoriented(&points, |l, i| l[i], 0, 1, 2, 3, 4)
839                == in_sphere_unoriented(&points, |l, i| l[i], 0, 1, 2, 4, 3))
840                == (orient_3d(&points, |l, i| l[i], 0, 1, 2, 3)
841                    != orient_3d(&points, |l, i| l[i], 0, 1, 2, 4))
842        );
843    }
844
845    // Not sure how to test this properly in a non-tedious way.
846    // Let's just test the first degenerate expansion for now.
847    #[test]
848    fn test_in_sphere_unoriented_cospherical() {
849        let points = [[0,0,0], [0,0,0], [1,0,0], [0,0,1], [0,1,0]];
850        let points = points
851            .iter()
852            .copied()
853            .map(|[x, y, z]| Vector3::new(x as f64, y as f64, z as f64))
854            .collect::<Vec<_>>();
855        // Trusting the insertion sort now
856        assert!(in_sphere_unoriented(&points, |l, i| l[i], 1, 2, 3, 4, 0));
857        assert!(in_sphere_unoriented(&points, |l, i| l[i], 1, 3, 2, 4, 0));
858        assert!(in_sphere_unoriented(&points, |l, i| l[i], 2, 3, 1, 4, 0));
859        assert!(in_sphere_unoriented(&points, |l, i| l[i], 2, 4, 1, 3, 0));
860        assert!(in_sphere_unoriented(&points, |l, i| l[i], 3, 4, 1, 2, 0));
861        assert!(in_sphere_unoriented(&points, |l, i| l[i], 3, 4, 2, 1, 0));
862        assert!(
863            (in_sphere_unoriented(&points, |l, i| l[i], 0, 1, 2, 3, 4)
864                == in_sphere_unoriented(&points, |l, i| l[i], 0, 1, 2, 4, 3))
865                == (orient_3d(&points, |l, i| l[i], 0, 1, 2, 3)
866                    != orient_3d(&points, |l, i| l[i], 0, 1, 2, 4))
867        );
868    }
869}