Convert to Sheet Metal takes a long thin plate (#1040)

Convert to Sheet Metal without bend edges refused every plate whose bounding-box diagonal reached a thousand times its thickness ("no outline on that face"), and just under that limit a hole smaller than the mesh's sag fell out of the outline and the DXF. The outline's tolerance is now at most a tenth of the thickness: a thin plate is meshed again at that tolerance. Nothing new is stored. Tests: long thin strips, sheets thousands of times longer than thick, and a strip keeping holes down to 1 mm.
This commit is contained in:
NOWN-DEV authored and GitHub committed 2026-10-14 08:12:41 -04:00
1 parent 3341e2d60b
commit 0f4cda0510
2 files changed
+71 -4

No files matched your search

+15 -4
View File
@@ -590,11 +590,22 @@ pub(super) fn sheet_eval(
FeatureKind::SheetConvert { body, face, rule, .. } => {
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
let tol = (mb.body.size() * 1e-3).max(1e-3);
let mesh = mb.body.tessellate(tol)?;
// Thickness: the body's extent along the face normal.
let ds: Vec<f64> = mesh.positions.iter().map(|p| (*p - *face).dot(n)).collect();
let (lo, hi) = (ds.iter().cloned().fold(f64::MAX, f64::min), ds.iter().cloned().fold(f64::MIN, f64::max));
let extent = |mesh: &solidcraft_geom::Mesh| {
mesh.positions.iter().map(|p| (*p - *face).dot(n)).fold((f64::MAX, f64::MIN), |(lo, hi), d| (lo.min(d), hi.max(d)))
};
// The tolerance (the mesh's chords, and how far off the face's plane a point of it may
// be) is at most a tenth of the thickness, not only a thousandth of the size. On a long
// thin plate that thousandth passes the thickness: the far side's triangles counted as
// this face's (no outline was left), and a hole of a smaller radius than that fell out.
let mut tol = (mb.body.size() * 1e-3).max(1e-3);
let mut mesh = mb.body.tessellate(tol)?;
let (mut lo, mut hi) = extent(&mesh);
if hi - lo > 0.0 && tol > (hi - lo) * 0.1 {
tol = ((hi - lo) * 0.1).max(1e-6);
mesh = mb.body.tessellate(tol)?;
(lo, hi) = extent(&mesh);
}
let t = hi - lo;
// The face outline: boundary edges of the triangles on that face's plane, where it runs
// along a round wall an arc of the wall's circle (not chords: the plate check below
@@ -745,3 +745,59 @@ fn a_plate_with_holes_drilled_here_converts_with_round_holes() {
assert!(circles.len() == 9 && circles.iter().all(|(radius, _)| (radius - r).abs() < 1e-9), "{circles:?}");
assert_eq!(dxf_entities(&mut s), (4, 0, 9));
}
/// A box `l` x `w` x `t` with round holes (centre, diameter) drilled through it, converted,
/// picked on its top face.
fn convert_box(l: f64, w: f64, t: f64, holes: &[([f64; 2], f64)]) -> Session {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": l, "width": w, "height": t, "body_name": "Plate"}));
for (c, d) in holes {
run(&mut s, "solid.hole", json!({"position": [c[0], c[1], t], "diameter": d}));
}
run(&mut s, "sheet.convert", json!({"body": "Plate", "face": [l * 0.4, w * 0.25, t]}));
s
}
/// A long thin strip converts. 1200 x 100 x 1 was refused with "no outline on that face": the
/// outline's tolerance was a thousandth of the body's size (1.2), more than the thickness, so the
/// far side's triangles counted as the picked face's and no boundary was left.
#[test]
fn a_long_thin_strip_converts() {
let mut s = convert_box(1200.0, 100.0, 1.0, &[]);
assert!((volume(&mut s) - 120_000.0).abs() < 1e-6 * 120_000.0, "{}", volume(&mut s));
assert!(same_size(flat_size(&mut s), (1200.0, 100.0), 1e-6), "{:?}", flat_size(&mut s));
let t = flat(&mut s)["flat_size_mm"][2].as_f64().unwrap_or(f64::NAN);
assert!((t - 1.0).abs() < 1e-9, "{t}");
assert_eq!(dxf_entities(&mut s), (4, 0, 0));
}
/// Plates further past that, a thousandth of their size two to six times their thickness: a
/// full sheet, a foil, a long bar.
#[test]
fn plates_thousands_of_times_longer_than_thick_convert() {
for (l, w, t) in [(3000.0, 1500.0, 0.5), (400.0, 100.0, 0.2), (6000.0, 20.0, 1.0)] {
let mut s = convert_box(l, w, t, &[]);
let want = l * w * t;
assert!((volume(&mut s) - want).abs() < 1e-6 * want, "{l} x {w} x {t}: {} vs {want}", volume(&mut s));
assert!(same_size(flat_size(&mut s), (l, w), 1e-6), "{l} x {w} x {t}: {:?}", flat_size(&mut s));
assert_eq!(dxf_entities(&mut s), (4, 0, 0), "{l} x {w} x {t}");
}
}
/// The strip with round holes down to its thickness across keeps every one as a circle. At 1200
/// long it was refused; just under the old limit, 990 long, it converted without the 1 mm hole:
/// the outline's chords could sag 0.99, more than that hole's radius, and it fell out.
#[test]
fn a_long_thin_strip_keeps_its_small_round_holes() {
let d = [10.0, 4.0, 2.0, 1.0];
for l in [990.0, 1200.0] {
let holes: Vec<([f64; 2], f64)> = d.iter().enumerate().map(|(i, d)| ([l * (i as f64 + 1.0) / 5.0, 50.0], *d)).collect();
let mut s = convert_box(l, 100.0, 1.0, &holes);
let want = l * 100.0 - PI / 4.0 * d.iter().map(|d| d * d).sum::<f64>();
assert!((volume(&mut s) - want).abs() < 1e-5 * want, "{l}: {} vs {want}", volume(&mut s));
let mut radii: Vec<f64> = flat_circles(&mut s).iter().map(|c| c.0).collect();
radii.sort_by(f64::total_cmp);
assert!(radii.len() == 4 && radii.iter().zip([0.5, 1.0, 2.0, 5.0]).all(|(a, b)| (a - b).abs() < 1e-9), "{l}: {radii:?}");
assert_eq!(dxf_entities(&mut s), (4, 0, 4), "{l}");
}
}