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};