tess: a face whose boundary still crosses itself after untangling is filled leniently, not the whole body refused

Two edges leaving a vertex almost tangentially (a cusp, as a round runs out on Poppy
Humanoid's creative_commons_license) separate by about Δκ·s²/2 at distance s while a chord of
length L sags by κL²/8: halving the chords never stops them crossing. untangle returned that
face's error for the whole plan, and the body's 1834 faces were not shown. It now returns the
faces still crossing, each with why, and plan_or_fill fills each leniently straight away and
names it in the stats (the rule of #7755): the other faces are planned as usual.

Test: a face named as still tangled is filled leniently and named with that reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-27 20:13:54 -04:00
committed by Irakli Kourtis
parent 0694fd4e8d
commit 1d70ac2673
2 changed files with 45 additions and 15 deletions
+28 -14
View File
@@ -192,12 +192,13 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
};
let mut boundaries: Vec<Boundary> =
crate::par::map("face boundaries", &indexed, curved >= PARALLEL_FACES, |&fi| face_boundary(fi, &nodes)).into_iter().collect::<Result<_>>()?;
untangle(b, &evals, &edge_faces, &mut nodes, &mut boundaries, &face_boundary)?;
let tangled = untangle(b, &evals, &edge_faces, &mut nodes, &mut boundaries, &face_boundary)?;
let planned = crate::par::map("face tessellation", &indexed, curved >= PARALLEL_FACES, |&fi| {
let (Some(face), Some(eval), Some(size), Some(fb)) = (faces.get(fi), evals.get(fi), sizes.get(fi), boundaries.get(fi)) else {
return Err(unsupported(format!("face {fi}: missing")));
};
let (fp, stats) = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size)?;
let refused = tangled.iter().find(|(f, _)| *f == fi).map(|(_, why)| why.as_str());
let (fp, stats) = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size, refused)?;
let fwd = forward.get(fi).copied().unwrap_or(true);
let fp =
FacePlan { flip: !fwd, triangles: if fwd { fp.triangles } else { fp.triangles.into_iter().map(|[a, b, c]| [a, c, b]).collect() }, ..fp };
@@ -356,7 +357,7 @@ fn untangle(
nodes: &mut [edges::Nodes],
boundaries: &mut [Boundary],
face_boundary: &(dyn Fn(usize, &[edges::Nodes]) -> Result<Boundary> + Sync),
) -> Result<()> {
) -> Result<Vec<(usize, String)>> {
let crossing = |fi: usize, fb: &Boundary| -> Vec<(u32, u32)> {
let Some(s) = evals.get(fi) else { return Vec::new() };
tangle::crossing(s, &fb.loops).into_iter().filter_map(|(l, k)| fb.segments.get(l)?.get(k).copied()).collect()
@@ -370,15 +371,21 @@ fn untangle(
.collect::<Result<_>>()?;
let mut tangled: Vec<(usize, Vec<(u32, u32)>)> = found.into_iter().enumerate().filter(|(_, c)| !c.is_empty()).collect();
for round in 0..=MAX_UNTANGLE {
let Some((fi, first)) = tangled.first() else { return Ok(()) };
if tangled.is_empty() {
return Ok(Vec::new());
}
if round == MAX_UNTANGLE {
let mut es: Vec<u32> = first.iter().map(|(e, _)| *e).collect();
es.sort_unstable();
es.dedup();
let kind = b.faces.get(*fi).map_or("?", |f| kind(&f.surface));
return Err(unsupported(format!(
"face {fi} ({kind}): boundary crosses itself: edges {es:?} run closer together than their chords can follow after {MAX_UNTANGLE} halvings"
)));
// Each face still crossing, with why: planned leniently, and named (`plan_or_fill`).
return Ok(tangled
.iter()
.map(|(fi, c)| {
let mut es: Vec<u32> = c.iter().map(|(e, _)| *e).collect();
es.sort_unstable();
es.dedup();
let kind = b.faces.get(*fi).map_or("?", |f| kind(&f.surface));
(*fi, format!("face {fi} ({kind}): boundary crosses itself: edges {es:?} run closer together than their chords can follow after {MAX_UNTANGLE} halvings"))
})
.collect());
}
let mut split: Vec<(u32, u32)> = tangled.iter().flat_map(|(_, c)| c.iter().copied()).collect();
split.sort_unstable();
@@ -405,7 +412,7 @@ fn untangle(
}
}
}
Ok(())
Ok(Vec::new())
}
/// Bounded work for the facet angle: a face whose boundary would get more than
@@ -626,7 +633,9 @@ fn holes_beside_outer(dom: &uv::Domain, loops: &mut [seam::Loop]) {
/// One face, planned by [`plan_face`]; a face it refuses because its loops cross in (u, v) is
/// planned once more leniently (see [`Lenient`]) and named in the stats with the first refusal,
/// so the defect upstream is still reported. When the lenient fill refuses too, the first
/// refusal is the error.
/// refusal is the error. `refused`: a face whose boundary still crosses itself after
/// [`untangle`] (two edges meeting tangentially at a cusp, whose chords cross however often
/// they are halved), filled leniently straight away with that reason.
fn plan_or_fill(
fi: usize,
kind: &str,
@@ -634,9 +643,14 @@ fn plan_or_fill(
loops: &[seam::Loop],
rules: &size::Rules,
size: (f64, [f64; 2]),
refused: Option<&str>,
) -> Result<(FacePlan, PlanStats)> {
let mut stats = PlanStats::default();
match plan_face(fi, kind, s, loops, rules, size, &mut stats, Lenient::No) {
let strict = match refused {
Some(why) => Err(unsupported(why.to_string())),
None => plan_face(fi, kind, s, loops, rules, size, &mut stats, Lenient::No),
};
match strict {
Ok(fp) => Ok((fp, stats)),
Err(first) => {
let mut stats = PlanStats::default();
+17 -1
View File
@@ -743,7 +743,7 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
fn a_face_filled_leniently_is_named_in_the_stats() {
let (s, l, rules) = a_face_crossing_itself();
let size = size::face_size(&s, &uv::domain(&s), &rules);
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size) else { panic!("filled leniently") };
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size, None) else { panic!("filled leniently") };
assert!(!fp.triangles.is_empty());
assert_eq!((stats.planar, stats.structured, stats.delaunay), (0, 0, 0));
let [lenient] = stats.lenient.as_slice() else { panic!("one lenient face: {:?}", stats.lenient) };
@@ -751,6 +751,22 @@ fn a_face_filled_leniently_is_named_in_the_stats() {
assert!(lenient.reason.contains("face 7 (spline)"), "{}", lenient.reason);
}
/// A face whose boundary still crosses itself after untangling (two edges leaving a vertex
/// tangentially: their chords cross however often they are halved, as on one of Poppy
/// Humanoid's faces) is filled leniently straight away with that reason, and named: the body's
/// other faces are planned as usual instead of the whole body failing with it.
#[test]
fn a_face_untangling_cannot_resolve_is_filled_leniently_and_named() {
let (s, l, rules) = a_face_crossing_itself();
let size = size::face_size(&s, &uv::domain(&s), &rules);
let why = "face 7 (spline): boundary crosses itself: edges [1, 2] run closer together than their chords can follow";
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size, Some(why)) else { panic!("filled leniently") };
assert!(!fp.triangles.is_empty());
let [lenient] = stats.lenient.as_slice() else { panic!("one lenient face: {:?}", stats.lenient) };
assert_eq!(lenient.face, 7);
assert!(lenient.reason.contains(why), "{}", lenient.reason);
}
#[test]
fn a_face_bounded_by_two_short_arcs_keeps_an_area_to_mesh() {
// A lens on z = 0 between two radius-1000 circles crossing at (±10, 0), in a record whose