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:
bherbruckandClaude Opus 5.5 committed 2026-10-08 11:37:33 +00:00
1 parent a01f191d25
commit 5b9749aad6
2 files changed
+123 -31

No files matched your search

+58 -31
View File
@@ -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)
}
+65
View File
@@ -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}");
}