mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
kernel: fillet_setback, constant-radius rounds with corners set back
Each (point, distance) sets back the corner at the body's nearest vertex: its patch starts that far along every edge there. Tested on the 60 x 40 x 20 box corner with r5 rounds set back 8 (volume between the convex hull and the rounds' prisms bounds, 15/36/23 topology, the box's bbox) and on refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,19 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
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> {
|
||||
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.
|
||||
|
||||
@@ -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<Body> {
|
||||
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<Body> {
|
||||
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<Body> {
|
||||
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<f64> = 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<Item = Vec3>) -> Result<Vec<[f64; 3]>> {
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord((body.size() * 2e-3).max(1e-3)))?;
|
||||
let ends: Vec<Vec3> = 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> {
|
||||
body.require_brep("chamfer")?;
|
||||
sizes_ok("chamfer", &vec![d1; edges.len()])?;
|
||||
|
||||
@@ -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<f64>) -> Result<Body> {
|
||||
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(_))));
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user