diff --git a/crates/kernel/src/blend.rs b/crates/kernel/src/blend.rs
index 67ff61f998..862695176a 100644
--- a/crates/kernel/src/blend.rs
+++ b/crates/kernel/src/blend.rs
@@ -212,6 +212,19 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result
{
crate::blend_smlib::fillet(body, edges, radius).and_then(|b| closed(body, b))
}
+/// [`fillet`] with corner setbacks: each `(at, distance)` sets back the corner at the vertex
+/// nearest `at`, a vertex where every edge is rounded, so its patch starts `distance` along
+/// each of its edges from the vertex (one distance for all of a corner's edges: SMLib keeps one
+/// per corner). The rounds run whole up to their sections there, and the patch between them
+/// meets each face in a curve from one round's contact line to the next. The distance must reach
+/// past where the rounds would meet without it (past the radius at a square corner); a corner
+/// where some edge is not rounded is refused as not supported, a setback as long as an edge at
+/// its corner as too large.
+pub fn fillet_setback(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)]) -> Result {
+ body.require_brep("fillet")?;
+ crate::blend_smlib::fillet_setback(body, edges, radius, setbacks).and_then(|b| closed(body, b))
+}
+
/// [`fillet`] whose corners where three blended edges meet are G2 patches. SMLib's fillet
/// corners are a sphere or a Coons patch that meets the rounds tangentially, so this is refused
/// rather than quietly given those corners; [`fillet`] (the default) is unchanged.
diff --git a/crates/kernel/src/blend_smlib.rs b/crates/kernel/src/blend_smlib.rs
index aac1c49784..64096cfb79 100644
--- a/crates/kernel/src/blend_smlib.rs
+++ b/crates/kernel/src/blend_smlib.rs
@@ -37,6 +37,12 @@ pub(crate) fn fillet_each(body: &Body, edges: &[Vec3], radii: &[f64]) -> Result<
imp::blend(body, "fillet", Kind::Round, edges, radii)
}
+/// A constant-radius round whose corners at `setbacks` (a point at each corner's vertex, and the
+/// distance its patch starts along each edge there) are set back ([`crate::fillet_setback`]).
+pub(crate) fn fillet_setback(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)]) -> Result {
+ imp::blend_setback(body, edges, &vec![radius; edges.len()], setbacks)
+}
+
/// Curvature-continuous rounds with the setback of a round of `radius` ([`crate::fillet_g2`]).
pub(crate) fn fillet_g2(body: &Body, edges: &[Vec3], radius: f64) -> Result {
imp::blend(body, "G2 fillet", Kind::Curvature, edges, &vec![radius; edges.len()])
@@ -179,6 +185,42 @@ mod imp {
run(body, what, &picked.edges, |b| b.blend(kind, &picked.points, sizes, tol))
}
+ /// Rounds with corner setbacks: each corner point picks the body's nearest vertex (within
+ /// the pick tolerance), handed to SMLib at that vertex's own position.
+ pub(super) fn blend_setback(body: &Body, edges: &[Vec3], sizes: &[f64], setbacks: &[(Vec3, f64)]) -> Result {
+ let what = "fillet with corner setbacks";
+ body.require_brep(what)?;
+ sizes_ok(what, sizes)?;
+ if setbacks.is_empty() || setbacks.len() > solidcraft_smlib::MAX_POINTS {
+ return Err(KernelError::Invalid(format!("{what}: give 1…{} corners", solidcraft_smlib::MAX_POINTS)));
+ }
+ if setbacks.iter().any(|(p, d)| !p.is_finite() || !(d.is_finite() && *d > 1e-6 && *d < 1e6)) {
+ return Err(KernelError::Invalid(format!("{what}: each corner is a finite point with a positive, finite setback")));
+ }
+ let picked = resolve(body, edges)?;
+ let corners = corner_vertices(body, setbacks.iter().map(|(p, _)| *p))?;
+ let distances: Vec = setbacks.iter().map(|(_, d)| *d).collect();
+ let tol = pick_tolerance(body);
+ run(body, what, &picked.edges, |b| b.blend_setback(Blend::Round, &picked.points, sizes, &corners, &distances, tol))
+ }
+
+ /// The body's vertex nearest each point, within the pick tolerance: the ends of its edges.
+ fn corner_vertices(body: &Body, picks: impl Iterator- ) -> Result> {
+ let mesh = body.mesh(&crate::MeshTolerance::chord((body.size() * 2e-3).max(1e-3)))?;
+ let ends: Vec = mesh.edges.iter().flat_map(|l| l.first().into_iter().chain(l.last())).copied().collect();
+ let reach = pick_tolerance(body);
+ picks
+ .map(|p| {
+ ends.iter()
+ .map(|v| (v.dist(p), *v))
+ .filter(|(d, _)| *d <= reach)
+ .min_by(|a, b| a.0.total_cmp(&b.0))
+ .map(|(_, v)| [v.x, v.y, v.z])
+ .ok_or_else(|| KernelError::Invalid(format!("no corner near the setback point ({:.3}, {:.3}, {:.3})", p.x, p.y, p.z)))
+ })
+ .collect()
+ }
+
pub(super) fn sides(body: &Body, edges: &[Vec3], d1: f64, side: crate::ChamferSide, flip: bool) -> Result {
body.require_brep("chamfer")?;
sizes_ok("chamfer", &vec![d1; edges.len()])?;
diff --git a/crates/kernel/src/fillet_setback_tests.rs b/crates/kernel/src/fillet_setback_tests.rs
new file mode 100644
index 0000000000..d1e0a886c4
--- /dev/null
+++ b/crates/kernel/src/fillet_setback_tests.rs
@@ -0,0 +1,44 @@
+use solidcraft_geom::Vec3;
+
+use crate::*;
+
+/// The 60 x 40 x 20 box with r5 rounds on the three edges at (60, 40, 20).
+fn corner_case(setback: Option) -> Result {
+ let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
+ let edges = [Vec3::new(30.0, 40.0, 20.0), Vec3::new(60.0, 20.0, 20.0), Vec3::new(60.0, 40.0, 10.0)];
+ match setback {
+ Some(s) => fillet_setback(&b, &edges, 5.0, &[(Vec3::new(60.0, 40.0, 20.0), s)]),
+ None => fillet(&b, &edges, 5.0),
+ }
+}
+
+/// Set back 8 along each edge: outside the corner cube [52, 60] x [32, 40] x [12, 20] the body
+/// is the sphere corner's, and inside it the convex body holds the hull of its points on the
+/// cube's faces (299.0201) and lies within the three rounds' prisms (512 - 225 (1 - pi/4) -
+/// 125 (sqrt 2 - 1)); see capi/tests/ops_kernel.cpp, corner_setback. The patch is SMLib's two
+/// quadrilaterals per side: 15 faces, 36 edges, 23 vertices; the box stays the box.
+#[test]
+fn a_corner_set_back_8_blends_through_a_larger_patch() {
+ let pi = std::f64::consts::PI;
+ let sphere = measure(&corner_case(None).unwrap()).unwrap().volume;
+ let out = corner_case(Some(8.0)).unwrap();
+ assert!(out.validity().is_empty(), "{:?}", out.validity());
+ let m = measure(&out).unwrap();
+ let in_c_sphere = 512.0 - 225.0 * (1.0 - pi / 4.0) - 125.0 * (1.0 - pi / 6.0);
+ let prisms = 512.0 - 225.0 * (1.0 - pi / 4.0) - 125.0 * (2f64.sqrt() - 1.0);
+ let outside = sphere - in_c_sphere;
+ assert!(m.volume > outside + 299.0201 && m.volume < outside + prisms, "{} not in [{}, {}]", m.volume, outside + 299.0201, outside + prisms);
+ assert_eq!((m.faces, m.edges, m.vertices), (15, 36, 23));
+ assert!((m.bbox.min - Vec3::new(0.0, 0.0, 0.0)).len() < 1e-6 && (m.bbox.max - Vec3::new(60.0, 40.0, 20.0)).len() < 1e-6, "{:?}", m.bbox);
+}
+
+/// A setback point at no vertex, at a corner where no edge is rounded, or as long as an edge
+/// there, is refused.
+#[test]
+fn a_setback_off_the_rounded_corner_is_refused() {
+ let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
+ let edges = [Vec3::new(30.0, 40.0, 20.0), Vec3::new(60.0, 20.0, 20.0), Vec3::new(60.0, 40.0, 10.0)];
+ assert!(fillet_setback(&b, &edges, 5.0, &[(Vec3::new(30.0, 20.0, 10.0), 8.0)]).is_err());
+ assert!(fillet_setback(&b, &edges, 5.0, &[(Vec3::new(0.0, 0.0, 0.0), 8.0)]).is_err());
+ assert!(matches!(corner_case(Some(20.0)), Err(KernelError::TooLarge(_))));
+}
diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs
index 65e1bcea8a..ba77210cda 100644
--- a/crates/kernel/src/lib.rs
+++ b/crates/kernel/src/lib.rs
@@ -39,6 +39,8 @@ mod emboss;
mod face_source_tests;
mod fair;
mod fill;
+#[cfg(test)]
+mod fillet_setback_tests;
mod freeform;
pub mod fuzz;
mod heal;
@@ -150,7 +152,7 @@ mod validity;
pub mod watch;
mod whole_face;
-pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_max, fillet_max_each, tangent_chain};
+pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_max, fillet_max_each, fillet_setback, tangent_chain};
pub use blend_corner::fillet_radii;
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
pub use body::{Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint};