Fillets and chamfers where edges meet at corners on a curved wall or a knife-edge tip (#851) (#935)

Rolling-ball blends now handle knife edges, where the two faces have opposite normals; an edge that runs out along an end face tangent to one side; and rounds that end in an earlier round on curved faces, picked together or one after another. The three edges at a vertex of smooth faces, rounded to one radius, end in a piece of sphere. When the given order of up to four edges is refused, or leaves a face boundary off its surface, other orders are tried.

Closes #851.

Co-authored-by: lasselian <22126420+lasselian@users.noreply.github.com>
This commit is contained in:
Lasse Lianandlasselian authored and GitHub committed 2026-10-13 23:21:47 -04:00
1 parent 45f92277cb
commit a2c7396160
10 files changed
+1364 -54

No files matched your search

+23
View File
@@ -4,6 +4,7 @@
use mt::{BoundedCurve, ParametricCurve, ParametricSurface, ParametricSurface3D, SearchNearestParameter};
use super::*;
use crate::blend::plane_normal;
use crate::{Body, BoolOp, boolean, cylinder, fillet};
fn tee(scale: f64, origin: Vec3) -> Body {
@@ -352,6 +353,28 @@ fn a_refused_edge_is_not_rolled_again_after_blends_far_from_it() {
assert!(fillet(&body, &ends, 3.0).is_ok());
}
/// At a knife edge (the plane and the wall's normals opposite) the ball's centre is the same
/// whichever face is side 1: `r` inside the plane and `r` outside the wall's cylinder.
#[test]
fn knife_edge_ball_centre_does_not_depend_on_the_side_order() {
const R: f64 = 21.0;
let tip = Vec3::new(62.0 + 1.0 / 6.0, 0.0, 5.0);
let slab = crate::box_solid(Vec3::new(0.0, -R, 0.0), Vec3::new(tip.x, 0.0, 10.0)).unwrap();
let disc = cylinder(Vec3::new(tip.x, -R, 0.0), Vec3::Z, R, 10.0).unwrap();
let body = boolean(&slab, &disc, BoolOp::Cut).unwrap().unwrap();
let faces: Vec<&mt::Face> = body.solid.face_iter().collect();
let plane = faces.iter().copied().find(|f| plane_normal(f).is_some_and(|n| n.y > 0.99)).unwrap();
let wall = faces.iter().copied().find(|f| plane_normal(f).is_none()).unwrap();
for r in [0.5, 2.0, 5.0] {
let centre = Vec3::new(tip.x - (4.0 * R * r).sqrt(), -r, tip.z);
// `cusp` is +1 when n1 × t points into the cusp: the wall's normal is −y, the plane's +y.
for (f1, f2, cusp) in [(wall, plane, 1.0), (plane, wall, -1.0)] {
let s = solve(&[f1], &[f2], tip, Vec3::Z, r, -1.0, cusp, None, 1e-6).unwrap();
assert!(s.c.dist(centre) < 1e-6, "r={r} cusp={cusp}: centre {:?}, expected {centre:?}", s.c);
}
}
}
/// #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.