fix: report every fillet refusal of a radius too big as too large (#477)

`fit: reduce` acts only on refusals marked too large. Three kinds weren't:
the corner build's contact lines leaving their faces or running backwards,
and a curved chain's contact reaching past the end of the seam it trims. Two
more were hidden: a later edge's too-large refusal lost to an earlier edge's
other error, and the corner build's lost to the local blend's in `fillet`.
The messages are unchanged.

Refs #293

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Irakli KourtisandClaude Opus 5.5 authored and GitHub committed 2026-10-12 21:49:00 -04:00
1 parent ccc36dc1f9
commit aa156f4a9f
5 files changed
+65 -7

No files matched your search

+9 -2
View File
@@ -634,8 +634,10 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
done = Some(x);
break;
}
// Reported: the first refusal, unless a later one is a size a smaller radius may
// fit (the others may be refused only until that edge is blended).
Err(e) => {
if first_err.is_none() {
if first_err.is_none() || (matches!(e, KernelError::TooLarge(_)) && !matches!(first_err, Some(KernelError::TooLarge(_)))) {
first_err = Some(e);
}
}
@@ -909,7 +911,12 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
// Corners the local blends refuse (an earlier round at an edge's end): radius per edge.
blend(body, edges, radius, Shape::Round, "fillet").or_else(|e| {
let sel: Vec<(Vec3, f64)> = edges.iter().map(|p| (*p, radius)).collect();
crate::blend_corner::fillet_radii(body, &sel).map_err(|_| e)
// The local blend's refusal, unless only the corner build found the radius too large.
crate::blend_corner::fillet_radii(body, &sel).map_err(|e2| match (&e, e2) {
(KernelError::TooLarge(_), _) => e,
(_, e2 @ KernelError::TooLarge(_)) => e2,
_ => e,
})
})
}
+3 -3
View File
@@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet};
use solidcraft_geom::Vec3;
use truck_modeling::{self as mt, builder};
use crate::blend::{plane_normal, unsupported, vtx};
use crate::blend::{plane_normal, too_large, unsupported, vtx};
use crate::body::{Body, Solid, p3, v3};
use crate::{KernelError, Result, guard};
@@ -331,7 +331,7 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
let check_extension = |q: Vec3, fi: usize| -> Result<Vec3> {
let face = faces.get(fi).ok_or_else(|| KernelError::Failed("face".into()))?;
if !crate::blend::extension_fits(face, v, q, size * 1e-7) {
return Err(unsupported("a fillet larger than the faces next to it"));
return Err(too_large("a fillet larger than the faces next to it"));
}
Ok(q)
};
@@ -465,7 +465,7 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
let (pa, pb) = (vtx(&a), vtx(&b));
let orig = vtx(e.absolute_back()) - vtx(e.absolute_front());
if (pb - pa).dot(orig) <= size * 1e-9 * orig.len() {
return Err(unsupported("a fillet larger than the faces next to it"));
return Err(too_large("a fillet larger than the faces next to it"));
}
let x = builder::line(&a, &b);
lines.insert((e.id(), f), x.clone());
+2 -2
View File
@@ -300,14 +300,14 @@ fn trim_curved_seam(seam: &mt::Edge, a: Option<mt::Vertex>, b: Option<mt::Vertex
.unwrap_or(end);
let t = curve.search_nearest_parameter(v.point(), hint, 100).ok_or_else(|| unsupported("the blend is larger than the neighbouring faces"))?;
if !(t.is_finite() && from_p3(mt::ParametricCurve::subs(&curve, t)).dist(from_p3(v.point())) <= size * 1e-5) {
return Err(unsupported("the blend is larger than the neighbouring faces"));
return Err(too_large("the blend is larger than the neighbouring faces"));
}
Ok(t)
};
let (ta, tb) = (at(&a, lo)?, at(&b, hi)?);
let margin = (hi - lo) * 1e-9;
if ta < lo - margin || tb > hi + margin || tb - ta < (hi - lo) * 1e-6 {
return Err(unsupported("the blend is larger than the neighbouring faces"));
return Err(too_large("the blend is larger than the neighbouring faces"));
}
if tb < hi {
let _ = curve.cut(tb);
+28
View File
@@ -299,3 +299,31 @@ fn closed_curved_seam_is_not_supported() {
assert!(err.to_string().contains("not supported yet: a closed curved edge"), "{err}");
}
}
#[test]
fn rolling_ball_past_the_end_seam_is_too_large_and_reduces() {
// The crossing cylinders of the test above with the small one's centre inside the large
// one: a round of 3 would reach past the end of the cap arc it trims, so it is too large
// (a smaller one fits), and the largest that fits is the analytic round over the full height
// (checked on its surface: its added volume is too small beside the body's to measure).
let (r1, r2, d, h) = (17.0, 9.5, 12.0, 10.0);
let (c1, c2) = (Vec3::ZERO, Vec3::new(d, 0.0, 0.0));
let body = boolean(&cylinder(c1, Vec3::Z, r1, h).unwrap(), &cylinder(c2, Vec3::Z, r2, h).unwrap(), BoolOp::Union).unwrap().unwrap();
let along = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
let corner = Vec3::new(along, (r1 * r1 - along * along).sqrt(), 0.0);
let at = [corner + Vec3::Z * (h / 2.0)];
let e = fillet(&body, &at, 3.0).unwrap_err();
assert!(matches!(e, KernelError::TooLarge(_)), "{e}");
let (r, blend) = crate::fillet_max(&body, &at, 3.0).unwrap();
assert!(r > 1.5 && r < 3.0, "{r}");
assert!(blend.validity().is_empty(), "{:?}", blend.validity());
let g = gap(c1, r1, c2, r2, corner, r);
let surfaces: Vec<_> = blend.solid.face_iter().map(|f| f.oriented_surface()).filter(|s| matches!(s, mt::Surface::NurbsSurface(_))).collect();
let mut worst: f64 = 0.0;
for z in [0.0, 0.25, 0.5, 0.75, 1.0] {
for p in &g.arc {
worst = worst.max(nearest_blend(&surfaces, *p + Vec3::Z * (h * z)).0);
}
}
assert!(worst < 1e-3, "blend surface is {worst} mm off the analytic round at r {r}");
}
+23
View File
@@ -356,6 +356,29 @@ fn fillet_max_finds_the_largest_radius_that_works() {
assert!(fillet_max(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).is_none());
}
/// A corner's rounds too large for its faces are refused as too large, so a fit can reduce
/// them, also where only the corner build finds it (the edge-by-edge blend failing otherwise).
#[test]
fn corner_rounds_larger_than_their_faces_are_too_large() {
let (l, eps) = (20.0, 1e-9);
let b = box_solid(Vec3::ZERO, Vec3::new(l, l, l)).unwrap();
let corner = [Vec3::new(10.0, 0.0, 0.0), Vec3::new(0.0, 10.0, 0.0), Vec3::new(0.0, 0.0, 10.0)];
// As wide as the faces: the first edge's round uses up its neighbour, then the edge-by-edge
// blend can't find it; the corner build is what says the radius is too large.
let e = fillet(&b, &corner, l).unwrap_err();
assert!(matches!(e, KernelError::TooLarge(_)), "{e}");
let (r, body) = fillet_max(&b, &corner, l).unwrap();
assert!((r - 19.99).abs() < eps, "{r}");
assert!(body.validity().is_empty(), "{:?}", body.validity());
// Three rounds along the edges past the corner cube, and the cube less its sphere octant.
let removed = 3.0 * (1.0 - PI / 4.0) * r * r * (l - r) + r * r * r * (1.0 - PI / 6.0);
assert!(rel(measure(&body).unwrap().volume, l * l * l - removed) < 2e-4);
// Different radii per edge, one wider than its faces.
let sel: Vec<(Vec3, f64)> = corner.iter().copied().zip([21.0, 5.0, 5.0]).collect();
let e = crate::blend_corner::fillet_radii(&b, &sel).unwrap_err();
assert!(matches!(e, KernelError::TooLarge(_)), "{e}");
}
#[test]
fn tapered_extrudes() {
// 40 x 40 square, 30 up, 10° outward: a frustum.