Fold on flange panels (the side toward the flange's own bend stays; flanges and folds on the moving part ride along) and through cut-outs whose edges run along or across the bend; slanted or curved edges across a bend still say not supported (they would need helical edges)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
a01f191d25
commit
5b9749aad6
2 files changed
+123
-31
No files matched your search
+58
-31
@@ -909,12 +909,13 @@ impl SheetBody {
|
||||
Ok(self.flanges.len() - 1)
|
||||
}
|
||||
|
||||
/// Fold the base panel along a line on its top or bottom face (world points `a`, `b`).
|
||||
/// The bend zone (width BA = θ·(R + K·T)) is taken out of the existing sheet, so the flat
|
||||
/// pattern keeps its size; `position` puts the line at the zone's centre (`centerline`),
|
||||
/// start or end (`start`/`end`, from the fixed side), or on the mould line (`mould`: where
|
||||
/// the outer faces meet, so the fixed side measures the same to it folded as flat).
|
||||
/// The side holding `fixed` (default: the larger side) stays put; `flip` folds the other way.
|
||||
/// Fold a panel (the base or a flange's) along a line on its top or bottom face (world
|
||||
/// points `a`, `b`). The bend zone (width BA = θ·(R + K·T)) is taken out of the existing
|
||||
/// sheet, so the flat pattern keeps its size; `position` puts the line at the zone's centre
|
||||
/// (`centerline`), start or end (`start`/`end`, from the fixed side), or on the mould line
|
||||
/// (`mould`: where the outer faces meet, so the fixed side measures the same to it folded as
|
||||
/// flat). On the base the side holding `fixed` (default: the larger side) stays put; on a
|
||||
/// flange the side toward its own bend does. `flip` folds the other way.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn add_fold(&mut self, a: Vec3, b: Vec3, angle: f64, radius: f64, position: &str, flip: bool, fixed: Option<Vec3>) -> Result<usize> {
|
||||
if !(angle > 1e-6 && angle <= std::f64::consts::PI - 1e-6) {
|
||||
@@ -923,31 +924,51 @@ impl SheetBody {
|
||||
if !(radius >= 0.0) {
|
||||
return Err(DocError::Invalid("the bend radius must not be negative".into()));
|
||||
}
|
||||
let inv = mat_inverse(&self.world(None)).ok_or_else(|| DocError::Invalid("sheet frame".into()))?;
|
||||
let (fa, fb) = (apply_point(&inv, a), apply_point(&inv, b));
|
||||
// The panel the line lies on: both ends on its top or bottom face, the middle inside it.
|
||||
let on_face = |q: Vec3| q.z.abs() < 1e-4 || (q.z - self.t).abs() < 1e-4;
|
||||
if !(on_face(fa) && on_face(fb)) {
|
||||
return Err(DocError::Invalid("the fold line must lie on the base face of the sheet (folds on flanges are not supported yet)".into()));
|
||||
let mut found = None;
|
||||
for (pi, r) in self.panels() {
|
||||
let Some(inv) = mat_inverse(&self.world(pi)) else { continue };
|
||||
let (fa, fb) = (apply_point(&inv, a), apply_point(&inv, b));
|
||||
if !(on_face(fa) && on_face(fb) && (fa.z - fb.z).abs() < 1e-4) {
|
||||
continue;
|
||||
}
|
||||
let pts = dedup(&r.outer.ccw().polyline(1e-3));
|
||||
let (a2, b2) = (Vec2::new(fa.x, fa.y), Vec2::new(fb.x, fb.y));
|
||||
if point_in(&pts, (a2 + b2) * 0.5) || polys_cross(&pts, &[a2, b2, (a2 + b2) * 0.5]) {
|
||||
found = Some((pi, inv, a2, b2, pts));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (a2, b2) = (Vec2::new(fa.x, fa.y), Vec2::new(fb.x, fb.y));
|
||||
let (panel, inv, a2, b2, pts) = found.ok_or_else(|| DocError::Invalid("the fold line must lie on a flat face of the sheet".into()))?;
|
||||
let d = (b2 - a2).normalized().ok_or_else(|| DocError::Invalid("the fold line has no length".into()))?;
|
||||
let pts = dedup(&self.base.outer.ccw().polyline(1e-3));
|
||||
let mut n = Vec2::new(-d.y, d.x);
|
||||
let l0 = a2.dot(n);
|
||||
// Which side moves: away from `fixed`, else the smaller side.
|
||||
// Which side moves. A flange keeps the side its own bend is on.
|
||||
let side_area = |n: Vec2| signed_area(&clip(&pts, n * l0, n)).abs();
|
||||
let moves_n = match fixed {
|
||||
Some(p) => {
|
||||
let moves_n = match (panel, fixed) {
|
||||
(Some(j), _) => {
|
||||
let f = self.flanges.get(j).ok_or_else(|| DocError::Invalid("flange".into()))?;
|
||||
let ba = self.allowance(f);
|
||||
// The panel's attaching edge must stay whole on the fixed side.
|
||||
let (e0, e1) = (f.p + f.n * ba - f.d * f.ext.0, f.p + f.n * ba + f.d * (f.len + f.ext.1));
|
||||
let (s0, s1) = (e0.dot(n) - l0, e1.dot(n) - l0);
|
||||
if s0 * s1 < -1e-12 {
|
||||
return Err(DocError::Invalid("a fold on a flange must run across it, clear of the flange's own bend".into()));
|
||||
}
|
||||
(s0 + s1) < 0.0
|
||||
}
|
||||
(None, Some(p)) => {
|
||||
let q = apply_point(&inv, p);
|
||||
Vec2::new(q.x, q.y).dot(n) < l0
|
||||
}
|
||||
None => side_area(n) <= side_area(n * -1.0),
|
||||
(None, None) => side_area(n) <= side_area(n * -1.0),
|
||||
};
|
||||
if !moves_n {
|
||||
n = n * -1.0;
|
||||
}
|
||||
let l = a2.dot(n);
|
||||
let probe = SheetFlange { parent: None, p: a2, d, len: 1.0, n, angle, radius, leg: 0.0, ext: (0.0, 0.0), shape: None };
|
||||
let probe = SheetFlange { parent: panel, p: a2, d, len: 1.0, n, angle, radius, leg: 0.0, ext: (0.0, 0.0), shape: None };
|
||||
let ba = self.allowance(&probe);
|
||||
let s0 = match position {
|
||||
"centerline" | "center" | "centre" => l - ba / 2.0,
|
||||
@@ -962,28 +983,32 @@ impl SheetBody {
|
||||
if signed_area(&fixed_part).abs() < 1e-9 || signed_area(&moving).abs() < 1e-9 {
|
||||
return Err(DocError::Invalid("the fold line must cross the sheet with material on both sides of the bend".into()));
|
||||
}
|
||||
// The bend zone must be a rectangle across the sheet.
|
||||
// The bend zone must be a rectangle across the panel (a slanted or curved edge would
|
||||
// run across the bend as a helix, which the sheet's faces can't carry yet).
|
||||
let us: Vec<f64> = strip.iter().map(|q| q.dot(d)).collect();
|
||||
let (u0, u1) = (us.iter().cloned().fold(f64::MAX, f64::min), us.iter().cloned().fold(f64::MIN, f64::max));
|
||||
if ((u1 - u0) * ba - signed_area(&strip).abs()).abs() > 1e-6 * (u1 - u0).max(1.0) * ba.max(1.0) {
|
||||
return Err(DocError::Invalid("not supported yet: folds whose bend zone crosses a slanted or curved edge of the sheet".into()));
|
||||
}
|
||||
if self.holes.iter().any(|h| {
|
||||
let hp = dedup(&h.outer.ccw().polyline(1e-3));
|
||||
hp.iter().any(|q| point_in(&strip, *q)) || polys_cross(&strip, &hp)
|
||||
}) {
|
||||
return Err(DocError::Invalid("not supported yet: folds through a cut-out".into()));
|
||||
}
|
||||
let up = !flip;
|
||||
let leg = moving.iter().map(|q| q.dot(n) - s0 - ba).fold(0.0, f64::max);
|
||||
let p0 = n * s0 + d * u0;
|
||||
let mut ccw = moving.clone();
|
||||
if signed_area(&ccw) < 0.0 {
|
||||
ccw.reverse();
|
||||
let ccw = |mut v: Vec<Vec2>| {
|
||||
if signed_area(&v) < 0.0 {
|
||||
v.reverse();
|
||||
}
|
||||
v.into_iter().map(snap).collect::<Vec<_>>()
|
||||
};
|
||||
match panel {
|
||||
None => self.base = Region2 { outer: Loop2::polygon(&fixed_part).ccw(), holes: self.base.holes.clone() },
|
||||
Some(j) => {
|
||||
if let Some(f) = self.flanges.get_mut(j) {
|
||||
f.shape = Some(ccw(fixed_part.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.base = Region2 { outer: Loop2::polygon(&fixed_part).ccw(), holes: self.base.holes.clone() };
|
||||
self.flanges.push(SheetFlange {
|
||||
parent: None,
|
||||
parent: panel,
|
||||
p: snap(p0),
|
||||
d,
|
||||
len: u1 - u0,
|
||||
@@ -992,13 +1017,13 @@ impl SheetBody {
|
||||
radius,
|
||||
leg,
|
||||
ext: (0.0, 0.0),
|
||||
shape: Some(ccw.into_iter().map(snap).collect()),
|
||||
shape: Some(ccw(moving)),
|
||||
});
|
||||
let i = self.flanges.len() - 1;
|
||||
// Flanges on the moving part's edges now hang off the fold and move with it.
|
||||
for k in 0..i {
|
||||
let Some(f) = self.flanges.get(k) else { continue };
|
||||
if f.parent.is_some() {
|
||||
if f.parent != panel {
|
||||
continue;
|
||||
}
|
||||
let (e0, e1) = ((f.p - p0).dot(n), (f.p + f.d * f.len - p0).dot(n));
|
||||
@@ -1010,6 +1035,8 @@ impl SheetBody {
|
||||
return Err(DocError::Invalid("not supported yet: a fold through a flange's bend".into()));
|
||||
}
|
||||
}
|
||||
// Cut-outs may cross the bend where their edges run along or across it (the faces
|
||||
// check the rest when the sheet is built).
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
|
||||
@@ -233,3 +233,68 @@ fn fold_carries_flanges_on_the_moving_side() {
|
||||
assert!(e.execute("SheetMetalFoldCmd", &json!({"points": [[40, 0, 2.5], [40, 60, 2.5]], "position": "sideways"})).is_err());
|
||||
assert!(e.execute("SheetMetalFoldCmd", &json!({"points": [[0.5, 0, 2.5], [0.5, 60, 2.5]]})).is_err(), "no material past the bend");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_on_a_flange_panel() {
|
||||
let (t, r, k) = (2.5, 2.5, 0.44);
|
||||
let ba = FRAC_PI_2 * (r + k * t);
|
||||
let mut s = Session::default();
|
||||
base(&mut s, 100.0, 60.0);
|
||||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 30}));
|
||||
let flat0 = flat(&mut s)["flat_size_mm"].clone();
|
||||
let v0 = volume(&mut s);
|
||||
// The flange stands up at the plate's y = 0 edge: its outer face is the body's min y.
|
||||
let y = bbox(&mut s)["min"][1].as_f64().unwrap_or(f64::NAN);
|
||||
run(&mut s, "SheetMetalFoldCmd", json!({"points": [[10, y, 18], [90, y, 18]]}));
|
||||
let f = flat(&mut s);
|
||||
for i in 0..2 {
|
||||
assert!(
|
||||
(f["flat_size_mm"][i].as_f64().unwrap_or(0.0) - flat0[i].as_f64().unwrap_or(1.0)).abs() < 1e-6,
|
||||
"the flat pattern keeps its size: {f}"
|
||||
);
|
||||
}
|
||||
assert_eq!(f["bends"].as_array().map(Vec::len), Some(2));
|
||||
let len = f["bends"][1]["length"].as_f64().unwrap_or(0.0);
|
||||
assert!(len > 90.0, "{f}");
|
||||
let want = v0 - ba * t * len + FRAC_PI_2 * t * (r + t / 2.0) * len;
|
||||
assert!(rel(volume(&mut s), want) < 1e-4, "{} {want}", volume(&mut s));
|
||||
// The top part now leans over the plate (folded up, toward its top face).
|
||||
let b = bbox(&mut s);
|
||||
assert!(b["max"][2].as_f64().unwrap_or(99.0) < 25.0, "{b}");
|
||||
// Folding across the flange's own bend is refused.
|
||||
assert!(s.execute("SheetMetalFoldCmd", &json!({"points": [[50, y, 2], [50, y, 25]]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_through_cut_outs() {
|
||||
let mut s = Session::default();
|
||||
base(&mut s, 100.0, 60.0);
|
||||
// A slot across the fold line (edges along and across the bend).
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Slot"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [30, 20], "p1": [50, 40]}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "Extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "direction": "symmetric"}));
|
||||
let mut plain = Session::default();
|
||||
base(&mut plain, 100.0, 60.0);
|
||||
fold_line(&mut plain);
|
||||
run(&mut plain, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl"}));
|
||||
fold_line(&mut s);
|
||||
run(&mut s, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl"}));
|
||||
let f = flat(&mut s);
|
||||
assert_eq!(f["cutouts"].as_array().map(Vec::len), Some(1), "{f}");
|
||||
let (v, vp) = (volume(&mut s), volume(&mut plain));
|
||||
// The slot takes 20 x 20 of the flat sheet; across the bend it removes the bend's share.
|
||||
assert!(v < vp - 900.0 && v > vp - 1100.0, "{v} {vp}");
|
||||
run(&mut s, "FusionSheetmetalUnfoldCommand", json!({}));
|
||||
assert!((volume(&mut s) - (15000.0 - 1000.0)).abs() < 1e-3, "{}", volume(&mut s));
|
||||
// A round hole across the bend is not supported yet, and says so.
|
||||
let mut c = Session::default();
|
||||
base(&mut c, 100.0, 60.0);
|
||||
run(&mut c, "SketchCreate", json!({"plane": "XY", "name": "Hole"}));
|
||||
run(&mut c, "CircleCenterRadius", json!({"center": [40, 30], "radius": 5}));
|
||||
run(&mut c, "SketchStop", json!({}));
|
||||
run(&mut c, "Extrude", json!({"sketch": "Hole", "through_all": true, "operation": "cut", "direction": "symmetric"}));
|
||||
fold_line(&mut c);
|
||||
let e = c.execute("SheetMetalFoldCmd", &json!({"sketch": "FoldLine", "curve": "fl"})).map(|_| ()).unwrap_err().to_string();
|
||||
assert!(e.contains("not supported yet"), "{e}");
|
||||
}
|
||||
Reference in new issue
Block a user