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 <[email protected]> * Robustness goldens: seed 72's smooth-seam fillet is now a rejected pick, not unsupported Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
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(_))));
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
"69": {"area":6451.122768,"bodies":1,"edges":132,"error":"offset: a corner whose faces don't meet the same way after t","faces":27,"outcome":"failed","rejected":0,"shells":3,"steps":["revolved section","join box","shell","shell"],"unsupported":0,"volume":4761.477514},
|
||||
"70": {"area":3682.669157,"bodies":1,"edges":24,"error":"lumps: shell boundaries intersect or touch; cavity ownership","faces":12,"outcome":"failed","rejected":0,"shells":2,"steps":["extruded profile","join box","hole"],"unsupported":1,"volume":9155.027162},
|
||||
"71": {"area":3980.38113,"bodies":1,"edges":50,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","cut cylinder","hole","join cylinder"],"unsupported":0,"volume":15750.655245},
|
||||
"72": {"area":5770.270336,"bodies":2,"edges":46,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":2,"steps":["box","join cylinder","hole","join box","join box","cut box","cut cylinder","cut cylinder"],"unsupported":1,"volume":16081.801492},
|
||||
"72": {"area":5770.270336,"bodies":2,"edges":46,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":2,"steps":["box","join cylinder","hole","join box","join box","cut box","cut cylinder","cut cylinder"],"unsupported":0,"volume":16081.801492},
|
||||
"73": {"area":6868.162117,"bodies":1,"edges":40,"error":"offset: a corner whose faces don't meet the same way after t","faces":18,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","join box","shell"],"unsupported":1,"volume":36461.939381},
|
||||
"74": {"area":9294.57311,"bodies":1,"edges":22,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole"],"unsupported":0,"volume":55866.183074},
|
||||
"75": {"area":7893.117931,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","chamfer","rectangular pattern","rectangular pattern","cut box"],"unsupported":0,"volume":38757.62083},
|
||||
@@ -101,6 +101,7 @@
|
||||
"notes": [
|
||||
{"reason":"First record: what each seed builds on main at 1a0100cd","seeds":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95]},
|
||||
{"reason":"seed 0 meshes differently since display tessellation plans (#805), which landed alongside these goldens (#826)","seeds":[0]},
|
||||
{"reason":"the kernel fixes merged together on 13 October (#838, #839, #841, #855, #856) change what these seeds build: several joins that used to fail now succeed, small volume and area shifts, and shell now refuses seeds 6, 14, 43 and 73 with an offset corner error (seed 14 built before: #897)","seeds":[2,6,14,22,36,43,50,63,71,73,94]}
|
||||
{"reason":"the kernel fixes merged together on 13 October (#838, #839, #841, #855, #856) change what these seeds build: several joins that used to fail now succeed, small volume and area shifts, and shell now refuses seeds 6, 14, 43 and 73 with an offset corner error (seed 14 built before: #897)","seeds":[2,6,14,22,36,43,50,63,71,73,94]},
|
||||
{"reason":"#936: a fillet or chamfer of a smooth seam is refused as an invalid pick (no corner to round) instead of as unsupported","seeds":[72]}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user