tess: a face no method plans is filled leniently in (u, v), and named in the stats

A face whose boundary crosses itself in (u, v) (a cusp, a sliver whose sides
sag across each other) was an error, and the body had no display plan. It is
now planned once more with a lenient constrained Delaunay in (u, v): sides
crossing one already kept are left out, the rest is seeded and refined as any
face, and its points lie on the surface, where the GPU evaluates them. The
display has no hole, but the face is still a defect upstream, in its pcurves
or seams: PlanStats::lenient names it with the strict plan's refusal, and the
display plan logs a warning for each such face, so it is never silent.

The face boundaries keep coming from the exact pcurves (d61efb306); cdt::Fill
gets its `lenient` field back (dropped in 3f2585569). The old flat fill (#4021,
tess/fallback.rs) stays gone: develop removed it in 9138e8704. A face the
lenient fill refuses too (loops that do not close in (u, v), or wind round a
seam or pole) stays an error naming the face. Closes #4453's intent on develop.

(cherry picked from commit 84de5aa071)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-21 07:40:29 -04:00
committed by Irakli Kourtis
parent 4f6f254d04
commit 676919d4d6
4 changed files with 168 additions and 18 deletions
+7 -1
View File
@@ -950,7 +950,13 @@ impl Body {
return Err(KernelError::Invalid("a mesh body has no surfaces to mesh".into()));
}
let (tol, turn) = t.rules(self.size());
guard("mesh", || crate::tess::plan(&self.record, tol, turn).map(|(plan, _)| plan))
let (plan, stats) = guard("mesh", || crate::tess::plan(&self.record, tol, turn))?;
// A face filled leniently has no hole on screen, but its pcurves or seams are still wrong:
// it is reported, never silent.
for f in &stats.lenient {
log::warn!("display plan: face {} filled leniently in (u, v), a defect in its pcurves or seams: {}", f.face, f.reason);
}
Ok(plan)
}
/// The mesh of `plan`, a plan [`Body::mesh_plan`] made of this body: what [`Body::mesh`]
+16 -4
View File
@@ -50,6 +50,10 @@ pub(crate) struct Fill<'a> {
/// varies: a plane is shaded the same by any triangles, so planar faces skip it (around many
/// small holes it grades out from each rim and multiplies the triangles).
pub shape: bool,
/// A side crossing one already kept is left out instead of refusing the loops: the last
/// resort for a face whose boundary crosses itself in (u, v), so the display has no hole
/// (the planner names the face in its stats as a defect upstream).
pub lenient: bool,
}
/// Triangulate the region bounded by `loops` (ranges of `boundary`, each closed). Boundary
@@ -83,6 +87,9 @@ pub(crate) fn triangulate(boundary: &[[f64; 2]], loops: &[std::ops::Range<usize>
continue;
}
if !cdt.can_add_constraint(a, b) {
if fill.lenient {
continue;
}
return None;
}
cdt.add_constraint(a, b);
@@ -476,7 +483,7 @@ mod tests {
for refine in [false, true] {
let refine = refine.then_some((Sag { eval: &flat, tol: 1e-3, h: 0.1, turn: None, scale: [2.0, 1.0] }, 2000));
let label = refine.is_some();
let Some((extra, tris)) = triangulate(&b, &loops, Fill { seeds: &[[0.2, 0.2], [0.5, 0.5]], refine, shape: true }) else {
let Some((extra, tris)) = triangulate(&b, &loops, Fill { seeds: &[[0.2, 0.2], [0.5, 0.5]], refine, shape: true, lenient: false }) else {
panic!("meshes")
};
let mut uv = b.clone();
@@ -496,10 +503,15 @@ mod tests {
}
#[test]
fn crossing_loops_are_refused() {
fn crossing_loops_are_refused_unless_lenient() {
let b = [[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]];
let whole = std::slice::from_ref(&(0..4));
assert!(triangulate(&b, whole, Fill { seeds: &[], refine: None, shape: false }).is_none());
assert!(triangulate(&b, whole, Fill { seeds: &[], refine: None, shape: false, lenient: false }).is_none());
// Lenient, the crossing side is left out and what the rest encloses is filled.
let Some((_, tris)) = triangulate(&b, whole, Fill { seeds: &[], refine: None, shape: false, lenient: true }) else {
panic!("a lenient fill")
};
assert!(!tris.is_empty());
}
#[test]
@@ -523,7 +535,7 @@ mod tests {
// A parameter that is zero in exact arithmetic, left at 1e-300 by projection.
let b = [[1e-300, 0.0], [1.0, -1e-310], [1.0, 1.0], [0.0, 1.0]];
let whole = std::slice::from_ref(&(0..4));
let Some((extra, tris)) = triangulate(&b, whole, Fill { seeds: &[], refine: None, shape: false }) else { panic!("meshes") };
let Some((extra, tris)) = triangulate(&b, whole, Fill { seeds: &[], refine: None, shape: false, lenient: false }) else { panic!("meshes") };
assert!(extra.is_empty());
assert_eq!(tris.len(), 2);
}
+76 -11
View File
@@ -13,10 +13,14 @@
//! 2. otherwise, the general method for every face: constrained Delaunay in the face's (u, v),
//! with interior seeds and Ruppert refinement, every node then evaluated on the exact
//! surface (and refined where a triangle sags off it);
//! 3. a face even that refuses (its loops cross or do not close in (u, v), or wind round a
//! seam or pole), or whose pcurves do not lie on its edges, is an error naming the face (and
//! the edge) and why. Its cause is upstream, in the record's pcurves or seams, and is fixed
//! there; there is no other mesher to fall back to.
//! 3. a face even that refuses because its loops cross in (u, v) (a cusp, a sliver whose sides
//! sag across each other) is still a defect upstream, in the record's pcurves or seams, and is
//! named in the plan's stats ([`PlanStats::lenient`], with why it was refused) so it is
//! reported and fixed there; it is filled leniently in (u, v) (the sides crossing one already
//! kept are left out, see [`Lenient`]) so the display has no hole. A face that refuses even
//! that (its loops do not close in (u, v), or wind round a seam or pole), or whose pcurves do
//! not lie on its edges, is an error naming the face (and the edge) and why; there is no
//! other mesher to fall back to.
pub(crate) mod cdt;
pub(crate) mod collar;
@@ -41,12 +45,33 @@ use solidcraft_geom::tess::{EvalSurface, FacePlan, TessPlan};
use crate::{KernelError, Result};
/// Whether [`plan_face`] may leave out of its constrained Delaunay the boundary sides that cross
/// a side already kept, and not hold the triangles to every side: the last resort for a face
/// that crosses itself in (u, v) (a cusp, a sliver whose sides sag across each other), so the
/// display has no hole. Its points stay in (u, v), so a curved face is still drawn on its
/// surface. Such a face is named in [`PlanStats::lenient`]: its pcurves or seams are wrong.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Lenient {
No,
Yes,
}
/// How the faces of a plan were meshed.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PlanStats {
pub planar: usize,
pub structured: usize,
pub delaunay: usize,
/// Faces every method refused, filled leniently in (u, v) (see [`Lenient`]) so the display
/// has no hole: each still a defect upstream, in its pcurves or seams.
pub lenient: Vec<LenientFace>,
}
/// A face filled leniently: its index in the record and why the strict plan refused it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LenientFace {
pub face: usize,
pub reason: String,
}
/// Curved faces from which their sizes are worked out on the worker pool.
@@ -171,8 +196,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
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 mut stats = PlanStats::default();
let fp = plan_face(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size, &mut stats)?;
let (fp, stats) = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size)?;
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 };
@@ -188,6 +212,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
stats.planar += s.planar;
stats.structured += s.structured;
stats.delaunay += s.delaunay;
stats.lenient.extend(s.lenient);
}
plan.edges = nodes.into_iter().map(|n| n.points).collect();
plan.edge_faces = edge_faces.into_iter().map(|fs| fs.into_iter().map(|f| u32::try_from(f).unwrap_or(u32::MAX)).collect()).collect();
@@ -564,8 +589,38 @@ 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.
fn plan_or_fill(
fi: usize,
kind: &str,
s: &EvalSurface,
loops: &[seam::Loop],
rules: &size::Rules,
size: (f64, [f64; 2]),
) -> Result<(FacePlan, PlanStats)> {
let mut stats = PlanStats::default();
match plan_face(fi, kind, s, loops, rules, size, &mut stats, Lenient::No) {
Ok(fp) => Ok((fp, stats)),
Err(first) => {
let mut stats = PlanStats::default();
match plan_face(fi, kind, s, loops, rules, size, &mut stats, Lenient::Yes) {
Ok(fp) => {
stats.lenient.push(LenientFace { face: fi, reason: first.to_string() });
Ok((fp, stats))
}
Err(_) => Err(first),
}
}
}
}
/// One face: boundary loops in 3D (shared edge nodes) to parameters and triangles.
/// `size`: the face's target side length (mm) and the surface's mean speed along u and v.
/// `lenient`: the last resort for a face no method could plan (see [`Lenient`]).
#[allow(clippy::too_many_arguments)]
fn plan_face(
fi: usize,
kind: &str,
@@ -574,6 +629,7 @@ fn plan_face(
rules: &size::Rules,
size: (f64, [f64; 2]),
stats: &mut PlanStats,
lenient: Lenient,
) -> Result<FacePlan> {
let tol = rules.tol;
let (h, speed) = size;
@@ -659,13 +715,21 @@ fn plan_face(
let seeds = cdt::lattice(bbox, step, &polys, speed);
let eval = |u: f64, v: f64| s.derivs(u, v).map(|(p, su, sv)| (p, su.cross(sv).normalized()));
let sag = cdt::Sag { eval: &eval, tol, h, turn: rules.turn, scale: speed };
let fill = cdt::Fill { seeds: &seeds, refine: Some((sag, MAX_REFINE)), shape: !matches!(s, EvalSurface::Plane { .. }) };
let fill = cdt::Fill {
seeds: &seeds,
refine: Some((sag, MAX_REFINE)),
shape: !matches!(s, EvalSurface::Plane { .. }),
lenient: lenient == Lenient::Yes,
};
cdt::triangulate(&uvs, &ranges, fill).ok_or_else(|| fail("boundary crosses itself"))
};
// Planar and curved faces alike: a structured grid where the boundary runs along the
// parameter box (a rectangle, a cylinder half), a Coons grid on four corners, else Delaunay.
let planar = matches!(s, EvalSurface::Plane { .. });
let (extra, triangles) = {
// The lenient fill is the constrained Delaunay alone: the caller names the face in the stats.
let (extra, triangles) = if lenient == Lenient::Yes {
delaunay()?
} else {
let mut us = uv::divide(s, 0, bbox[0], bbox[1], rules, h, MAX_LINES);
let mut vs = uv::divide(s, 1, bbox[1], bbox[0], rules, h, MAX_LINES);
grid::thin(&mut us, &mut vs, grid::MAX_FACE_GRID);
@@ -755,7 +819,8 @@ fn plan_face(
let pole_pair = |i: u32, j: u32| i != j && fixed.get(i as usize).zip(fixed.get(j as usize)).is_some_and(|(p, q)| p == q);
let mut triangles = triangles;
triangles.retain(|t| !(pole_pair(t[0], t[1]) || pole_pair(t[1], t[2]) || pole_pair(t[2], t[0])));
if !closed_boundary(&ranges, &fixed, &uvs, &triangles) {
// A lenient fill leaves out the sides that cross: the boundary is followed everywhere else.
if lenient == Lenient::No && !closed_boundary(&ranges, &fixed, &uvs, &triangles) {
return Err(fail("triangulation does not follow the boundary"));
}
let mut all = uvs;
@@ -896,7 +961,7 @@ fn plan_pieces(f: &Region) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
let seeds = cdt::lattice(bbox, [f.h / f.speed[0], f.h / f.speed[1]], &polys, f.speed);
let eval = |u: f64, v: f64| s.derivs(u, v).map(|(p, su, sv)| (p, su.cross(sv).normalized()));
let sag = cdt::Sag { eval: &eval, tol: f.rules.tol, h: f.h, turn: f.rules.turn, scale: f.speed };
cdt::triangulate(&luv, &ranges, cdt::Fill { seeds: &seeds, refine: Some((sag, MAX_REFINE)), shape: true })
cdt::triangulate(&luv, &ranges, cdt::Fill { seeds: &seeds, refine: Some((sag, MAX_REFINE)), shape: true, lenient: false })
})?;
let (lextra, ltris) = local;
let base = u32::try_from(n + extra.len()).ok()?;
+69 -2
View File
@@ -99,7 +99,7 @@ fn a_box_is_a_regular_grid_on_every_face() {
fn primitives_are_watertight_with_the_right_volume() {
for tol in [0.05, 0.005] {
let s = check("box", &ok(crate::box_solid(Vec3::ZERO, Vec3::new(10.0, 6.0, 4.0))), tol, 240.0);
assert_eq!(s, PlanStats { planar: 6, structured: 0, delaunay: 0 });
assert_eq!(s, PlanStats { planar: 6, structured: 0, delaunay: 0, lenient: Vec::new() });
let s = check("cylinder", &ok(crate::cylinder(Vec3::new(1.0, 2.0, 3.0), Vec3::new(0.0, 1.0, 1.0), 2.5, 7.0)), tol, PI * 2.5 * 2.5 * 7.0);
assert_eq!(s.delaunay, 0, "cylinder walls are structured");
let s = check("torus", &ok(crate::torus(Vec3::ZERO, 6.0, 1.5)), tol, 2.0 * PI * PI * 6.0 * 1.5 * 1.5);
@@ -645,7 +645,7 @@ fn loops_touching_at_a_shared_vertex_bound_the_face() {
let uvs: Vec<[f64; 2]> = outer.iter().chain(&left).chain(&right).copied().collect();
let fixed: Vec<Vec3> = uvs.iter().map(|q| Vec3::new(q[0], q[1], 0.0)).collect();
let ranges = vec![0..4, 4..7, 7..10];
let fill = cdt::Fill { seeds: &[], refine: None, shape: false };
let fill = cdt::Fill { seeds: &[], refine: None, shape: false, lenient: false };
let Some((_, triangles)) = cdt::triangulate(&uvs, &ranges, fill) else { panic!("triangulates") };
assert!(closed_boundary(&ranges, &fixed, &uvs, &triangles), "the triangles follow both holes through their shared vertex");
}
@@ -683,3 +683,70 @@ fn a_thin_lune_whose_chords_cross_meshes() {
assert_watertight(&m, "lune");
assert!(m.measure().volume > 0.0, "inside out");
}
/// A doubly curved patch 40 mm across, and a loop on it in (u, v) whose right side folds back
/// past itself near the top: the last chord of the fold crosses the side, as a cusp or a sliver
/// whose sides sag across each other leaves a face's boundary.
fn a_face_crossing_itself() -> (EvalSurface, seam::Loop, size::Rules) {
let mut ctrl = Vec::new();
for j in 0..3 {
for i in 0..3 {
let bump = if i == 1 && j == 1 { 12.0 } else { 0.0 };
ctrl.push([20.0 * f64::from(i), 20.0 * f64::from(j), bump, 1.0]);
}
}
let s = EvalSurface::Nurbs {
degree: [2, 2],
knots: [vec![0., 0., 0., 1., 1., 1.], vec![0., 0., 0., 1., 1., 1.]],
count: [3, 3],
ctrl,
closed: [false, false],
};
let corners = [[0.1, 0.1], [0.9, 0.1], [0.9, 0.7], [0.7, 0.95], [0.86, 0.8], [0.1, 0.9]];
let mut uv: Vec<[f64; 2]> = Vec::new();
for k in 0..corners.len() {
let (a, b) = (corners[k], corners[(k + 1) % corners.len()]);
for t in 0..8 {
let f = f64::from(t) / 8.0;
uv.push([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f]);
}
}
let pts: Vec<Vec3> = uv.iter().filter_map(|q| s.derivs(q[0], q[1]).map(|(p, _, _)| p)).collect();
assert_eq!(pts.len(), uv.len());
(s, seam::Loop { uv, pts }, size::Rules::new(0.05, 40.0, None))
}
/// A curved face crossing itself in (u, v) is refused by every method; the lenient fill leaves
/// the crossing side out and plans the rest in (u, v), with points inside the face on its surface
/// (the GPU evaluates them there), not flat between boundary nodes.
#[test]
fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
let (s, l, rules) = a_face_crossing_itself();
let size = size::face_size(&s, &uv::domain(&s), &rules);
let loops = std::slice::from_ref(&l);
let mut stats = PlanStats::default();
assert!(plan_face(0, "spline", &s, loops, &rules, size, &mut stats, Lenient::No).is_err());
let Ok(fp) = plan_face(0, "spline", &s, loops, &rules, size, &mut stats, Lenient::Yes) else { panic!("a lenient fill") };
assert!(!fp.triangles.is_empty());
assert!(fp.uv.len() > fp.fixed.len(), "{} points, {} on the boundary", fp.uv.len(), fp.fixed.len());
// Each point inside the face on the surface where its parameters put it.
let mesh = TessPlan { surfaces: vec![s.clone()], faces: vec![FacePlan { surface: Some(0), ..fp.clone() }], ..TessPlan::default() }.evaluate();
for (q, p) in fp.uv.iter().zip(&mesh.positions).skip(fp.fixed.len()) {
let Some((on, _, _)) = s.derivs(q[0], q[1]) else { panic!("evaluates at {q:?}") };
assert!(on.dist(*p) < 1e-9, "{q:?}");
}
}
/// The face filled leniently is named in the plan's stats with why the strict plan refused it, and
/// counted under no method: the display has no hole, and the defect is still reported.
#[test]
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") };
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) };
assert_eq!(lenient.face, 7);
assert!(lenient.reason.contains("face 7 (spline)"), "{}", lenient.reason);
}