Mesh edges that do not evaluate without a panic; refuse such boolean results (#1252) (#1364)

The mesher samples edges through a fallible evaluation, so an intersection curve whose projection fails to converge no longer panics; a boolean result with an edge that does not evaluate is refused with an error. Fixes #1252.
This commit is contained in:
Lasse Lian authored and GitHub committed 2026-10-15 11:10:55 -04:00
1 parent 27529d9742
commit 3fa8fbd162
8 files changed
+144 -7

No files matched your search

+7
View File
@@ -75,6 +75,13 @@ Changes:
and the face was then left unmeshed, which also failed every boolean with the body (#882). A
walk that succeeds is kept as it was: where a loop starts decides how it is laid out across a
periodic surface's seam.
- `tessellation/mod.rs`, `triangulation.rs`: edges are sampled through a new `TrySubs` trait
(`try_subs`, `approx_subs`), a bound of `PolylineableCurve`, instead of `subs`. truck's
`IntersectionCurve::subs` unwraps a projection onto its two surfaces that can fail to
converge, which panicked while meshing (#1252). A failed sample at an interval's end uses the
intersection curve's leader point, so the polyline stays finite; a failed interior probe is
NaN, which already counts as flat. `truck-shapeops`' `ShapeOpsCurve` carries the same bound,
and `truck-modeling` is now a regular dependency (it was a dev-dependency).
- `tessellation/triangulation.rs`: the inside test of a face's boundary skips loops whose
parameter box is clear of the point (it tested every segment of every loop, so a face with
hundreds of holes meshed in quadratic time).
+3
View File
@@ -68,6 +68,9 @@ version = "0.5.0"
[dependencies.truck-geometry]
version = "0.5.0"
[dependencies.truck-modeling]
version = "0.6.0"
[dependencies.truck-polymesh]
version = "0.6.0"
+41 -2
View File
@@ -25,12 +25,51 @@ pub fn preview_face<S: RobustMeshableSurface>(surface: &S, loops: &[Vec<Point3>]
triangulation::preview_face(surface, loops, steps)
}
/// SolidCraft: sampling a curve that may fail to evaluate. truck's `IntersectionCurve::subs`
/// unwraps the projection of its leader's point onto both surfaces, which does not converge
/// everywhere; the mesher samples through this trait so that a failed sample is data, not a panic.
pub trait TrySubs: ParametricCurve3D {
/// The point at `t`, or `None` where the curve does not evaluate.
fn try_subs(&self, t: f64) -> Option<Point3> { Some(self.subs(t)) }
/// A point near the curve at `t` that always exists (an intersection curve's leader).
fn approx_subs(&self, t: f64) -> Point3 { self.subs(t) }
}
impl TrySubs for truck_geometry::prelude::Line<Point3> {}
impl TrySubs for truck_geometry::prelude::BSplineCurve<Point3> {}
impl TrySubs for truck_geometry::prelude::NurbsCurve<Vector4> {}
impl TrySubs for PolylineCurve {}
impl<C, S> TrySubs for truck_geometry::prelude::IntersectionCurve<C, S>
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
fn try_subs(&self, t: f64) -> Option<Point3> { self.search_triple(t).map(|triple| triple.0) }
fn approx_subs(&self, t: f64) -> Point3 { self.leader().subs(t) }
}
impl TrySubs for truck_modeling::Curve {
fn try_subs(&self, t: f64) -> Option<Point3> {
match self {
truck_modeling::Curve::IntersectionCurve(curve) => TrySubs::try_subs(curve, t),
other => Some(other.subs(t)),
}
}
fn approx_subs(&self, t: f64) -> Point3 {
match self {
truck_modeling::Curve::IntersectionCurve(curve) => TrySubs::approx_subs(curve, t),
other => other.subs(t),
}
}
}
/// Gathered the traits used in tessellation.
pub trait PolylineableCurve:
ParametricCurve3D + BoundedCurve + ParameterDivision1D<Point = Point3> + Parallelizable {
ParametricCurve3D + BoundedCurve + ParameterDivision1D<Point = Point3> + Parallelizable + TrySubs {
}
impl<
C: ParametricCurve3D + BoundedCurve + ParameterDivision1D<Point = Point3> + Parallelizable,
C: ParametricCurve3D + BoundedCurve + ParameterDivision1D<Point = Point3> + Parallelizable + TrySubs,
> PolylineableCurve for C
{
}
+9 -4
View File
@@ -863,15 +863,20 @@ pub(super) fn face_cap(faces: usize) -> usize {
/// tolerance is still halved while it turns more than the deflection's angle.
fn bounded_polyline<C: PolylineableCurve>(curve: &C, range: (f64, f64), d: Deflection) -> PolylineCurve {
let (tol, turn2) = (d.chord, d.turn2());
let mut out = vec![curve.subs(range.0)];
// A sample that does not evaluate (an intersection curve whose projection fails): an interval
// end falls back to the curve's approximation so the polyline stays finite; an interior probe
// is NaN, which counts as flat.
let end = |t: f64| curve.try_subs(t).unwrap_or_else(|| curve.approx_subs(t));
let probe = |t: f64| curve.try_subs(t).unwrap_or(Point3::new(f64::NAN, f64::NAN, f64::NAN));
let mut out = vec![end(range.0)];
let mut stack: Vec<(f64, f64, usize)> = vec![(range.0, range.1, 0)];
while let Some((a, b, depth)) = stack.pop() {
let (pa, pb) = (curve.subs(a), curve.subs(b));
let (pa, pb) = (end(a), end(b));
let t = a + (b - a) * 0.4729;
let chord = pa + (pb - pa) * 0.4729;
let d2 = curve.subs(t).distance2(chord);
let d2 = probe(t).distance2(chord);
let tm = 0.5 * (a + b);
let dm = curve.subs(tm).distance2(pa.midpoint(pb));
let dm = probe(tm).distance2(pa.midpoint(pb));
let flat = !(d2 >= tol * tol || dm >= tol * tol);
let flat = flat && !d.turns(turn2, d2.max(dm), pa.distance2(pb));
if flat || depth >= 24 || out.len() + stack.len() >= MAX_EDGE_POINTS {
@@ -27,6 +27,7 @@ impl<S> ShapeOpsSurface for S where S: ParametricSurface3D
/// Only solids consisting of edges whose curve is implemented this trait can be used for set operations.
pub trait ShapeOpsCurve<S: ShapeOpsSurface>:
ParametricCurve3D
+ TrySubs
+ ParameterDivision1D<Point = Point3>
+ Cut
+ Invertible
@@ -37,6 +38,7 @@ pub trait ShapeOpsCurve<S: ShapeOpsSurface>:
+ Sync {
}
impl<C, S: ShapeOpsSurface> ShapeOpsCurve<S> for C where C: ParametricCurve3D
+ TrySubs
+ ParameterDivision1D<Point = Point3>
+ Cut
+ Invertible