use super::interner::{Interner, Vid};
use super::predicates::{cmp_lex, orient2d, orient2d_any};
use super::{fixed, interval};
use super::{DropAxis, ImplicitPoint, Lpi, Sign, Tpi};
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
#[inline]
fn e(p: [f64; 3]) -> ImplicitPoint {
ImplicitPoint::Explicit(p)
}
#[inline]
fn orient2d_v(it: &Interner, a: Vid, b: Vid, c: Vid, axis: DropAxis) -> Sign {
let (pa, pb, pc) = (it.get(a), it.get(b), it.get(c));
if let (ImplicitPoint::Explicit(_), ImplicitPoint::Explicit(_), ImplicitPoint::Explicit(_)) =
(pa, pb, pc)
{
return orient2d(pa, pb, pc, axis);
}
if let Some(s) = interval::orient2d_from_lam_iv(it.lam_iv(a), it.lam_iv(b), it.lam_iv(c), axis) {
return s;
}
if let (Some(la), Some(lb), Some(lc)) = (it.lam(a), it.lam(b), it.lam(c)) {
if let Some(s) = fixed::orient2d_from_lam(la, lb, lc, axis) {
return s;
}
}
orient2d_any(pa, pb, pc, axis)
}
#[inline]
fn cmp_lex_v(it: &Interner, a: Vid, b: Vid) -> Sign {
if let Some(s) = interval::cmp_lex_from_lam_iv(it.lam_iv(a), it.lam_iv(b)) {
return s;
}
if let (Some(la), Some(lb)) = (it.lam(a), it.lam(b)) {
if let Some(s) = fixed::cmp_lex_from_lam(la, lb) {
return s;
}
}
let (pa, pb) = (it.get(a), it.get(b));
cmp_lex(pa, pb)
}
#[derive(Clone)]
pub struct Constraint {
pub a: ImplicitPoint,
pub b: ImplicitPoint,
}
pub struct RetriInput {
pub tri: [[f64; 3]; 3],
pub constraints: Vec<Constraint>,
pub points: Vec<ImplicitPoint>,
}
#[inline]
fn normal_idx(a: DropAxis) -> usize {
match a {
DropAxis::X => 0,
DropAxis::Y => 1,
DropAxis::Z => 2,
}
}
fn axis_candidates(t: &[[f64; 3]; 3]) -> [DropAxis; 3] {
let sub = |a: [f64; 3], b: [f64; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
let u = sub(t[1], t[0]);
let v = sub(t[2], t[0]);
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let mag = [n[0].abs(), n[1].abs(), n[2].abs()];
let mut axes = [DropAxis::X, DropAxis::Y, DropAxis::Z];
axes.sort_by(|&a, &b| {
let (ia, ib) = (normal_idx(a), normal_idx(b));
mag[ib]
.partial_cmp(&mag[ia])
.unwrap_or(Ordering::Equal)
.then(ia.cmp(&ib))
});
axes
}
pub fn projection_axis(t: &[[f64; 3]; 3]) -> Option<(DropAxis, Sign)> {
for axis in axis_candidates(t) {
let w = orient2d(&e(t[0]), &e(t[1]), &e(t[2]), axis);
if w != Sign::Zero {
return Some((axis, w));
}
}
None
}
pub struct Canonical {
pub corners: [Vid; 3],
pub segments: Vec<(Vid, Vid)>,
pub points: Vec<Vid>,
}
fn lex_cmp(it: &Interner, a: Vid, b: Vid) -> Ordering {
match cmp_lex_v(it, a, b) {
Sign::Negative => Ordering::Less,
Sign::Positive => Ordering::Greater,
Sign::Zero => Ordering::Equal, }
}
pub fn canonicalize(input: &RetriInput, interner: &mut Interner) -> Canonical {
let corners = [
interner.intern(e(input.tri[0])),
interner.intern(e(input.tri[1])),
interner.intern(e(input.tri[2])),
];
let mut segments: Vec<(Vid, Vid)> = input
.constraints
.iter()
.filter_map(|c| {
let va = interner.intern(c.a.clone());
let vb = interner.intern(c.b.clone());
if va == vb {
None } else if lex_cmp(interner, va, vb) == Ordering::Greater {
Some((vb, va))
} else {
Some((va, vb))
}
})
.collect();
segments.sort_by(|&(a0, a1), &(b0, b1)| {
lex_cmp(interner, a0, b0).then_with(|| lex_cmp(interner, a1, b1))
});
segments.dedup();
let mut points: Vec<Vid> = input.points.iter().map(|p| interner.intern(p.clone())).collect();
points.sort_by(|&a, &b| lex_cmp(interner, a, b));
points.dedup();
Canonical { corners, segments, points }
}
pub type SubTri = [Vid; 3];
pub struct Mesh2d {
pub tris: Vec<SubTri>,
pub axis: DropAxis,
pub w0: Sign,
pub unrecovered: usize,
pub audit_needed: bool,
pub coords: BTreeMap<Vid, Option<[f64; 2]>>,
}
enum Locate {
Interior,
OnEdge,
OnVertex,
Outside,
}
fn locate(it: &Interner, tri: SubTri, p: Vid, axis: DropAxis, w0: Sign) -> Locate {
if tri.contains(&p) {
return Locate::OnVertex;
}
let s = [
orient2d_v(it, tri[0], tri[1], p, axis),
orient2d_v(it, tri[1], tri[2], p, axis),
orient2d_v(it, tri[2], tri[0], p, axis),
];
if s.iter().any(|&x| x == w0.flip()) {
return Locate::Outside;
}
match s.iter().filter(|&&x| x == Sign::Zero).count() {
0 => Locate::Interior,
1 => Locate::OnEdge,
_ => Locate::OnVertex, }
}
const PREFILTER_MIN: usize = 32;
#[inline]
fn project2d(p: [f64; 3], axis: DropAxis) -> [f64; 2] {
match axis {
DropAxis::X => [p[1], p[2]],
DropAxis::Y => [p[0], p[2]],
DropAxis::Z => [p[0], p[1]],
}
}
#[inline]
fn coord2d_cached(
it: &Interner,
v: Vid,
axis: DropAxis,
cache: &mut BTreeMap<Vid, Option<[f64; 2]>>,
) -> Option<[f64; 2]> {
if let Some(&c) = cache.get(&v) {
return c;
}
let c = fixed::point_to_f64(it.get(v)).map(|p3| project2d(p3, axis));
cache.insert(v, c);
c
}
#[inline]
fn aabb_excludes(
it: &Interner,
tri: SubTri,
p2: [f64; 2],
axis: DropAxis,
cache: &mut BTreeMap<Vid, Option<[f64; 2]>>,
) -> bool {
let (a, b, c) = match (
coord2d_cached(it, tri[0], axis, cache),
coord2d_cached(it, tri[1], axis, cache),
coord2d_cached(it, tri[2], axis, cache),
) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => return false,
};
let min_x = a[0].min(b[0]).min(c[0]);
let max_x = a[0].max(b[0]).max(c[0]);
let min_y = a[1].min(b[1]).min(c[1]);
let max_y = a[1].max(b[1]).max(c[1]);
let mx = 1e-6 + p2[0].abs() * 1e-9;
let my = 1e-6 + p2[1].abs() * 1e-9;
p2[0] < min_x - mx || p2[0] > max_x + mx || p2[1] < min_y - my || p2[1] > max_y + my
}
#[inline]
fn tri_aabb_disjoint(
it: &Interner,
tri: SubTri,
bx: [f64; 4],
axis: DropAxis,
cache: &mut BTreeMap<Vid, Option<[f64; 2]>>,
) -> bool {
let (a, b, c) = match (
coord2d_cached(it, tri[0], axis, cache),
coord2d_cached(it, tri[1], axis, cache),
coord2d_cached(it, tri[2], axis, cache),
) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => return false,
};
let tmin_x = a[0].min(b[0]).min(c[0]);
let tmax_x = a[0].max(b[0]).max(c[0]);
let tmin_y = a[1].min(b[1]).min(c[1]);
let tmax_y = a[1].max(b[1]).max(c[1]);
let m = 1e-6 + bx[2].abs().max(bx[3].abs()).max(tmax_x.abs()).max(tmax_y.abs()) * 1e-9;
tmax_x < bx[0] - m || tmin_x > bx[2] + m || tmax_y < bx[1] - m || tmin_y > bx[3] + m
}
fn insert_point(mesh: &mut Mesh2d, it: &Interner, p: Vid) {
let axis = mesh.axis;
let w0 = mesh.w0;
let pc = if mesh.tris.len() > PREFILTER_MIN {
coord2d_cached(it, p, axis, &mut mesh.coords)
} else {
None
};
let mut cavity = Vec::new();
for ti in 0..mesh.tris.len() {
let tri = mesh.tris[ti];
if let Some(p2) = pc {
if aabb_excludes(it, tri, p2, axis, &mut mesh.coords) {
continue;
}
}
match locate(it, tri, p, axis, w0) {
Locate::OnVertex => return,
Locate::Interior | Locate::OnEdge => cavity.push(ti),
Locate::Outside => {}
}
}
if cavity.is_empty() {
return; }
let cavity_set: BTreeSet<usize> = cavity.iter().copied().collect();
let mut edges: BTreeSet<(Vid, Vid)> = BTreeSet::new();
for &ti in &cavity {
let [a, b, c] = mesh.tris[ti];
edges.insert((a, b));
edges.insert((b, c));
edges.insert((c, a));
}
let boundary: Vec<(Vid, Vid)> = edges
.iter()
.copied()
.filter(|&(u, v)| !edges.contains(&(v, u)))
.filter(|&(u, v)| orient2d_v(it, u, v, p, axis) != Sign::Zero)
.collect();
mesh.tris = mesh
.tris
.iter()
.enumerate()
.filter(|(i, _)| !cavity_set.contains(i))
.map(|(_, t)| *t)
.collect();
for (u, v) in boundary {
mesh.tris.push([u, v, p]);
}
}
pub fn triangulate_points(input: &RetriInput, interner: &mut Interner) -> Option<Mesh2d> {
let (axis, w0) = projection_axis(&input.tri)?;
let canon = canonicalize(input, interner);
let mut mesh = Mesh2d {
tris: vec![canon.corners],
axis,
w0,
unrecovered: 0,
audit_needed: false,
coords: BTreeMap::new(),
};
let mut pts: BTreeSet<Vid> = BTreeSet::new();
for &(lo, hi) in &canon.segments {
pts.insert(lo);
pts.insert(hi);
}
pts.extend(canon.points.iter().copied());
for &c in &canon.corners {
pts.remove(&c);
}
let mut ordered: Vec<Vid> = pts.into_iter().collect();
ordered.sort_by(|&a, &b| lex_cmp(interner, a, b));
for p in ordered {
insert_point(&mut mesh, interner, p);
}
Some(mesh)
}
fn strictly_outside(it: &Interner, a: Vid, b: Vid, c: Vid, p: Vid, axis: DropAxis, w0: Sign) -> bool {
let opp = w0.flip();
orient2d_v(it, a, b, p, axis) == opp
|| orient2d_v(it, b, c, p, axis) == opp
|| orient2d_v(it, c, a, p, axis) == opp
}
pub fn earcut(it: &Interner, ring: &[Vid], axis: DropAxis, w0: Sign) -> Vec<SubTri> {
let mut poly: Vec<Vid> = ring.to_vec();
let mut out = Vec::new();
while poly.len() > 3 {
let n = poly.len();
let mut best: Option<usize> = None;
for i in 0..n {
let a = poly[(i + n - 1) % n];
let b = poly[i];
let c = poly[(i + 1) % n];
if orient2d_v(it, a, b, c, axis) != w0 {
continue;
}
let empty = poly
.iter()
.all(|&v| v == a || v == b || v == c || strictly_outside(it, a, b, c, v, axis, w0));
if !empty {
continue;
}
best = Some(match best {
None => i,
Some(j) if cmp_lex_v(it, b, poly[j]) == Sign::Negative => i,
Some(j) => j,
});
}
let i = match best {
Some(i) => i,
None => {
for k in 1..poly.len() - 1 {
out.push([poly[0], poly[k], poly[k + 1]]);
}
return out;
}
};
let n = poly.len();
out.push([poly[(i + n - 1) % n], poly[i], poly[(i + 1) % n]]);
poly.remove(i);
}
out.push([poly[0], poly[1], poly[2]]);
out
}
#[inline]
fn tri_edges(t: SubTri) -> [(Vid, Vid); 3] {
[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])]
}
fn edge_exists(mesh: &Mesh2d, s: Vid, t: Vid) -> bool {
mesh.tris.iter().any(|tri| tri.contains(&s) && tri.contains(&t))
}
fn orient_ring(it: &Interner, ring: Vec<Vid>, axis: DropAxis, w0: Sign) -> Vec<Vid> {
let n = ring.len();
let i = (0..n).min_by(|&x, &y| lex_cmp(it, ring[x], ring[y])).unwrap();
let w = orient2d_v(it, ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n], axis);
if w == w0 {
ring
} else {
let mut r = ring;
r.reverse();
r
}
}
fn recover_subsegment(mesh: &mut Mesh2d, it: &Interner, a: Vid, b: Vid) {
if edge_exists(mesh, a, b) {
return;
}
let audit_before = mesh.audit_needed;
mesh.audit_needed = true;
let (axis, w0) = (mesh.axis, mesh.w0);
let crosses = |u: Vid, v: Vid| {
let s1 = orient2d_v(it, a, b, u, axis);
let s2 = orient2d_v(it, a, b, v, axis);
let s3 = orient2d_v(it, u, v, a, axis);
let s4 = orient2d_v(it, u, v, b, axis);
s1 != Sign::Zero && s2 != Sign::Zero && s1 != s2
&& s3 != Sign::Zero && s4 != Sign::Zero && s3 != s4
};
let ab_box: Option<[f64; 4]> = if mesh.tris.len() > PREFILTER_MIN {
match (
coord2d_cached(it, a, axis, &mut mesh.coords),
coord2d_cached(it, b, axis, &mut mesh.coords),
) {
(Some(a2), Some(b2)) => Some([
a2[0].min(b2[0]),
a2[1].min(b2[1]),
a2[0].max(b2[0]),
a2[1].max(b2[1]),
]),
_ => None,
}
} else {
None
};
let channel: Vec<usize> = (0..mesh.tris.len())
.filter(|&ti| {
let tri = mesh.tris[ti];
if let Some(bx) = ab_box {
if tri_aabb_disjoint(it, tri, bx, axis, &mut mesh.coords) {
return false;
}
}
tri_edges(tri).iter().any(|&(u, v)| crosses(u, v))
})
.collect();
if channel.is_empty() {
return;
}
let channel_set: BTreeSet<usize> = channel.iter().copied().collect();
let mut edges: BTreeSet<(Vid, Vid)> = BTreeSet::new();
for &ti in &channel {
for e in tri_edges(mesh.tris[ti]) {
edges.insert(e);
}
}
let mut next: BTreeMap<Vid, Vid> = BTreeMap::new();
for &(u, v) in &edges {
if !edges.contains(&(v, u)) {
next.insert(u, v);
}
}
let start = if next.contains_key(&a) {
a
} else {
match next.keys().next() {
Some(&v) => v,
None => return,
}
};
let mut loop_v = vec![start];
let mut cur = match next.get(&start) {
Some(&v) => v,
None => return,
};
while cur != start {
loop_v.push(cur);
cur = match next.get(&cur) {
Some(&v) => v,
None => return,
};
if loop_v.len() > next.len() + 1 {
return; }
}
let loop_set: BTreeSet<Vid> = loop_v.iter().copied().collect();
let mut lost: Vec<Vid> = channel
.iter()
.flat_map(|&ti| mesh.tris[ti])
.filter(|v| !loop_set.contains(v))
.collect::<BTreeSet<Vid>>()
.into_iter()
.collect();
lost.sort_by(|&x, &y| lex_cmp(it, x, y)); let ia = loop_v.iter().position(|&x| x == a);
let ib = loop_v.iter().position(|&x| x == b);
let mut new_tris: Vec<[Vid; 3]> = Vec::new();
let mut fan_hub: Option<Vid> = None;
match (ia, ib) {
(Some(ia), Some(ib)) => {
let n = loop_v.len();
let rot: Vec<Vid> = (0..n).map(|k| loop_v[(ia + k) % n]).collect();
let jb = (ib + n - ia) % n;
let arc1: Vec<Vid> = rot[0..=jb].to_vec(); let mut arc2: Vec<Vid> = rot[jb..].to_vec(); arc2.push(a); for ring in [arc1, arc2] {
if ring.len() >= 3 {
let oriented = orient_ring(it, ring, axis, w0);
new_tris.extend(earcut(it, &oriented, axis, w0));
}
}
}
_ => {
let inner = if ia.is_none() { a } else { b };
let oriented = orient_ring(it, loop_v.clone(), axis, w0);
let n = oriented.len();
let star = !loop_set.contains(&inner)
&& (0..n).all(|k| {
orient2d_v(it, inner, oriented[k], oriented[(k + 1) % n], axis) == w0
});
if star {
for k in 0..n {
new_tris.push([inner, oriented[k], oriented[(k + 1) % n]]);
}
fan_hub = Some(inner);
} else {
new_tris.extend(earcut(it, &oriented, axis, w0));
}
}
}
mesh.tris = mesh
.tris
.iter()
.enumerate()
.filter(|(i, _)| !channel_set.contains(i))
.map(|(_, t)| *t)
.collect();
mesh.tris.extend(new_tris);
for v in lost {
if Some(v) == fan_hub {
continue; }
insert_point(mesh, it, v);
}
if edge_exists(mesh, a, b) {
mesh.audit_needed = audit_before; }
}
fn between(it: &Interner, s: Vid, t: Vid, v: Vid) -> bool {
let sv = cmp_lex_v(it, s, v);
sv != Sign::Zero && sv == cmp_lex_v(it, v, t)
}
fn enforce_constraint(mesh: &mut Mesh2d, it: &Interner, s: Vid, t: Vid) {
let axis = mesh.axis;
let verts: BTreeSet<Vid> = mesh.tris.iter().flatten().copied().collect();
let seg_box: Option<[f64; 4]> = if verts.len() > PREFILTER_MIN {
match (
coord2d_cached(it, s, axis, &mut mesh.coords),
coord2d_cached(it, t, axis, &mut mesh.coords),
) {
(Some(s2), Some(t2)) => Some([
s2[0].min(t2[0]),
s2[1].min(t2[1]),
s2[0].max(t2[0]),
s2[1].max(t2[1]),
]),
_ => None,
}
} else {
None
};
let mut on_seg: Vec<Vid> = verts
.into_iter()
.filter(|&v| {
if v == s || v == t {
return false;
}
if let Some(bx) = seg_box {
if let Some(vc) = coord2d_cached(it, v, axis, &mut mesh.coords) {
let mx = 1e-6 + vc[0].abs() * 1e-9;
let my = 1e-6 + vc[1].abs() * 1e-9;
if vc[0] < bx[0] - mx
|| vc[0] > bx[2] + mx
|| vc[1] < bx[1] - my
|| vc[1] > bx[3] + my
{
return false; }
}
}
orient2d_v(it, s, t, v, axis) == Sign::Zero && between(it, s, t, v)
})
.collect();
on_seg.sort_by(|&x, &y| lex_cmp(it, x, y));
if cmp_lex_v(it, s, t) == Sign::Positive {
on_seg.reverse(); }
let mut chain = vec![s];
chain.extend(on_seg);
chain.push(t);
for w in chain.windows(2) {
recover_subsegment(mesh, it, w[0], w[1]);
}
}
pub fn triangulate(input: &RetriInput, interner: &mut Interner) -> Option<Mesh2d> {
let (axis, w0) = projection_axis(&input.tri)?;
let canon = canonicalize(input, interner);
let mut mesh = Mesh2d {
tris: vec![canon.corners],
axis,
w0,
unrecovered: 0,
audit_needed: false,
coords: BTreeMap::new(),
};
let mut pts: BTreeSet<Vid> = BTreeSet::new();
for &(lo, hi) in &canon.segments {
pts.insert(lo);
pts.insert(hi);
}
pts.extend(canon.points.iter().copied());
for &c in &canon.corners {
pts.remove(&c);
}
let mut ordered: Vec<Vid> = pts.into_iter().collect();
ordered.sort_by(|&a, &b| lex_cmp(interner, a, b));
for p in ordered {
insert_point(&mut mesh, interner, p);
}
let mut converged = false;
for _pass in 0..4 {
let before = mesh.tris.clone();
for &(s, t) in &canon.segments {
enforce_constraint(&mut mesh, interner, s, t);
}
if mesh.tris == before {
converged = true;
break;
}
}
if !converged {
mesh.audit_needed = true;
}
if !mesh.audit_needed {
return Some(mesh);
}
for &(cs, ct) in &canon.segments {
let verts: BTreeSet<Vid> = mesh.tris.iter().flatten().copied().collect();
let mut on_seg: Vec<Vid> = verts
.into_iter()
.filter(|&v| {
v != cs
&& v != ct
&& orient2d_v(interner, cs, ct, v, axis) == Sign::Zero
&& between(interner, cs, ct, v)
})
.collect();
on_seg.sort_by(|&x, &y| lex_cmp(interner, x, y));
if cmp_lex_v(interner, cs, ct) == Sign::Positive {
on_seg.reverse();
}
let mut chain = vec![cs];
chain.extend(on_seg);
chain.push(ct);
for w in chain.windows(2) {
if !edge_exists(&mesh, w[0], w[1]) {
mesh.unrecovered += 1;
}
}
}
Some(mesh)
}
pub fn triangulation_topology_hash(input: &RetriInput) -> u64 {
let mut interner = Interner::new();
let mesh = match triangulate(input, &mut interner) {
Some(m) => m,
None => return 0,
};
let mut tris: Vec<[Vid; 3]> = mesh
.tris
.iter()
.map(|&t| {
let mut s = t;
s.sort_unstable();
s
})
.collect();
tris.sort_unstable();
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for t in tris {
for v in t {
h ^= v as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
h
}
pub fn retriangulation_manifest() -> u64 {
let t = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]];
let lpi = ImplicitPoint::Lpi(Lpi {
p: [3.0, 3.0, -1.0],
q: [3.0, 3.0, 1.0],
r: [0.0, 0.0, 0.0],
s: [1.0, 0.0, 0.0],
t: [0.0, 1.0, 0.0],
});
let tpi = ImplicitPoint::Tpi(Tpi {
planes: [
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[3.0, 0.0, 0.0], [3.0, 1.0, 0.0], [3.0, 0.0, 1.0]],
[[0.0, 5.0, 0.0], [1.0, 5.0, 0.0], [0.0, 5.0, 1.0]],
],
});
let x = ImplicitPoint::Explicit;
let cons = vec![
Constraint { a: x([2.0, 2.0, 0.0]), b: x([6.0, 2.0, 0.0]) },
Constraint { a: x([2.0, 2.0, 0.0]), b: x([2.0, 6.0, 0.0]) },
Constraint { a: lpi.clone(), b: x([6.0, 2.0, 0.0]) },
Constraint { a: tpi, b: x([2.0, 6.0, 0.0]) },
Constraint { a: x([5.0, 1.0, 0.0]), b: lpi },
];
triangulation_topology_hash(&RetriInput { tri: t, constraints: cons, points: Vec::new() })
}
#[cfg(test)]
mod tests {
use super::super::rational::point_of;
use super::super::Lpi;
use super::*;
#[test]
fn phase_a_picks_a_nonzero_axis_and_winding() {
let t = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]];
let (axis, w) = projection_axis(&t).unwrap();
assert_eq!(axis, DropAxis::Z);
assert_ne!(w, Sign::Zero);
let t2 = [[0., 0., 0.], [0., 0., 1.], [1., 0., 0.]];
assert_eq!(projection_axis(&t2).unwrap().0, DropAxis::Y);
let t3 = [[0., 0., 0.], [1., 1., 1.], [2., 2., 2.]];
assert!(projection_axis(&t3).is_none());
}
#[test]
fn phase_a_is_deterministic_on_a_45_degree_face() {
let t = [[0., 0., 0.], [1., -1., 0.], [1., -1., 2.]];
let a = projection_axis(&t);
let b = projection_axis(&t);
assert_eq!(a.map(|x| x.0), b.map(|x| x.0));
assert!(a.is_some());
}
#[test]
fn phase_b_canonical_order_is_independent_of_input_order() {
let t = [[0., 0., 0.], [4., 0., 0.], [0., 4., 0.]]; let lpi = ImplicitPoint::Lpi(Lpi {
p: [1., 1., -1.],
q: [1., 1., 1.],
r: [0., 0., 0.],
s: [1., 0., 0.],
t: [0., 1., 0.],
});
let c1 = Constraint { a: e([2., 0., 0.]), b: e([0., 2., 0.]) };
let c2 = Constraint { a: lpi, b: e([3., 0., 0.]) };
let materialise = |cons: Vec<Constraint>| {
let mut it = Interner::new();
let canon = canonicalize(&RetriInput { tri: t, constraints: cons, points: Vec::new() }, &mut it);
canon
.segments
.iter()
.map(|&(lo, hi)| (point_of(it.get(lo)), point_of(it.get(hi))))
.collect::<Vec<_>>()
};
let forward = materialise(vec![c1.clone(), c2.clone()]);
let backward = materialise(vec![c2.clone(), c1.clone()]);
assert_eq!(forward, backward, "canonical segment order depends on input order");
let with_dup = materialise(vec![c1.clone(), c1.clone(), c2.clone()]);
assert_eq!(with_dup.len(), 2, "duplicate constraint not deduped");
}
#[test]
fn phase_c_covers_t_exactly_with_correct_orientation() {
use super::super::rational::tri_area2;
use num_rational::BigRational;
use num_traits::Zero;
let t = [[0., 0., 0.], [6., 0., 0.], [0., 6., 0.]]; let lpi = ImplicitPoint::Lpi(Lpi {
p: [2., 2., -1.],
q: [2., 2., 1.],
r: [0., 0., 0.],
s: [1., 0., 0.],
t: [0., 1., 0.],
}); let cons = vec![
Constraint { a: lpi, b: e([3., 3., 0.]) }, Constraint { a: e([1., 1., 0.]), b: e([4., 1., 0.]) },
];
let mut it = Interner::new();
let mesh = triangulate_points(&RetriInput { tri: t, constraints: cons, points: Vec::new() }, &mut it).unwrap();
for &tri in &mesh.tris {
assert_eq!(
orient2d_v(&it, tri[0], tri[1], tri[2], mesh.axis),
mesh.w0,
"sub-tri not oriented w0: {tri:?}"
);
}
let area2 = |tri: SubTri| {
tri_area2(
&point_of(it.get(tri[0])),
&point_of(it.get(tri[1])),
&point_of(it.get(tri[2])),
mesh.axis,
)
};
let sum = mesh.tris.iter().fold(BigRational::zero(), |acc, &tr| acc + area2(tr));
let t_area = tri_area2(
&point_of(&e(t[0])),
&point_of(&e(t[1])),
&point_of(&e(t[2])),
mesh.axis,
);
assert_eq!(sum, t_area, "sub-triangles do not exactly cover T");
}
#[test]
fn phase_c_topology_is_input_order_independent() {
let t = [[0., 0., 0.], [5., 0., 0.], [0., 5., 0.]];
let lpi = ImplicitPoint::Lpi(Lpi {
p: [1., 1., -1.],
q: [1., 1., 1.],
r: [0., 0., 0.],
s: [1., 0., 0.],
t: [0., 1., 0.],
});
let c1 = Constraint { a: lpi, b: e([2., 2., 0.]) };
let c2 = Constraint { a: e([3., 1., 0.]), b: e([1., 3., 0.]) };
let topo = |cons: Vec<Constraint>| {
let mut it = Interner::new();
let mesh = triangulate_points(&RetriInput { tri: t, constraints: cons, points: Vec::new() }, &mut it).unwrap();
let mut tris: Vec<_> = mesh
.tris
.iter()
.map(|&tri| {
let mut p = [
point_of(it.get(tri[0])),
point_of(it.get(tri[1])),
point_of(it.get(tri[2])),
];
p.sort();
p
})
.collect();
tris.sort();
tris
};
assert_eq!(
topo(vec![c1.clone(), c2.clone()]),
topo(vec![c2, c1]),
"Phase C topology depends on input order"
);
}
#[test]
fn phase_e_earcut_covers_a_concave_polygon_deterministically() {
use super::super::rational::tri_area2;
use num_rational::BigRational;
use num_traits::Zero;
let pts = [[0., 0., 0.], [4., 0., 0.], [4., 3., 0.], [2., 1.5, 0.], [0., 3., 0.]];
let mut it = Interner::new();
let ring: Vec<Vid> = pts.iter().map(|&p| it.intern(e(p))).collect();
let axis = DropAxis::Z;
let pt = |v: Vid| point_of(it.get(v));
let origin = point_of(&e([0., 0., 0.]));
let mut poly2a = BigRational::zero();
for i in 0..ring.len() {
let j = (i + 1) % ring.len();
poly2a += tri_area2(&pt(ring[i]), &pt(ring[j]), &origin, axis);
}
let w0 = if poly2a > BigRational::zero() { Sign::Positive } else { Sign::Negative };
let tris = earcut(&it, &ring, axis, w0);
assert_eq!(tris.len(), ring.len() - 2, "wrong triangle count");
for &tri in &tris {
assert_eq!(
orient2d_v(&it, tri[0], tri[1], tri[2], axis),
w0,
"earcut triangle not oriented w0"
);
}
let area_sum = tris
.iter()
.fold(BigRational::zero(), |acc, &t| acc + tri_area2(&pt(t[0]), &pt(t[1]), &pt(t[2]), axis));
assert_eq!(area_sum, poly2a, "earcut does not exactly cover the polygon");
let mut rotated = ring.clone();
rotated.rotate_left(2);
let tris2 = earcut(&it, &rotated, axis, w0);
let canon = |ts: &[SubTri]| {
let mut v: Vec<_> = ts
.iter()
.map(|&t| {
let mut s = [pt(t[0]), pt(t[1]), pt(t[2])];
s.sort();
s
})
.collect();
v.sort();
v
};
assert_eq!(canon(&tris), canon(&tris2), "earcut depends on ring start vertex");
}
#[test]
fn phase_d_recovers_a_crossing_diagonal() {
use super::super::rational::tri_area2;
use num_rational::BigRational;
use num_traits::Zero;
let mut it = Interner::new();
let a = it.intern(e([0., 0., 0.]));
let b = it.intern(e([2., 0., 0.]));
let c = it.intern(e([2., 2., 0.]));
let d = it.intern(e([0., 2., 0.]));
let mut mesh = Mesh2d {
tris: vec![[a, b, c], [a, c, d]],
axis: DropAxis::Z,
w0: Sign::Positive,
unrecovered: 0,
audit_needed: false,
coords: BTreeMap::new(),
};
let pt = |v: Vid| point_of(it.get(v));
let origin = point_of(&e([0., 0., 0.]));
let ring = [a, b, c, d];
let quad_area = (0..4).fold(BigRational::zero(), |s, i| {
s + tri_area2(&pt(ring[i]), &pt(ring[(i + 1) % 4]), &origin, DropAxis::Z)
});
recover_subsegment(&mut mesh, &it, b, d);
assert!(
mesh.tris.iter().any(|t| t.contains(&b) && t.contains(&d)),
"b–d was not recovered as an edge"
);
assert!(
!mesh.tris.iter().any(|t| t.contains(&a) && t.contains(&c)),
"the crossed diagonal a–c is still present"
);
for &tri in &mesh.tris {
assert_eq!(
orient2d_v(&it, tri[0], tri[1], tri[2], mesh.axis),
mesh.w0,
"recovered triangle not oriented w0"
);
}
let sum = mesh
.tris
.iter()
.fold(BigRational::zero(), |acc, &t| acc + tri_area2(&pt(t[0]), &pt(t[1]), &pt(t[2]), mesh.axis));
assert_eq!(sum, quad_area, "recovery changed the covered area");
}
#[test]
fn phase_d_full_triangulate_satisfies_constraints_and_covers_t() {
use super::super::rational::tri_area2;
use num_rational::BigRational;
use num_traits::Zero;
let t = [[0., 0., 0.], [6., 0., 0.], [0., 6., 0.]];
let cons = vec![
Constraint { a: e([1., 1., 0.]), b: e([3., 1., 0.]) },
Constraint { a: e([3., 1., 0.]), b: e([1., 3., 0.]) },
Constraint { a: e([3., 3., 0.]), b: e([1., 3., 0.]) },
];
let mut it = Interner::new();
let mesh = triangulate(&RetriInput { tri: t, constraints: cons.clone(), points: Vec::new() }, &mut it).unwrap();
let cverts: Vec<(Vid, Vid)> =
cons.iter().map(|c| (it.intern(c.a.clone()), it.intern(c.b.clone()))).collect();
let corners = [it.intern(e(t[0])), it.intern(e(t[1])), it.intern(e(t[2]))];
for &(s, tt) in &cverts {
assert!(edge_exists(&mesh, s, tt), "constraint {s}-{tt} not satisfied as an edge");
}
let pt = |v: Vid| point_of(it.get(v));
for &tri in &mesh.tris {
assert_eq!(
orient2d_v(&it, tri[0], tri[1], tri[2], mesh.axis),
mesh.w0,
"triangle not oriented w0"
);
}
let sum = mesh
.tris
.iter()
.fold(BigRational::zero(), |acc, &tr| acc + tri_area2(&pt(tr[0]), &pt(tr[1]), &pt(tr[2]), mesh.axis));
let t_area = tri_area2(&pt(corners[0]), &pt(corners[1]), &pt(corners[2]), mesh.axis);
assert_eq!(sum, t_area, "triangulation does not exactly cover T");
}
#[test]
fn retriangulation_manifest_is_pinned() {
const PINNED: u64 = 0xef5b_32fd_d838_4776;
let m = super::retriangulation_manifest();
assert_eq!(m, PINNED, "retriangulation topology manifest changed: 0x{m:016x}");
}
}