Sheet metal: an Edge Flange can choose its own corner relief (#1481)

An Edge Flange feature can set the shape and size of the relief at the corners its flanges make, in place of the sheet metal rule's. Design format 24.

Co-authored-by: NOWN-DEV <NOWN-DEV@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-15 20:22:31 -04:00
1 parent c361630954
commit 4cbf9cf607
22 files changed
+543 -43

No files matched your search

+32 -3
View File
@@ -35,6 +35,9 @@ const RULE_FIELDS: [(&str, &str); 8] = [
/// The corner relief shapes the manager offers (the rule's `corner_relief_shape`) and their labels.
const RELIEF_SHAPES: [&str; 4] = ["notch", "round", "rectangular", "tear"];
const RELIEF_SHAPE_LABELS: [&str; 4] = ["Notch", "Round", "Rectangular", "Tear"];
/// The corner relief shapes an edge flange offers: the rule's (index 0, nothing of its own), then
/// the shapes of `RELIEF_SHAPES` in order.
const FLANGE_RELIEF_LABELS: [&str; 5] = ["Rule", "Notch", "Round", "Rectangular", "Tear"];
#[derive(Clone, Debug)]
pub enum Sm {
@@ -52,6 +55,10 @@ pub enum Sm {
/// Edge flanges: the edges with an angle and/or a height of their own (the picked point,
/// angle, height; an empty text means the dialog's), from the rows per edge.
own: Vec<(Vec3, String, String)>,
/// Edge flanges: the corner relief shape of their own (index into `FLANGE_RELIEF_LABELS`;
/// 0 is the rule's) and its size (empty: the rule's).
relief: usize,
relief_size: String,
},
Hem {
length: String,
@@ -134,6 +141,8 @@ fn new_flange(ty: usize) -> Sm {
reverse: false,
rule: 0,
own: Vec::new(),
relief: 0,
relief_size: String::new(),
}
}
@@ -199,7 +208,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
let t = Tokens::get();
let mut enter = false;
match k {
Sm::Flange { ty, height, angle, radius, position, flip, distance, reverse, rule, own } => {
Sm::Flange { ty, height, angle, radius, position, flip, distance, reverse, rule, own, relief, relief_size } => {
*ty = flange_type(inputs);
row_label(ui, "Type");
ui.label(FLANGE_TYPES.get(*ty).copied().unwrap_or(""));
@@ -220,6 +229,15 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
crate::params_dialog::complete(ui, &r, radius);
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
// The relief at the corners these flanges make: the rule's unless set here.
row_label(ui, "Corner Relief Shape");
combo(ui, "sm_flange_relief", &FLANGE_RELIEF_LABELS, relief);
ui.end_row();
row_label(ui, "Corner Relief");
let r = ui.add(egui::TextEdit::singleline(relief_size).hint_text(crate::i18n::tr_label("from the rule")));
crate::params_dialog::complete(ui, &r, relief_size);
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
// With several edges, a row for each: an angle and a height of its own, left
// empty for the values above (shown as the hints).
let points = edge_point_list(inputs);
@@ -514,7 +532,7 @@ fn flange_edges(inputs: &[SelInput], own: &[(Vec3, String, String)]) -> Vec<Valu
pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
let s = &app.session;
let (cmd, mut p): (&str, Value) = match k {
Sm::Flange { height, angle, radius, position, flip, distance, reverse, rule, own, .. } => {
Sm::Flange { height, angle, radius, position, flip, distance, reverse, rule, own, relief, relief_size, .. } => {
let mut p = match flange_type(inputs) {
1 => {
let mut p = json!({"type": "edge", "edges": flange_edges(inputs, own), "height": height, "angle": angle,
@@ -522,6 +540,12 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
if !radius.trim().is_empty() {
p["radius"] = json!(radius);
}
if let Some(shape) = relief.checked_sub(1).and_then(|i| RELIEF_SHAPES.get(i)) {
p["corner_relief_shape"] = json!(shape);
}
if !relief_size.trim().is_empty() {
p["corner_relief"] = json!(relief_size);
}
p
}
2 => {
@@ -667,7 +691,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
}
(f, curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect())
}
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body } => {
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
if let Some(b) = body {
extra.insert("body".into(), json!(b));
}
@@ -692,6 +716,11 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
reverse: false,
rule: 0,
own,
relief: corner_relief_shape
.as_deref()
.and_then(|name| RELIEF_SHAPES.iter().position(|s| s.eq_ignore_ascii_case(name.trim())))
.map_or(0, |i| i + 1),
relief_size: corner_relief.clone().unwrap_or_default(),
};
(f, picked)
}
+72
View File
@@ -365,6 +365,78 @@ fn rules_dialog_picks_the_corner_relief_shape() {
assert_eq!((rounds(&h.app), torn), (Some(0), Some(4)));
}
/// The Flange dialog's relief rows: Rule (the default) and an empty size leave the relief to the rule,
/// a shape and a size reach the feature, Edit Feature shows them again, and Rule takes them back.
#[test]
fn flange_dialog_picks_a_corner_relief_of_its_own() {
let mut h = crate::scenario::Harness::new();
h.app
.run(
"sheet.manage_rules",
json!({"name": "AL2", "thickness": "2 mm", "k_factor": 0.5, "bend_radius": "1 mm", "gap": "0.5 mm", "active": true}),
)
.unwrap();
h.app.run("sketch.create", json!({"plane": "XY", "name": "Base"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [120, 90]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let edges: Vec<Sel> = [[60.0, 0.0, 2.0], [0.0, 45.0, 2.0]].iter().filter_map(|p| edge_sel(&h.app.session, Vec3::new(p[0], p[1], p[2]))).collect();
assert_eq!(edges.len(), 2);
h.app.session.selection = edges;
h.app.start("sheet.flange");
h.frames(3);
let shown = |app: &SolveApp| match app.dialog.as_ref().map(|d| &d.kind) {
Some(Kind::Sheet(Sm::Flange { relief, relief_size, .. })) => Some((*relief, relief_size.clone())),
_ => None,
};
let relief = |app: &SolveApp| match &app.session.doc.features.last().map(|f| &f.kind) {
Some(FeatureKind::SheetFlange { corner_relief_shape, corner_relief, .. }) => Some((corner_relief_shape.clone(), corner_relief.clone())),
_ => None,
};
// Untouched, the dialog asks for nothing: the rule's relief.
assert_eq!(shown(&h.app), Some((0, String::new())));
let d = h.app.dialog.clone().unwrap();
let c = apply_commands(&h.app, &d).unwrap();
assert!(c[0].1.get("corner_relief_shape").is_none() && c[0].1.get("corner_relief").is_none(), "{}", c[0].1);
// Round from the selector, 6 mm in the size row.
h.step(&json!({"click": {"handle": "dialog:Corner Relief Shape"}})).unwrap();
h.step(&json!({"click": {"handle": "dialog:Corner Relief Shape:Round"}})).unwrap();
assert_eq!(shown(&h.app), Some((2, String::new())));
if let Some(Kind::Sheet(Sm::Flange { relief_size, .. })) = h.app.dialog.as_mut().map(|d| &mut d.kind) {
*relief_size = "6 mm".into();
}
let d = h.app.dialog.clone().unwrap();
let c = apply_commands(&h.app, &d).unwrap();
assert_eq!((&c[0].1["corner_relief_shape"], &c[0].1["corner_relief"]), (&json!("round"), &json!("6 mm")));
h.step(&json!({"widget": "OK"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
assert_eq!(relief(&h.app), Some((Some("round".into()), Some("6 mm".into()))));
assert!(h.app.session.model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{:?}", h.app.session.model.results);
let arcs = |app: &SolveApp| {
app.session
.model
.state()
.sheets
.last()
.map(|s| s.base.outer.segs.iter().filter(|g| matches!(g, solidcraft_engine::geom::Seg2::Arc { .. })).count())
};
assert_eq!(arcs(&h.app), Some(1), "the round is cut at the one corner the two flanges make");
// Edit Feature shows both again; Rule and an empty size take the feature back to the rule's relief.
let id = h.app.session.doc.features.last().map(|f| f.id).unwrap();
let mut e = edit_dialog(&h.app, id, None).unwrap();
assert!(matches!(&e.kind, Kind::Sheet(Sm::Flange { relief: 2, relief_size, .. }) if relief_size == "6 mm"));
let c = apply_commands(&h.app, &e).unwrap();
assert_eq!((&c[0].1["params"]["corner_relief_shape"], &c[0].1["params"]["corner_relief"]), (&json!("round"), &json!("6 mm")));
if let Kind::Sheet(Sm::Flange { relief, relief_size, .. }) = &mut e.kind {
*relief = 0;
relief_size.clear();
}
let commands = apply_commands(&h.app, &e).unwrap();
run_all(&mut h.app, commands);
assert_eq!(relief(&h.app), Some((None, None)));
assert_eq!(arcs(&h.app), Some(0));
}
#[test]
fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
let mut app = plate();