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:
1 parent
27529d9742
commit
3fa8fbd162
8 files changed
+144
-7
No files matched your search
Generated
+1
@@ -3965,6 +3965,7 @@ dependencies = [
|
||||
"spade",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
@@ -116,9 +116,30 @@ pub(crate) fn shapeops(h: &truck_shapeops::History, tool: &Solid, tool_edges: Op
|
||||
Some((faces, edges))
|
||||
}
|
||||
|
||||
/// Samples along each edge when checking that its curve evaluates.
|
||||
const EVALUATION_SAMPLES: usize = 32;
|
||||
|
||||
/// Whether every edge of `solid` evaluates along its whole range. An intersection curve projects
|
||||
/// its leader's point onto both surfaces, which fails to converge where a boolean made the edge
|
||||
/// from an approximation that strayed (#1252). Such a result is no solid to keep: the mesher,
|
||||
/// healing and measuring all sample the edge and stumble over the gap.
|
||||
pub(crate) fn edges_evaluate(solid: &Solid) -> bool {
|
||||
use truck_meshalgo::prelude::TrySubs;
|
||||
use truck_modeling::BoundedCurve;
|
||||
solid.boundaries().iter().flat_map(|shell| shell.edge_iter()).all(|edge| {
|
||||
let curve = edge.curve();
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
(0..=EVALUATION_SAMPLES).all(|k| {
|
||||
let t = t0 + (t1 - t0) * k as f64 / EVALUATION_SAMPLES as f64;
|
||||
curve.try_subs(t).is_some_and(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The body made of `solid` healed (keeping faces apart along `keep`), with the history whose
|
||||
/// faces and edges (in `solid`'s orders) are given before healing, carried through it.
|
||||
/// Operands with `counts` faces and edges.
|
||||
/// Operands with `counts` faces and edges. A result with an edge that does not evaluate is
|
||||
/// refused (see [`edges_evaluate`]).
|
||||
pub(crate) fn heal_build(
|
||||
solid: Solid,
|
||||
size: f64,
|
||||
@@ -128,6 +149,9 @@ pub(crate) fn heal_build(
|
||||
counts: &[(usize, usize)],
|
||||
) -> Result<Body> {
|
||||
let (healed, map) = crate::heal::heal_traced(solid, size, keep);
|
||||
if !edges_evaluate(&healed) {
|
||||
return Err(crate::KernelError::Failed("an edge of the result does not evaluate".into()));
|
||||
}
|
||||
let carried = faces.zip(edges).zip(map).and_then(|((f, e), m)| m.carry(&f, &e));
|
||||
let (faces, edges) = carried.unzip();
|
||||
record::build(healed, counts, faces, edges.as_ref())
|
||||
|
||||
@@ -58,3 +58,59 @@ fn open_intersection_keeps_endpoints_on_both_planes() {
|
||||
assert_eq!(curve.subs(hi), end);
|
||||
assert_eq!(curve.subs((lo + hi) / 2.0), Point3::new(2.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
/// A triangular face whose first edge is an intersection curve of two planes: the parallel planes
|
||||
/// `z = 0` and `z = 1` when `support_gap` is `1.0` (the projection onto both never converges), the
|
||||
/// same plane twice when it is `0.0` (the leader lies on both and always evaluates).
|
||||
fn triangle_with_intersection_edge(support_gap: f64) -> truck_modeling::Shell {
|
||||
use truck_modeling::{Curve, Edge, Face, IntersectionCurve, Leader, Line, Shell, Surface, Vertex, Wire};
|
||||
let plane = |z: f64| Surface::Plane(Plane::new(Point3::new(0.0, 0.0, z), Point3::new(1.0, 0.0, z), Point3::new(0.0, 1.0, z)));
|
||||
let (v0, v1, v2) = (Vertex::new(Point3::new(0.0, 0.0, 0.5)), Vertex::new(Point3::new(1.0, 0.0, 0.5)), Vertex::new(Point3::new(0.5, 1.0, 0.5)));
|
||||
let leader = Leader::Polyline(PolylineCurve(vec![Point3::new(0.0, 0.0, 0.5), Point3::new(1.0, 0.0, 0.5)]));
|
||||
let bad = IntersectionCurve::new_unchecked(Box::new(plane(0.5 - support_gap / 2.0)), Box::new(plane(0.5 + support_gap / 2.0)), leader, 0.01);
|
||||
let line = |a: &Vertex, b: &Vertex| Edge::new(a, b, Curve::Line(Line(a.point(), b.point())));
|
||||
let wire = Wire::from(vec![Edge::new(&v0, &v1, Curve::IntersectionCurve(bad)), line(&v1, &v2), line(&v2, &v0)]);
|
||||
Shell::from(vec![Face::new(vec![wire], plane(0.5))])
|
||||
}
|
||||
|
||||
/// Meshing an edge whose intersection curve does not evaluate (#1252): `IntersectionCurve::subs`
|
||||
/// unwraps a failed projection, and the mesher sampled the edge through it. The two supports here
|
||||
/// are parallel planes, so the projection never converges. Nothing may panic (wasm aborts on one,
|
||||
/// and the native guard prints the message first) and the mesh stays finite.
|
||||
#[test]
|
||||
fn meshing_an_edge_that_does_not_evaluate_neither_panics_nor_goes_non_finite() {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use truck_meshalgo::prelude::RobustMeshableShape;
|
||||
let shell = triangle_with_intersection_edge(1.0);
|
||||
|
||||
let me = std::thread::current().id();
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let c2 = count.clone();
|
||||
let prev = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |_| {
|
||||
if std::thread::current().id() == me {
|
||||
c2.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}));
|
||||
let meshed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| shell.robust_triangulation(0.01)));
|
||||
std::panic::set_hook(prev);
|
||||
assert_eq!(count.load(Ordering::SeqCst), 0, "panics while meshing");
|
||||
let meshed = meshed.unwrap();
|
||||
for face in meshed.face_iter() {
|
||||
if let Some(mesh) = face.surface() {
|
||||
assert!(mesh.positions().iter().all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A boolean's result with an edge that does not evaluate is refused with an error (#1252). The
|
||||
/// mesher's panic on such an edge used to reject the attempt; now that it no longer panics, the
|
||||
/// result must be refused explicitly or a plausible-looking mesh would let it through.
|
||||
#[test]
|
||||
fn a_result_with_an_edge_that_does_not_evaluate_is_refused() {
|
||||
use truck_modeling::Solid;
|
||||
let solid = |gap: f64| Solid::new_unchecked(vec![triangle_with_intersection_edge(gap)]);
|
||||
assert!(!crate::boolean_record::edges_evaluate(&solid(1.0)));
|
||||
assert!(crate::boolean_record::edges_evaluate(&solid(0.0)));
|
||||
}
|
||||
Vendored
+7
@@ -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).
|
||||
|
||||
Vendored
+3
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user