feat: Convert to Sheet Metal takes a solid or a shelled body by its bend edges (#725)

Convert to Sheet Metal now takes a solid or a shelled body by its bend edges (`bends`): the faces reached from the fixed face across those edges become the sheet, keeping the part's envelope, with the flat pattern by the rule's K-factor and radius. Other edges between sheet faces are cut open. The result works with edge flanges, cuts, Unfold/Refold, the flat pattern and DXF. A conversion without `bends` is the plate conversion as before. The optional `bends` field is noted under format 17. Fixes #647.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
NOWN-DEVandClaude Opus 5.5 authored and GitHub committed 2026-10-13 10:52:28 -04:00
1 parent a513304917
commit a1cb8d4d36
16 files changed
+1044 -20

No files matched your search

+22 -3
View File
@@ -148,7 +148,9 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
),
"sheet.unfold" => (Sm::Unfold { refold: false }, vec![SelInput::new("Body (last if none)", BODIES, false)]),
"sheet.refold" => (Sm::Unfold { refold: true }, vec![SelInput::new("Body (last if none)", BODIES, false)]),
"sheet.convert" => (Sm::Convert { rule: 0 }, vec![SelInput::new("Face", PLANAR_FACES, false)]),
"sheet.convert" => {
(Sm::Convert { rule: 0 }, vec![SelInput::new("Fixed Face", PLANAR_FACES, false), SelInput::new("Bend Edges", EDGES, true)])
}
"sheet.manage_rules" => {
let active = app.session.doc.sheet.active.clone();
let rules = rule_list(app);
@@ -586,8 +588,16 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
(if *refold { "sheet.refold" } else { "sheet.unfold" }, p)
}
Sm::Convert { rule } => {
let Some(Sel::Face { body, point, .. }) = items(inputs, 0).first() else { return Err("select the plate's large face first".into()) };
let Some(Sel::Face { body, point, .. }) = items(inputs, 0).first() else {
return Err("select the fixed face first (a plate's large face)".into());
};
let mut p = json!({"body": body, "face": pt(*point)});
// The sharp edges of a solid or shelled body that become bends (a plate has none).
let bends: Vec<Value> =
items(inputs, 1).iter().filter_map(|x| if let Sel::Edge { point, .. } = x { Some(pt(*point)) } else { None }).collect();
if !bends.is_empty() {
p["bends"] = json!(bends);
}
if let Some(r) = rule_value(app, *rule) {
p["rule"] = json!(r);
}
@@ -711,7 +721,16 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
return Some((Kind::Sheet(k), ins, extra));
}
FeatureKind::SheetUnfold { body, refold } => (Sm::Unfold { refold: *refold }, body.iter().map(|n| Sel::Body { name: n.clone() }).collect()),
FeatureKind::SheetConvert { face, rule, .. } => (Sm::Convert { rule: rule_index(rule) }, face_sel(s, *face).into_iter().collect()),
FeatureKind::SheetConvert { face, rule, bends, .. } => {
let (_, mut ins) = start(app, "sheet.convert")?;
if let Some(i) = ins.first_mut() {
i.items = face_sel(s, *face).into_iter().collect();
}
if let Some(i) = ins.get_mut(1) {
i.items = bends.iter().filter_map(|p| edge_sel(s, *p)).collect();
}
return Some((Kind::Sheet(Sm::Convert { rule: rule_index(rule) }), ins, extra));
}
_ => return None,
};
let id = match &k {
+38
View File
@@ -421,3 +421,41 @@ fn edge_flanges_can_have_an_angle_and_a_height_of_their_own() {
json!([{"point": pt(point(1)), "angle": "100 deg"}, {"point": pt(point(3)), "angle": "90 deg", "height": "15 mm"}])
);
}
/// Convert takes a fixed face and, for a solid or shelled body, its bend edges; Edit Feature
/// brings both back.
#[test]
fn convert_takes_the_fixed_face_and_the_bend_edges() {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 100, "width": 60, "height": 30, "body_name": "Tray"})).unwrap();
s.execute("solid.shell", &json!({"faces": [[50, 30, 30]], "thickness": 2})).unwrap();
let mut app = SolveApp::new(s, Services::default());
app.start("sheet.convert");
let mut d = app.dialog.clone().unwrap();
assert_eq!(d.inputs.iter().map(|i| i.label).collect::<Vec<_>>(), ["Fixed Face", "Bend Edges"]);
assert!(apply_commands(&app, &d).is_err(), "nothing to convert without a face");
// The face alone is the plate conversion: no `bends`.
d.inputs[0].items = face_sel(&app.session, Vec3::new(50.0, 30.0, 0.0)).into_iter().collect();
assert!(apply_commands(&app, &d).unwrap()[0].1.get("bends").is_none());
let edges: Vec<Sel> = [[50.0, 0.0, 0.0], [100.0, 30.0, 0.0], [50.0, 60.0, 0.0], [0.0, 30.0, 0.0]]
.iter()
.filter_map(|p| edge_sel(&app.session, Vec3::new(p[0], p[1], p[2])))
.collect();
d.inputs[1].items = edges;
let c = apply_commands(&app, &d).unwrap();
assert_eq!(c[0].0, "sheet.convert");
assert_eq!(c[0].1["bends"], json!([[50.0, 0.0, 0.0], [100.0, 30.0, 0.0], [50.0, 60.0, 0.0], [0.0, 30.0, 0.0]]));
run_all(&mut app, c);
let bends = |app: &mut SolveApp| app.session.execute("sheet.flat_pattern", &json!({})).unwrap()["bends"].as_array().map(Vec::len);
assert_eq!(bends(&mut app), Some(4));
// Edit Feature: the face and the four edges are back in their rows, and OK rebuilds the sheet.
let f = app.session.doc.features.last().unwrap().clone();
let e = edit_dialog(&app, f.id, None).unwrap();
assert_eq!((e.inputs[0].items.len(), e.inputs[1].items.len()), (1, 4));
let c = apply_commands(&app, &e).unwrap();
assert_eq!(c[0].0, "timeline.redefine");
assert_eq!(c[0].1["params"]["bends"].as_array().map(Vec::len), Some(4));
run_all(&mut app, c);
assert_eq!(bends(&mut app), Some(4));
assert!(app.session.model.results.iter().all(|r| r.error.is_none()), "{:?}", app.session.model.results);
}