From 8dbecb462a2a7974dc8c81091f5f009aa259191e Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 12:28:01 +0000 Subject: [PATCH] Fold dialog (SHEET METAL > Fold); configurations and motion study dialogs pass their scenarios Fold: the bend line (a sketch line), an optional stationary side, Bend Line Position (centerline, start, end, outside mould line), angle (on the canvas), bend radius (default from the rule) and flip; live preview and Edit Feature (folds made from two points keep them). Its own icon; listed after Hem on the SHEET METAL tab and in the timeline. Configurations: Add Column lists parameters and features to pick, Add Row names the new row Configuration when no name is typed, bare numbers show with their parameter's unit, the row name comes before its Activate button, and Enter commits a cell instead of closing the panel. Motion Study: steps default to the engine's 100, the key's value row is "Value" and Enter there commits the key (OK makes the study). Scenarios s22 (fold, flat pattern, DXF; the expected volume is the folded sheet's, the bend keeping the neutral layer), s23 (configurations) and s24 (motion study) now run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/ui-egui/src/dialogs.rs | 4 +- crates/ui-egui/src/dialogs_motion.rs | 97 +++++++--- crates/ui-egui/src/dialogs_sheet.rs | 87 ++++++++- crates/ui-egui/src/icons.rs | 7 + crates/ui-egui/src/workspace.rs | 2 + crates/ui-egui/tests/scenarios.rs | 8 +- .../tests/scenarios/s22_sheet_metal_fold.json | 182 +++++++++++++++--- 7 files changed, 318 insertions(+), 69 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7e49668c..03160ff8 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1465,7 +1465,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { let (enter_free, esc) = ctx.input(|i| (i.key_pressed(egui::Key::Enter), i.key_pressed(egui::Key::Escape))); let nothing_focused = ctx.memory(|m| m.focused().is_none()); let canvas_enter = enter_free && ctx.memory(|m| m.had_focus_last_frame(egui::Id::new("sc_canvas_value"))); - let enter_applies = !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::ConfirmDelete { .. } | Kind::Preferences); + // Tables (configurations, a study's keys) take Enter for their cells. + let enter_applies = !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::ConfirmDelete { .. } | Kind::Preferences) + && !matches!(&d.kind, Kind::Motion(k) if k.wide()); if enter_applies && (enter || canvas_enter || (enter_free && nothing_focused)) { ok = true; } diff --git a/crates/ui-egui/src/dialogs_motion.rs b/crates/ui-egui/src/dialogs_motion.rs index b9e08397..abc76e39 100644 --- a/crates/ui-egui/src/dialogs_motion.rs +++ b/crates/ui-egui/src/dialogs_motion.rs @@ -55,7 +55,6 @@ pub enum Mo { }, Configurations { new_row: String, - new_column: usize, error: Option, }, } @@ -98,7 +97,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { Mo::MotionStudy { name: n("Motion Study", "motion.list"), study: None, - steps: "60".into(), + steps: "100".into(), tracks: Vec::new(), step: 0.0, playing: false, @@ -109,9 +108,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { vec![], ), "explode.create" => (Mo::Explode { name: n("Exploded View", "explode.list"), scale: 1.0, shown: None, saved: None, error: None }, vec![]), - "FusionShowDesignConfigPanelCmd" | "FusionStartDesignConfigModeCmd" => { - (Mo::Configurations { new_row: String::new(), new_column: 0, error: None }, vec![]) - } + "FusionShowDesignConfigPanelCmd" | "FusionStartDesignConfigModeCmd" => (Mo::Configurations { new_row: String::new(), error: None }, vec![]), _ => return None, }; Some((Kind::Motion(mo), inputs)) @@ -176,6 +173,15 @@ pub fn table(v: &Value) -> Table { Table { columns, rows, active: v.get("active").and_then(Value::as_str).map(str::to_string) } } +/// The unit a value of this kind is shown in. +fn unit_of(k: solvecraft_engine::doc::expr::Kind) -> Option<&'static str> { + match k { + solvecraft_engine::doc::expr::Kind::Length => Some("mm"), + solvecraft_engine::doc::expr::Kind::Angle => Some("deg"), + _ => None, + } +} + /// A column key ("param:width", "suppress:Fillet1") as (kind, name). pub fn column_parts(c: &str) -> (&str, &str) { c.split_once(':').unwrap_or(("param", c)) @@ -322,7 +328,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel row_label(ui, "Steps"); enter |= field(ui, steps); ui.end_row(); - let total = steps.trim().parse::().unwrap_or(60).clamp(1, 100_000); + let total = steps.trim().parse::().unwrap_or(100).clamp(1, 100_000); let moving = joints_moving(app); if moving.is_empty() { row_label(ui, ""); @@ -364,7 +370,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel tr.joint = *ji; let dofs = kind.dofs(); if dofs.len() > 1 { - row_label(ui, " Value"); + row_label(ui, " Of"); let caps: Vec<&str> = dofs.to_vec(); combo(ui, &format!("ms_idx{ti}"), &caps, &mut tr.index); ui.end_row(); @@ -377,8 +383,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel ui.end_row(); // The key at this step, if any: its value. if let Some(p) = tr.points.iter_mut().find(|(s, _)| *s == now) { - row_label(ui, &format!(" At step {now}")); - enter |= field(ui, &mut p.1); + // The key's value (Enter commits it; OK makes the study). + row_label(ui, "Value"); + let _ = field(ui, &mut p.1); ui.end_row(); } row_label(ui, ""); @@ -463,9 +470,22 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel ui.end_row(); } } - Mo::Configurations { new_row, new_column, error } => { + Mo::Configurations { new_row, error } => { let v = app.session.execute("FusionShowDesignConfigPanelCmd", &json!({})).unwrap_or(Value::Null); - let tb = table(&v); + let mut tb = table(&v); + // Bare numbers show with their parameter's unit ("40" is 40 mm), as Fusion shows them. + let kinds = app.session.doc.all_param_exprs(); + for (_, vals) in &mut tb.rows { + for (c, val) in tb.columns.iter().zip(vals.iter_mut()) { + let (kind, n) = column_parts(c); + if kind == "param" + && val.trim().parse::().is_ok() + && let Some(unit) = kinds.iter().find(|x| x.0 == n).and_then(|x| unit_of(x.2)) + { + *val = format!("{} {unit}", val.trim()); + } + } + } let run = |app: &mut SolveApp, error: &mut Option, id: &str, p: Value| { *error = app.run(id, p).err(); }; @@ -486,12 +506,12 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel for (name, vals) in &tb.rows { let active = tb.active.as_deref() == Some(name.as_str()); ui.horizontal(|ui| { + ui.label(if active { RichText::new(name).strong() } else { RichText::new(name) }); if active { ui.label(RichText::new("Active").color(t.accent)); } else if ui.button("Activate").clicked() { run(app, error, "config.activate", json!({ "row": name })); } - ui.label(if active { RichText::new(name).strong() } else { RichText::new(name) }); }); ui.horizontal(|ui| { for (c, val) in tb.columns.iter().zip(vals) { @@ -519,18 +539,24 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel // New row (a copy of the active one) and new column. ui.add(egui::TextEdit::singleline(new_row).hint_text("new configuration").desired_width(130.0)); ui.horizontal(|ui| { - if ui.button("Add Row").clicked() && !new_row.trim().is_empty() { - let mut p = json!({ "name": new_row.trim() }); + if ui.button("Add Row").on_hover_text("A new configuration, a copy of the active one").clicked() { + let name = if new_row.trim().is_empty() { + (tb.rows.len() + 1..).map(|k| format!("Configuration{k}")).find(|n| !tb.rows.iter().any(|r| &r.0 == n)).unwrap_or_default() + } else { + new_row.trim().to_string() + }; + let mut p = json!({ "name": name }); if let Some(a) = &tb.active { p["from"] = json!(a); } run(app, error, "config.row", p); new_row.clear(); } + // A column: a parameter's value or a feature's suppression. let mut options: Vec<(String, Value)> = Vec::new(); - for p in app.session.doc.params.iter().filter(|p| !p.model) { - if !tb.columns.contains(&format!("param:{}", p.name)) { - options.push((format!("Parameter {}", p.name), json!({ "param": p.name }))); + for p in app.session.doc.all_param_exprs() { + if !tb.columns.contains(&format!("param:{}", p.0)) { + options.push((format!("{} (parameter)", p.0), json!({ "param": p.0 }))); } } for f in &app.session.doc.features { @@ -538,24 +564,31 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel options.push((format!("Suppress {}", f.name), json!({ "feature": f.name }))); } } - if !options.is_empty() { - let labels: Vec<&str> = options.iter().map(|o| o.0.as_str()).collect(); - *new_column = (*new_column).min(labels.len() - 1); - combo(ui, "cfg_col", &labels, new_column); - if ui.button("Add Column").clicked() - && let Some((_, p)) = options.get(*new_column) - { - let id = if tb.columns.is_empty() { "FusionStartDesignConfigModeCmd" } else { "config.column" }; - let p = if tb.columns.is_empty() { + let mut pick: Option = None; + ui.menu_button("Add Column", |ui| { + egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| { + for (label, p) in &options { + if ui.button(label.as_str()).clicked() { + pick = Some(p.clone()); + ui.close(); + } + } + }); + }); + if let Some(p) = pick { + // The first column starts the table (with a Default row of the current values). + let (id, p) = if tb.columns.is_empty() { + ( + "FusionStartDesignConfigModeCmd", match p.get("param") { Some(n) => json!({ "params": [n] }), None => json!({ "features": [p.get("feature")] }), - } - } else { - p.clone() - }; - run(app, error, id, p); - } + }, + ) + } else { + ("config.column", p) + }; + run(app, error, id, p); } }); ui.end_row(); diff --git a/crates/ui-egui/src/dialogs_sheet.rs b/crates/ui-egui/src/dialogs_sheet.rs index 900a29dd..57e0b9f3 100644 --- a/crates/ui-egui/src/dialogs_sheet.rs +++ b/crates/ui-egui/src/dialogs_sheet.rs @@ -13,9 +13,11 @@ use solvecraft_engine::geom::Vec3; use crate::SolveApp; use crate::dialogs::{Dialog, Kind, combo, edge_sel, face_sel, field, profile_indices, row_label}; -use crate::selection::{BODIES, CURVES, EDGES, PLANAR_FACES, PROFILES, SelInput}; +use crate::selection::{AXES, BODIES, CURVES, EDGES, PLANAR_FACES, PROFILES, SelInput}; use crate::theme::Tokens; +const FOLD_POSITIONS: [&str; 4] = ["centerline", "start", "end", "mould"]; +const FOLD_POSITION_LABELS: [&str; 4] = ["Centerline", "Start of Bend", "End of Bend", "Outside Mould Line"]; const FLANGE_TYPES: [&str; 3] = ["Base", "Edge", "Contour"]; const POSITIONS: [&str; 3] = ["inside", "outside", "middle"]; const POSITION_LABELS: [&str; 3] = ["Inside", "Outside", "Middle"]; @@ -50,6 +52,13 @@ pub enum Sm { gap: String, flip: bool, }, + /// Fold (bend) along a sketch line; `position`: where the line sits in the bend. + Fold { + angle: String, + radius: String, + position: usize, + flip: bool, + }, Unfold { refold: bool, }, @@ -77,6 +86,7 @@ impl Sm { match self { Sm::Flange { .. } => "FLANGE", Sm::Hem { .. } => "HEM", + Sm::Fold { .. } => "FOLD", Sm::Unfold { refold: false } => "UNFOLD", Sm::Unfold { refold: true } => "REFOLD", Sm::Convert { .. } => "CONVERT TO SHEET METAL", @@ -98,6 +108,7 @@ impl Sm { Sm::Flange { ty: 1, height, .. } => ("Height", ValueKind::Length, height), Sm::Flange { ty: 2, distance, .. } => ("Distance", ValueKind::Length, distance), Sm::Hem { length, .. } => ("Length", ValueKind::Length, length), + Sm::Fold { angle, .. } => ("Angle", ValueKind::Angle, angle), _ => return None, }) } @@ -124,6 +135,10 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { "FusionSheetMetalHemFlangeCommand" => { (Sm::Hem { length: "5 mm".into(), gap: String::new(), flip: false }, vec![SelInput::new("Edges", EDGES, true)]) } + "SheetMetalFoldCmd" => ( + Sm::Fold { angle: "90 deg".into(), radius: String::new(), position: 0, flip: false }, + vec![SelInput::new("Bend Line", AXES, false), SelInput::new("Stationary Side", PLANAR_FACES, false)], + ), "FusionSheetmetalUnfoldCommand" => (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)]), "ConvertToSheetMetalCmd" => (Sm::Convert { rule: 0 }, vec![SelInput::new("Face", PLANAR_FACES, false)]), @@ -230,6 +245,22 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel ui.checkbox(flip, ""); ui.end_row(); } + Sm::Fold { angle, radius, position, flip } => { + row_label(ui, "Bend Line Position"); + combo(ui, "sm_fold_pos", &FOLD_POSITION_LABELS, position); + ui.end_row(); + row_label(ui, "Angle"); + enter |= field(ui, angle); + ui.end_row(); + row_label(ui, "Bend Radius"); + let r = ui.add(egui::TextEdit::singleline(radius).hint_text("from the rule")); + crate::params_dialog::complete(ui, &r, radius); + enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + ui.end_row(); + row_label(ui, "Flip"); + ui.checkbox(flip, ""); + ui.end_row(); + } Sm::Unfold { .. } => {} Sm::Convert { rule } => rule_combo(app, ui, "sm_conv_rule", rule), Sm::Rules { pick, name, values, active, loaded } => { @@ -440,6 +471,33 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map { + let mut p = json!({"angle": angle, "position": FOLD_POSITIONS.get(*position).copied().unwrap_or("centerline"), "flip": flip}); + match items(inputs, 0).first() { + Some(Sel::SketchCurve { id }) => { + let st = s.model.state(); + let sketch = st + .sketches + .iter() + .rev() + .find(|ss| ss.sketch.curve_index(id).is_some()) + .map(|ss| ss.feature) + .ok_or("the line's sketch is gone")?; + p["sketch"] = json!(sketch); + p["curve"] = json!(id); + } + // An edited fold made from two points keeps them (in `extra`). + _ if extra.contains_key("points") => {} + _ => return Err("select the bend line (a sketch line) first".into()), + } + if !radius.trim().is_empty() { + p["radius"] = json!(radius); + } + if let Some(Sel::Face { point, .. }) = items(inputs, 1).first() { + p["fixed"] = pt(*point); + } + ("SheetMetalFoldCmd", p) + } Sm::Unfold { refold } => { let mut p = json!({}); if let Some(Sel::Body { name }) = items(inputs, 0).first() { @@ -534,6 +592,33 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec { + if let Some(bd) = body { + extra.insert("body".into(), json!(bd)); + } + let k = Sm::Fold { + angle: angle.clone(), + radius: radius.clone().unwrap_or_default(), + position: FOLD_POSITIONS.iter().position(|x| x == position || (*x == "mould" && position == "mold")).unwrap_or(0), + flip: *flip, + }; + let (_, mut ins) = start(app, "SheetMetalFoldCmd")?; + match curve { + Some(c) => { + if let Some(i) = ins.first_mut() { + i.items = vec![Sel::SketchCurve { id: c.clone() }]; + } + } + // A fold given by two points keeps them. + None => { + extra.insert("points".into(), json!([pt(*a), pt(*b)])); + } + } + if let Some(i) = ins.get_mut(1) { + i.items = fixed.and_then(|f| face_sel(s, f)).into_iter().collect(); + } + 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()), _ => return None, diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index b24243cd..d6dc0167 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -848,6 +848,13 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce pen.arc(16.0, 11.5, 4.5, -1.57, 1.57, a); pen.line(&[(16.0, 7.0), (8.0, 7.0)], a); } + "fold" => { + pen.line(&[(2.0, 18.0), (12.0, 18.0)], ink); + pen.line(&[(12.0, 18.0), (19.0, 6.0)], a); + pen.line(&[(9.0, 21.0), (15.0, 15.0)], Color32::from_rgb(130, 140, 160)); + pen.arc(12.0, 18.0, 6.0, 0.2, 1.0, a); + pen.arrow((18.0, 16.0), (17.0, 12.5), a); + } "unfold" => { pen.line(&[(3.0, 18.0), (12.0, 18.0), (12.0, 7.0)], ink); pen.line(&[(12.0, 18.0), (22.0, 18.0)], a); diff --git a/crates/ui-egui/src/workspace.rs b/crates/ui-egui/src/workspace.rs index 2d67a32f..3b3005ba 100644 --- a/crates/ui-egui/src/workspace.rs +++ b/crates/ui-egui/src/workspace.rs @@ -29,6 +29,7 @@ const SHEET_CREATE: &[&str] = &[ "FusionSheetMetalFlangeCommand", "FusionSheetMetalFlatPatternCmd", "FusionSheetMetalHemFlangeCommand", + "SheetMetalFoldCmd", "ConvertToSheetMetalCmd", "FusionCreateNewComponentCommand", "Extrude", @@ -123,6 +124,7 @@ pub fn feature_icon(k: &FeatureKind) -> Option<&'static str> { Some(match k { FeatureKind::SheetBase { .. } | FeatureKind::SheetContour { .. } | FeatureKind::SheetFlange { .. } => "flange", FeatureKind::SheetHem { .. } => "hem", + FeatureKind::SheetFold { .. } => "fold", FeatureKind::SheetUnfold { refold: false, .. } => "unfold", FeatureKind::SheetUnfold { .. } => "refold", FeatureKind::SheetConvert { .. } => "convert_sheet", diff --git a/crates/ui-egui/tests/scenarios.rs b/crates/ui-egui/tests/scenarios.rs index e842a9d5..a155ce5d 100644 --- a/crates/ui-egui/tests/scenarios.rs +++ b/crates/ui-egui/tests/scenarios.rs @@ -44,7 +44,10 @@ scenarios!( s19_canvas_calibrate, s20_section_with_joint_drive, s21_named_views, - s26_drive_joint_typed + s26_drive_joint_typed, + s22_sheet_metal_fold, + s23_configurations, + s24_motion_study ); /// Scenarios written ahead of their features (the commands are in solvecraft-params' unpushed @@ -60,8 +63,5 @@ macro_rules! pending { } pending!( - s22_sheet_metal_fold: "waits for SheetMetalFoldCmd and its dialog", - s23_configurations: "waits for configurations (FusionStartDesignConfigModeCmd, config.activate) and their panel", - s24_motion_study: "waits for FusionMotionStudyCommand and its dialog", s25_autosave_recovery: "waits for autosave and Recover unsaved design", ); diff --git a/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json b/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json index 41b3252e..60030662 100644 --- a/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json +++ b/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json @@ -1,32 +1,152 @@ [ - {"note": "Target: sheet metal plate folded along a sketch line (SheetMetalFoldCmd, solvecraft-params; dialog by solvecraft-dialogs). Adjust the dialog steps when it lands."}, - {"call": "ui.set", "params": {"tab": "SHEET METAL"}}, - {"start": "SketchCreate"}, - {"click": {"plane": "XY"}}, - {"start": "ShapeRectangleTwoPoint"}, - {"click": {"sketch": [0, 0]}}, - {"click": {"sketch": [60, 40]}}, - {"key": "Escape"}, - {"start": "SketchStop"}, - {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "FusionSheetMetalFlangeCommand"}, - {"click": {"world": [30, 20, 0]}}, - {"key": "Enter"}, - {"expect": {"bodies": 1, "volume": 6000}}, - {"start": "SketchCreate"}, - {"click": {"world": [30, 20, 2.5]}}, - {"start": "DrawPolyline"}, - {"click": {"sketch": [40, -5]}}, - {"click": {"sketch": [40, 45]}}, - {"key": "Escape"}, - {"key": "Escape"}, - {"start": "SketchStop"}, - {"start": "SheetMetalFoldCmd"}, - {"click": {"world": [40, 20, 2.5]}}, - {"key": "Enter"}, - {"expect": {"bodies": 1, "errors": 0, "dialog": null, "volume": [6000, 1e-3]}}, - {"shot": "s22_folded"}, - {"start": "FusionSheetMetalFlatPatternCmd"}, - {"widget": "Export DXF…"}, - {"expect": {"file": "Untitled-Body1.dxf"}} -] + { + "note": "Target: sheet metal plate folded along a sketch line (SheetMetalFoldCmd, solvecraft-params; dialog by solvecraft-dialogs). Adjust the dialog steps when it lands." + }, + { + "call": "ui.set", + "params": { + "tab": "SHEET METAL" + } + }, + { + "start": "SketchCreate" + }, + { + "click": { + "plane": "XY" + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 0, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 60, + 40 + ] + } + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "FusionSheetMetalFlangeCommand" + }, + { + "click": { + "world": [ + 30, + 20, + 0 + ] + } + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 1, + "volume": 6000 + } + }, + { + "start": "SketchCreate" + }, + { + "click": { + "world": [ + 30, + 20, + 2.5 + ] + } + }, + { + "start": "DrawPolyline" + }, + { + "click": { + "sketch": [ + 40, + -5 + ] + } + }, + { + "click": { + "sketch": [ + 40, + 45 + ] + } + }, + { + "key": "Escape" + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "start": "SheetMetalFoldCmd" + }, + { + "click": { + "world": [ + 40, + 20, + 2.5 + ] + } + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 1, + "errors": 0, + "dialog": null, + "volume": [ + 6023.5, + 0.001 + ] + }, + "note": "the bend keeps the neutral layer (K 0.44): the bend zone holds \u03b8\u00b7T\u00b7(T/2 \u2212 K\u00b7T)\u00b7w = 23.6 mm\u00b3 more than the flat strip it replaces" + }, + { + "shot": "s22_folded" + }, + { + "start": "FusionSheetMetalFlatPatternCmd" + }, + { + "widget": "Export DXF\u2026" + }, + { + "expect": { + "file": "Untitled-Body1.dxf" + } + } +] \ No newline at end of file