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

+25 -1
View File
@@ -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)));
}