use super::*;
pub(in crate::watertight_tessellation) fn triangle_area(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])) * 0.5
}
pub(super) fn point_in_triangle(p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
let d1 = triangle_area(p, a, b);
let d2 = triangle_area(p, b, c);
let d3 = triangle_area(p, c, a);
let has_negative = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
let has_positive = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
!(has_negative && has_positive)
}
pub(in crate::watertight_tessellation) fn segments_properly_cross(
p: [f64; 2],
q: [f64; 2],
r: [f64; 2],
s: [f64; 2],
epsilon: f64,
) -> bool {
let d1 = triangle_area(r, s, p);
let d2 = triangle_area(r, s, q);
let d3 = triangle_area(p, q, r);
let d4 = triangle_area(p, q, s);
((d1 > epsilon && d2 < -epsilon) || (d1 < -epsilon && d2 > epsilon))
&& ((d3 > epsilon && d4 < -epsilon) || (d3 < -epsilon && d4 > epsilon))
}
fn point_in_ring(vertices: &[FaceVertex], indices: &[usize], p: [f64; 2]) -> bool {
crossing_parity_inside(indices.len(), |offset| vertices[indices[offset]].uv, p)
}
pub(super) fn crossing_parity_inside(count: usize, uv: impl Fn(usize) -> [f64; 2], p: [f64; 2]) -> bool {
let mut inside = false;
for offset in 0..count {
let a = uv(offset);
let b = uv((offset + 1) % count);
if (a[1] > p[1]) != (b[1] > p[1]) {
let t = (p[1] - a[1]) / (b[1] - a[1]);
let x = a[0] + t * (b[0] - a[0]);
if x > p[0] {
inside = !inside;
}
}
}
inside
}
pub(in crate::watertight_tessellation) fn find_split_diagonal(
vertices: &[FaceVertex],
indices: &[usize],
epsilon: f64,
region: &RegionGuard,
) -> Option<(usize, usize)> {
let count = indices.len();
let mut best: Option<(usize, usize, usize, f64)> = None; for oa in 0..count {
for ob in (oa + 2)..count {
if oa == 0 && ob == count - 1 {
continue; }
let a = vertices[indices[oa]].uv;
let b = vertices[indices[ob]].uv;
let len2 = (b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2);
if len2 <= epsilon {
continue;
}
let balance = (ob - oa).min(count - (ob - oa));
if let Some((_, _, best_balance, best_len2)) = best {
if balance < best_balance || (balance == best_balance && len2 <= best_len2) {
continue; }
}
let mut valid = true;
for e in 0..count {
let e1 = (e + 1) % count;
if e == oa || e == ob || e1 == oa || e1 == ob {
continue;
}
if segments_properly_cross(
a,
b,
vertices[indices[e]].uv,
vertices[indices[e1]].uv,
epsilon,
) {
valid = false;
break;
}
}
if !valid {
continue;
}
if !region.diagonal_ok(indices[oa], indices[ob], epsilon) {
continue;
}
let mid = [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
if !point_in_ring(vertices, indices, mid) {
continue;
}
best = Some((oa, ob, balance, len2));
}
}
best.map(|(oa, ob, _, _)| (oa, ob))
}
pub(in crate::watertight_tessellation) struct RegionGuard<'a> {
pub(in crate::watertight_tessellation) vertices: &'a [FaceVertex],
pub(in crate::watertight_tessellation) ring: &'a [usize],
pub(in crate::watertight_tessellation) max_split_span: Option<(usize, f64)>,
}
impl RegionGuard<'_> {
fn diagonal_ok(&self, from: usize, to: usize, epsilon: f64) -> bool {
let a = self.vertices[from].uv;
let b = self.vertices[to].uv;
if let Some((axis, maximum)) = self.max_split_span {
if (b[axis] - a[axis]).abs() > maximum {
return false;
}
}
let midpoint = [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
if !point_in_ring(self.vertices, self.ring, midpoint) {
return false;
}
let count = self.ring.len();
for offset in 0..count {
let e0 = self.ring[offset];
let e1 = self.ring[(offset + 1) % count];
if e0 == from || e0 == to || e1 == from || e1 == to {
continue;
}
if segments_properly_cross(a, b, self.vertices[e0].uv, self.vertices[e1].uv, epsilon) {
return false;
}
}
true
}
fn ear_ok(&self, previous: usize, current: usize, next: usize, epsilon: f64) -> bool {
let a = self.vertices[previous].uv;
let b = self.vertices[current].uv;
let c = self.vertices[next].uv;
let centroid = [(a[0] + b[0] + c[0]) / 3.0, (a[1] + b[1] + c[1]) / 3.0];
if triangle_area(a, b, c).abs() > epsilon.sqrt().max(1e-12)
&& !point_in_ring(self.vertices, self.ring, centroid)
{
return false;
}
let count = self.ring.len();
for offset in 0..count {
let e0 = self.ring[offset];
let e1 = self.ring[(offset + 1) % count];
if e0 == previous
|| e0 == current
|| e0 == next
|| e1 == previous
|| e1 == current
|| e1 == next
{
continue;
}
let p = self.vertices[e0].uv;
let q = self.vertices[e1].uv;
if segments_properly_cross(a, c, p, q, epsilon)
|| segments_properly_cross(a, b, p, q, epsilon)
|| segments_properly_cross(b, c, p, q, epsilon)
{
return false;
}
}
true
}
}
pub(in crate::watertight_tessellation) fn ear_clip(
vertices: &[FaceVertex],
indices: &mut Vec<usize>,
region: &RegionGuard,
) -> Vec<[usize; 3]> {
let mut triangles = Vec::new();
let epsilon = {
let total: f64 = indices
.windows(2)
.map(|pair| {
let a = vertices[pair[0]].uv;
let b = vertices[pair[1]].uv;
((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt()
})
.sum();
(total / indices.len().max(1) as f64).powi(2) * 1e-12
};
let mut guard = 0usize;
while indices.len() > 3 {
let count = indices.len();
let mut clipped = false;
for offset in 0..count {
let previous = indices[(offset + count - 1) % count];
let current = indices[offset];
let next = indices[(offset + 1) % count];
let a = vertices[previous].uv;
let b = vertices[current].uv;
let c = vertices[next].uv;
if triangle_area(a, b, c) <= epsilon {
continue;
}
let mut contains_other = false;
for &other in indices.iter() {
if other == previous || other == current || other == next {
continue;
}
let p = vertices[other].uv;
if (p[0] - a[0]).abs() + (p[1] - a[1]).abs() <= 1e-14
|| (p[0] - b[0]).abs() + (p[1] - b[1]).abs() <= 1e-14
|| (p[0] - c[0]).abs() + (p[1] - c[1]).abs() <= 1e-14
{
continue;
}
if point_in_triangle(p, a, b, c) {
contains_other = true;
break;
}
}
if contains_other {
continue;
}
if !region.ear_ok(previous, current, next, epsilon) {
continue;
}
triangles.push([previous, current, next]);
indices.remove(offset);
clipped = true;
break;
}
if !clipped {
guard += 1;
if std::env::var("BREP_DEBUG_TESS").is_ok() {
eprintln!(
"ear_clip stall #{guard}: {} vertices remain: {:?}",
indices.len(),
indices
.iter()
.map(|&i| (vertices[i].uv[0], vertices[i].uv[1]))
.collect::<Vec<_>>()
);
}
if let Some((oa, ob)) = find_split_diagonal(&vertices, indices, epsilon, region) {
let mut first: Vec<usize> = indices[oa..=ob].to_vec();
let mut second: Vec<usize> = indices[ob..].to_vec();
second.extend_from_slice(&indices[..=oa]);
indices.clear();
triangles.extend(ear_clip(vertices, &mut first, region));
triangles.extend(ear_clip(vertices, &mut second, region));
return triangles;
}
if guard > 2 {
for offset in 1..indices.len() - 1 {
triangles.push([indices[0], indices[offset], indices[offset + 1]]);
}
indices.clear();
return triangles;
}
let flattest = (0..indices.len())
.min_by(|&a_offset, &b_offset| {
let flatness = |offset: usize| {
let count = indices.len();
let a = vertices[indices[(offset + count - 1) % count]].uv;
let b = vertices[indices[offset]].uv;
let c = vertices[indices[(offset + 1) % count]].uv;
triangle_area(a, b, c).abs()
};
flatness(a_offset).total_cmp(&flatness(b_offset))
})
.unwrap_or(0);
let count = indices.len();
triangles.push([
indices[(flattest + count - 1) % count],
indices[flattest],
indices[(flattest + 1) % count],
]);
indices.remove(flattest);
}
}
if indices.len() == 3 {
triangles.push([indices[0], indices[1], indices[2]]);
}
triangles
}
pub(super) fn lawson_flips(
vertices: &[FaceVertex],
triangles: &mut [[usize; 3]],
boundary_pairs: &HashSet<(usize, usize)>,
scale: [f64; 2],
) {
lawson_flips_guarded(vertices, triangles, boundary_pairs, scale, None);
}
pub(super) fn lawson_flips_guarded(
vertices: &[FaceVertex],
triangles: &mut [[usize; 3]],
boundary_pairs: &HashSet<(usize, usize)>,
scale: [f64; 2],
chord_guard: Option<(&FaceRecord, f64, bool)>,
) {
let mut memo = FaceMemo::default();
lawson_flips_guarded_with_memo(vertices, triangles, boundary_pairs, scale, chord_guard, &mut memo);
}
pub(super) fn lawson_flips_guarded_with_memo(
vertices: &[FaceVertex],
triangles: &mut [[usize; 3]],
boundary_pairs: &HashSet<(usize, usize)>,
scale: [f64; 2],
chord_guard: Option<(&FaceRecord, f64, bool)>,
memo: &mut FaceMemo,
) {
lawson_flips_incremental(
vertices, triangles, boundary_pairs, scale, chord_guard, memo,
);
}
#[derive(Default)]
pub(in crate::watertight_tessellation) struct FaceMemo {
midpoints: FxHashMap<(usize, usize), Vec3>,
normals: FxHashMap<usize, Option<Vec3>>,
}
impl FaceMemo {
pub(in crate::watertight_tessellation) fn midpoint(
&mut self,
face: &FaceRecord,
vertices: &[FaceVertex],
a: usize,
b: usize,
periodic: bool,
) -> Result<Vec3, String> {
let key = (a.min(b), a.max(b));
if let Some(&point) = self.midpoints.get(&key) {
return Ok(point);
}
let uv = [
(vertices[a].uv[0] + vertices[b].uv[0]) * 0.5,
(vertices[a].uv[1] + vertices[b].uv[1]) * 0.5,
];
let point = if periodic {
face.surface.evaluate_extended(uv[0], uv[1])?
} else {
face.surface.evaluate(uv[0], uv[1])?
};
self.midpoints.insert(key, point);
Ok(point)
}
pub(in crate::watertight_tessellation) fn normal(
&mut self,
face: &FaceRecord,
vertices: &[FaceVertex],
index: usize,
periodic: bool,
) -> Option<Vec3> {
if let Some(&normal) = self.normals.get(&index) {
return normal;
}
let uv = vertices[index].uv;
let normal = (|| {
let (_, su, sv) = if periodic {
face.surface.deriv1_extended(uv[0], uv[1]).ok()?
} else {
face.surface.deriv1(uv[0], uv[1]).ok()?
};
su.cross(sv).normalized().ok()
})();
self.normals.insert(index, normal);
normal
}
}
#[derive(Default)]
struct Owners {
count: usize,
inline: [usize; 3],
spill: Vec<usize>,
}
impl Owners {
fn as_slice(&self) -> &[usize] {
if self.spill.is_empty() {
&self.inline[..self.count]
} else {
&self.spill
}
}
fn len(&self) -> usize {
self.as_slice().len()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn spill_out(&mut self) {
if self.spill.is_empty() {
self.spill = self.inline[..self.count].to_vec();
}
}
fn push(&mut self, index: usize) {
if !self.spill.is_empty() {
self.spill.push(index);
} else if self.count < self.inline.len() {
self.inline[self.count] = index;
self.count += 1;
} else {
self.spill_out();
self.spill.push(index);
}
}
fn insert(&mut self, offset: usize, index: usize) {
if self.spill.is_empty() && self.count < self.inline.len() {
self.inline.copy_within(offset..self.count, offset + 1);
self.inline[offset] = index;
self.count += 1;
} else {
self.spill_out();
self.spill.insert(offset, index);
}
}
fn remove(&mut self, offset: usize) {
if self.spill.is_empty() {
self.inline.copy_within(offset + 1..self.count, offset);
self.count -= 1;
} else {
self.spill.remove(offset);
if self.spill.is_empty() {
self.count = 0;
}
}
}
}
#[derive(Clone, Copy, Default)]
struct TriangleCornerAngles([Option<f64>; 3]);
impl TriangleCornerAngles {
fn angle(&mut self, triangle: [usize; 3], apex: usize, compute: impl FnOnce() -> f64) -> f64 {
let corner = triangle.iter().position(|&vertex| vertex == apex).expect("opposite corner in triangle");
if let Some(value) = self.0[corner] {
return value;
}
let value = compute();
self.0[corner] = Some(value);
value
}
fn clear(&mut self) {
self.0 = [None; 3];
}
}
fn lawson_flips_incremental(
vertices: &[FaceVertex],
triangles: &mut [[usize; 3]],
boundary_pairs: &HashSet<(usize, usize)>,
scale: [f64; 2],
chord_guard: Option<(&FaceRecord, f64, bool)>,
memo: &mut FaceMemo,
) {
let scaled = |index: usize| -> [f64; 2] {
[
vertices[index].uv[0] * scale[0],
vertices[index].uv[1] * scale[1],
]
};
let corner_angle = |apex: usize, first: usize, second: usize| -> f64 {
let p = scaled(apex);
let a = scaled(first);
let b = scaled(second);
let u = [a[0] - p[0], a[1] - p[1]];
let v = [b[0] - p[0], b[1] - p[1]];
(u[0] * v[1] - u[1] * v[0])
.abs()
.atan2(u[0] * v[0] + u[1] * v[1])
};
let uv_area = |a: usize, b: usize, c: usize| -> f64 {
triangle_area(vertices[a].uv, vertices[b].uv, vertices[c].uv)
};
const MAX_FLIP_ROUNDS: usize = 2048;
let maximum_rounds = (triangles.len() + 16).min(MAX_FLIP_ROUNDS);
let mut sag_memo: HashMap<(usize, usize), f64> = HashMap::new();
let mut corner_angles = vec![TriangleCornerAngles::default(); triangles.len()];
let mut total_flips = 0usize;
let _timer = stage_timer(Stage::Flip);
tess_profile(|p| p.flip_calls += 1);
let build_started = profile_started();
let mut edge_owners: FxHashMap<(usize, usize), Owners> = FxHashMap::default();
edge_owners.reserve(triangles.len() * 3 / 2 + 1);
for (index, triangle) in triangles.iter().enumerate() {
for corner in 0..3 {
let a = triangle[corner];
let b = triangle[(corner + 1) % 3];
edge_owners.entry((a.min(b), a.max(b))).or_default().push(index);
}
}
let mut agenda: Vec<(usize, usize)> = edge_owners.keys().copied().collect();
agenda.sort_unstable();
let mut changes: Vec<(usize, [usize; 3], [usize; 3])> = Vec::new();
let mut touched: Vec<bool> = Vec::with_capacity(triangles.len());
let protected = ProtectedSegments::new(boundary_pairs.iter().copied(), |index| {
vertices[index].uv
});
tess_profile(|p| p.flip_build_ms += elapsed_ms(build_started));
for _round in 0..maximum_rounds {
tess_profile(|p| p.flip_rounds += 1);
changes.clear();
touched.clear();
touched.resize(triangles.len(), false);
let mut flipped_any = false;
let scan_started = profile_started();
for &(a, b) in &agenda {
tess_profile(|p| p.flip_edges_visited += 1);
let Some(owners) = edge_owners.get(&(a, b)) else {
continue;
};
let owners = owners.as_slice();
if boundary_pairs.contains(&(a, b)) || owners.len() != 2 {
continue;
}
let (first, second) = (owners[0], owners[1]);
if touched[first] || touched[second] {
continue;
}
let other = |triangle: [usize; 3]| {
triangle
.into_iter()
.find(|&vertex| vertex != a && vertex != b)
};
let (Some(c), Some(d)) = (other(triangles[first]), other(triangles[second])) else {
continue;
};
if c == d {
continue;
}
let first_angle = corner_angles[first].angle(triangles[first], c, || corner_angle(c, a, b));
let second_angle = corner_angles[second].angle(triangles[second], d, || corner_angle(d, a, b));
if first_angle + second_angle <= std::f64::consts::PI + 1e-9 {
continue;
}
if let Some((face, chord_tolerance, periodic)) = chord_guard {
let mut sag = |x: usize, y: usize| -> Option<f64> {
let key = (x.min(y), x.max(y));
if let Some(&cached) = sag_memo.get(&key) {
return Some(cached);
}
tess_profile(|p| p.flip_sag_evals += 1);
let middle = memo.midpoint(face, vertices, x, y, periodic).ok()?;
let value = deflection_to_chord(
middle,
vertices[x].position,
vertices[y].position,
);
sag_memo.insert(key, value);
Some(value)
};
let new_sag = sag(c, d);
let old_sag = sag(a, b);
if let (Some(new_sag), Some(old_sag)) = (new_sag, old_sag) {
if new_sag > chord_tolerance && new_sag > old_sag {
continue;
}
}
}
let (a, b) = if uv_area(a, b, c) > 0.0 {
(a, b)
} else {
(b, a)
};
let candidates = [[c, a, d], [c, d, b]];
if candidates
.iter()
.any(|&[x, y, z]| uv_area(x, y, z) <= 1e-14)
{
continue;
}
if !protected.is_empty()
&& protected.crosses(c, d, |index| vertices[index].uv, 1e-14)
{
continue;
}
changes.push((first, triangles[first], candidates[0]));
changes.push((second, triangles[second], candidates[1]));
corner_angles[first].clear();
corner_angles[second].clear();
triangles[first] = candidates[0];
triangles[second] = candidates[1];
touched[first] = true;
touched[second] = true;
flipped_any = true;
total_flips += 1;
tess_profile(|p| p.flips += 1);
}
tess_profile(|p| p.flip_scan_ms += elapsed_ms(scan_started));
let apply_started = profile_started();
for &(index, old, _) in &changes {
for corner in 0..3 {
let a = old[corner];
let b = old[(corner + 1) % 3];
let key = (a.min(b), a.max(b));
let owners = edge_owners.get_mut(&key).expect("old triangle edge owner");
let offset = owners.as_slice().binary_search(&index).expect("old triangle owner index");
owners.remove(offset);
if owners.is_empty() {
edge_owners.remove(&key);
}
}
}
agenda.clear();
for &(index, _, new) in &changes {
for corner in 0..3 {
let a = new[corner];
let b = new[(corner + 1) % 3];
let key = (a.min(b), a.max(b));
let owners = edge_owners.entry(key).or_default();
let offset = owners.as_slice().binary_search(&index).unwrap_or_else(|offset| offset);
owners.insert(offset, index);
agenda.push(key);
}
}
agenda.sort_unstable();
agenda.dedup();
tess_profile(|p| p.flip_apply_ms += elapsed_ms(apply_started));
if !flipped_any {
break;
}
}
if std::env::var("BREP_DEBUG_TESS").is_ok() {
eprintln!(
" lawson: {total_flips} flips over {} triangles",
triangles.len()
);
}
}