Fillets and chamfers on runs of neighbouring edges along a planar face's loop (two or three top edges of a box): the blends meet in mitres at the sharp corners between them and end square where unblended edges go on (those edges and the walls' seams stop at the blend)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
bee763959c
commit
01b349842c
4 files changed
+156
-34
No files matched your search
+2
-1
@@ -58,7 +58,8 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
- Fillets and chamfers: straight edges (convex or concave) between planar faces with perpendicular
|
||||
planar end faces; 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),
|
||||
plate outlines), loops with sharp corners between straight edges (the blends meet in mitres), runs of
|
||||
neighbouring edges along a planar face's loop (two top edges of a box: mitred, ending square),
|
||||
rounded corners rounded again (sphere octants, tori at concave corners); every edge of a
|
||||
convex planar body or of an extruded part; fillets inside patterns; closed chains of curved
|
||||
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
|
||||
|
||||
@@ -318,6 +318,19 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
cur = blend(&cur, &rest, r, shape, what)?;
|
||||
return blend(&cur, &loop_pts, r, shape, what);
|
||||
}
|
||||
// Neighbouring edges along a planar face's loop (sharp corners between them: the blends
|
||||
// meet in mitres), ending where unblended edges go on.
|
||||
if edges.len() >= 2 {
|
||||
let size = cur.size();
|
||||
let solid = cur.deep_copy();
|
||||
let ids = edge_ids(&cur, &solid, edges)?;
|
||||
if ids.len() == edges.len()
|
||||
&& let Some(Ok(res)) = crate::loopblend::loop_blend(&solid, &ids, size, r, shape == Shape::Round)
|
||||
&& let Ok(b) = guard(what, || Ok(res)).and_then(Body::new)
|
||||
{
|
||||
return Ok(b);
|
||||
}
|
||||
}
|
||||
if let Some(groups) = loop_groups(&cur, edges)? {
|
||||
for g in groups {
|
||||
let size = cur.size();
|
||||
|
||||
+119
-31
@@ -39,6 +39,12 @@ impl Seg {
|
||||
Seg::Line { a, .. } | Seg::Arc { a, .. } => a,
|
||||
}
|
||||
}
|
||||
fn end(&self) -> Vec3 {
|
||||
match *self {
|
||||
Seg::Line { b, .. } => b,
|
||||
Seg::Arc { a, c, axis, angle, .. } => turn(a, c, axis, angle),
|
||||
}
|
||||
}
|
||||
/// Unit tangent (direction of travel) at a point of the segment.
|
||||
fn tangent(&self, p: Vec3) -> Option<Vec3> {
|
||||
match *self {
|
||||
@@ -144,10 +150,21 @@ pub(crate) fn loop_blend(solid: &Solid, edge_ids: &[mt::EdgeID], size: f64, r: f
|
||||
let Some(n) = plane_normal(f) else { continue };
|
||||
for (wi, w) in f.boundaries().iter().enumerate() {
|
||||
let ids: Vec<mt::EdgeID> = w.edge_iter().map(|e| e.id()).collect();
|
||||
if ids.len() != edge_ids.len() || !edge_ids.iter().all(|e| ids.contains(e)) {
|
||||
if !edge_ids.iter().all(|e| ids.contains(e)) {
|
||||
continue;
|
||||
}
|
||||
return Some(build(solid, si, &faces, fi, wi, n, size, tol, r, round));
|
||||
if ids.len() == edge_ids.len() {
|
||||
return Some(build(solid, si, &faces, fi, wi, n, size, tol, r, round, None));
|
||||
}
|
||||
// A run of neighbouring edges of the loop (its ends meet unblended edges).
|
||||
let m = ids.len();
|
||||
let inside = |i: usize| ids.get(i % m).is_some_and(|x| edge_ids.contains(x));
|
||||
let starts: Vec<usize> = (0..m).filter(|i| inside(*i) && !inside(*i + m - 1)).collect();
|
||||
if let [s0] = starts[..]
|
||||
&& (0..edge_ids.len()).all(|j| inside(s0 + j))
|
||||
{
|
||||
return Some(build(solid, si, &faces, fi, wi, n, size, tol, r, round, Some((s0, edge_ids.len()))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,10 +172,28 @@ pub(crate) fn loop_blend(solid: &Solid, edge_ids: &[mt::EdgeID], size: f64, r: f
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n: Vec3, size: f64, tol: f64, r: f64, round: bool) -> Result<Solid> {
|
||||
fn build(
|
||||
solid: &Solid,
|
||||
si: usize,
|
||||
faces: &[&mt::Face],
|
||||
fi: usize,
|
||||
wi: usize,
|
||||
n: Vec3,
|
||||
size: f64,
|
||||
tol: f64,
|
||||
r: f64,
|
||||
round: bool,
|
||||
run: Option<(usize, usize)>,
|
||||
) -> Result<Solid> {
|
||||
let f = faces.get(fi).ok_or_else(|| KernelError::Failed("face".into()))?;
|
||||
let wire = f.boundaries().get(wi).cloned().ok_or_else(|| KernelError::Failed("loop".into()))?;
|
||||
let edges: Vec<mt::Edge> = wire.edge_iter().cloned().collect();
|
||||
let all_edges: Vec<mt::Edge> = wire.edge_iter().cloned().collect();
|
||||
// The blended edges in loop order: the whole loop, or a run of it (open).
|
||||
let open = run.is_some();
|
||||
let edges: Vec<mt::Edge> = match run {
|
||||
None => all_edges,
|
||||
Some((s0, len)) => (0..len).filter_map(|j| all_edges.get((s0 + j) % all_edges.len().max(1)).cloned()).collect(),
|
||||
};
|
||||
let edge_segs: Vec<Seg> = edges
|
||||
.iter()
|
||||
.map(|e| seg_of(e, (size * 1e-5).max(1e-6)))
|
||||
@@ -169,9 +204,15 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let owner: Vec<usize> = split.iter().map(|x| x.0).collect();
|
||||
let segs: Vec<Seg> = split.iter().map(|x| x.1).collect();
|
||||
let k = segs.len();
|
||||
// Joints: k round a whole loop; k + 1 along a run (its two ends included).
|
||||
let nj = if open { k + 1 } else { k };
|
||||
let nxt = |i: usize| if open { i + 1 } else { (i + 1) % k };
|
||||
// Joints: smooth, or sharp between two lines (the blends then meet in a mitre).
|
||||
let mut mitre = vec![false; k];
|
||||
let mut mitre = vec![false; nj];
|
||||
for i in 0..k {
|
||||
if open && i == 0 {
|
||||
continue;
|
||||
}
|
||||
let (Some(prev), Some(cur)) = (segs.get((i + k - 1) % k), segs.get(i)) else { continue };
|
||||
let p = cur.start();
|
||||
let (Some(ta), Some(tb)) = (prev.tangent(p), cur.tangent(p)) else { return Err(unsupported("degenerate loop edge")) };
|
||||
@@ -256,12 +297,14 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let mut pa = Vec::with_capacity(k);
|
||||
let mut pb = Vec::with_capacity(k);
|
||||
let mut centres = Vec::with_capacity(k);
|
||||
for i in 0..k {
|
||||
let p = segs.get(i).map(|s| s.start()).unwrap_or_default();
|
||||
let (m, t, sb, dc) = frame(i, p).ok_or_else(|| unsupported("the face and wall meet at an angle the blend can't take"))?;
|
||||
let mut pa = Vec::with_capacity(nj);
|
||||
let mut pb = Vec::with_capacity(nj);
|
||||
let mut centres = Vec::with_capacity(nj);
|
||||
// Joint i: the start of piece i (the end of the last piece for a run's far end).
|
||||
let joint_point = |i: usize| if i < k { segs.get(i).map(|s| s.start()) } else { segs.last().map(|s| s.end()) };
|
||||
for i in 0..nj {
|
||||
let p = joint_point(i).unwrap_or_default();
|
||||
let (m, t, sb, dc) = frame(i.min(k - 1), p).ok_or_else(|| unsupported("the face and wall meet at an angle the blend can't take"))?;
|
||||
centres.push(p + (m + t).normalized().unwrap_or(m) * dc);
|
||||
if !mitre.get(i).copied().unwrap_or(false) {
|
||||
pa.push(p + m * sb);
|
||||
@@ -287,14 +330,11 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
}
|
||||
// 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];
|
||||
let mut collapsed = vec![false; k.max(nj)];
|
||||
for (i, s) in segs.iter().enumerate() {
|
||||
if let Seg::Arc { a, c, .. } = *s {
|
||||
let toward = into(s, a).dot(c - a) > 0.0;
|
||||
if toward
|
||||
&& (a.dist(c) - r).abs() <= tol * 10.0
|
||||
&& !mitre.get(i).copied().unwrap_or(true)
|
||||
&& !mitre.get((i + 1) % k).copied().unwrap_or(true)
|
||||
if toward && (a.dist(c) - r).abs() <= tol * 10.0 && !mitre.get(i).copied().unwrap_or(true) && !mitre.get(nxt(i)).copied().unwrap_or(true)
|
||||
{
|
||||
if let Some(x) = collapsed.get_mut(i) {
|
||||
*x = true;
|
||||
@@ -312,7 +352,7 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
for i in 0..k {
|
||||
if collapsed.get(i).copied().unwrap_or(false)
|
||||
&& let Some(v) = va.get(i).cloned()
|
||||
&& let Some(slot) = va.get_mut((i + 1) % k)
|
||||
&& let Some(slot) = va.get_mut(nxt(i))
|
||||
{
|
||||
*slot = v;
|
||||
}
|
||||
@@ -320,12 +360,12 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let vb: Vec<mt::Vertex> = pb.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
let centre = |i: usize| centres.get(i).copied().unwrap_or_default();
|
||||
let profile_mid = |i: usize| {
|
||||
let (Some(s), c) = (segs.get(i), centre(i)) else { return Vec3::ZERO };
|
||||
c + (s.start() - c).normalized().unwrap_or(n) * r
|
||||
let (Some(p), c) = (joint_point(i), centre(i)) else { return Vec3::ZERO };
|
||||
c + (p - c).normalized().unwrap_or(n) * r
|
||||
};
|
||||
// Profiles at the joints (face side → wall side).
|
||||
let mut profiles: Vec<mt::Edge> = Vec::with_capacity(k);
|
||||
for i in 0..k {
|
||||
let mut profiles: Vec<mt::Edge> = Vec::with_capacity(nj);
|
||||
for i in 0..nj {
|
||||
let (Some(a), Some(b)) = (va.get(i), vb.get(i)) else { return Err(KernelError::Failed("joint".into())) };
|
||||
if !round {
|
||||
profiles.push(builder::line(a, b));
|
||||
@@ -368,7 +408,7 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
if std::ptr::eq(vs, va.as_slice()) && collapsed.get(i).copied().unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (vs.get(i)?, vs.get((i + 1) % k)?);
|
||||
let (a, b) = (vs.get(i)?, vs.get(nxt(i))?);
|
||||
Some(match *s {
|
||||
Seg::Line { .. } => builder::line(a, b),
|
||||
Seg::Arc { mid, .. } => builder::circle_arc(a, b, p3(offset(mid, s))),
|
||||
@@ -391,16 +431,28 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
(0..k).map(|i| rail(&vb, i, &wall_off)).collect::<Option<_>>().ok_or_else(|| KernelError::Failed("rail".into()))?;
|
||||
// Seams between neighbouring walls at the joints get shorter.
|
||||
// Model vertices at the joints (the second half of a circle starts at none).
|
||||
let joint_vertex: Vec<Option<mt::Vertex>> = (0..k)
|
||||
let mut joint_vertex: Vec<Option<mt::Vertex>> = (0..k)
|
||||
.map(|i| if i == 0 || owner.get(i) != owner.get(i - 1) { owner.get(i).and_then(|o| edges.get(*o)).map(|e| e.front().clone()) } else { None })
|
||||
.collect();
|
||||
if open {
|
||||
joint_vertex.push(edges.last().map(|e| e.back().clone()));
|
||||
}
|
||||
// At a run's ends the face's next loop edges stop at the face-side point.
|
||||
let on_face = |e: &mt::Edge| f.edge_iter().any(|x| x.id() == e.id());
|
||||
let mut seam_subst: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
|
||||
let mut seam_ends: HashMap<mt::EdgeID, (mt::Edge, Option<mt::Vertex>, Option<mt::Vertex>)> = HashMap::new();
|
||||
let loop_ids: Vec<mt::EdgeID> = edges.iter().map(|e| e.id()).collect();
|
||||
for (i, v) in joint_vertex.iter().enumerate() {
|
||||
let Some(v) = v else { continue };
|
||||
for g in faces {
|
||||
for e in g.edge_iter() {
|
||||
if loop_ids.contains(&e.id()) || seam_subst.contains_key(&e.id()) || !(e.front() == v || e.back() == v) {
|
||||
if loop_ids.contains(&e.id()) || !(e.front() == v || e.back() == v) {
|
||||
continue;
|
||||
}
|
||||
// Once per edge and joint (an edge between both ends of a run is met twice).
|
||||
if seam_ends.get(&e.id()).is_some_and(|x: &(mt::Edge, Option<mt::Vertex>, Option<mt::Vertex>)| {
|
||||
(x.0.absolute_front() == v && x.1.is_some()) || (x.0.absolute_back() == v && x.2.is_some())
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let (fa, fb) = (e.absolute_front().clone(), e.absolute_back().clone());
|
||||
@@ -408,17 +460,31 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
if !matches!(e.curve(), mt::Curve::Line(_)) {
|
||||
return Err(unsupported("the wall seams must be straight"));
|
||||
}
|
||||
let along = (from_p3(other.point()) - from_p3(v.point())).dot(pb.get(i).copied().unwrap_or_default() - from_p3(v.point()));
|
||||
let reach = (pb.get(i).copied().unwrap_or_default() - from_p3(v.point())).len2();
|
||||
let face_side = on_face(&e);
|
||||
let target = if face_side { pa.get(i) } else { pb.get(i) }.copied().unwrap_or_default();
|
||||
let along = (from_p3(other.point()) - from_p3(v.point())).dot(target - from_p3(v.point()));
|
||||
let reach = (target - from_p3(v.point())).len2();
|
||||
if along <= reach + tol {
|
||||
return Err(unsupported("the blend is taller than the walls"));
|
||||
return Err(unsupported(if face_side { "the blend is longer than the next edge" } else { "the blend is taller than the walls" }));
|
||||
}
|
||||
let nb = if face_side { va.get(i) } else { vb.get(i) }.ok_or_else(|| KernelError::Failed("joint".into()))?;
|
||||
let slot = seam_ends.entry(e.id()).or_insert_with(|| (e.absolute_clone(), None, None));
|
||||
if &fa == v {
|
||||
slot.1 = Some(nb.clone());
|
||||
} else {
|
||||
slot.2 = Some(nb.clone());
|
||||
}
|
||||
let nb = vb.get(i).ok_or_else(|| KernelError::Failed("joint".into()))?;
|
||||
let ne = if &fa == v { builder::line(nb, &fb) } else { builder::line(&fa, nb) };
|
||||
seam_subst.insert(e.id(), ne);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (id, (e, a, b)) in seam_ends {
|
||||
let a = a.unwrap_or_else(|| e.absolute_front().clone());
|
||||
let b = b.unwrap_or_else(|| e.absolute_back().clone());
|
||||
if from_p3(a.point()).dist(from_p3(b.point())) < tol * 10.0 {
|
||||
return Err(unsupported("the blend is longer than the next edge"));
|
||||
}
|
||||
seam_subst.insert(id, builder::line(&a, &b));
|
||||
}
|
||||
// Rebuild faces.
|
||||
let idx_of = |id: mt::EdgeID| loop_ids.iter().position(|x| *x == id);
|
||||
let rebuild = |g: &mt::Face, rails: &[Option<mt::Edge>]| -> Result<mt::Face> {
|
||||
@@ -444,6 +510,28 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
};
|
||||
es.push(ne);
|
||||
}
|
||||
// A run's end: the face's edge and the wall's seam now stop at the two ends of the
|
||||
// end profile, which closes the gap between them.
|
||||
if open {
|
||||
let mut fixed: Vec<mt::Edge> = Vec::with_capacity(es.len() + 2);
|
||||
let m = es.len();
|
||||
for j in 0..m {
|
||||
let (Some(cur), Some(next)) = (es.get(j), es.get((j + 1) % m)) else { continue };
|
||||
fixed.push(cur.clone());
|
||||
if cur.back() == next.front() {
|
||||
continue;
|
||||
}
|
||||
for ji in [0, nj - 1] {
|
||||
let (Some(a), Some(b), Some(pr)) = (va.get(ji), vb.get(ji), profiles.get(ji)) else { continue };
|
||||
if cur.back() == a && next.front() == b {
|
||||
fixed.push(pr.clone());
|
||||
} else if cur.back() == b && next.front() == a {
|
||||
fixed.push(pr.inverse());
|
||||
}
|
||||
}
|
||||
}
|
||||
es = fixed;
|
||||
}
|
||||
wires.push(mt::Wire::from(es));
|
||||
}
|
||||
crate::heal::absolute_face(g, wires).ok_or_else(|| KernelError::Failed("loop blend: a face could not be rebuilt".into()))
|
||||
@@ -465,7 +553,7 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
use mt::{ParametricSurface3D, SearchNearestParameter};
|
||||
for i in 0..k {
|
||||
let (Some(s), Some(pr), Some(pr_next), Some(fr), Some(wr)) =
|
||||
(segs.get(i), profiles.get(i), profiles.get((i + 1) % k), face_rails.get(i), wall_rails.get(i))
|
||||
(segs.get(i), profiles.get(i), profiles.get(nxt(i)), face_rails.get(i), wall_rails.get(i))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -138,8 +138,11 @@ fn fillets_on_several_edges_and_angles() {
|
||||
let removed = r * r / (phi / 2.0).tan() - r * r * (PI - phi) / 2.0;
|
||||
let v0 = measure(&p).unwrap().volume;
|
||||
assert!(rel(measure(&fp).unwrap().volume, v0 - removed * 10.0) < 2e-4, "{} vs {}", measure(&fp).unwrap().volume, v0 - removed * 10.0);
|
||||
// Adjacent edges at a blended corner are not supported yet (an error, not a crash).
|
||||
assert!(fillet(&b, &[Vec3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 15.0, 20.0)], 2.0).is_err());
|
||||
// Adjacent edges meeting at a corner: their blends meet in a mitre.
|
||||
let two = fillet(&b, &[Vec3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 15.0, 20.0)], 2.0).unwrap();
|
||||
let cut = 24000.0 - measure(&two).unwrap().volume;
|
||||
let a = 4.0 * (1.0 - PI / 4.0);
|
||||
assert!(cut > a * (20.0 + 30.0 - 2.0) && cut < a * 50.0, "{cut}");
|
||||
// Concave edge: not supported.
|
||||
let u = boolean(&b, &box_solid(Vec3::new(10.0, 10.0, 15.0), Vec3::new(20.0, 20.0, 30.0)).unwrap(), BoolOp::Union).unwrap().unwrap();
|
||||
assert!(fillet(&u, &[Vec3::new(15.0, 10.0, 20.0)], 1.0).is_err());
|
||||
@@ -1175,3 +1178,20 @@ fn booleans_found_by_the_survey() {
|
||||
let vu = measure(&u).unwrap().volume;
|
||||
assert!(vu > PI * 64.0 * 20.0 && vu < PI * 64.0 * 20.0 + 1620.0, "{vu}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_two_edges_meeting_at_a_corner() {
|
||||
// Two top edges of a box meeting at a corner (the vertical edge there stays sharp): the
|
||||
// fillets meet in a mitre and stop square at the far walls.
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap();
|
||||
let f = fillet(&b, &[Vec3::new(20.0, 0.0, 20.0), Vec3::new(0.0, 15.0, 20.0)], 3.0).unwrap();
|
||||
let removed = 24000.0 - measure(&f).unwrap().volume;
|
||||
// Each edge loses r²(1 − π/4) along its length; at the mitred corner the two cuts overlap
|
||||
// in a region of volume r³(1 − π/4)·… — bounded between the sum less one corner and the sum.
|
||||
let a = 9.0 * (1.0 - PI / 4.0);
|
||||
let (lo, hi) = (a * (40.0 + 30.0) - a * 3.0, a * (40.0 + 30.0));
|
||||
assert!(removed > lo && removed < hi, "removed {removed} in ({lo}, {hi})");
|
||||
// Three edges: front, left and back.
|
||||
let f3 = fillet(&b, &[Vec3::new(20.0, 0.0, 20.0), Vec3::new(0.0, 15.0, 20.0), Vec3::new(20.0, 30.0, 20.0)], 3.0).unwrap();
|
||||
assert!(measure(&f3).unwrap().volume < measure(&f).unwrap().volume);
|
||||
}
|
||||
Reference in new issue
Block a user