Sheet metal Fold (SheetMetalFoldCmd): fold the base panel along a sketch line (followed when the sketch changes) or two points on its face; angle, bend radius and K-factor from the rule; the bend zone (BA = θ(R + K·T)) comes out of the existing sheet so the flat pattern keeps its size; the line at the bend's centre, start, end or on the mould line; the side holding a picked point stays (default the larger), flip folds down; flanges on the moving part ride along
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
4ae4b45d91
commit
9f8915996d
9 files changed
+327
No files matched your search
@@ -193,6 +193,13 @@ impl FeatureKind {
|
||||
FeatureKind::Thread { face, .. } => pt(face),
|
||||
FeatureKind::SheetFlange { edges, .. } | FeatureKind::SheetHem { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::SheetConvert { face, .. } => pt(face),
|
||||
FeatureKind::SheetFold { a, b, fixed, .. } => {
|
||||
pt(a);
|
||||
pt(b);
|
||||
if let Some(f) = fixed {
|
||||
pt(f);
|
||||
}
|
||||
}
|
||||
FeatureKind::Boss { position, direction, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
|
||||
@@ -49,6 +49,7 @@ impl FeatureKind {
|
||||
sk(path_sketch);
|
||||
}
|
||||
FeatureKind::Pipe { path_sketch, .. } => sk(path_sketch),
|
||||
FeatureKind::SheetFold { sketch: Some(s), .. } => sk(s),
|
||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
||||
FeatureKind::Hole { points: Some(p), .. } => sk(&mut p.sketch),
|
||||
FeatureKind::Pattern { features, pattern, .. } => {
|
||||
|
||||
@@ -460,6 +460,28 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// Fold a sheet along a line on its base face: a sketch line (followed when the sketch
|
||||
/// changes), else the points `a`, `b`.
|
||||
SheetFold {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sketch: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
curve: Option<String>,
|
||||
a: Vec3,
|
||||
b: Vec3,
|
||||
angle: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
radius: Option<String>,
|
||||
/// centerline | start | end | mould
|
||||
position: String,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
/// A point on the side that stays put (default: the larger side).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fixed: Option<Vec3>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// Unfold a sheet body flat (all bends), or refold it.
|
||||
SheetUnfold {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -742,6 +764,7 @@ impl FeatureKind {
|
||||
| FeatureKind::SheetContour { .. }
|
||||
| FeatureKind::SheetFlange { .. }
|
||||
| FeatureKind::SheetHem { .. }
|
||||
| FeatureKind::SheetFold { .. }
|
||||
| FeatureKind::SheetUnfold { .. }
|
||||
| FeatureKind::SheetConvert { .. }
|
||||
)
|
||||
@@ -787,6 +810,7 @@ impl FeatureKind {
|
||||
FeatureKind::SheetContour { .. } => "ContourFlangeFeature",
|
||||
FeatureKind::SheetFlange { .. } => "EdgeFlangeFeature",
|
||||
FeatureKind::SheetHem { .. } => "HemFeature",
|
||||
FeatureKind::SheetFold { .. } => "FoldFeature",
|
||||
FeatureKind::SheetUnfold { refold: false, .. } => "UnfoldFeature",
|
||||
FeatureKind::SheetUnfold { .. } => "RefoldFeature",
|
||||
FeatureKind::SheetConvert { .. } => "ConvertToSheetMetalFeature",
|
||||
@@ -840,6 +864,7 @@ impl FeatureKind {
|
||||
FeatureKind::SheetContour { .. } => "ContourFlange",
|
||||
FeatureKind::SheetFlange { .. } => "EdgeFlange",
|
||||
FeatureKind::SheetHem { .. } => "Hem",
|
||||
FeatureKind::SheetFold { .. } => "Fold",
|
||||
FeatureKind::SheetUnfold { refold: false, .. } => "Unfold",
|
||||
FeatureKind::SheetUnfold { .. } => "Refold",
|
||||
FeatureKind::SheetConvert { .. } => "ConvertToSheetMetal",
|
||||
@@ -946,6 +971,10 @@ impl FeatureKind {
|
||||
v.extend([height.as_str(), angle]);
|
||||
v.extend(radius.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::SheetFold { angle, radius, .. } => {
|
||||
v.push(angle);
|
||||
v.extend(radius.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::SheetHem { length, gap, .. } => {
|
||||
v.push(length);
|
||||
v.extend(gap.iter().map(String::as_str));
|
||||
@@ -1275,6 +1304,7 @@ impl Document {
|
||||
FeatureKind::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } if *path_sketch == id => gone.push(f.id),
|
||||
FeatureKind::Emboss { sketch, .. } | FeatureKind::Rib { sketch, .. } if *sketch == id => gone.push(f.id),
|
||||
FeatureKind::SheetBase { sketch, .. } | FeatureKind::SheetContour { sketch, .. } if *sketch == id => gone.push(f.id),
|
||||
FeatureKind::SheetFold { sketch: Some(s), .. } if *s == id => gone.push(f.id),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1521,6 +1521,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::SheetContour { .. }
|
||||
| FeatureKind::SheetFlange { .. }
|
||||
| FeatureKind::SheetHem { .. }
|
||||
| FeatureKind::SheetFold { .. }
|
||||
| FeatureKind::SheetUnfold { .. }
|
||||
| FeatureKind::SheetConvert { .. } => more::sheet_eval(doc, vals, f, st),
|
||||
FeatureKind::Boss { .. } | FeatureKind::Lip { .. } | FeatureKind::SnapFit { .. } | FeatureKind::Rest { .. } => {
|
||||
|
||||
@@ -368,6 +368,35 @@ pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Fea
|
||||
}
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetFold { sketch, curve, a, b, angle, radius, position, flip, fixed, body } => {
|
||||
// The sketch line, where it is now.
|
||||
let (a, b) = match (sketch, curve) {
|
||||
(Some(sk), Some(c)) => {
|
||||
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk}")))?;
|
||||
let ci = ss.sketch.curve_index(c).ok_or_else(|| DocError::Unknown(format!("curve `{c}`")))?;
|
||||
match ss.sketch.segs(ci).as_slice() {
|
||||
[Seg2::Line { a, b }] => (ss.plane.to_world(*a), ss.plane.to_world(*b)),
|
||||
_ => return Err(DocError::Invalid("the fold line must be a sketch line".into())),
|
||||
}
|
||||
}
|
||||
_ => (*a, *b),
|
||||
};
|
||||
let i = sheet_for(st, body, Some((a + b) * 0.5))?;
|
||||
let ang = val(vals, angle, Kind::Angle)?;
|
||||
let rr = match radius {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => {
|
||||
let rule = st.sheets.get(i).map(|s| s.rule.clone()).unwrap_or_default();
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.bend_radius
|
||||
}
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
if sh.flat {
|
||||
return Err(DocError::Invalid("refold the sheet before folding it".into()));
|
||||
}
|
||||
sh.add_fold(a, b, ang, rr, position, *flip, *fixed)?;
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetUnfold { body, refold } => {
|
||||
let i = match body {
|
||||
Some(_) => sheet_for(st, body, None)?,
|
||||
|
||||
@@ -187,6 +187,12 @@ impl FeatureKind {
|
||||
v.push(("Bend radius", r, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::SheetFold { angle, radius, .. } => {
|
||||
v.push(("Angle", angle, A));
|
||||
if let Some(r) = radius {
|
||||
v.push(("Bend radius", r, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::SheetHem { length, gap, .. } => {
|
||||
v.push(("Length", length, L));
|
||||
if let Some(g) = gap {
|
||||
|
||||
@@ -156,6 +156,10 @@ pub struct SheetFlange {
|
||||
/// The panel runs on past the bend's ends by these (toward a neighbouring flange).
|
||||
#[serde(default)]
|
||||
pub ext: (f64, f64),
|
||||
/// A fold's panel: the part of the sheet past the bend (flat coordinates, counter-clockwise)
|
||||
/// instead of a rectangle `leg` long.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub shape: Option<Vec<Vec2>>,
|
||||
}
|
||||
|
||||
/// A sheet metal body (see the module docs).
|
||||
@@ -318,6 +322,9 @@ impl SheetBody {
|
||||
|
||||
/// Flat region of a flange's panel.
|
||||
pub fn panel(&self, f: &SheetFlange) -> Region2 {
|
||||
if let Some(sh) = &f.shape {
|
||||
return Region2 { outer: Loop2::polygon(sh).ccw(), holes: Vec::new() };
|
||||
}
|
||||
let ba = self.allowance(f);
|
||||
let a = f.p + f.n * ba - f.d * f.ext.0;
|
||||
let b = f.p + f.n * ba + f.d * (f.len + f.ext.1);
|
||||
@@ -831,6 +838,7 @@ impl SheetBody {
|
||||
angle: if up { angle } else { -angle },
|
||||
radius,
|
||||
leg: leg.max(0.0),
|
||||
shape: None,
|
||||
ext: (0.0, 0.0),
|
||||
});
|
||||
made.push(self.flanges.len() - 1);
|
||||
@@ -896,10 +904,115 @@ impl SheetBody {
|
||||
radius: gap,
|
||||
leg,
|
||||
ext: (0.0, 0.0),
|
||||
shape: None,
|
||||
});
|
||||
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.
|
||||
#[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) {
|
||||
return Err(DocError::Invalid("the fold angle must be between 0 and 180°".into()));
|
||||
}
|
||||
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));
|
||||
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 (a2, b2) = (Vec2::new(fa.x, fa.y), Vec2::new(fb.x, fb.y));
|
||||
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.
|
||||
let side_area = |n: Vec2| signed_area(&clip(&pts, n * l0, n)).abs();
|
||||
let moves_n = match fixed {
|
||||
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),
|
||||
};
|
||||
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 ba = self.allowance(&probe);
|
||||
let s0 = match position {
|
||||
"centerline" | "center" | "centre" => l - ba / 2.0,
|
||||
"start" => l,
|
||||
"end" => l - ba,
|
||||
"mould" | "mold" => l - (radius + self.t) * (angle / 2.0).tan(),
|
||||
o => return Err(DocError::Invalid(format!("unknown fold position `{o}` (centerline, start, end or mould)"))),
|
||||
};
|
||||
let fixed_part = clip(&pts, n * s0, n * -1.0);
|
||||
let moving = clip(&pts, n * (s0 + ba), n);
|
||||
let strip = clip(&clip(&pts, n * s0, n), n * (s0 + ba), n * -1.0);
|
||||
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.
|
||||
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();
|
||||
}
|
||||
self.base = Region2 { outer: Loop2::polygon(&fixed_part).ccw(), holes: self.base.holes.clone() };
|
||||
self.flanges.push(SheetFlange {
|
||||
parent: None,
|
||||
p: snap(p0),
|
||||
d,
|
||||
len: u1 - u0,
|
||||
n,
|
||||
angle: if up { angle } else { -angle },
|
||||
radius,
|
||||
leg,
|
||||
ext: (0.0, 0.0),
|
||||
shape: Some(ccw.into_iter().map(snap).collect()),
|
||||
});
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
let (e0, e1) = ((f.p - p0).dot(n), (f.p + f.d * f.len - p0).dot(n));
|
||||
if e0.min(e1) >= ba - 1e-9 {
|
||||
if let Some(f) = self.flanges.get_mut(k) {
|
||||
f.parent = Some(i);
|
||||
}
|
||||
} else if e0.max(e1) > 1e-9 {
|
||||
return Err(DocError::Invalid("not supported yet: a fold through a flange's bend".into()));
|
||||
}
|
||||
}
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
/// Map a region in a world plane to flat coordinates through a panel's placement.
|
||||
pub fn region_to_flat(&self, panel: Option<usize>, plane: &Plane, r: &Region2) -> Option<Region2> {
|
||||
let inv = mat_inverse(&self.world(panel))?;
|
||||
@@ -1179,6 +1292,7 @@ pub fn contour_flange(body: &str, rule: &str, rv: &RuleValues, plane: &Plane, pt
|
||||
radius: r,
|
||||
leg: leg.max(0.0),
|
||||
ext: (0.0, 0.0),
|
||||
shape: None,
|
||||
};
|
||||
let ba = sheet.allowance(&f);
|
||||
sheet.flanges.push(f);
|
||||
|
||||
@@ -22,6 +22,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SHEET METAL", "CREATE")
|
||||
.icon("hem")
|
||||
.params("edges: [[x,y,z] on sheet edges]; length (to the outer face); gap? (inner radius, default: rule); flip?; body?"),
|
||||
CommandSpec::new("SheetMetalFoldCmd", "Fold", fold).at("SHEET METAL", "CREATE").icon("fold").params(
|
||||
"line: sketch + curve (a sketch line on the sheet's base face; followed when the sketch changes) or points: [[x,y,z], [x,y,z]]; \
|
||||
angle? (default 90 deg); radius? (default: rule); position?: centerline|start|end|mould (where the line sits in the bend, default centerline); \
|
||||
fixed?: [x,y,z] on the side that stays (default: the larger side); flip? (fold the other way); body?",
|
||||
),
|
||||
CommandSpec::new("FusionSheetmetalUnfoldCommand", "Unfold", unfold).at("SHEET METAL", "MODIFY").icon("unfold").params("body? (default: the last sheet body) — all bends"),
|
||||
CommandSpec::new("sheet.refold", "Refold", refold).at("SHEET METAL", "MODIFY").icon("refold").params("body? (default: the last unfolded sheet)"),
|
||||
CommandSpec::new("ConvertToSheetMetalCmd", "Convert to Sheet Metal", convert)
|
||||
@@ -133,6 +138,48 @@ fn hem(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
)
|
||||
}
|
||||
|
||||
fn fold(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "SheetMetalFoldCmd";
|
||||
let (sketch, curve, a, b) = match p.get("points") {
|
||||
Some(v) => {
|
||||
let pts = v.as_array().map(|a| a.iter().filter_map(vec3).collect::<Vec<Vec3>>()).unwrap_or_default();
|
||||
let [a, b] = pts[..] else { return Err(bad(cmd, "`points` must be two points [x, y, z]")) };
|
||||
(None, None, a, b)
|
||||
}
|
||||
None => {
|
||||
let sk = sketch_id(s, p.get("sketch"), cmd, "sketch")?;
|
||||
let c = str_(p, "curve").ok_or_else(|| bad(cmd, "give `sketch` + `curve` (a sketch line) or `points`"))?.to_string();
|
||||
let st = s.model.state();
|
||||
let ss = st.sketch(sk).ok_or_else(|| bad(cmd, "no such sketch"))?;
|
||||
let ci = ss.sketch.curve_index(&c).ok_or_else(|| bad(cmd, format!("no curve `{c}` in the sketch")))?;
|
||||
let (a, b) = match ss.sketch.segs(ci).as_slice() {
|
||||
[solvecraft_geom::Seg2::Line { a, b }] => (ss.plane.to_world(*a), ss.plane.to_world(*b)),
|
||||
_ => return Err(bad(cmd, "the fold line must be a sketch line")),
|
||||
};
|
||||
(Some(sk), Some(c), a, b)
|
||||
}
|
||||
};
|
||||
if a.dist(b) < 1e-9 {
|
||||
return Err(bad(cmd, "the fold line has no length"));
|
||||
}
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "90 deg".into());
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let radius = expr(p, "radius");
|
||||
if let Some(r) = &radius {
|
||||
check_expr(s, r, Kind::Length, cmd, "radius")?;
|
||||
}
|
||||
let position = str_(p, "position").unwrap_or("centerline").to_ascii_lowercase();
|
||||
if !matches!(position.as_str(), "centerline" | "start" | "end" | "mould" | "mold") {
|
||||
return Err(bad(cmd, "`position` must be centerline, start, end or mould"));
|
||||
}
|
||||
let fixed = match p.get("fixed") {
|
||||
Some(v) => Some(vec3(v).ok_or_else(|| bad(cmd, "`fixed` must be a point [x, y, z]"))?),
|
||||
None => None,
|
||||
};
|
||||
let body = str_(p, "body").map(str::to_string);
|
||||
add_feature(s, p, FeatureKind::SheetFold { sketch, curve, a, b, angle, radius, position, flip: bool_(p, "flip").unwrap_or(false), fixed, body })
|
||||
}
|
||||
|
||||
fn unfold(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::SheetUnfold { body: str_(p, "body").map(str::to_string), refold: false })
|
||||
}
|
||||
|
||||
@@ -141,3 +141,95 @@ fn unfold_cut_refold_and_convert() {
|
||||
run(&mut t, "PrimitiveCylinder", json!({"radius": 10, "height": 30, "body_name": "Rod"}));
|
||||
assert!(t.execute("ConvertToSheetMetalCmd", &json!({"body": "Rod", "face": [0, 0, 30]})).is_err());
|
||||
}
|
||||
|
||||
/// A sketch line across a 100 x 60 plate's top face at x = 40.
|
||||
fn fold_line(s: &mut Session) {
|
||||
run(s, "SketchCreate", json!({"plane": "XY", "offset": 2.5, "name": "FoldLine"}));
|
||||
run(s, "DrawPolyline", json!({"points": [[40, 0], [40, 60]], "ids": ["fl"]}));
|
||||
run(s, "SketchStop", json!({}));
|
||||
}
|
||||
|
||||
fn bbox(s: &mut Session) -> Value {
|
||||
run(s, "MeasureCommand", json!({}))["bodies"][0]["bbox"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_takes_the_bend_out_of_the_flat_sheet() {
|
||||
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);
|
||||
fold_line(&mut s);
|
||||
run(&mut s, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl"}));
|
||||
// The flat pattern keeps its size; the bend's centre line is the sketch line.
|
||||
let f = flat(&mut s);
|
||||
assert!((f["flat_size_mm"][0].as_f64().unwrap_or(0.0) - 100.0).abs() < 1e-6, "{f}");
|
||||
assert!((f["flat_size_mm"][1].as_f64().unwrap_or(0.0) - 60.0).abs() < 1e-6, "{f}");
|
||||
assert!((f["bends"][0]["allowance"].as_f64().unwrap_or(0.0) - ba).abs() < 1e-9, "{f}");
|
||||
// Folded: the zone's flat volume BA·T·w becomes the bend's θ·T·(R + T/2)·w.
|
||||
let want = 15000.0 - ba * t * 60.0 + FRAC_PI_2 * t * (r + t / 2.0) * 60.0;
|
||||
assert!(rel(volume(&mut s), want) < 1e-4, "{} {want}", volume(&mut s));
|
||||
// The smaller side (x < 40) stands up: outer face at the zone start less R + T, leg 40 − BA/2
|
||||
// tall above the bend.
|
||||
let b = bbox(&mut s);
|
||||
assert!((b["min"][0].as_f64().unwrap_or(0.0) - (40.0 + ba / 2.0 - r - t)).abs() < 1e-6, "{b}");
|
||||
assert!((b["max"][2].as_f64().unwrap_or(0.0) - (t + r + 40.0 - ba / 2.0)).abs() < 1e-6, "{b}");
|
||||
assert!((b["max"][0].as_f64().unwrap_or(0.0) - 100.0).abs() < 1e-6, "{b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_positions_fixed_side_and_flip() {
|
||||
let (t, r, k) = (2.5, 2.5, 0.44);
|
||||
let ba = FRAC_PI_2 * (r + k * t);
|
||||
let case = |p: Value| {
|
||||
let mut s = Session::default();
|
||||
base(&mut s, 100.0, 60.0);
|
||||
fold_line(&mut s);
|
||||
let mut q = json!({"sketch": "FoldLine", "curve": "fl"});
|
||||
for (k, v) in p.as_object().into_iter().flatten() {
|
||||
q[k] = v.clone();
|
||||
}
|
||||
run(&mut s, "SheetMetalFoldCmd", q);
|
||||
bbox(&mut s)
|
||||
};
|
||||
let x0 = |b: &Value| b["min"][0].as_f64().unwrap_or(f64::NAN);
|
||||
// Start: the bend starts at the line (on the fixed side), so the folded outer face is R + T
|
||||
// short of it. End: the bend ends there. Mould: the outer face lands on the line.
|
||||
assert!((x0(&case(json!({"position": "start"}))) - (40.0 - r - t)).abs() < 1e-6);
|
||||
assert!((x0(&case(json!({"position": "end"}))) - (40.0 + ba - r - t)).abs() < 1e-6);
|
||||
assert!((x0(&case(json!({"position": "mould"}))) - 40.0).abs() < 1e-6);
|
||||
// Keep the small side: the large one (x > 40) stands up instead.
|
||||
let b = case(json!({"fixed": [10, 30, 2.5]}));
|
||||
assert!((b["max"][0].as_f64().unwrap_or(0.0) - (40.0 - ba / 2.0 + r + t)).abs() < 1e-6, "{b}");
|
||||
assert!((b["max"][2].as_f64().unwrap_or(0.0) - (t + r + 60.0 - ba / 2.0)).abs() < 1e-6, "{b}");
|
||||
// Flipped: folds down, below the sheet.
|
||||
let b = case(json!({"flip": true}));
|
||||
assert!((b["min"][2].as_f64().unwrap_or(0.0) + (r + 40.0 - ba / 2.0)).abs() < 1e-6, "{b}");
|
||||
// A 45° fold: the leg leans.
|
||||
let b = case(json!({"angle": "45 deg"}));
|
||||
assert!(b["max"][2].as_f64().unwrap_or(0.0) < t + r + 40.0 - ba / 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_carries_flanges_on_the_moving_side() {
|
||||
let mut s = Session::default();
|
||||
base(&mut s, 100.0, 60.0);
|
||||
// A flange on the left edge, then a fold between it and the rest.
|
||||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[0, 30, 2.5]], "height": 20}));
|
||||
let v0 = volume(&mut s);
|
||||
fold_line(&mut s);
|
||||
run(&mut s, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl", "fixed": [90, 30, 2.5]}));
|
||||
// The flange rode along: it is above the plate now, nothing left at z < 0.
|
||||
let b = bbox(&mut s);
|
||||
assert!(b["min"][2].as_f64().unwrap_or(-1.0) > -1e-6, "{b}");
|
||||
let ba = FRAC_PI_2 * (2.5 + 0.44 * 2.5);
|
||||
let want = v0 - ba * 2.5 * 60.0 + FRAC_PI_2 * 2.5 * (2.5 + 1.25) * 60.0;
|
||||
assert!(rel(volume(&mut s), want) < 1e-4, "{} {want}", volume(&mut s));
|
||||
assert!(flat(&mut s)["bends"].as_array().map(Vec::len) == Some(2));
|
||||
// Errors: off the sheet, through the flange's bend.
|
||||
let mut e = Session::default();
|
||||
base(&mut e, 100.0, 60.0);
|
||||
assert!(e.execute("SheetMetalFoldCmd", &json!({"points": [[40, 0, 30], [40, 60, 30]]})).is_err());
|
||||
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");
|
||||
}
|
||||
Reference in new issue
Block a user