feat: fit reduce lowers the radius only where it doesn't fit (#496)
Fillet fit: reduce lowers the radius only where it doesn't fit: picked edges are grouped where they can limit each other (sharing a face or vertex), and each group gets the requested radius where it fits, otherwise the largest that does. Refs #293. Co-authored-by: Iraklis Kourtis <jyviko@gmail.com>
This commit is contained in:
1 parent
6d5df4b9b3
commit
0f0bb09858
9 files changed
+252
-35
No files matched your search
+123
-7
@@ -828,22 +828,28 @@ fn smooth_loop(w: &mt::Wire) -> bool {
|
||||
|
||||
/// The B-rep edges nearest the points (each once).
|
||||
fn edge_ids(cur: &Body, solid: &Solid, pts: &[Vec3]) -> Result<Vec<mt::EdgeID>> {
|
||||
let size = cur.size();
|
||||
let tol = (size * 2e-3).max(1e-3);
|
||||
let all = Body::unique_edges(solid);
|
||||
let mut ids = Vec::new();
|
||||
for p in pts {
|
||||
if let Some((idx, dist)) = cur.nearest_edge(*p, tol)?
|
||||
&& dist <= size * 0.05 + 1e-3
|
||||
&& let Some(e) = all.get(idx)
|
||||
&& !ids.contains(&e.id())
|
||||
if let Some(id) = nearest_edge_id(cur, &all, *p)?
|
||||
&& !ids.contains(&id)
|
||||
{
|
||||
ids.push(e.id());
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// The edge of `all` (`cur`'s edges, in their order) a point picks: the nearest, if near enough.
|
||||
fn nearest_edge_id(cur: &Body, all: &[mt::Edge], p: Vec3) -> Result<Option<mt::EdgeID>> {
|
||||
let size = cur.size();
|
||||
let tol = (size * 2e-3).max(1e-3);
|
||||
Ok(match cur.nearest_edge(p, tol)? {
|
||||
Some((idx, dist)) if dist <= size * 0.05 + 1e-3 => all.get(idx).map(|e| e.id()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// When the edges make up whole boundary loops of planar faces: the edge points per loop.
|
||||
fn loop_groups(cur: &Body, pts: &[Vec3]) -> Result<Option<Vec<Vec<Vec3>>>> {
|
||||
Ok(loop_split(cur, pts)?.and_then(|(g, rest)| rest.is_empty().then_some(g)))
|
||||
@@ -955,6 +961,116 @@ pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Option<(f64, Body
|
||||
best.map(|b| (lo as f64 * q, b))
|
||||
}
|
||||
|
||||
/// [`fillet_max`] for each group of the edges that may limit each other's radius (sharing a
|
||||
/// face or a vertex, directly or through other edges in the group), so a round that fits isn't
|
||||
/// reduced for a tight edge elsewhere. The radius per edge (in `edges`' order) and the result;
|
||||
/// one radius for all (as [`fillet_max`]) when a group is refused for another reason than its
|
||||
/// size, the groups don't build together, or there are too many to search (each search is up
|
||||
/// to 16 fillets, and this runs in the live preview).
|
||||
pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Option<(Vec<f64>, Body)> {
|
||||
const MAX_GROUPS: usize = 8;
|
||||
const MAX_SEARCHES: usize = 2;
|
||||
let one = || fillet_max(body, edges, radius).map(|(r, b)| (vec![r; edges.len()], b));
|
||||
let Some(groups) = interacting_groups(body, edges).filter(|g| g.len() > 1 && g.len() <= MAX_GROUPS) else { return one() };
|
||||
let mut radii = vec![radius; edges.len()];
|
||||
let mut searches = 0;
|
||||
for g in &groups {
|
||||
let pts: Vec<Vec3> = g.iter().filter_map(|i| edges.get(*i).copied()).collect();
|
||||
let r = match fillet(body, &pts, radius) {
|
||||
Ok(_) => continue,
|
||||
Err(KernelError::TooLarge(_)) if searches < MAX_SEARCHES => {
|
||||
searches += 1;
|
||||
match fillet_max(body, &pts, radius) {
|
||||
Some((r, _)) => r,
|
||||
None => return one(),
|
||||
}
|
||||
}
|
||||
Err(_) => return one(),
|
||||
};
|
||||
for i in g {
|
||||
if let Some(x) = radii.get_mut(*i) {
|
||||
*x = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
match fillet_each(body, edges, &radii) {
|
||||
Ok(b) => Some((radii, b)),
|
||||
Err(_) => one(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The fillet of each edge at its own radius (`radii` in `edges`' order): built together where
|
||||
/// the corner build takes it, else the edges of each radius together, the largest first.
|
||||
pub fn fillet_each(body: &Body, edges: &[Vec3], radii: &[f64]) -> Result<Body> {
|
||||
if radii.len() != edges.len() {
|
||||
return Err(KernelError::Invalid("one radius per edge".into()));
|
||||
}
|
||||
if let Some(r) = radii.first().filter(|r| radii.iter().all(|x| x.to_bits() == r.to_bits())) {
|
||||
return fillet(body, edges, *r);
|
||||
}
|
||||
let sel: Vec<(Vec3, f64)> = edges.iter().copied().zip(radii.iter().copied()).collect();
|
||||
if let Ok(b) = crate::blend_corner::fillet_radii(body, &sel) {
|
||||
return Ok(b);
|
||||
}
|
||||
let mut by_radius: Vec<(f64, Vec<Vec3>)> = Vec::new();
|
||||
for (p, r) in sel {
|
||||
match by_radius.iter_mut().find(|(x, _)| x.to_bits() == r.to_bits()) {
|
||||
Some((_, pts)) => pts.push(p),
|
||||
None => by_radius.push((r, vec![p])),
|
||||
}
|
||||
}
|
||||
by_radius.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
let mut cur = body.clone();
|
||||
for (r, pts) in &by_radius {
|
||||
cur = fillet(&cur, pts, *r)?;
|
||||
}
|
||||
Ok(cur)
|
||||
}
|
||||
|
||||
/// The points' edges (found as [`edge_ids`] finds them, on the body as the blends see it) grouped
|
||||
/// where they share a face or a vertex, directly or through other points' edges: indices into
|
||||
/// `pts`. `None` when a point has no edge near it.
|
||||
fn interacting_groups(body: &Body, pts: &[Vec3]) -> Option<Vec<Vec<usize>>> {
|
||||
let cur = crate::boolean_record::healed(body).ok()?;
|
||||
let all = Body::unique_edges(&cur.solid);
|
||||
// What each point's edge touches: its faces and its ends.
|
||||
let mut groups: Vec<(Vec<mt::FaceID>, Vec<mt::VertexID>, Vec<usize>)> = Vec::new();
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
let id = nearest_edge_id(&cur, &all, *p).ok()??;
|
||||
let edge = all.iter().find(|e| e.id() == id)?;
|
||||
let mut faces: Vec<mt::FaceID> = cur.solid.face_iter().filter(|f| f.edge_iter().any(|e| e.id() == id)).map(|f| f.id()).collect();
|
||||
let mut ends = vec![edge.front().id(), edge.back().id()];
|
||||
let mut members = vec![i];
|
||||
// Join every group touching one of these faces or ends.
|
||||
let (meet, rest): (Vec<_>, Vec<_>) =
|
||||
groups.into_iter().partition(|(fs, vs, _)| fs.iter().any(|f| faces.contains(f)) || vs.iter().any(|v| ends.contains(v)));
|
||||
for (fs, vs, ms) in meet {
|
||||
for f in fs {
|
||||
if !faces.contains(&f) {
|
||||
faces.push(f);
|
||||
}
|
||||
}
|
||||
for v in vs {
|
||||
if !ends.contains(&v) {
|
||||
ends.push(v);
|
||||
}
|
||||
}
|
||||
members.extend(ms);
|
||||
}
|
||||
groups = rest;
|
||||
groups.push((faces, ends, members));
|
||||
}
|
||||
Some(
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|(_, _, mut ms)| {
|
||||
ms.sort_unstable();
|
||||
ms
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Equal-distance chamfer of the edges nearest to the given points. Each edge is cut with a
|
||||
/// prism whose only face inside the body is the chamfer plane (so neighbouring chamfers meet in
|
||||
/// a mitre); the local operation is the fallback.
|
||||
|
||||
@@ -70,7 +70,7 @@ mod thread_tests;
|
||||
mod topo;
|
||||
mod tspline;
|
||||
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_max};
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_max, fillet_max_each};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{fillet_chord, fillet_variable, fillet_variable_points};
|
||||
pub use body::{Body, CapSignature, DISPLAY_ANGLE, EdgeInfo, FaceInfo, FacePaint, FaceSource, Paint};
|
||||
|
||||
@@ -382,6 +382,36 @@ fn fillet_max_finds_the_largest_radius_that_works() {
|
||||
assert!(fillet_max(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).is_none());
|
||||
}
|
||||
|
||||
/// Edges that share no face get their own largest radius: a 2 mm fin's top edge takes at most
|
||||
/// its thickness, while a corner of the block it stands on, far from it, keeps the 4 asked for.
|
||||
/// Edges that limit each other (the post's four, sharing faces) still share one.
|
||||
#[test]
|
||||
fn fillet_max_each_reduces_only_where_the_radius_does_not_fit() {
|
||||
let (block, fin) = (40.0 * 30.0 * 20.0, 2.0 * 30.0 * 10.0);
|
||||
let body = boolean(
|
||||
&box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(),
|
||||
&box_solid(Vec3::new(19.0, 0.0, 20.0), Vec3::new(21.0, 30.0, 30.0)).unwrap(),
|
||||
BoolOp::Union,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let edges = [Vec3::new(21.0, 15.0, 30.0), Vec3::new(40.0, 0.0, 10.0)];
|
||||
assert!(matches!(fillet(&body, &edges, 4.0), Err(KernelError::TooLarge(_))));
|
||||
let (one, _) = fillet_max(&body, &edges, 4.0).unwrap();
|
||||
let (radii, b) = fillet_max_each(&body, &edges, 4.0).unwrap();
|
||||
assert_eq!(radii.get(1), Some(&4.0), "{radii:?}");
|
||||
let fin_r = radii.first().copied().unwrap();
|
||||
assert!(fin_r < 4.0 && (fin_r - one).abs() < 1e-9, "{radii:?}, one radius {one}");
|
||||
assert!(b.validity().is_empty(), "{:?}", b.validity());
|
||||
// Each round takes (1 − π/4)·r² along its edge: the fin's 30 long, the corner's 20.
|
||||
let removed = (1.0 - PI / 4.0) * (fin_r * fin_r * 30.0 + 16.0 * 20.0);
|
||||
assert!(rel(measure(&b).unwrap().volume, block + fin - removed) < 2e-4);
|
||||
// The post's four edges limit each other: one radius, as `fillet_max`.
|
||||
let post = box_solid(Vec3::new(14.0, 0.0, 0.0), Vec3::new(28.0, 6.0, 10.0)).unwrap();
|
||||
let mids = [Vec3::new(14.0, 0.0, 5.0), Vec3::new(28.0, 0.0, 5.0), Vec3::new(14.0, 6.0, 5.0), Vec3::new(28.0, 6.0, 5.0)];
|
||||
assert_eq!(fillet_max_each(&post, &mids, 3.5).map(|x| x.0), Some(vec![3.0; 4]));
|
||||
}
|
||||
|
||||
/// A corner's rounds too large for its faces are refused as too large, so a fit can reduce
|
||||
/// them, also where only the corner build finds it (the edge-by-edge blend failing otherwise).
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user