use super::*;
use super::bridge::point_in_polygon;
use rustc_hash::{FxHashMap, FxHashSet};
fn ring_box(ring: &[FaceVertex]) -> [f64; 4] {
ring.iter().fold(
[f64::INFINITY, f64::NEG_INFINITY, f64::INFINITY, f64::NEG_INFINITY],
|[u0, u1, v0, v1], vertex| {
[
u0.min(vertex.uv[0]),
u1.max(vertex.uv[0]),
v0.min(vertex.uv[1]),
v1.max(vertex.uv[1]),
]
},
)
}
fn boxes_apart(a: [f64; 4], b: [f64; 4], margin: f64) -> bool {
a[1] + margin < b[0] || b[1] + margin < a[0] || a[3] + margin < b[2] || b[3] + margin < a[2]
}
pub(in crate::watertight_tessellation) struct HoleBoxes {
boxes: Vec<[f64; 4]>,
margin: f64,
}
impl HoleBoxes {
pub(in crate::watertight_tessellation) fn new(holes: &[Vec<FaceVertex>]) -> Self {
let boxes: Vec<[f64; 4]> = holes.iter().map(|hole| ring_box(hole)).collect();
let magnitude = boxes
.iter()
.flat_map(|b| b.iter().map(|value| value.abs()))
.fold(0.0f64, f64::max);
Self {
boxes,
margin: 64.0 * f64::EPSILON * magnitude.max(1e-300),
}
}
pub(in crate::watertight_tessellation) fn any_contains(&self, holes: &[Vec<FaceVertex>], p: [f64; 2]) -> bool {
self.boxes.iter().zip(holes).any(|(b, hole)| {
p[0] >= b[0] - self.margin
&& p[0] <= b[1] + self.margin
&& p[1] >= b[2] - self.margin
&& p[1] <= b[3] + self.margin
&& point_in_polygon(hole, p)
})
}
}
pub(in crate::watertight_tessellation) fn certified_planar_trim(
v: &[FaceVertex],
ts: &[[usize; 3]],
outer: &[FaceVertex],
holes: &[Vec<FaceVertex>],
) -> bool {
tess_profile(|p| p.certify_calls += 1);
if ts.is_empty() {
return false;
}
let expected =
signed_area(outer).abs() - holes.iter().map(|h| signed_area(h).abs()).sum::<f64>();
if !expected.is_finite() || expected <= 0.0 {
return false;
}
let (lo, hi) = outer.iter().chain(holes.iter().flatten()).fold(
([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]),
|(mut lo, mut hi), p| {
for k in 0..2 {
lo[k] = lo[k].min(p.uv[k]);
hi[k] = hi[k].max(p.uv[k]);
}
(lo, hi)
},
);
let scale = ((hi[0] - lo[0]) * (hi[1] - lo[1])).abs().max(expected);
type K = (u64, u64);
let key = |p: &FaceVertex| (p.uv[0].to_bits(), p.uv[1].to_bits());
let ek = |a: K, b: K| if a <= b { (a, b) } else { (b, a) };
let hole_boxes = HoleBoxes::new(holes);
let mut boundary = FxHashSet::default();
let mut directed_boundary = FxHashSet::default();
for ring in std::iter::once(outer).chain(holes.iter().map(Vec::as_slice)) {
for i in 0..ring.len() {
let edge = (key(&ring[i]), key(&ring[(i + 1) % ring.len()]));
if !boundary.insert(ek(edge.0, edge.1)) || !directed_boundary.insert(edge) {
return false;
}
}
}
let mut area = 0.0;
let mut edges = FxHashMap::default();
let mut directed_edges = FxHashMap::default();
let mut unique = FxHashSet::default();
for &[a, b, c] in ts {
if a >= v.len() || b >= v.len() || c >= v.len() {
return false;
}
let uv = [v[a].uv, v[b].uv, v[c].uv];
let signed = triangle_area(uv[0], uv[1], uv[2]);
if !signed.is_finite() || signed <= 0.0 {
return false;
}
let ar = signed;
area += ar;
if ar > 64.0 * f64::EPSILON * scale {
let p = [
(uv[0][0] + uv[1][0] + uv[2][0]) / 3.0,
(uv[0][1] + uv[1][1] + uv[2][1]) / 3.0,
];
tess_profile(|p| p.certify_centroid_tests += 1);
if !point_in_polygon(outer, p) || hole_boxes.any_contains(holes, p) {
return false;
}
}
let mut t = [key(&v[a]), key(&v[b]), key(&v[c])];
if t[0] == t[1] || t[1] == t[2] || t[2] == t[0] {
return false;
}
t.sort_unstable();
if !unique.insert(t) {
return false;
}
for (x, y) in [(a, b), (b, c), (c, a)] {
*edges.entry(ek(key(&v[x]), key(&v[y]))).or_insert(0usize) += 1;
*directed_edges
.entry((key(&v[x]), key(&v[y])))
.or_insert(0usize) += 1;
}
}
let roundoff = 64.0 * f64::EPSILON * scale * (ts.len() + boundary.len()) as f64;
(area - expected).abs() <= roundoff
&& (area - expected).abs() <= expected * 1e-10
&& boundary.iter().all(|e| edges.get(e).copied() == Some(1))
&& edges
.iter()
.all(|(e, &n)| n == if boundary.contains(e) { 1 } else { 2 })
&& directed_boundary.iter().all(|&(a, b)| {
directed_edges.get(&(a, b)).copied() == Some(1)
&& directed_edges.get(&(b, a)).copied().unwrap_or(0) == 0
})
&& directed_edges.iter().all(|(&(a, b), &n)| {
n == 1
&& (directed_boundary.contains(&(a, b))
|| directed_edges.get(&(b, a)).copied() == Some(1))
})
}
pub(in crate::watertight_tessellation) fn valid_planar_pslg(outer: &[FaceVertex], holes: &[Vec<FaceVertex>]) -> bool {
let _timer = stage_timer(Stage::Pslg);
let rings = std::iter::once(outer)
.chain(holes.iter().map(Vec::as_slice))
.collect::<Vec<_>>();
if signed_area(outer) <= 0.0 || holes.iter().any(|hole| signed_area(hole) >= 0.0) {
return false;
}
let (lo, hi) = rings.iter().flat_map(|ring| ring.iter()).fold(
([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]),
|(mut lo, mut hi), vertex| {
for axis in 0..2 {
lo[axis] = lo[axis].min(vertex.uv[axis]);
hi[axis] = hi[axis].max(vertex.uv[axis]);
}
(lo, hi)
},
);
let span = (hi[0] - lo[0]).abs().max((hi[1] - lo[1]).abs()).max(1e-12);
let epsilon = 256.0 * f64::EPSILON * span * span;
let on_segment = |point: [f64; 2], a: [f64; 2], b: [f64; 2]| {
triangle_area(a, b, point).abs() <= epsilon
&& point[0] >= a[0].min(b[0]) - epsilon
&& point[0] <= a[0].max(b[0]) + epsilon
&& point[1] >= a[1].min(b[1]) - epsilon
&& point[1] <= a[1].max(b[1]) + epsilon
};
let margin = epsilon + 4.0 * f64::EPSILON * span;
let ring_boxes: Vec<[f64; 4]> = rings.iter().map(|ring| ring_box(ring)).collect();
let segment_box = |a: [f64; 2], b: [f64; 2]| -> [f64; 4] {
[a[0].min(b[0]), a[0].max(b[0]), a[1].min(b[1]), a[1].max(b[1])]
};
for (ring_a, a) in rings.iter().enumerate() {
for edge_a in 0..a.len() {
let (a0, a1) = (a[edge_a].uv, a[(edge_a + 1) % a.len()].uv);
if a0 == a1 {
return false;
}
let box_a = segment_box(a0, a1);
for (ring_b, b) in rings.iter().enumerate().skip(ring_a) {
if ring_a != ring_b && boxes_apart(ring_boxes[ring_a], ring_boxes[ring_b], margin) {
continue;
}
for edge_b in 0..b.len() {
if ring_a == ring_b
&& (edge_a == edge_b
|| (edge_a + 1) % a.len() == edge_b
|| (edge_b + 1) % b.len() == edge_a)
{
continue;
}
let (b0, b1) = (b[edge_b].uv, b[(edge_b + 1) % b.len()].uv);
if boxes_apart(box_a, segment_box(b0, b1), margin) {
continue;
}
tess_profile(|p| p.pslg_segment_tests += 1);
let cross = segments_properly_cross(a0, a1, b0, b1, epsilon);
let touch = on_segment(a0, b0, b1)
|| on_segment(a1, b0, b1)
|| on_segment(b0, a0, a1)
|| on_segment(b1, a0, a1);
if cross || touch {
if std::env::var("BREP_DEBUG_PSLG").is_ok() {
eprintln!(
"[pslg] reject {} ringA={ring_a}(e{edge_a}) ringB={ring_b}(e{edge_b}) a=[{:.6},{:.6}]->[{:.6},{:.6}] b=[{:.6},{:.6}]->[{:.6},{:.6}]",
if cross { "CROSS" } else { "TOUCH" },
a0[0], a0[1], a1[0], a1[1], b0[0], b0[1], b1[0], b1[1]
);
}
return false;
}
}
}
}
}
for hole in holes {
if !point_in_polygon(outer, hole[0].uv) {
return false;
}
}
for a in 0..holes.len() {
for b in a + 1..holes.len() {
if point_in_polygon(&holes[a], holes[b][0].uv)
|| point_in_polygon(&holes[b], holes[a][0].uv)
{
return false;
}
}
}
true
}
pub(in crate::watertight_tessellation) struct DirectPlanar {
pub(in crate::watertight_tessellation) vertices: Vec<FaceVertex>,
pub(in crate::watertight_tessellation) triangles: Vec<[usize; 3]>,
}
#[derive(Clone, Copy)]
struct IndexedPoint {
position: spade::Point2<f64>,
authored: usize,
}
impl spade::HasPosition for IndexedPoint {
type Scalar = f64;
fn position(&self) -> spade::Point2<f64> {
self.position
}
}
pub(in crate::watertight_tessellation) fn direct_planar_cdt(
mut outer: Vec<FaceVertex>,
mut holes: Vec<Vec<FaceVertex>>,
) -> Option<DirectPlanar> {
use spade::{ConstrainedDelaunayTriangulation, Point2, Triangulation};
if outer.len() < 3 || holes.is_empty() || holes.iter().any(|h| h.len() < 3) {
return None;
}
let canon = |r: &mut Vec<FaceVertex>| {
let s = (0..r.len()).min_by(|&a, &b| {
r[a].uv[0]
.total_cmp(&r[b].uv[0])
.then_with(|| r[a].uv[1].total_cmp(&r[b].uv[1]))
})?;
r.rotate_left(s);
Some(())
};
canon(&mut outer)?;
for h in &mut holes {
canon(h)?;
}
holes.sort_by(|a, b| {
a[0].uv[0]
.total_cmp(&b[0].uv[0])
.then_with(|| a[0].uv[1].total_cmp(&b[0].uv[1]))
.then_with(|| a.len().cmp(&b.len()))
});
if !valid_planar_pslg(&outer, &holes) {
return None;
}
let mut vertices = outer.clone();
let mut ranges = vec![0..outer.len()];
for h in &holes {
let s = vertices.len();
vertices.extend_from_slice(h);
ranges.push(s..vertices.len());
}
let mut seen = HashSet::new();
if vertices
.iter()
.any(|p| !seen.insert((p.uv[0].to_bits(), p.uv[1].to_bits())))
{
return None;
}
let first = 0;
let second = (1..vertices.len()).max_by(|&a, &b| {
let d = |i: usize| {
let x = vertices[i].uv[0] - vertices[0].uv[0];
let y = vertices[i].uv[1] - vertices[0].uv[1];
x * x + y * y
};
d(a).total_cmp(&d(b))
})?;
let third = (0..vertices.len())
.filter(|&i| i != first && i != second)
.max_by(|&a, &b| {
triangle_area(vertices[first].uv, vertices[second].uv, vertices[a].uv)
.abs()
.total_cmp(
&triangle_area(vertices[first].uv, vertices[second].uv, vertices[b].uv).abs(),
)
})?;
let p = vertices[first].uv;
let q = vertices[second].uv;
let r = vertices[third].uv;
let (a, b, c, d) = (q[0] - p[0], q[1] - p[1], r[0] - p[0], r[1] - p[1]);
let det = a * d - b * c;
if det == 0.0 {
return None;
}
let o = vertices[first].position;
let d1 = vertices[second].position.sub(o);
let d2 = vertices[third].position.sub(o);
let su = d1.scale(d).sub(d2.scale(b)).scale(1.0 / det);
let sv = d2.scale(a).sub(d1.scale(c)).scale(1.0 / det);
let ps = vertices
.iter()
.map(|x| x.position.sub(o).length())
.fold(1e-6f64, f64::max);
if vertices.iter().any(|x| {
o.add(su.scale(x.uv[0] - p[0]))
.add(sv.scale(x.uv[1] - p[1]))
.sub(x.position)
.length()
> ps * 1e-6
}) {
return None;
}
let mut cdt = ConstrainedDelaunayTriangulation::<IndexedPoint>::new();
let mut handles = Vec::new();
for (i, p) in vertices.iter().enumerate() {
let h = cdt
.insert(IndexedPoint {
position: Point2::new(p.uv[0], p.uv[1]),
authored: i,
})
.ok()?;
if h.index() != i {
return None;
}
handles.push(h);
}
if cdt.num_vertices() != vertices.len() {
return None;
}
let mut boundary = HashSet::new();
let mut directed_boundary = HashSet::new();
for range in &ranges {
for i in range.clone() {
let n = if i + 1 == range.end {
range.start
} else {
i + 1
};
let made = cdt.try_add_constraint(handles[i], handles[n]);
if made.len() > 1 || !cdt.exists_constraint(handles[i], handles[n]) {
return None;
}
boundary.insert((i.min(n), i.max(n)));
directed_boundary.insert((i, n));
}
}
let actual = cdt
.undirected_edges()
.filter(|e| e.data().is_constraint_edge())
.map(|e| {
let [a, b] = e.vertices();
let (a, b) = (a.data().authored, b.data().authored);
(a.min(b), a.max(b))
})
.collect::<HashSet<_>>();
if actual != boundary {
return None;
}
let mut triangles = Vec::new();
let hole_boxes = HoleBoxes::new(&holes);
for f in cdt.inner_faces() {
let mut t = f.vertices().map(|x| x.data().authored);
let uv = t.map(|i| vertices[i].uv);
let m = [
(uv[0][0] + uv[1][0] + uv[2][0]) / 3.0,
(uv[0][1] + uv[1][1] + uv[2][1]) / 3.0,
];
if !point_in_polygon(&outer, m) || hole_boxes.any_contains(&holes, m) {
continue;
}
let ar = triangle_area(uv[0], uv[1], uv[2]);
if ar == 0.0 {
return None;
}
if ar < 0.0 {
t.swap(1, 2);
}
let minimum = (0..3).min_by_key(|&index| t[index]).unwrap_or(0);
t.rotate_left(minimum);
triangles.push(t);
}
triangles.sort_unstable();
if !certified_planar_trim(&vertices, &triangles, &outer, &holes) {
return None;
}
let mut used = vec![false; vertices.len()];
let mut dir = HashMap::new();
let mut uniq = HashSet::new();
for &[a, b, c] in &triangles {
for i in [a, b, c] {
used[i] = true;
}
let mut k = [a, b, c];
k.sort_unstable();
if !uniq.insert(k) {
return None;
}
for e in [(a, b), (b, c), (c, a)] {
*dir.entry(e).or_insert(0usize) += 1;
}
}
if used.iter().any(|&x| !x) {
return None;
}
for &(a, b) in &directed_boundary {
if dir.get(&(a, b)).copied() != Some(1) || dir.get(&(b, a)).copied().unwrap_or(0) != 0 {
return None;
}
}
for (&(a, b), &n) in &dir {
if n != 1 || (!directed_boundary.contains(&(a, b)) && dir.get(&(b, a)).copied() != Some(1))
{
return None;
}
}
Some(DirectPlanar {
vertices,
triangles,
})
}
pub(in crate::watertight_tessellation) struct CdtMesh {
pub(in crate::watertight_tessellation) vertices: Vec<FaceVertex>,
pub(in crate::watertight_tessellation) triangles: Vec<[usize; 3]>,
pub(in crate::watertight_tessellation) boundary_pairs: HashSet<(usize, usize)>,
}
pub(in crate::watertight_tessellation) fn surface_cdt(outer: &[FaceVertex], holes: &[Vec<FaceVertex>], scale: [f64; 2]) -> Option<CdtMesh> {
let _timer = stage_timer(Stage::Cdt);
use spade::{ConstrainedDelaunayTriangulation, Point2, Triangulation};
let decline = |why: &str| -> Option<CdtMesh> {
if std::env::var("BREP_DEBUG_CDT").is_ok() {
eprintln!("[cdt] decline: {why} (outer={}, holes={})", outer.len(), holes.len());
}
None
};
if outer.len() < 3 || holes.iter().any(|h| h.len() < 3) {
return decline("degenerate loop");
}
if !valid_planar_pslg(outer, holes) {
return decline("not a simple PSLG");
}
let mut vertices: Vec<FaceVertex> = outer.to_vec();
let mut ranges = vec![0..outer.len()];
for h in holes {
let s = vertices.len();
vertices.extend_from_slice(h);
ranges.push(s..vertices.len());
}
let mut seen = HashSet::new();
if vertices
.iter()
.any(|p| !seen.insert((p.uv[0].to_bits(), p.uv[1].to_bits())))
{
return None;
}
let [su, sv] = [scale[0].max(1e-12), scale[1].max(1e-12)];
let build_timer = stage_timer(Stage::CdtBuild);
let mut cdt = ConstrainedDelaunayTriangulation::<IndexedPoint>::new();
let mut handles = Vec::with_capacity(vertices.len());
for (i, p) in vertices.iter().enumerate() {
let h = cdt
.insert(IndexedPoint {
position: Point2::new(p.uv[0] * su, p.uv[1] * sv),
authored: i,
})
.ok()?;
if h.index() != i {
return decline("coincident insert");
}
handles.push(h);
}
if cdt.num_vertices() != vertices.len() {
return None;
}
let mut boundary_pairs: HashSet<(usize, usize)> = HashSet::new();
let mut directed_boundary: HashSet<(usize, usize)> = HashSet::new();
for range in &ranges {
for i in range.clone() {
let n = if i + 1 == range.end { range.start } else { i + 1 };
let made = cdt.try_add_constraint(handles[i], handles[n]);
if made.len() > 1 || !cdt.exists_constraint(handles[i], handles[n]) {
return decline("constraint had to be split");
}
boundary_pairs.insert((i.min(n), i.max(n)));
directed_boundary.insert((i, n));
}
}
let actual = cdt
.undirected_edges()
.filter(|e| e.data().is_constraint_edge())
.map(|e| {
let [a, b] = e.vertices();
let (a, b) = (a.data().authored, b.data().authored);
(a.min(b), a.max(b))
})
.collect::<HashSet<_>>();
if actual != boundary_pairs {
return decline("constraint set mismatch");
}
drop(build_timer);
let flood_timer = stage_timer(Stage::CdtFlood);
let mut adjacency: HashMap<usize, [(Option<usize>, bool); 3]> = HashMap::new();
let mut queue: std::collections::VecDeque<(usize, bool)> = std::collections::VecDeque::new();
for face in cdt.inner_faces() {
let face_index = face.fix().index();
let mut entry = [(None, false); 3];
for (slot, edge) in face.adjacent_edges().into_iter().enumerate() {
let is_constraint = edge.is_constraint_edge();
let neighbor = edge.rev().face();
if neighbor.is_outer() {
queue.push_back((face_index, is_constraint));
} else if let Some(inner) = neighbor.as_inner() {
entry[slot] = (Some(inner.index()), is_constraint);
}
}
adjacency.insert(face_index, entry);
}
let mut inside: HashMap<usize, bool> = HashMap::new();
while let Some((face_index, parity)) = queue.pop_front() {
if inside.contains_key(&face_index) {
continue;
}
inside.insert(face_index, parity);
for &(neighbor, is_constraint) in &adjacency[&face_index] {
if let Some(neighbor) = neighbor {
if !inside.contains_key(&neighbor) {
queue.push_back((neighbor, parity ^ is_constraint));
}
}
}
}
let mut triangles: Vec<[usize; 3]> = Vec::new();
for f in cdt.inner_faces() {
if !inside.get(&f.fix().index()).copied().unwrap_or(false) {
continue;
}
let mut t = f.vertices().map(|x| x.data().authored);
let uv = t.map(|i| vertices[i].uv);
if triangle_area(uv[0], uv[1], uv[2]) < 0.0 {
t.swap(1, 2);
}
triangles.push(t);
}
drop(flood_timer);
if triangles.len() < ranges.iter().map(|r| r.len()).sum::<usize>().saturating_sub(2) {
return decline("too few interior triangles");
}
let _check_timer = stage_timer(Stage::CdtCheck);
let mut dir: HashMap<(usize, usize), usize> = HashMap::new();
for &[a, b, c] in &triangles {
for e in [(a, b), (b, c), (c, a)] {
*dir.entry(e).or_insert(0) += 1;
}
}
for (&(a, b), &n) in &dir {
if n != 1 {
return decline("overlapping triangles (directed edge reused)");
}
let opp = dir.get(&(b, a)).copied().unwrap_or(0);
if directed_boundary.contains(&(a, b)) {
if opp != 0 {
if std::env::var("BREP_DEBUG_CDT").is_ok() {
eprintln!(
"[cdt] both-sides edge a={a} b={b} uv_a=({:.5},{:.5}) uv_b=({:.5},{:.5})",
vertices[a].uv[0], vertices[a].uv[1], vertices[b].uv[0], vertices[b].uv[1]
);
}
return decline("boundary edge used from both sides");
}
} else if opp != 1 {
if std::env::var("BREP_DEBUG_CDT").is_ok() {
eprintln!(
"[cdt] no-twin edge a={a} b={b} uv_a=({:.5},{:.5}) uv_b=({:.5},{:.5})",
vertices[a].uv[0], vertices[a].uv[1], vertices[b].uv[0], vertices[b].uv[1]
);
}
return decline("interior edge not shared by a twin");
}
}
Some(CdtMesh {
vertices,
triangles,
boundary_pairs,
})
}