Constrained Delaunay tetrahedralization (boundary recovery) and mesh.tet.generate (M2) (#29)

* predicates: LNC points, generic insphere and its perturbed form (Diazzi et al. 2023 §4.3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh: constrained Delaunay tetrahedralization (segment recovery with LNC Steiner points, gift-wrapping facet recovery, labelling)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* cdt: rounding repair, mesh.tet.generate, corpus CDT column; review cleanups (shared perturbation rule, one facet scan, cached coordinates, worklist segment recovery)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh.tet.generate is ready in the catalogue; corpus table with the CDT column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 18:37:09 -04:00
1 parent e688f6f934
commit aa68bbbcdf
24 files changed
+2876 -56

No files matched your search

+132 -4
View File
@@ -1,7 +1,10 @@
//! Indirect predicates on implicit points (Attene 2020, §4–§5), from the paper only.
//!
//! An implicit point is kept as the explicit points that define it and is never rounded: a
//! line–plane intersection ([`Lpi`], §4.2) or a three-plane intersection ([`Tpi`]). Each has
//! line–plane intersection ([`Lpi`], §4.2), a three-plane intersection ([`Tpi`]), or a linear
//! combination of two points ([`Lnc`], the Steiner points of Diazzi, Panozzo, Vaxman, Attene,
//! *Constrained Delaunay Tetrahedrization: A Robust and Practical Approach*, ACM TOG 42(6), 2023,
//! §4.3). Each has
//! homogeneous coordinates `(X, W)` that are polynomials in the defining coordinates, so a
//! predicate on implicit points is the sign of a polynomial times the signs of the `W`s
//! (Attene §4.3–§4.6, §5.1: "count how many of them have a negative sign").
@@ -19,9 +22,10 @@
use std::cmp::Ordering;
use super::dyadic::Dyadic;
use super::eft::{two_diff, two_product, two_sum};
use super::interval::Interval;
use super::ring::{Ring, Signed, det2, det3};
use super::{Point2, Point3, Sign, orient2d, orient3d};
use super::ring::{Ring, Signed, det2, det3, sq3};
use super::{Point2, Point3, Sign, insphere, orient2d, orient3d};
use crate::{Error, Result};
/// A coordinate axis: the axis compared by [`generic_compare`], or dropped by
@@ -34,6 +38,9 @@ pub enum Axis {
}
impl Axis {
/// The three axes, in order.
pub const ALL: [Axis; 3] = [Axis::X, Axis::Y, Axis::Z];
pub(crate) fn index(self) -> usize {
match self {
Axis::X => 0,
@@ -127,6 +134,15 @@ fn tpi_hom<T: Ring>(planes: &[[[f64; 3]; 3]; 3]) -> Hom<T> {
Hom { x, w: Some(det3(&[n1, n2, n3])) }
}
/// `a + t (b - a)` (Diazzi et al. 2023, §4.3 write `t v₁ + (1 - t) v₂`; this form keeps every
/// operand a double, so it is exact in the ring). `W = 1`.
fn lnc_hom<T: Ring>(a: &[f64; 3], b: &[f64; 3], t: f64) -> Hom<T> {
let (a, b, t) = (lift::<T>(a), lift::<T>(b), T::from_f64(t));
let d = sub3(&b, &a);
let x = std::array::from_fn(|k| coord(&a, k) + t.clone() * coord(&d, k));
Hom { x, w: None }
}
/// Decides the sign of `W` exactly: filter first, integers if needed.
fn exact_w_sign(filter: &Hom<Interval>, exact: impl FnOnce() -> Hom<Dyadic>) -> Sign {
w_sign(&filter.w).or_else(|| w_sign(&exact().w)).unwrap_or(Sign::Zero)
@@ -172,12 +188,61 @@ impl Tpi {
}
}
/// The point `a + t (b - a)` of segment `a b`, `0 < t < 1`: an LNC point (LiNear Combination,
/// Diazzi et al. 2023, §4.3), the Steiner points of constrained Delaunay boundary recovery. It
/// lies exactly on the segment, and points on one segment are ordered by `t`.
#[derive(Debug, Clone)]
pub struct Lnc {
a: [f64; 3],
b: [f64; 3],
t: f64,
filter: [Interval; 3],
}
impl Lnc {
/// Fails unless `a ≠ b` and `t` is an admissible double strictly between 0 and 1.
pub fn new(a: &Point3, b: &Point3, t: f64) -> Result<Self> {
if !(t > 0.0 && t < 1.0 && super::is_admissible(t)) {
return Err(Error::Invalid(format!("segment parameter {t:e} is not an admissible number strictly between 0 and 1")));
}
if a == b {
return Err(Error::Invalid("a point on a segment needs two distinct endpoints".into()));
}
let filter = lnc_hom::<Interval>(&a.0, &b.0, t).x;
Ok(Self { a: a.0, b: b.0, t, filter })
}
/// The segment parameter `t`.
pub fn t(&self) -> f64 {
self.t
}
/// The segment's endpoints `a` (at `t = 0`) and `b` (at `t = 1`).
pub fn endpoints(&self) -> [[f64; 3]; 2] {
[self.a, self.b]
}
/// The coordinates rounded to doubles, within about one unit in the last place of each
/// (error-free transforms: `b - a` and `t (b - a)` are kept as two-term sums).
pub fn rounded(&self) -> [f64; 3] {
std::array::from_fn(|k| {
let (a, b) = (self.a[k], self.b[k]);
let (d, d_lo) = two_diff(b, a);
let (p, p_lo) = two_product(self.t, d);
let tail = p_lo + self.t * d_lo;
let (s, s_lo) = two_sum(a, p);
s + (s_lo + tail)
})
}
}
/// An explicit point or an implicit one, for the `generic_*` predicates.
#[derive(Debug, Clone)]
pub enum GenericPoint {
Explicit(Point3),
Lpi(Lpi),
Tpi(Tpi),
Lnc(Lnc),
}
impl From<Point3> for GenericPoint {
@@ -187,7 +252,8 @@ impl From<Point3> for GenericPoint {
}
impl GenericPoint {
fn explicit(&self) -> Option<&Point3> {
/// The point, when it is explicit.
pub fn explicit(&self) -> Option<&Point3> {
match self {
GenericPoint::Explicit(p) => Some(p),
_ => None,
@@ -199,6 +265,7 @@ impl GenericPoint {
GenericPoint::Explicit(p) => Hom { x: lift(&p.0), w: None },
GenericPoint::Lpi(l) => l.filter.clone(),
GenericPoint::Tpi(t) => t.filter.clone(),
GenericPoint::Lnc(l) => Hom { x: l.filter, w: None },
}
}
@@ -207,6 +274,7 @@ impl GenericPoint {
GenericPoint::Explicit(p) => Hom { x: lift(&p.0), w: None },
GenericPoint::Lpi(l) => lpi_hom(&l.points),
GenericPoint::Tpi(t) => tpi_hom(&t.planes),
GenericPoint::Lnc(l) => lnc_hom(&l.a, &l.b, l.t),
}
}
@@ -215,6 +283,7 @@ impl GenericPoint {
pub fn approx(&self) -> [f64; 3] {
match self {
GenericPoint::Explicit(p) => p.0,
GenericPoint::Lnc(l) => l.rounded(),
_ => {
let h = self.filter_hom();
let w = h.w.map_or(1.0, |w| w.mid());
@@ -222,6 +291,30 @@ impl GenericPoint {
}
}
}
/// A box that certainly contains the point: `(lo, hi)` per axis. Unbounded (`±∞`) when the
/// filter cannot bound the division of an LPI or TPI.
pub fn bounds(&self) -> ([f64; 3], [f64; 3]) {
match self {
GenericPoint::Explicit(p) => (p.0, p.0),
GenericPoint::Lnc(l) => (l.filter.map(|x| x.lo()), l.filter.map(|x| x.hi())),
_ => {
let h = self.filter_hom();
let mut lo = [f64::NEG_INFINITY; 3];
let mut hi = [f64::INFINITY; 3];
if let Some(w) = h.w.filter(|w| w.lo() > 0.0 || w.hi() < 0.0) {
for (k, x) in h.x.iter().enumerate() {
let q = [x.lo() / w.lo(), x.lo() / w.hi(), x.hi() / w.lo(), x.hi() / w.hi()];
if q.iter().all(|v| v.is_finite()) {
lo[k] = q.iter().copied().fold(f64::INFINITY, f64::min).next_down();
hi[k] = q.iter().copied().fold(f64::NEG_INFINITY, f64::max).next_up();
}
}
}
(lo, hi)
}
}
}
}
/// Runs a formula through the interval filter, then exactly.
@@ -264,6 +357,41 @@ pub fn generic_orient3d(a: &GenericPoint, b: &GenericPoint, c: &GenericPoint, d:
if flipped { -sign } else { sign }
}
/// `det[p_i - e, |p_i - e|²]` (rows `a, b, c, d`), Shewchuk's in-sphere determinant, in
/// homogeneous form: `R_i = W_e X_i - W_i X_e = W_i W_e (p_i - e)`, and row `i` scaled by the
/// positive `(W_i W_e)²` becomes `[W_i W_e R_i, |R_i|²]`, so the sign needs no division.
fn insphere_formula<T: Ring + Signed>([a, b, c, d, e]: [Hom<T>; 5]) -> Option<Sign> {
let row = |p: &Hom<T>| -> ([T; 3], T) {
let r: [T; 3] = std::array::from_fn(|k| mulw(coord(&p.x, k), &e.w) - mulw(coord(&e.x, k), &p.w));
let [x, y, z] = &r;
let lift = sq3(x, y, z);
let scale = match (&p.w, &e.w) {
(None, None) => None,
(Some(w), None) | (None, Some(w)) => Some(w.clone()),
(Some(w), Some(v)) => Some(w.clone() * v.clone()),
};
(r.map(|v| mulw(v, &scale)), lift)
};
let [(ra, la), (rb, lb), (rc, lc), (rd, ld)] = [row(&a), row(&b), row(&c), row(&d)];
// Expansion along the lift column: -la·M_a + lb·M_b - lc·M_c + ld·M_d.
let m = |p: &[T; 3], q: &[T; 3], r: &[T; 3]| det3(&[p.clone(), q.clone(), r.clone()]);
let det = ld * m(&ra, &rb, &rc) - lc * m(&ra, &rb, &rd) + lb * m(&ra, &rc, &rd) - la * m(&rb, &rc, &rd);
det.sign()
}
/// [`insphere`] on explicit or implicit points, same sign convention (positive when `e` is
/// inside the sphere through `a, b, c, d`, for `orient3d(a, b, c, d) > 0`). Exact.
pub fn generic_insphere(a: &GenericPoint, b: &GenericPoint, c: &GenericPoint, d: &GenericPoint, e: &GenericPoint) -> Sign {
if let (Some(a), Some(b), Some(c), Some(d), Some(e)) = (a.explicit(), b.explicit(), c.explicit(), d.explicit(), e.explicit()) {
return insphere(a, b, c, d, e);
}
// The 5×5 form with rows `[p, |p|², 1]` is alternating in its arguments, so moving an
// explicit point to the pivot only flips the sign.
let (points, flipped) = pivot_last([a, b, c, d, e]);
let sign = decide(points, insphere_formula, insphere_formula);
if flipped { -sign } else { sign }
}
/// `det[a' - c'; b' - c']` of the projections: `det[R_a; R_b] / (W_a W_b W_c²)`.
fn orient2d_formula<T: Ring + Signed>([a, b, c]: [Hom<T>; 3], (u, v): (usize, usize)) -> Option<Sign> {
let row = |p: &Hom<T>, k: usize| mulw(coord(&p.x, k), &c.w) - mulw(coord(&c.x, k), &p.w);
+10
View File
@@ -27,6 +27,16 @@ impl Interval {
if lo.is_nan() || hi.is_nan() { Self::NAN } else { Self { lo: lo.next_down(), hi: hi.next_up() } }
}
/// The lower bound.
pub(crate) fn lo(&self) -> f64 {
self.lo
}
/// The upper bound.
pub(crate) fn hi(&self) -> f64 {
self.hi
}
/// The midpoint, an approximation of the value.
pub(crate) fn mid(&self) -> f64 {
0.5 * self.lo + 0.5 * self.hi
+7 -2
View File
@@ -13,6 +13,11 @@
//! - M. Attene, *Indirect Predicates for Geometric Constructions*, CAD 126:102856, 2020 (paper
//! only; his code is LGPL and is not read): [`Lpi`], [`Tpi`], [`GenericPoint`] and the
//! `generic_*` predicates.
//! - L. Diazzi, D. Panozzo, A. Vaxman, M. Attene, *Constrained Delaunay Tetrahedrization: A
//! Robust and Practical Approach*, ACM TOG 42(6):181, 2023, §4.3 (paper only; the authors'
//! code is GPL/LGPL and is not read): the [`Lnc`] point on a segment, [`generic_insphere`],
//! and its perturbed form [`generic_insphere_sos`] with the same lexicographic lift as
//! [`insphere_sos`].
//!
//! # Arithmetic assumptions
//! IEEE 754 binary64, round-to-nearest-even, no extended intermediate precision, no FMA
@@ -40,8 +45,8 @@ mod ring;
mod sos;
pub use adaptive::{incircle, incircle_filter, insphere, insphere_filter, orient2d, orient2d_filter, orient3d, orient3d_filter};
pub use indirect::{Axis, GenericPoint, Lpi, Tpi, generic_compare, generic_orient2d, generic_orient3d, orient2d_projected};
pub use sos::{incircle_sos, insphere_sos};
pub use indirect::{Axis, GenericPoint, Lnc, Lpi, Tpi, generic_compare, generic_insphere, generic_orient2d, generic_orient3d, orient2d_projected};
pub use sos::{generic_insphere_sos, generic_lex, incircle_sos, insphere_sos};
use crate::{Error, Result};
+50 -2
View File
@@ -20,6 +20,7 @@
use std::cmp::Ordering;
use super::indirect::{Axis, GenericPoint, generic_compare, generic_insphere, generic_orient3d};
use super::{Point2, Point3, Sign, incircle, insphere, orient2d, orient3d};
/// Lexicographic order on coordinates (total: `f64::total_cmp`).
@@ -50,7 +51,14 @@ pub fn insphere_sos(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3)
#[cold]
fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3) -> Sign {
for i in ranked(&[a.0, b.0, c.0, d.0, e.0]) {
perturbed_cofactors([a, b, c, d, e], ranked(&[a.0, b.0, c.0, d.0, e.0]), orient3d)
}
/// The sign of the first nonzero lift cofactor, in rank order (`order`: argument positions by
/// decreasing rank). The single statement of the perturbation rule for every point type.
fn perturbed_cofactors<P>(points: [&P; 5], order: [usize; 5], orient: impl Fn(&P, &P, &P, &P) -> Sign) -> Sign {
let [a, b, c, d, e] = points;
for i in order {
let [p, q, r, s] = match i {
0 => [b, c, d, e],
1 => [a, c, d, e],
@@ -58,7 +66,7 @@ fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3
3 => [a, b, c, e],
_ => [a, b, c, d],
};
let minor = orient3d(p, q, r, s);
let minor = orient(p, q, r, s);
if !minor.is_zero() {
// Row i + 1 (1-based), lift column 4: cofactor sign (-1)^(i + 5).
return if i % 2 == 1 { minor } else { -minor };
@@ -67,6 +75,46 @@ fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3
Sign::Zero
}
/// The lexicographic order of two points' exact coordinates (`x`, then `y`, then `z`): the rank
/// order of the perturbation, for explicit and implicit points alike. On explicit points it is
/// the order [`insphere_sos`] uses (callers admit `+0.0`, never `-0.0`).
pub fn generic_lex(p: &GenericPoint, q: &GenericPoint) -> Ordering {
if let (Some(p), Some(q)) = (p.explicit(), q.explicit()) {
return lex(&p.coords(), &q.coords());
}
Axis::ALL.into_iter().map(|a| generic_compare(p, q, a)).find(|o| o.is_ne()).unwrap_or(Ordering::Equal)
}
/// [`insphere_sos`] on explicit or implicit points: the same lift by lexicographic rank, so a
/// triangulation built with [`insphere_sos`] stays consistent when implicit points are added.
/// Never [`Sign::Zero`] when `a, b, c, d` is not coplanar; points must be pairwise distinct.
pub fn generic_insphere_sos(a: &GenericPoint, b: &GenericPoint, c: &GenericPoint, d: &GenericPoint, e: &GenericPoint) -> Sign {
if let (Some(a), Some(b), Some(c), Some(d), Some(e)) = (a.explicit(), b.explicit(), c.explicit(), d.explicit(), e.explicit()) {
return insphere_sos(a, b, c, d, e);
}
let exact = generic_insphere(a, b, c, d, e);
if !exact.is_zero() {
return exact;
}
let points = [a, b, c, d, e];
// Positions by decreasing rank, ties by position: an insertion sort, which needs no total
// order guarantee from the comparator to stay in bounds.
let mut order = [0usize, 1, 2, 3, 4];
for i in 1..order.len() {
let mut j = i;
while j > 0 {
let (Some(&x), Some(&y)) = (order.get(j - 1), order.get(j)) else { break };
let (Some(px), Some(py)) = (points.get(x), points.get(y)) else { break };
if generic_lex(py, px) != Ordering::Greater {
break;
}
order.swap(j - 1, j);
j -= 1;
}
}
perturbed_cofactors(points, order, generic_orient3d)
}
/// [`incircle`] with symbolic perturbation: never [`Sign::Zero`] when `a, b, c` is not
/// collinear. Same conventions as [`insphere_sos`].
#[inline]
+135
View File
@@ -605,3 +605,138 @@ fn undefined_constructions_are_rejected() {
assert_eq!(generic_compare(&t, &GenericPoint::Explicit(o), axis), Ordering::Equal);
}
}
// ---------------------------------------------------------------------------------------------
// LNC points and the generic in-sphere (Diazzi et al. 2023, §4.3)
// ---------------------------------------------------------------------------------------------
fn r_lnc(a: &Point3, b: &Point3, t: f64) -> RPoint {
let (a, b) = (rp(a), rp(b));
std::array::from_fn(|k| a[k].clone() + q(t) * (b[k].clone() - a[k].clone()))
}
fn r_insphere(p: &[RPoint; 5]) -> Sign {
let rows: Vec<Vec<BigRational>> = (0..4).map(|i| lifted(rsub(&p[i], &p[4]).to_vec())).collect();
rsign(&rdet(&rows))
}
#[test]
fn lnc_points_lie_on_their_segment_in_parameter_order() {
let mut r = Rng(11);
for _ in 0..200 {
let (a, b) = (rand_pt(&mut r, 10.0), rand_pt(&mut r, 10.0));
let (t0, t1) = (r.unit().clamp(1e-9, 0.5), 0.5 + r.unit().clamp(1e-9, 0.49));
let (p0, p1) = (GenericPoint::Lnc(Lnc::new(&a, &b, t0).unwrap()), GenericPoint::Lnc(Lnc::new(&a, &b, t1).unwrap()));
let (ga, gb) = (GenericPoint::Explicit(a), GenericPoint::Explicit(b));
for axis in [Axis::X, Axis::Y, Axis::Z] {
assert_eq!(generic_orient2d(&ga, &gb, &p0, axis), Sign::Zero);
assert_eq!(generic_orient2d(&p0, &p1, &gb, axis), Sign::Zero);
let k = axis.index();
let want = r_lnc(&a, &b, t0)[k].cmp(&r_lnc(&a, &b, t1)[k]);
assert_eq!(generic_compare(&p0, &p1, axis), want);
}
let exact = r_lnc(&a, &b, t0);
let rounded = p0.approx();
let (lo, hi) = p0.bounds();
for k in 0..3 {
let x = exact[k].to_f64().unwrap();
assert!((rounded[k] - x).abs() <= 2.0 * f64::EPSILON * x.abs().max(1e-300), "{} vs {x}", rounded[k]);
assert!(q(lo[k]) <= exact[k] && exact[k] <= q(hi[k]));
}
}
let (a, b) = (p3(0.0, 0.0, 0.0), p3(1.0, 1.0, 1.0));
for bad in [0.0, 1.0, -0.5, 1.5, f64::NAN, f64::INFINITY, 1e-300] {
assert!(Lnc::new(&a, &b, bad).is_err(), "{bad}");
}
assert!(Lnc::new(&a, &a, 0.5).is_err());
}
#[test]
fn generic_orient3d_and_insphere_match_rationals_on_lnc_points() {
let mut r = Rng(12);
let mut checked = 0;
for i in 0..120 {
let pts: Vec<Point3> = (0..9).map(|_| rand_pt(&mut r, 1.0)).collect();
let t = r.unit().clamp(1e-6, 1.0 - 1e-6);
let lnc = Lnc::new(&pts[0], &pts[1], t).unwrap();
let Ok(lpi) = Lpi::new(&pts[2], &pts[3], &pts[4], &pts[5], &pts[6]) else { continue };
let g = [GenericPoint::Lnc(lnc), GenericPoint::Lpi(lpi)];
let rg = [r_lnc(&pts[0], &pts[1], t), r_lpi(&pts[2], &pts[3], &pts[4], &pts[5], &pts[6])];
let ex: Vec<Point3> = (0..5).map(|_| rand_pt(&mut r, 1.0)).collect();
for mask in 0u32..243 {
if !(i + mask as usize).is_multiple_of(5) {
continue;
}
let (mut gp, mut rpv) = (Vec::new(), Vec::new());
let mut m = mask;
for e in &ex {
match m % 3 {
0 => {
gp.push(GenericPoint::Explicit(*e));
rpv.push(rp(e));
}
k => {
gp.push(g[k as usize - 1].clone());
rpv.push(rg[k as usize - 1].clone());
}
}
m /= 3;
}
let want = r_orient3d(&[rpv[0].clone(), rpv[1].clone(), rpv[2].clone(), rpv[3].clone()]);
assert_eq!(generic_orient3d(&gp[0], &gp[1], &gp[2], &gp[3]), want, "orient mask {mask}");
let want = r_insphere(&[rpv[0].clone(), rpv[1].clone(), rpv[2].clone(), rpv[3].clone(), rpv[4].clone()]);
assert_eq!(generic_insphere(&gp[0], &gp[1], &gp[2], &gp[3], &gp[4]), want, "insphere mask {mask}");
checked += 1;
}
}
assert!(checked > 3000, "{checked}");
// The explicit fast path and the formula agree on the sign convention.
let (a, b, c, d) = (p3(0.0, 0.0, 0.0), p3(1.0, 0.0, 0.0), p3(0.0, 1.0, 0.0), p3(0.0, 0.0, 1.0));
let inside = GenericPoint::Lnc(Lnc::new(&a, &p3(1.0, 1.0, 1.0), 0.25).unwrap());
let e = |p: &Point3| GenericPoint::Explicit(*p);
let shewchuk_positive = orient3d(&b, &a, &c, &d) == Sign::Positive;
let (x, y) = if shewchuk_positive { (b, a) } else { (a, b) };
assert_eq!(insphere(&x, &y, &c, &d, &p3(0.25, 0.25, 0.25)), Sign::Positive);
assert_eq!(generic_insphere(&e(&x), &e(&y), &e(&c), &e(&d), &inside), Sign::Positive);
}
/// On a grid, a segment's midpoint (an LNC point) is a grid point: the perturbed in-sphere of
/// the implicit form must give the explicit form's answer, including every cospherical tie.
#[test]
fn perturbed_generic_insphere_agrees_with_explicit_points() {
let mut r = Rng(13);
let mut ties = 0;
for _ in 0..800 {
let v: Vec<[f64; 3]> = (0..5).map(|_| [r.int(1) as f64, r.int(1) as f64, r.int(1) as f64]).collect();
if (0..5).any(|i| (0..i).any(|j| v[i] == v[j])) {
continue;
}
let p: Vec<Point3> = v.iter().map(|c| p3(c[0], c[1], c[2])).collect();
if orient3d(&p[0], &p[1], &p[2], &p[3]).is_zero() {
continue;
}
// Each point as the midpoint of a segment through it (direction from the generator).
let g: Vec<GenericPoint> = p
.iter()
.map(|c| {
if r.int(1) == 0 {
return GenericPoint::Explicit(*c);
}
let d = [r.int(2) as f64, r.int(2) as f64, (r.int(2) as f64) + 7.0];
let x = c.coords();
let a = p3(x[0] - d[0], x[1] - d[1], x[2] - d[2]);
let b = p3(x[0] + d[0], x[1] + d[1], x[2] + d[2]);
GenericPoint::Lnc(Lnc::new(&a, &b, 0.5).unwrap())
})
.collect();
let want = insphere_sos(&p[0], &p[1], &p[2], &p[3], &p[4]);
assert_eq!(generic_insphere_sos(&g[0], &g[1], &g[2], &g[3], &g[4]), want, "{v:?}");
ties += insphere(&p[0], &p[1], &p[2], &p[3], &p[4]).is_zero() as usize;
for i in 0..5 {
for j in 0..5 {
assert_eq!(generic_lex(&g[i], &g[j]), v[i].partial_cmp(&v[j]).unwrap());
}
}
}
assert!(ties > 20, "{ties}");
}