Fillet and chamfer: refuse a smooth seam as having no corner to round (#936)
* Fillet and chamfer: refuse a smooth seam as having no corner to round Picking the seams where R10 rounds run into the faces beside them, and asking for an R1 fillet there, failed with "not supported yet: blending a smooth edge (... both faces next to the edge must be planar)". That reads like a missing feature, but the faces meet tangentially at those edges and there is nothing to round. An edge whose two faces are tangent along it is now refused as an invalid pick that names the edge and asks for the sharp edges. Fixes #896. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Robustness goldens: seed 72's smooth-seam fillet is now a rejected pick, not unsupported Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8fb5742706
commit
7c85970bfc
3 files changed
+68
-2
No files matched your search
@@ -860,6 +860,17 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
return Err(KernelError::Invalid(format!("no edge near {:?}", [p.x, p.y, p.z])));
|
||||
}
|
||||
let edge = Body::unique_edges(&solid).into_iter().nth(idx).ok_or_else(|| KernelError::Invalid("edge index".into()))?;
|
||||
// Faces that meet smoothly (a round's seam with the face it runs into) leave no
|
||||
// corner to blend: say so rather than that blending it isn't supported.
|
||||
if tangent_faces(&solid, &edge) {
|
||||
if first_err.is_none() {
|
||||
first_err = Some(KernelError::Invalid(format!(
|
||||
"the edge at [{:.3}, {:.3}, {:.3}] is smooth: the faces on either side meet tangentially there, so there is no corner to {what}; pick the sharp edges",
|
||||
p.x, p.y, p.z
|
||||
)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Roll a ball along the edge's smooth chain (the points of `todo` on it go with it).
|
||||
let roll = |ids: &[mt::EdgeID]| {
|
||||
let on_chain: Vec<usize> = todo
|
||||
@@ -1064,6 +1075,26 @@ fn curved_case(solid: &Solid, edge: &mt::Edge) -> bool {
|
||||
solid.face_iter().filter(|f| f.edge_iter().any(|e| e.id() == edge.id())).any(|f| !matches!(f.oriented_surface(), mt::Surface::Plane(_)))
|
||||
}
|
||||
|
||||
/// Whether the two faces of `edge` meet tangentially along it (sampled at a quarter, the
|
||||
/// middle and three quarters of the edge): a seam, not a corner.
|
||||
fn tangent_faces(solid: &Solid, edge: &mt::Edge) -> bool {
|
||||
use mt::{BoundedCurve, ParametricCurve, ParametricSurface3D, SearchParameter};
|
||||
let faces: Vec<&mt::Face> = solid.face_iter().filter(|f| f.edge_iter().any(|e| e.id() == edge.id())).take(3).collect();
|
||||
let [f1, f2] = faces[..] else { return false };
|
||||
let c = edge.curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
let normal = |f: &mt::Face, q: mt::Point3| -> Option<Vec3> {
|
||||
let s = f.oriented_surface();
|
||||
let (u, v) = s.search_parameter(q, None, 100)?;
|
||||
let n = s.normal(u, v);
|
||||
Vec3::new(n.x, n.y, n.z).normalized()
|
||||
};
|
||||
[0.25, 0.5, 0.75].iter().all(|k| {
|
||||
let q = c.subs(t0 + (t1 - t0) * k);
|
||||
matches!((normal(f1, q), normal(f2, q)), (Some(a), Some(b)) if a.dot(b) > 1.0 - 1e-6)
|
||||
})
|
||||
}
|
||||
|
||||
/// The chain of edges continuing `edge` smoothly at its ends (its ids): closed, or open where
|
||||
/// the next edge turns sharply.
|
||||
fn smooth_chain(solid: &Solid, edge: &mt::Edge) -> Option<Vec<mt::EdgeID>> {
|
||||
|
||||
@@ -351,3 +351,37 @@ fn a_refused_edge_is_not_rolled_again_after_blends_far_from_it() {
|
||||
// The far edges alone still round.
|
||||
assert!(fillet(&body, &ends, 3.0).is_ok());
|
||||
}
|
||||
|
||||
/// #896: the seams where rounds run into the faces beside them are picked to be rounded again.
|
||||
/// The faces meet tangentially there, so there is nothing to round: the refusal says so, rather
|
||||
/// than that blending such an edge isn't supported yet.
|
||||
#[test]
|
||||
fn a_smooth_seam_is_refused_as_having_no_corner() {
|
||||
let b = crate::box_solid(Vec3::ZERO, Vec3::new(150.0, 200.0, 70.0)).unwrap();
|
||||
let mut edges = Vec::new();
|
||||
for (x, y) in [(0.0, 0.0), (150.0, 0.0), (0.0, 200.0), (150.0, 200.0)] {
|
||||
edges.push(Vec3::new(x, y, 35.0));
|
||||
}
|
||||
for (a, z) in [(0.0, 0.0), (150.0, 0.0), (0.0, 70.0), (150.0, 70.0)] {
|
||||
edges.push(Vec3::new(a, 100.0, z));
|
||||
}
|
||||
for (y, z) in [(0.0, 0.0), (200.0, 0.0), (0.0, 70.0), (200.0, 70.0)] {
|
||||
edges.push(Vec3::new(75.0, y, z));
|
||||
}
|
||||
let rounded = fillet(&b, &edges, 10.0).unwrap();
|
||||
// The seam on the top face beside the round of the edge along y at x = 0, and the seam
|
||||
// with the end face x = 0 beside the round of the top edge along y.
|
||||
for seam in [Vec3::new(10.0, 100.0, 70.0), Vec3::new(0.0, 100.0, 60.0)] {
|
||||
for chamfer in [false, true] {
|
||||
let r = if chamfer { crate::chamfer(&rounded, &[seam], 1.0) } else { fillet(&rounded, &[seam], 1.0) };
|
||||
match r {
|
||||
Err(KernelError::Invalid(m)) => assert!(m.contains("smooth") && m.contains("no corner"), "{m}"),
|
||||
other => panic!("{seam:?}: {:?}", other.map(|_| "a body")),
|
||||
}
|
||||
}
|
||||
}
|
||||
// A sharp edge of a box with one round still takes a fillet beside the round's seams.
|
||||
let one = fillet(&crate::box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(), &[Vec3::new(40.0, 30.0, 10.0)], 5.0).unwrap();
|
||||
assert!(fillet(&one, &[Vec3::new(20.0, 0.0, 20.0)], 1.0).is_ok());
|
||||
assert!(matches!(fillet(&one, &[Vec3::new(35.0, 30.0, 10.0)], 1.0), Err(KernelError::Invalid(_))));
|
||||
}
|
||||
Reference in new issue
Block a user