Join chamfers across unequal sloped rim walls
This commit is contained in:
1 parent
db4f885bac
commit
5835bb10d4
4 files changed
+113
-13
No files matched your search
@@ -69,6 +69,7 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
|
||||
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
|
||||
corners around pocket rims, with unequal wall slopes and extensions confined to the face;
|
||||
connected equal-distance chamfers also trim each other across unequal sloped rim walls;
|
||||
whole smooth loops of a planar face, picked whole or by one edge (tangent
|
||||
chain), including circles and walls at any angle (boss bases and tops, bores, pocket floors,
|
||||
plate outlines), loops with sharp corners between straight edges (the blends meet in mitres), runs of
|
||||
|
||||
@@ -785,6 +785,14 @@ pub fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
// At a pocket rim the cutters cannot extend beyond the selected edges. Rebuild
|
||||
// the corners locally, allowing adjacent chamfers to reach different depths
|
||||
// on their common wall seam.
|
||||
if edges.len() > 1
|
||||
&& let Ok(result) = crate::blend_corner::chamfer(body, edges, distance)
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
// Coplanar neighbours as one face (as after a join), so corners look as they should.
|
||||
let mut cur = Body::new(crate::heal::heal_keep(body.deep_copy(), body.size(), body.split_keep()))?;
|
||||
if let Some(groups) = loop_groups(&cur, edges)? {
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result<Body> {
|
||||
let mut first: Option<KernelError> = None;
|
||||
// Earlier rounds at the edges' ends come off and are blended again with the new edges.
|
||||
for _ in 0..4 {
|
||||
let err = match build(&cur, &sel) {
|
||||
let err = match build(&cur, &sel, true) {
|
||||
Ok(b) => return Ok(b),
|
||||
Err(e) => e,
|
||||
};
|
||||
@@ -54,6 +54,14 @@ pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result<Body> {
|
||||
Err(first.unwrap_or_else(|| unsupported("these fillets")))
|
||||
}
|
||||
|
||||
/// Local equal-distance chamfers, including pocket rims where adjacent walls have
|
||||
/// different slopes. The planar strips trim each other at each selected corner.
|
||||
pub(crate) fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
|
||||
let cur = Body::new(crate::heal::heal_keep(body.deep_copy(), body.size(), body.split_keep()))?;
|
||||
let selected: Vec<_> = edges.iter().map(|p| (*p, distance)).collect();
|
||||
build(&cur, &selected, false)
|
||||
}
|
||||
|
||||
/// A face of the shell being rebuilt, or a new face.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Owner {
|
||||
@@ -174,7 +182,7 @@ fn connect(from: &mt::Vertex, to: &mt::Vertex, pool: &[(mt::Edge, Vec<Owner>)],
|
||||
Err(KernelError::Failed("fillet corner: a boundary does not close".into()))
|
||||
}
|
||||
|
||||
fn build(cur: &Body, sel_pts: &[(Vec3, f64)]) -> Result<Body> {
|
||||
fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
|
||||
let size = cur.size();
|
||||
let tol = (size * 2e-3).max(1e-3);
|
||||
let solid = cur.deep_copy();
|
||||
@@ -252,16 +260,20 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)]) -> Result<Body> {
|
||||
return Err(unsupported("the faces at the edge are tangent"));
|
||||
}
|
||||
let r = radius.get(id).copied().unwrap_or(1.0);
|
||||
let s = r / (alpha / 2.0).tan();
|
||||
let s = if round { r / (alpha / 2.0).tan() } else { r };
|
||||
let w = (alpha / 2.0).sin();
|
||||
// The cylinder, well past both ends (sections on oblique end faces reach beyond them).
|
||||
let m = 4.0 * (s + r);
|
||||
let (q0, q1) = (p0 - d * m, p1 + d * m);
|
||||
let ctrl = vec![vec![h(q0 + ta * s, 1.0), h(q1 + ta * s, 1.0)], vec![h(q0, w), h(q1, w)], vec![h(q0 + tb * s, 1.0), h(q1 + tb * s, 1.0)]];
|
||||
let surf = mt::Surface::NurbsSurface(mt::NurbsSurface::new(mt::BSplineSurface::new(
|
||||
(mt::KnotVec::bezier_knot(2), mt::KnotVec::bezier_knot(1)),
|
||||
ctrl,
|
||||
)));
|
||||
let surf = if round {
|
||||
mt::Surface::NurbsSurface(mt::NurbsSurface::new(mt::BSplineSurface::new(
|
||||
(mt::KnotVec::bezier_knot(2), mt::KnotVec::bezier_knot(1)),
|
||||
ctrl,
|
||||
)))
|
||||
} else {
|
||||
mt::Surface::Plane(mt::Plane::new(p3(q0 + ta * s), p3(q1 + ta * s), p3(q0 + tb * s)))
|
||||
};
|
||||
let mid = (p0 + p1) * 0.5;
|
||||
let axis = mid + ta * s - na * r;
|
||||
let out = ((ta + tb) * -1.0).normalized().unwrap_or(na);
|
||||
@@ -337,7 +349,7 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)]) -> Result<Body> {
|
||||
endpt.insert((*e1, *vid, Some(f12)), a.clone());
|
||||
endpt.insert((*e3, *vid, None), b.clone());
|
||||
endpt.insert((*e1, *vid, Some(f31)), b.clone());
|
||||
let c = edge(&a, &b, mt::Curve::NurbsCurve(conic(p2, v, p3_, s1.w)))?;
|
||||
let c = if round { edge(&a, &b, mt::Curve::NurbsCurve(conic(p2, v, p3_, s1.w)))? } else { builder::line(&a, &b) };
|
||||
list.push((c, vec![Owner::Face(f23), Owner::Cyl(*e1)]));
|
||||
}
|
||||
([(ea, ua), (eb, ub)], [(e3, u3)]) => {
|
||||
@@ -355,7 +367,9 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)]) -> Result<Body> {
|
||||
endpt.insert((*ea, *vid, Some(fab)), qv.clone());
|
||||
endpt.insert((*eb, *vid, Some(fab)), qv.clone());
|
||||
let (la, lb) = ((xa - v).len(), (xb - v).len());
|
||||
let crease_to = |end: Vec3| -> Result<mt::Curve> { crease(sa, sb, q, end, size) };
|
||||
let crease_to = |end: Vec3| -> Result<mt::Curve> {
|
||||
if round { crease(sa, sb, q, end, size) } else { Ok(mt::Curve::Line(mt::Line(p3(q), p3(end)))) }
|
||||
};
|
||||
if (la - lb).abs() <= size * 1e-9 {
|
||||
let xv = builder::vertex(p3(xb));
|
||||
endpt.insert((*e3, *vid, None), xv.clone());
|
||||
@@ -376,10 +390,16 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)]) -> Result<Body> {
|
||||
let c0 = (x - v).dot(tt) - small.s;
|
||||
let c1 = big.w * (0.0 - small.s);
|
||||
let c2 = (z - v).dot(tt) - small.s;
|
||||
let t = bernstein_root(c0, c1, c2).ok_or_else(|| unsupported("fillets this different in size at one corner"))?;
|
||||
let y = {
|
||||
// A chamfer section is a line, not the cylinder's conic.
|
||||
// Where it meets the other strip's contact line is also
|
||||
// where the crease ends; the remainder trims that wall.
|
||||
let t = if round { bernstein_root(c0, c1, c2) } else { Some(c0 / (c0 - c2)).filter(|t| *t > 0.0 && *t < 1.0) }
|
||||
.ok_or_else(|| unsupported("blends this different in size at one corner"))?;
|
||||
let y = if round {
|
||||
use mt::ParametricCurve;
|
||||
crate::body::from_p3(sec.subs(t))
|
||||
} else {
|
||||
x + (z - x) * t
|
||||
};
|
||||
{
|
||||
use mt::Cut;
|
||||
@@ -390,13 +410,16 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)]) -> Result<Body> {
|
||||
endpt.insert((*e3, *vid, None), xv.clone());
|
||||
endpt.insert((eb_, *vid, Some(fbig3)), xv.clone());
|
||||
endpt.insert((es, *vid, Some(fsmall3)), yv.clone());
|
||||
let piece = edge(&xv, &yv, mt::Curve::NurbsCurve(sec))?;
|
||||
let piece = if round { edge(&xv, &yv, mt::Curve::NurbsCurve(sec))? } else { builder::line(&xv, &yv) };
|
||||
list.push((piece, vec![Owner::Face(fsmall3), Owner::Cyl(eb_)]));
|
||||
let c = edge(&qv, &yv, crease_to(y)?)?;
|
||||
list.push((c, vec![Owner::Cyl(*ea), Owner::Cyl(*eb)]));
|
||||
}
|
||||
}
|
||||
([(e1, u1), (e2, u2), (e3, u3)], []) => {
|
||||
if !round {
|
||||
return Err(unsupported("three chamfers at one corner in the local blend"));
|
||||
}
|
||||
let (s1, s2, s3) = (sel(*e1)?, sel(*e2)?, sel(*e3)?);
|
||||
let (f12, f23, f31) = (face_with(*e1, *e2)?, face_with(*e2, *e3)?, face_with(*e3, *e1)?);
|
||||
let q12 = meet(v, (*u1, s1.t(f12), s1.s), (*u2, s2.t(f12), s2.s))?;
|
||||
@@ -1150,4 +1173,59 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chamfers_meet_on_unequal_sloped_rim_walls() {
|
||||
use solvecraft_geom::{Loop2, Plane, Vec2};
|
||||
for margin in [1.0, 4.0] {
|
||||
let rectangle = |x0, y0, x1, y1| Loop2::polygon(&[Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1)]);
|
||||
let slab = box_solid(Vec3::ZERO, Vec3::new(100.0, 60.0, 30.0)).unwrap();
|
||||
let pocket = crate::loft(&[
|
||||
(Plane::XY.offset(30.0), rectangle(20.0, margin, 80.0, 50.0)),
|
||||
(Plane::XY.offset(10.0), rectangle(30.0, 20.0, 70.0, 40.0)),
|
||||
])
|
||||
.unwrap();
|
||||
let basin = crate::boolean(&slab, &pocket, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let picks = [Vec3::new(50.0, margin, 30.0), Vec3::new(80.0, 27.0, 30.0), Vec3::new(50.0, 50.0, 30.0), Vec3::new(20.0, 27.0, 30.0)];
|
||||
for count in [2, 3, 4] {
|
||||
if margin == 1.0 {
|
||||
assert!(crate::chamfer(&basin, &picks[..count], 2.0).is_err());
|
||||
assert!(crate::chamfer(&basin, &picks[..count], 1.0).is_err());
|
||||
}
|
||||
let distance = if margin == 1.0 { 0.5 } else { 2.0 };
|
||||
let result = crate::chamfer(&basin, &picks[..count], distance).unwrap();
|
||||
assert!(result.validity().is_empty());
|
||||
assert_eq!(result.unmeshed_faces(), 0);
|
||||
assert!(vol(&result) < vol(&basin));
|
||||
assert!(vol(&basin) - vol(&result) < distance.powi(2) * (60.0 + 50.0 - margin));
|
||||
let reverse: Vec<_> = picks[..count].iter().copied().rev().collect();
|
||||
let reversed = crate::chamfer(&basin, &reverse, distance).unwrap();
|
||||
assert!((vol(&result) - vol(&reversed)).abs() < 1e-6);
|
||||
// These are exact planes, with every boundary on its owning face.
|
||||
for f in result.solid.face_iter() {
|
||||
let Some(n) = plane_normal(f) else { continue };
|
||||
let origin = vtx(&f.vertex_iter().next().unwrap());
|
||||
for v in f.vertex_iter() {
|
||||
assert!((vtx(&v) - origin).dot(n).abs() < 1e-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_slope_rim_loops_cannot_escape_the_face() {
|
||||
use solvecraft_geom::{Loop2, Plane, Vec2};
|
||||
let rectangle = |x0, y0, x1, y1| Loop2::polygon(&[Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1)]);
|
||||
let slab = box_solid(Vec3::ZERO, Vec3::new(100.0, 60.0, 30.0)).unwrap();
|
||||
let pocket =
|
||||
crate::loft(&[(Plane::XY.offset(30.0), rectangle(20.0, 1.0, 80.0, 50.0)), (Plane::XY.offset(10.0), rectangle(30.0, 11.0, 70.0, 40.0))])
|
||||
.unwrap();
|
||||
let basin = crate::boolean(&slab, &pocket, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let picks = [Vec3::new(50.0, 1.0, 30.0), Vec3::new(80.0, 27.0, 30.0), Vec3::new(50.0, 50.0, 30.0), Vec3::new(20.0, 27.0, 30.0)];
|
||||
assert!(crate::chamfer(&basin, &picks, 2.0).is_err());
|
||||
assert!(fillet(&basin, &picks, 4.0).is_err());
|
||||
assert!(crate::chamfer(&basin, &picks, 0.5).is_ok());
|
||||
assert!(fillet(&basin, &picks, 0.5).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,7 @@ fn build(
|
||||
if !(phi > 1e-3 && phi < std::f64::consts::PI - 1e-3) {
|
||||
return None;
|
||||
}
|
||||
let sb = r / (phi / 2.0).tan();
|
||||
let sb = if round { r / (phi / 2.0).tan() } else { r };
|
||||
Some((m, t, sb, r / (phi / 2.0).sin()))
|
||||
};
|
||||
// The straight seam between the walls at a sharp joint (direction away from the face).
|
||||
@@ -328,6 +328,19 @@ fn build(
|
||||
}
|
||||
pb.push(p + e * (sb / h1));
|
||||
}
|
||||
// Sharp joints between straight rim edges can extend beyond the old corner.
|
||||
// Equal wall slopes reach this path instead of the corner builder, and must
|
||||
// also reject an extension through the outer wall or another hole.
|
||||
for (i, q) in pa.iter().enumerate() {
|
||||
// Smooth arc transitions and open-run ends retain their existing checks.
|
||||
if !mitre.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let p = joint_point(i).ok_or_else(|| unsupported("degenerate loop joint"))?;
|
||||
if !crate::blend::extension_fits(f, p, *q, size * 1e-7) {
|
||||
return Err(unsupported("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
}
|
||||
// The offset arcs must not turn inside out; an arc of exactly the blend's radius shrinks
|
||||
// to a point (a rounded corner rounded again: the blend there is a sphere).
|
||||
let mut collapsed = vec![false; k.max(nj)];
|
||||
|
||||
Reference in new issue
Block a user