Sheet metal: a cut-out with a corner a hair beside a bend line refolds (#1095)
Edges shorter than the model tolerance are dropped from the flat cells, and the refold sews touching corners, so a cut-out across a bend line no longer fails with a degenerate edge.
This commit is contained in:
1 parent
c469adf456
commit
c3aae64a63
4 files changed
+433
-15
No files matched your search
+147
-4
@@ -1081,7 +1081,17 @@ impl SheetBody {
|
||||
if points > MAX_FLAT_POINTS || out.len() >= MAX_FLAT_PIECES {
|
||||
return Err(flat_budget_error());
|
||||
}
|
||||
out.push(Cell { piece, outer: region.outer.segs, holes: region.holes.into_iter().map(|l| l.segs).collect() });
|
||||
// A piece shorter than the model's tolerance is no edge: a cut-out's corner a hair
|
||||
// beside a bend line comes out of the booleans as corners a hair apart, on the
|
||||
// panel and on the bend, joined by such an edge or by a loop of them on its own.
|
||||
// The sew welds the corners left on either side.
|
||||
let hairless = |l: Loop2| -> Vec<Seg2> { l.segs.into_iter().filter(|e| e.length() >= LINEAR_TOL).collect() };
|
||||
let outer = hairless(region.outer);
|
||||
if outer.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let holes = region.holes.into_iter().map(hairless).filter(|h| !h.is_empty()).collect();
|
||||
out.push(Cell { piece, outer, holes });
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
@@ -1264,7 +1274,7 @@ impl SheetBody {
|
||||
// origin. Fitting planes after a distant component transfer loses precision.
|
||||
let mut local = self.clone();
|
||||
local.frame = Plane::XY;
|
||||
let body = kernel::sew(&local.faces(&cells, 0.0, self.t, &mut work)?)?;
|
||||
let body = kernel::sew_touching(&local.faces(&cells, 0.0, self.t, &mut work)?)?;
|
||||
let body = local.machine_pockets(body, &mut work)?;
|
||||
let body = local.machine_bores(body, &mut work)?;
|
||||
self.place_local_solid(body)
|
||||
@@ -1434,7 +1444,7 @@ impl SheetBody {
|
||||
})?;
|
||||
kernel::extrude_planar_regions(&self.frame, &[region], 0.0, self.t)?
|
||||
} else {
|
||||
kernel::sew(&self.faces(std::slice::from_ref(&cell), 0.0, self.t, &mut work)?)?
|
||||
kernel::sew_touching(&self.faces(std::slice::from_ref(&cell), 0.0, self.t, &mut work)?)?
|
||||
};
|
||||
if kernel::boolean(&solid, &drill, kernel::BoolOp::Intersect)?.is_none() {
|
||||
continue;
|
||||
@@ -1538,7 +1548,7 @@ impl SheetBody {
|
||||
Piece::Panel(pi) => apply_vector(&self.world(pi), Vec3::Z).cross(plane.normal()).len() > 1e-6,
|
||||
})
|
||||
.collect();
|
||||
cells.iter().map(|c| Ok(kernel::sew(&self.faces(std::slice::from_ref(c), 0.0, self.t, &mut work)?)?)).collect()
|
||||
cells.iter().map(|c| Ok(kernel::sew_touching(&self.faces(std::slice::from_ref(c), 0.0, self.t, &mut work)?)?)).collect()
|
||||
}
|
||||
|
||||
fn through_pocket_regions(&self, work: &mut FlatWork) -> Result<Vec<Region2>> {
|
||||
@@ -4746,6 +4756,42 @@ mod tests {
|
||||
s.flat_regions().unwrap().iter().map(Region2::area).sum()
|
||||
}
|
||||
|
||||
/// A plate (0, 0) (100, 0) (85, 20) (0, 20) with a 30 mm inside flange on its slanted edge
|
||||
/// (2 mm sheet, R 2, K 0.5: the folded sheet holds its flat area times the thickness), and a
|
||||
/// 4 mm² triangle cut out of the flat across the bend line, with a corner on the line or a
|
||||
/// hair (0.01 to 0.1 µm) to either side of it, in the middle of the bend and near its ends.
|
||||
/// The planar booleans leave such a corner as corners a hair apart on the base and on the
|
||||
/// bend, and the refold joined them by an edge as short ("sew: a degenerate edge").
|
||||
#[test]
|
||||
fn a_cut_out_corner_a_hair_beside_a_bend_line_refolds() {
|
||||
let rule = RuleValues { bend_radius: 2.0, ..alu() };
|
||||
let outline = [Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0), Vec2::new(85.0, 20.0), Vec2::new(0.0, 20.0)];
|
||||
for deg in [90.0f64, 120.0] {
|
||||
let mut s = base_flange("Body1", "AL2", &rule, Plane::XY, Region2 { outer: Loop2::polygon(&outline).ccw(), holes: vec![] });
|
||||
s.add_flange(Vec3::new(92.5, 10.0, 2.0), 30.0, deg.to_radians(), 2.0, "inside", false).unwrap();
|
||||
let (a0, v0) = (flat_area(&s), vol(&s.solid().unwrap()));
|
||||
if deg == 90.0 {
|
||||
// The base less 4 mm along the edge's normal (5 mm along x): 1750; the bend
|
||||
// 25 x 3π/2; the flange 25 x 26.
|
||||
assert!((a0 - (2400.0 + 37.5 * std::f64::consts::PI)).abs() < 1e-9, "{a0}");
|
||||
}
|
||||
// The measure follows the bend by facets.
|
||||
assert!((v0 / (a0 * 2.0) - 1.0).abs() < 1e-4, "{deg}°: {v0} vs {}", a0 * 2.0);
|
||||
let f = s.flanges[0].clone();
|
||||
for (along, off) in [(0.5, 0.0), (0.5, 1e-8), (0.5, 5e-8), (0.5, -5e-8), (0.5, 1e-7), (0.5, -1e-7), (0.16, 5e-8), (0.84, -5e-8)] {
|
||||
let what = format!("{deg}°, corner {along} along the bend, {off} mm beside it");
|
||||
let c = f.p + f.d * (f.len * along) + f.n * off;
|
||||
let triangle = Loop2::polygon(&[c, c - f.n * 2.8 - f.d * 0.4, c + f.n * 0.4 - f.d * 2.8]).ccw();
|
||||
let mut cut = s.clone();
|
||||
cut.holes.push(Region2 { outer: triangle, holes: vec![] });
|
||||
assert!((a0 - flat_area(&cut) - 4.0).abs() < 1e-6, "{what}: flat area {}", a0 - flat_area(&cut));
|
||||
let body = cut.solid().unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert!(body.validity().is_empty(), "{what}: {:?}", body.validity());
|
||||
assert!((v0 - vol(&body) - 8.0).abs() < 0.01, "{what}: {} vs 8", v0 - vol(&body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance between two parallel 3D lines through a0→a1 and b0→b1 (None if not parallel).
|
||||
fn parallel_gap(a0: Vec3, a1: Vec3, b0: Vec3, b1: Vec3) -> Option<f64> {
|
||||
let (u, v) = ((a1 - a0).normalized()?, (b1 - b0).normalized()?);
|
||||
@@ -6904,6 +6950,103 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The plate (0, 0) (100, 0) (85, 20) (0, 20) with a 30 mm inside flange on its slanted edge
|
||||
/// (2 mm sheet, R 2, K 0.5: the folded sheet holds its flat area times the thickness), and a
|
||||
/// triangle cut out of the flat with one corner on the bend line: where the line meets the
|
||||
/// plate's edge, at either end, the triangle on the bend or on the base; in the line's middle,
|
||||
/// from either side; and on the plate's straight edge, from inside. At the plate's edge the
|
||||
/// sheet is pinched (two wedges of material meet along the thickness); in the middle the
|
||||
/// triangle's tip touches the next piece. The refold ran the face through that corner twice
|
||||
/// ("This wire is not simple").
|
||||
#[test]
|
||||
fn a_cut_out_with_a_corner_on_a_bend_line_or_the_sheets_edge_refolds() {
|
||||
let rule = RuleValues { bend_radius: 2.0, ..alu() };
|
||||
let outline = [Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0), Vec2::new(85.0, 20.0), Vec2::new(0.0, 20.0)];
|
||||
for deg in [90.0f64, 120.0] {
|
||||
let mut s = base_flange("Body1", "AL2", &rule, Plane::XY, Region2 { outer: Loop2::polygon(&outline).ccw(), holes: vec![] });
|
||||
s.add_flange(Vec3::new(92.5, 10.0, 2.0), 30.0, deg.to_radians(), 2.0, "inside", false).unwrap();
|
||||
let (a0, v0) = (flat_area(&s), vol(&s.solid().unwrap()));
|
||||
let f = s.flanges[0].clone();
|
||||
let (start, end, middle) = (f.p, f.p + f.d * f.len, f.p + f.d * (f.len / 2.0));
|
||||
// The bend line runs from the plate's edge y = 0 to y = 20, the bend on its +n side,
|
||||
// square across at its ends; the base's corner at either end is at least 53.13°
|
||||
// wide, the edge running off at (0.6, -0.8) along and across the line. The triangles'
|
||||
// other corners as steps along the line and across it.
|
||||
let cases = [
|
||||
("on the bend at the line's start", start, [(2.0, 1.0), (0.4, 2.2)], 2.0),
|
||||
("on the bend at the line's end", end, [(-2.0, 1.0), (-0.4, 2.2)], 2.0),
|
||||
("on the base at the line's start", start, [(4.0, -1.0), (2.0, -2.0)], 3.0),
|
||||
("on the base at the line's middle", middle, [(-1.0, -2.0), (1.0, -2.0)], 2.0),
|
||||
("on the bend at the line's middle", middle, [(-1.0, 2.0), (1.0, 2.0)], 2.0),
|
||||
];
|
||||
for (what, c, others, area) in cases {
|
||||
let what = format!("{deg}°, {what}");
|
||||
let corners: Vec<Vec2> = std::iter::once(c).chain(others.iter().map(|(u, v)| c + f.d * *u + f.n * *v)).collect();
|
||||
let mut cut = s.clone();
|
||||
cut.holes.push(Region2 { outer: Loop2::polygon(&corners).ccw(), holes: vec![] });
|
||||
assert!((a0 - flat_area(&cut) - area).abs() < 1e-6, "{what}: flat area {}", a0 - flat_area(&cut));
|
||||
let body = cut.solid().unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert!(body.validity().is_empty() && body.shell_count() == 1, "{what}: {:?}", body.validity());
|
||||
assert!((v0 - vol(&body) - area * 2.0).abs() < 0.01, "{what}: {} vs {}", v0 - vol(&body), area * 2.0);
|
||||
}
|
||||
// A 4 mm² triangle standing on the plate's edge y = 20 at x = 40.
|
||||
let mut cut = s.clone();
|
||||
cut.holes
|
||||
.push(Region2 { outer: Loop2::polygon(&[Vec2::new(40.0, 20.0), Vec2::new(38.0, 18.0), Vec2::new(42.0, 18.0)]).ccw(), holes: vec![] });
|
||||
assert!((a0 - flat_area(&cut) - 4.0).abs() < 1e-6, "{deg}°, on the edge: flat area {}", a0 - flat_area(&cut));
|
||||
let body = cut.solid().unwrap_or_else(|e| panic!("{deg}°, on the edge: {e}"));
|
||||
assert!(body.validity().is_empty() && body.shell_count() == 1, "{deg}°, on the edge: {:?}", body.validity());
|
||||
assert!((v0 - vol(&body) - 8.0).abs() < 0.01, "{deg}°, on the edge: {} vs 8", v0 - vol(&body));
|
||||
}
|
||||
}
|
||||
|
||||
/// The plate (0, 0) (100, 0) (85, 20) (0, 20) with a 30 mm inside flange on its slanted edge
|
||||
/// (2 mm sheet, R 2, K 0.5: the folded sheet holds its flat area times the thickness), and a
|
||||
/// triangle cut out of the flat on one side of the bend line only, with a corner a hair (0.01
|
||||
/// or 0.05 µm) from the line, short of it or across: on the base, in the line's middle and
|
||||
/// near its ends; and at either end of the line, where it meets the plate's edge, on the base
|
||||
/// and on the bend. The planar booleans leave such a corner as corners a hair apart, on the
|
||||
/// base and on the bend; joined, they are a corner where the cut-out touches the bend line or
|
||||
/// the plate's edge. So the refold needs both: without the joining it failed with "sew: a
|
||||
/// degenerate edge", without the touching sew with "This wire is not simple".
|
||||
#[test]
|
||||
fn a_cut_out_on_one_side_of_a_bend_line_with_a_corner_a_hair_from_it_refolds() {
|
||||
let rule = RuleValues { bend_radius: 2.0, ..alu() };
|
||||
let outline = [Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0), Vec2::new(85.0, 20.0), Vec2::new(0.0, 20.0)];
|
||||
for deg in [90.0f64, 120.0] {
|
||||
let mut s = base_flange("Body1", "AL2", &rule, Plane::XY, Region2 { outer: Loop2::polygon(&outline).ccw(), holes: vec![] });
|
||||
s.add_flange(Vec3::new(92.5, 10.0, 2.0), 30.0, deg.to_radians(), 2.0, "inside", false).unwrap();
|
||||
let (a0, v0) = (flat_area(&s), vol(&s.solid().unwrap()));
|
||||
let f = s.flanges[0].clone();
|
||||
// The corner at a fraction of the line, the triangle's other corners as steps along the
|
||||
// line and across it (the bend on its +n side; the base's corner at either end of the
|
||||
// line is at least 53.13° wide), and the triangle's area.
|
||||
let cases = [
|
||||
("on the base in the line's middle", 0.5, [(-1.0, -2.0), (1.0, -2.0)], 2.0),
|
||||
("on the base near the line's start", 0.16, [(-1.0, -2.0), (1.0, -2.0)], 2.0),
|
||||
("on the base near the line's end", 0.84, [(-1.0, -2.0), (1.0, -2.0)], 2.0),
|
||||
("on the base at the line's start", 0.0, [(3.0, -2.0), (2.0, -2.0)], 1.0),
|
||||
("on the base at the line's end", 1.0, [(-2.0, -2.0), (-3.0, -2.0)], 1.0),
|
||||
("on the bend at the line's start", 0.0, [(1.25, 2.0), (0.25, 2.0)], 1.0),
|
||||
("on the bend at the line's end", 1.0, [(-0.25, 2.0), (-1.25, 2.0)], 1.0),
|
||||
];
|
||||
for (what, along, others, area) in cases {
|
||||
// Across the line toward the bend.
|
||||
for off in [-5e-8, -1e-8, 1e-8, 5e-8] {
|
||||
let what = format!("{deg}°, {what}, {off} mm across the line");
|
||||
let c = f.p + f.d * (f.len * along) + f.n * off;
|
||||
let corners: Vec<Vec2> = std::iter::once(c).chain(others.iter().map(|(u, v)| c + f.d * *u + f.n * *v)).collect();
|
||||
let mut cut = s.clone();
|
||||
cut.holes.push(Region2 { outer: Loop2::polygon(&corners).ccw(), holes: vec![] });
|
||||
assert!((a0 - flat_area(&cut) - area).abs() < 1e-5, "{what}: flat area {}", a0 - flat_area(&cut));
|
||||
let body = cut.solid().unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert!(body.validity().is_empty() && body.shell_count() == 1, "{what}: {:?}", body.validity());
|
||||
assert!((v0 - vol(&body) - area * 2.0).abs() < 0.01, "{what}: {} vs {}", v0 - vol(&body), area * 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tear_holds_at_every_angle() {
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
@@ -507,6 +507,139 @@ fn unfold_cut_refold_and_convert() {
|
||||
assert!(t.execute("sheet.convert", &json!({"body": "Block", "face": [50, 30, 2.5]})).is_err());
|
||||
}
|
||||
|
||||
/// A plate (15, -20) (0, 0) (-100, 0) (-100, -20) with a 20 mm inside flange on its slanted edge
|
||||
/// (2 mm sheet, R 2, K 0.5: the folded sheet holds its flat area times the thickness), unfolded,
|
||||
/// and a triangle cut out with a corner on the bend line: where the line ends on the plate's
|
||||
/// edge at (-5, 0), the triangle on the bend or on the base, or in the line's middle. The refold
|
||||
/// failed ("sew: face 10: This wire is not simple.").
|
||||
#[test]
|
||||
fn a_cut_out_with_a_corner_on_a_bend_line_refolds() {
|
||||
// The base (10, -20) (-5, 0) (-100, 0) (-100, -20): 2050; the bend 25 x 3π/2; the flange 25 x 16.
|
||||
let area = 2450.0 + 37.5 * PI;
|
||||
let cases = [
|
||||
([[-5.0, 0.0], [-3.0, -1.0], [-3.0, 1.0]], 2.0),
|
||||
([[-5.0, 0.0], [-7.0, -1.0], [-6.0, -3.0]], 2.5),
|
||||
([[2.5, -10.0], [2.1, -12.8], [-0.3, -9.6]], 4.0),
|
||||
([[2.5, -10.0], [4.7, -9.6], [3.5, -8.0]], 2.0),
|
||||
];
|
||||
for (triangle, cut) in cases {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "Rule", "active": true, "thickness": "2 mm", "k_factor": "0.5", "bend_radius": "2 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Outline"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[15, -20], [0, 0], [-100, 0], [-100, -20]], "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Outline", "rule": "Rule"}));
|
||||
run(&mut s, "sheet.flange", json!({"edges": [[7.5, -10, 2]], "height": 20, "radius": 2, "position": "inside"}));
|
||||
// The measure follows the bend by facets.
|
||||
let folded = volume(&mut s);
|
||||
assert!(rel(folded, area * 2.0) < 1e-4, "{folded} vs {}", area * 2.0);
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Cutout"}));
|
||||
run(&mut s, "sketch.line", json!({"points": triangle, "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Cutout", "through_all": true, "direction": "symmetric", "operation": "cut"}));
|
||||
assert!(rel(volume(&mut s), (area - cut) * 2.0) < 1e-9, "{triangle:?} flat: {}", volume(&mut s));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
let refolded = volume(&mut s);
|
||||
assert!((folded - refolded - cut * 2.0).abs() < 0.01, "{triangle:?} refolded: {refolded}, {} less", folded - refolded);
|
||||
}
|
||||
}
|
||||
|
||||
/// Plates with an inside flange on a slanted edge (2 mm sheet, R 2, K 0.5: the folded sheet
|
||||
/// holds its flat area times the thickness), unfolded, and a triangle cut out across the bend
|
||||
/// line with a corner 0.05 µm beside the line, or on it at x = 84.0764335, a border between
|
||||
/// two cells of the 1e-6 mm grid the sew finds vertices on. The refold failed ("sew: a
|
||||
/// degenerate edge", "sew: face 13: This wire is not closed."); it loses the triangle.
|
||||
#[test]
|
||||
fn a_cut_corner_a_hair_from_a_bend_line_refolds() {
|
||||
let root820 = 820f64.sqrt();
|
||||
let cases = [
|
||||
// Base 1750, bend 25 x 3π/2, flange 25 x 26; a triangle of 4 mm².
|
||||
(
|
||||
[[0.0, 0.0], [100.0, 0.0], [85.0, 20.0], [0.0, 20.0]],
|
||||
[92.5, 10.0, 2.0],
|
||||
30.0,
|
||||
[[87.49999996, 9.99999997], [85.49999996, 7.99999997], [89.49999996, 7.99999997]],
|
||||
2400.0 + 37.5 * PI,
|
||||
4.0,
|
||||
),
|
||||
// The edge is √820 long; the base loses 4 mm along its normal: 2444 − 4√820; the bend
|
||||
// √820 x 3π/2, the flange √820 x 36; a triangle of 3 mm².
|
||||
(
|
||||
[[0.0, 0.0], [100.0, 0.0], [88.0, 26.0], [0.0, 26.0]],
|
||||
[94.0, 13.0, 2.0],
|
||||
40.0,
|
||||
[[84.0764335, 24.9558467078], [82.0764335, 22.9558467078], [86.0764335, 23.9558467078]],
|
||||
2444.0 + root820 * (32.0 + 3.0 * PI / 2.0),
|
||||
3.0,
|
||||
),
|
||||
];
|
||||
for (outline, edge, height, triangle, area, cut) in cases {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "Rule", "active": true, "thickness": "2 mm", "k_factor": "0.5", "bend_radius": "2 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Outline"}));
|
||||
run(&mut s, "sketch.line", json!({"points": outline, "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Outline", "rule": "Rule"}));
|
||||
run(&mut s, "sheet.flange", json!({"edges": [edge], "height": height, "radius": 2, "position": "inside"}));
|
||||
// The measure follows the bend by facets.
|
||||
let folded = volume(&mut s);
|
||||
assert!(rel(folded, area * 2.0) < 1e-4, "{folded} vs {}", area * 2.0);
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Cutout"}));
|
||||
run(&mut s, "sketch.line", json!({"points": triangle, "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Cutout", "through_all": true, "direction": "symmetric", "operation": "cut"}));
|
||||
assert!(rel(volume(&mut s), (area - cut) * 2.0) < 1e-9, "flat: {}", volume(&mut s));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
let refolded = volume(&mut s);
|
||||
assert!((folded - refolded - cut * 2.0).abs() < 0.01, "refolded: {refolded}, {} less", folded - refolded);
|
||||
}
|
||||
}
|
||||
|
||||
/// A 100 x 60 plate (2 mm sheet, R 2, K 0.5) with a 42 mm flange (R 1) outside its bottom edge,
|
||||
/// unfolded, and a notch cut off the flat flange's free corner: an arc from the side edge,
|
||||
/// through (0.6, 0.6) from the corner, to the free edge, closed by lines through a point outside
|
||||
/// the sheet. The arc leaves the side edge 1.7997505 mm from the corner, in the folded flange at
|
||||
/// a height of 42 − 1.7997505 = 40.2002495 mm: a border between two cells of the 1e-6 mm grid the
|
||||
/// sew finds vertices on, and the arc's end and the edge's, worked out apart, differ by 7e-13
|
||||
/// mm. The refold failed ("sew: face 5: This wire is not closed."); it loses the notch.
|
||||
#[test]
|
||||
fn a_notch_whose_arc_ends_on_a_grid_border_refolds() {
|
||||
let t = 1.7997505;
|
||||
// The flat flange ends at 39 + the bend allowance (π/2)(R + K T) = 39 + π below the plate.
|
||||
let free = -(42.0 - 1.0 - 2.0 + FRAC_PI_2 * (1.0 + 0.5 * 2.0));
|
||||
let flat = 6000.0 + 100.0 * PI + 100.0 * 39.0;
|
||||
// The notch is the triangle corner, end, end less the circle's segment over its chord; the
|
||||
// circle's centre (a, a) from the corner is as far from (0.6, 0.6) as from the arc's ends.
|
||||
let a = (t * t - 0.72) / (2.0 * t - 2.4);
|
||||
let r2 = 2.0 * (a - 0.6) * (a - 0.6);
|
||||
let angle = 2.0 * (t * 2f64.sqrt() / (2.0 * r2.sqrt())).asin();
|
||||
let notch = t * t / 2.0 - r2 / 2.0 * (angle - angle.sin());
|
||||
assert!((notch - 0.88421).abs() < 1e-5, "{notch}");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "Rule", "active": true, "thickness": "2 mm", "k_factor": "0.5", "bend_radius": "2 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Outline"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [100, 0], [100, 60], [0, 60]], "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Outline", "rule": "Rule"}));
|
||||
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 42, "radius": 1, "position": "outside"}));
|
||||
// The measure follows the bend by facets.
|
||||
let folded = volume(&mut s);
|
||||
assert!(rel(folded, flat * 2.0) < 1e-4, "{folded} vs {}", flat * 2.0);
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Cutout"}));
|
||||
run(&mut s, "sketch.arc.three_point", json!({"start": [t, free], "through": [0.6, free + 0.6], "end": [0.0, free + t]}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[0.0, free + t], [-1.0, -44.0]]}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[-1.0, -44.0], [t, free]]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Cutout", "through_all": true, "direction": "symmetric", "operation": "cut"}));
|
||||
assert!(rel(volume(&mut s), (flat - notch) * 2.0) < 1e-6, "flat: {}", volume(&mut s));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
let refolded = volume(&mut s);
|
||||
assert!((folded - refolded - notch * 2.0).abs() < 0.01, "refolded: {refolded}, {} less", folded - refolded);
|
||||
}
|
||||
|
||||
/// A sketch line across a 100 x 60 plate's top face at x = 40.
|
||||
fn fold_line(s: &mut Session) {
|
||||
run(s, "sketch.create", json!({"plane": "XY", "offset": 2.5, "name": "FoldLine"}));
|
||||
|
||||
@@ -122,7 +122,7 @@ pub use polybool::{faceted_boolean, planar_boolean};
|
||||
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, offset_faces, shell};
|
||||
pub use prism::{PLANAR_FIT_TOL, extrude_planar_regions, planar_regions_boolean};
|
||||
pub use prism_measure::certified_prism_equivalent;
|
||||
pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew};
|
||||
pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew, sew_touching};
|
||||
pub use splitface::{SplitTool, split_body, split_faces, split_faces_with_map};
|
||||
pub use step::step_export;
|
||||
pub use step_in::{ImportedBody, StepImport, StepNode, step_cache_alive, step_import, step_import_shared, step_orientation_errors, step_validate};
|
||||
|
||||
+152
-10
@@ -47,6 +47,24 @@ impl EdgeSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/// The way the edge leaves its start (`None` when that cannot be told).
|
||||
fn start_dir(&self) -> Option<Vec3> {
|
||||
let d = match self {
|
||||
EdgeSpec::Line { a, b } => *b - *a,
|
||||
// The circle through a, mid and b, inverted about a, is the line through the images
|
||||
// of mid and b, which runs along the circle's tangent at a.
|
||||
EdgeSpec::Arc { a, b, mid } => {
|
||||
let (m, e) = (*mid - *a, *b - *a);
|
||||
m * (1.0 / m.len2()) - e * (1.0 / e.len2())
|
||||
}
|
||||
EdgeSpec::Chain { ctrl } => {
|
||||
let first = *ctrl.first()?;
|
||||
ctrl.iter().skip(1).map(|p| *p - first).find(|d| d.len() > 1e-12)?
|
||||
}
|
||||
};
|
||||
d.normalized()
|
||||
}
|
||||
|
||||
/// The same edge, run the other way.
|
||||
pub fn reversed(&self) -> EdgeSpec {
|
||||
match self {
|
||||
@@ -108,6 +126,81 @@ fn key(p: Vec3) -> Key {
|
||||
(q(p.x), q(p.y), q(p.z))
|
||||
}
|
||||
|
||||
/// How near (mm) two edge ends weld into one vertex wherever they fall on the grid of [`key`].
|
||||
/// The grid alone joins the ends in one of its cells, but not two ends a hair apart on either
|
||||
/// side of a cell's border: a corner at x = 84.0764335 lies on a border, and the same corner
|
||||
/// worked out again through a bend can land 1e-14 mm to the other side, leaving its face's loop
|
||||
/// open. The planar booleans close loops over gaps up to 1e-6 mm, so the weld reaches as far.
|
||||
const WELD: f64 = 1e-6;
|
||||
|
||||
/// The faces with every edge end within [`WELD`] of an end met earlier in a neighbouring cell of
|
||||
/// the grid moved onto that end, so that [`key`] finds one vertex for both; `None` when no end
|
||||
/// moves. Ends in one cell stay as they are (the grid already joins them).
|
||||
fn welded(faces: &[FaceSpec]) -> Option<Vec<FaceSpec>> {
|
||||
// The end each cell's ends weld to: the first met in it, or a nearer one in a neighbour.
|
||||
let mut at: HashMap<Key, Vec3> = HashMap::new();
|
||||
let mut moves = false;
|
||||
for f in faces {
|
||||
// A ruled surface's chains end on the vertices of its boundary.
|
||||
let ruled: Vec<&EdgeSpec> = match &f.surface {
|
||||
SurfSpec::Ruled { a, b } => vec![a, b],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for e in f.loops.iter().flatten().chain(ruled) {
|
||||
let (a, b) = e.ends();
|
||||
for p in [a, b] {
|
||||
let k = key(p);
|
||||
if !p.is_finite() || at.contains_key(&k) {
|
||||
continue;
|
||||
}
|
||||
let mut near = None;
|
||||
for (dx, dy, dz) in (-1i64..=1).flat_map(|x| (-1i64..=1).flat_map(move |y| (-1i64..=1).map(move |z| (x, y, z)))) {
|
||||
let n = (k.0.saturating_add(dx), k.1.saturating_add(dy), k.2.saturating_add(dz));
|
||||
if n != k
|
||||
&& let Some(q) = at.get(&n).filter(|q| q.dist(p) < WELD)
|
||||
{
|
||||
near = Some(*q);
|
||||
break;
|
||||
}
|
||||
}
|
||||
moves |= near.is_some();
|
||||
at.insert(k, near.unwrap_or(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !moves {
|
||||
return None;
|
||||
}
|
||||
let weld = |p: Vec3| if p.is_finite() { at.get(&key(p)).copied().filter(|q| key(*q) != key(p)).unwrap_or(p) } else { p };
|
||||
let edge = |e: &EdgeSpec| match e {
|
||||
EdgeSpec::Line { a, b } => EdgeSpec::Line { a: weld(*a), b: weld(*b) },
|
||||
EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: weld(*a), b: weld(*b), mid: *mid },
|
||||
EdgeSpec::Chain { ctrl } => {
|
||||
let mut ctrl = ctrl.to_vec();
|
||||
let n = ctrl.len();
|
||||
for i in [0, n.saturating_sub(1)] {
|
||||
if let Some(p) = ctrl.get_mut(i) {
|
||||
*p = weld(*p);
|
||||
}
|
||||
}
|
||||
EdgeSpec::Chain { ctrl: ctrl.into() }
|
||||
}
|
||||
};
|
||||
Some(
|
||||
faces
|
||||
.iter()
|
||||
.map(|f| FaceSpec {
|
||||
loops: f.loops.iter().map(|l| l.iter().map(edge).collect()).collect(),
|
||||
surface: match &f.surface {
|
||||
SurfSpec::Ruled { a, b } => SurfSpec::Ruled { a: edge(a), b: edge(b) },
|
||||
s => s.clone(),
|
||||
},
|
||||
probe: f.probe,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Cache {
|
||||
/// Vertices by point and fan (see [`corner_fans`]).
|
||||
@@ -172,6 +265,14 @@ pub fn sew(faces: &[FaceSpec]) -> Result<Body> {
|
||||
sew_impl(faces, false, None).map(|(body, _)| body)
|
||||
}
|
||||
|
||||
/// Sew the faces into a closed solid that may touch itself: where four faces meet along a
|
||||
/// straight edge (a cut-out's corner on the outline of a plate), each two of them get an edge of
|
||||
/// their own (see [`touching_sheets`]), and where faces meet at a point in fans joined by no edge
|
||||
/// (a hole's corner on its face's outline), each fan gets a vertex of its own.
|
||||
pub fn sew_touching(faces: &[FaceSpec]) -> Result<Body> {
|
||||
sew_impl(faces, true, None).map(|(body, _)| body)
|
||||
}
|
||||
|
||||
/// Separate boundary sheets at a shared geometric edge without moving it.
|
||||
/// Optional transient sources follow only unchanged produced face identities.
|
||||
/// Also reports the face each face of the body was built from (by its place in `faces`; `None`
|
||||
@@ -191,8 +292,8 @@ const SAME_WAY: f64 = 1e-6;
|
||||
|
||||
/// Where more than two faces meet along a straight edge (bodies touching along a line, or a wall
|
||||
/// of no thickness), which faces bound each sheet of it (each sheet gets its own edge), and the
|
||||
/// vertex fans that follow from that (see [`corner_fans`]). Only edges on planes and on the walls
|
||||
/// of extruded arcs are looked at.
|
||||
/// vertex fans that follow from that (see [`corner_fans`]). Only edges on planes, on the walls
|
||||
/// of extruded arcs and on ruled walls are looked at.
|
||||
///
|
||||
/// Each sheet bounds a wedge of material, so that pieces of material meeting along an edge stay
|
||||
/// apart, except where that would make a face touch itself: a hole touching the outline from
|
||||
@@ -204,7 +305,7 @@ const SAME_WAY: f64 = 1e-6;
|
||||
fn touching_sheets(faces: &[FaceSpec]) -> Result<(HashMap<EdgeUse, usize>, HashMap<Corner, usize>)> {
|
||||
let mut uses: HashMap<(Key, Key), Vec<(EdgeUse, Vec3, Vec3)>> = HashMap::new();
|
||||
for (fi, face) in faces.iter().enumerate() {
|
||||
if !matches!(face.surface, SurfSpec::Plane | SurfSpec::Extruded { .. }) {
|
||||
if !matches!(face.surface, SurfSpec::Plane | SurfSpec::Extruded { .. } | SurfSpec::Ruled { .. }) {
|
||||
continue;
|
||||
}
|
||||
for (li, lp) in face.loops.iter().enumerate() {
|
||||
@@ -289,7 +390,7 @@ fn edge_sheets(faces: &[FaceSpec], group: &[(EdgeUse, Vec3, Vec3)]) -> Result<[[
|
||||
let mut reference = None;
|
||||
for (u, a, b) in group {
|
||||
let face = faces.get(u.0).ok_or_else(|| fail("edge contact"))?;
|
||||
let (into, turn, ccw) = leaving(face, *a, *b, t).ok_or_else(|| fail("unsupported edge contact"))?;
|
||||
let (into, turn, ccw) = leaving(face, *u, *a, *b, t).ok_or_else(|| fail("unsupported edge contact"))?;
|
||||
let r: Vec3 = *reference.get_or_insert(into);
|
||||
let angle = r.cross(into).dot(t).atan2(r.dot(into)).rem_euclid(std::f64::consts::TAU);
|
||||
rays.push((*u, key(*a), turn, ccw, if angle > std::f64::consts::TAU - SAME_WAY { 0.0 } else { angle }));
|
||||
@@ -328,12 +429,12 @@ fn edge_sheets(faces: &[FaceSpec], group: &[(EdgeUse, Vec3, Vec3)]) -> Result<[[
|
||||
Ok([pairs(true)?, pairs(false)?])
|
||||
}
|
||||
|
||||
/// How a face leaves the straight edge `a`→`b` it bounds, seen along the edge's direction `t`:
|
||||
/// the way into the face square to the edge, how fast it turns from that way (its curvature,
|
||||
/// positive counter-clockwise about `t`; 0 for a plane), and whether its material is on its
|
||||
/// counter-clockwise side. None for other faces, and for an extruded wall the edge is not a
|
||||
/// ruling of.
|
||||
fn leaving(face: &FaceSpec, a: Vec3, b: Vec3, t: Vec3) -> Option<(Vec3, f64, bool)> {
|
||||
/// How a face leaves the straight edge `a`→`b` it bounds (its use `u`), seen along the edge's
|
||||
/// direction `t`: the way into the face square to the edge, how fast it turns from that way (its
|
||||
/// curvature, positive counter-clockwise about `t`; 0 for a plane or a ruled wall), and whether
|
||||
/// its material is on its counter-clockwise side. None for other faces, and for an extruded wall
|
||||
/// the edge is not a ruling of.
|
||||
fn leaving(face: &FaceSpec, u: EdgeUse, a: Vec3, b: Vec3, t: Vec3) -> Option<(Vec3, f64, bool)> {
|
||||
let along = (b - a).normalized()?;
|
||||
// The outward normal along the edge, and the centre of curvature (with its curvature).
|
||||
let (outward, centre) = match &face.surface {
|
||||
@@ -353,6 +454,15 @@ fn leaving(face: &FaceSpec, a: Vec3, b: Vec3, t: Vec3) -> Option<(Vec3, f64, boo
|
||||
let side = if radial(p).dot(out) < 0.0 { -1.0 } else { 1.0 };
|
||||
(r.normalized()? * side, Some((centre, 1.0 / r.len())))
|
||||
}
|
||||
// A ruled wall (a cut-out's wall across a bend, ruled between its traces on the bend's
|
||||
// two faces), standing on the edge: its normal there is square to the edge and to the way
|
||||
// the next edge leaves the edge's end into the wall, as a wall's top or bottom edge leaves
|
||||
// its upright one.
|
||||
SurfSpec::Ruled { .. } => {
|
||||
let lp = face.loops.get(u.1)?;
|
||||
let v = lp.get((u.2 + 1) % lp.len())?.start_dir()?;
|
||||
(along.cross(v - along * v.dot(along)).normalized()?, None)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
// A face's loops run counter-clockwise about its outward normal: it lies on their left.
|
||||
@@ -650,6 +760,12 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option<crate::
|
||||
if edge_uses > 500_000 {
|
||||
return Err(KernelError::Invalid("sew: edge budget exceeded".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let welded = welded(faces);
|
||||
let faces = welded.as_deref().unwrap_or(faces);
|
||||
for f in faces {
|
||||
for l in &f.loops {
|
||||
for e in l {
|
||||
let (a, b) = e.ends();
|
||||
// Ends that weld to one vertex would make truck panic on a zero-length edge (#268).
|
||||
@@ -937,6 +1053,32 @@ mod tests {
|
||||
assert!(sew(&faces[..5]).is_err());
|
||||
}
|
||||
|
||||
/// A 2.0000005 x 3 x 4 box: x = 2.0000005 mm is a border between two cells of the 1e-6 mm
|
||||
/// grid vertices are found on. The bottom face's edges meet at its corner (x, 3, 0) 2e-9 mm
|
||||
/// to either side of the border, as an arc's end and the end of the line it meets can: the
|
||||
/// ends weld and the box closes. Ends 3e-6 mm apart stay apart.
|
||||
#[test]
|
||||
fn edge_ends_a_hair_apart_across_a_grid_border_weld() {
|
||||
let x = 2.0000005;
|
||||
let p = |x: f64, y: f64, z: f64| Vec3::new(x, y, z);
|
||||
let (a, b, c, d) = (p(0., 0., 0.), p(x, 0., 0.), p(x, 3., 0.), p(0., 3., 0.));
|
||||
let (e, f, g, h) = (p(0., 0., 4.), p(x, 0., 4.), p(x, 3., 4.), p(0., 3., 4.));
|
||||
let line = |a, b| EdgeSpec::Line { a, b };
|
||||
let faces = |into: Vec3, from: Vec3| {
|
||||
let bottom = FaceSpec { loops: vec![vec![line(a, d), line(d, into), line(from, b), line(b, a)]], surface: SurfSpec::Plane, probe: None };
|
||||
vec![bottom, quad(e, f, g, h), quad(a, b, f, e), quad(b, c, g, f), quad(c, d, h, g), quad(d, a, e, h)]
|
||||
};
|
||||
let (lo, hi) = (p(x - 2e-9, 3., 0.), p(x + 2e-9, 3., 0.));
|
||||
assert_ne!(key(lo), key(hi));
|
||||
for (into, from) in [(lo, hi), (hi, lo), (lo, c), (hi, c)] {
|
||||
let body = sew(&faces(into, from)).unwrap_or_else(|e| panic!("{into:?} {from:?}: {e}"));
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let v = crate::measure(&body).unwrap().volume;
|
||||
assert!((v - x * 12.0).abs() < 1e-6, "{v}");
|
||||
}
|
||||
assert!(sew(&faces(p(x - 2e-6, 3., 0.), p(x + 1e-6, 3., 0.))).is_err());
|
||||
}
|
||||
|
||||
/// A 4 × 3 × 2 box whose +x wall bulges out as the cubic (4,0) (5,0) (5,3) (4,3), given as a
|
||||
/// chain of its two halves at the bottom (`z = 0`) and the top, the wall ruled between them.
|
||||
fn bulged_box(bottom: EdgeSpec, top: EdgeSpec) -> Vec<FaceSpec> {
|
||||
|
||||
Reference in new issue
Block a user