This commit is contained in:
1 parent
073acd295b
commit
762b6fe934
3 files changed
+105
-30
No files matched your search
+73
-30
@@ -1697,8 +1697,12 @@ fn region_samples(r: &Region2) -> Vec<Vec2> {
|
||||
/// Grow profile edges that lie on a face of a target body away from the profile, by a small
|
||||
/// amount, when the space they grow into is already material (join) or empty (cut). `probe`
|
||||
/// maps a sketch point to the world points where the tool would be (along the extrude, around
|
||||
/// the revolve). Only loops of straight edges are adjusted.
|
||||
/// the revolve). Loops of lines and arcs are adjusted: a line moves out, an arc keeps its centre
|
||||
/// and changes its radius (a rounded corner of a face cut whole, #1275). Arcs move only where
|
||||
/// both of their ends meet tangent neighbours that move with them; otherwise the loop is left
|
||||
/// as it is.
|
||||
fn grow_profile_for_coplanar(st: &ModelState, r: &Region2, op: Operation, targets: &[String], probe: &dyn Fn(Vec2) -> Vec<Vec3>) -> Region2 {
|
||||
use solidcraft_geom::Seg2;
|
||||
if !matches!(op, Operation::Cut | Operation::Join) {
|
||||
return r.clone();
|
||||
}
|
||||
@@ -1710,55 +1714,94 @@ fn grow_profile_for_coplanar(st: &ModelState, r: &Region2, op: Operation, target
|
||||
let size = bodies.iter().map(|b| b.body.size()).fold(0.0, f64::max);
|
||||
let delta = (size * 0.005).max(1e-3);
|
||||
let lp = r.outer.ccw();
|
||||
let lines: Option<Vec<(Vec2, Vec2)>> =
|
||||
lp.segs.iter().map(|s| if let solidcraft_geom::Seg2::Line { a, b } = *s { Some((a, b)) } else { None }).collect();
|
||||
let Some(lines) = lines else { return r.clone() };
|
||||
let segs = &lp.segs;
|
||||
if segs.is_empty() || !segs.iter().all(|s| matches!(s, Seg2::Line { .. } | Seg2::Arc { .. })) {
|
||||
return r.clone();
|
||||
}
|
||||
// The outward normal (away from the profile) at parameter `t` of a segment of the
|
||||
// counterclockwise loop: the tangent turned clockwise.
|
||||
let out_at = |s: &Seg2, t: f64| -> Option<Vec2> {
|
||||
match *s {
|
||||
Seg2::Line { a, b } => (b - a).normalized().map(|d| Vec2::new(d.y, -d.x)),
|
||||
Seg2::Arc { start, sweep, .. } => Some(Vec2::from_angle(start + sweep * t) * sweep.signum()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let inside_any = |p: Vec3| meshes.iter().any(|m| m.contains(p));
|
||||
let mut grow = vec![0.0; lines.len()];
|
||||
let mut grow = vec![0.0; segs.len()];
|
||||
let mut any = false;
|
||||
for (i, (a, b)) in lines.iter().enumerate() {
|
||||
let Some(dir) = (*b - *a).normalized() else { continue };
|
||||
let out = Vec2::new(dir.y, -dir.x);
|
||||
for (i, seg) in segs.iter().enumerate() {
|
||||
if let Seg2::Arc { radius, sweep, .. } = *seg
|
||||
&& sweep < 0.0
|
||||
&& radius <= 2.0 * delta
|
||||
{
|
||||
// A concave arc too small to shrink by the step.
|
||||
continue;
|
||||
}
|
||||
// Is the edge on a body face? Material on exactly one side of it.
|
||||
let mids = [a.lerp(*b, 0.25), a.lerp(*b, 0.5), a.lerp(*b, 0.75)];
|
||||
let side = |k: f64| mids.iter().flat_map(|m| probe(*m + out * (delta * k))).map(inside_any).collect::<Vec<bool>>();
|
||||
let mids: Vec<(Vec2, Vec2)> = [0.25, 0.5, 0.75].iter().filter_map(|t| Some((seg.point_at(*t), out_at(seg, *t)?))).collect();
|
||||
if mids.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let side = |k: f64| mids.iter().flat_map(|(m, out)| probe(*m + *out * (delta * k))).map(inside_any).collect::<Vec<bool>>();
|
||||
let (outer, inner) = (side(0.5), side(-0.5));
|
||||
let want_outer = op == Operation::Join;
|
||||
let on_face = outer.iter().all(|x| *x == want_outer) && inner.iter().any(|x| *x != want_outer);
|
||||
let room = side(1.0).iter().all(|x| *x == want_outer);
|
||||
if on_face && room {
|
||||
if let Some(g) = grow.get_mut(i) {
|
||||
*g = delta;
|
||||
}
|
||||
if on_face
|
||||
&& room
|
||||
&& let Some(g) = grow.get_mut(i)
|
||||
{
|
||||
*g = delta;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return r.clone();
|
||||
}
|
||||
// Offset the chosen lines and re-intersect neighbours.
|
||||
let n = lines.len();
|
||||
let shifted: Vec<(Vec2, Vec2)> = lines
|
||||
.iter()
|
||||
.zip(&grow)
|
||||
.map(|((a, b), g)| {
|
||||
let dir = (*b - *a).normalized().unwrap_or(Vec2::X);
|
||||
let out = Vec2::new(dir.y, -dir.x);
|
||||
(*a + out * *g, dir)
|
||||
})
|
||||
.collect();
|
||||
let mut pts = Vec::with_capacity(n);
|
||||
let n = segs.len();
|
||||
let tangent = |a: &Seg2, b: &Seg2| a.end_tangent().cross(b.start_tangent()).abs() < 1e-6 && a.end_tangent().dot(b.start_tangent()) > 0.0;
|
||||
// Where segment `i` starts once grown: the corner of it and the one before.
|
||||
let mut starts = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let (Some(&(p0, d0)), Some(&(p1, d1))) = (shifted.get((i + n - 1) % n), shifted.get(i)) else { return r.clone() };
|
||||
let (Some(prev), Some(cur), Some(&g0), Some(&g1)) = (segs.get((i + n - 1) % n), segs.get(i), grow.get((i + n - 1) % n), grow.get(i)) else {
|
||||
return r.clone();
|
||||
};
|
||||
let corner = cur.start();
|
||||
if g0 == 0.0 && g1 == 0.0 {
|
||||
starts.push(corner);
|
||||
continue;
|
||||
}
|
||||
if tangent(prev, cur) {
|
||||
// Moved together, a tangent joint moves along the common normal and stays tangent.
|
||||
if g0 != g1 {
|
||||
return r.clone();
|
||||
}
|
||||
let Some(out) = out_at(cur, 0.0) else { return r.clone() };
|
||||
starts.push(corner + out * g1);
|
||||
continue;
|
||||
}
|
||||
let (Seg2::Line { a: a0, b: b0 }, Seg2::Line { a: a1, b: b1 }) = (*prev, *cur) else { return r.clone() };
|
||||
let (Some(d0), Some(d1)) = ((b0 - a0).normalized(), (b1 - a1).normalized()) else { return r.clone() };
|
||||
let (p0, p1) = (a0 + Vec2::new(d0.y, -d0.x) * g0, a1 + Vec2::new(d1.y, -d1.x) * g1);
|
||||
let den = d0.cross(d1);
|
||||
if den.abs() < 1e-12 {
|
||||
pts.push(p1);
|
||||
starts.push(p1);
|
||||
continue;
|
||||
}
|
||||
let t = (p1 - p0).cross(d1) / den;
|
||||
pts.push(p0 + d0 * t);
|
||||
starts.push(p0 + d0 * t);
|
||||
}
|
||||
Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: r.holes.clone() }
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for (i, seg) in segs.iter().enumerate() {
|
||||
let (Some(&a), Some(&b), Some(&g)) = (starts.get(i), starts.get((i + 1) % n), grow.get(i)) else { return r.clone() };
|
||||
out.push(match *seg {
|
||||
Seg2::Line { .. } => Seg2::Line { a, b },
|
||||
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center, radius: radius + g * sweep.signum(), start, sweep },
|
||||
_ => return r.clone(),
|
||||
});
|
||||
}
|
||||
Region2 { outer: solidcraft_geom::Loop2 { segs: out }, holes: r.holes.clone() }
|
||||
}
|
||||
|
||||
/// Booleans struggle with coincident faces (a cut starting on the face it cuts, a boss
|
||||
|
||||
@@ -267,3 +267,24 @@ fn lip_swept_flush_round_a_corner_joins() {
|
||||
let volume = b.body.tessellate(1e-5).unwrap().measure().volume;
|
||||
assert!((volume - block - lip).abs() < 0.02, "{volume} expected {}", block + lip);
|
||||
}
|
||||
|
||||
/// #1275: an Extrude Cut of a whole face of a rounded-rectangle block with a bottom chamfer. The
|
||||
/// tool's corner cylinders lay on the block's; they are grown off them now, as straight edges
|
||||
/// were, and the cut takes 5 mm off the top.
|
||||
#[test]
|
||||
fn cut_of_a_whole_face_beside_round_corners_after_a_chamfer() {
|
||||
let s = replay(include_str!("../../../examples/regressions/cut_whole_face_after_bottom_chamfer.json"));
|
||||
let bodies = &s.model.state().bodies;
|
||||
assert_eq!(bodies.len(), 1);
|
||||
let b = &bodies[0];
|
||||
assert!(b.body.validity().is_empty(), "{:?}", b.body.validity());
|
||||
let pi = std::f64::consts::PI;
|
||||
let block = (40.0 * 40.0 - (4.0 - pi) * 8.0 * 8.0) * 15.0;
|
||||
// The chamfer's 2 × 2 triangle swept along the loop (Pappus: its centroid is 2/3 mm in).
|
||||
let chamfer = 2.0 * (4.0 * 24.0 + 2.0 * pi * (8.0 - 2.0 / 3.0));
|
||||
let mesh = b.body.tessellate(1e-4).unwrap();
|
||||
let volume = mesh.measure().volume;
|
||||
assert!((volume - (block - chamfer)).abs() < 0.05, "{volume} expected {}", block - chamfer);
|
||||
let top = mesh.bounds().max.z;
|
||||
assert!((top - 15.0).abs() < 1e-6, "top at {top}");
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"description": "A rounded-rectangle block (40 x 40, R8 corners, 20 high) with a 2 mm chamfer round its bottom loop, then an Extrude Cut of its whole top face 5 mm down. The tool's walls, its corner cylinders included, lay on the block's walls; only straight profile edges were grown off the walls, so the cut failed with 'no result' (#1275). Result: the block 15 high, less the chamfer.",
|
||||
"commands": [
|
||||
{"command": "sketch.create", "params": {"plane": "XY"}},
|
||||
{"command": "sketch.rectangle.rounded", "params": {"p0": [0, 0], "p1": [40, 40], "radius": 8}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": 20, "operation": "new"}},
|
||||
{"command": "solid.chamfer", "params": {"distance": 2, "edges": [[20, 0, 0], [40, 20, 0], [20, 40, 0], [0, 20, 0], [2.343, 2.343, 0], [37.657, 2.343, 0], [37.657, 37.657, 0], [2.343, 37.657, 0]]}},
|
||||
{"command": "solid.extrude", "params": {"face": [20, 20, 20], "distance": -5, "operation": "cut"}}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user