diff --git a/assets/locales/en.json b/assets/locales/en.json index 79c32e881b..7cc72175af 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -1366,6 +1366,7 @@ "settings.reset_intro": "These settings don't work on this computer and would keep SolidCraft from starting, so they were changed:", "settings.reset_title": "Settings were changed", "settings.supersampling_note": "Draws the view at a higher resolution and filters it down: smoother edges, lines and grid on screens below 4K. It costs GPU time: about 2.25× the pixels at 1.5× and 4× at 2×.", + "sheet.no_body": "there is no sheet metal body: start one with Flange > Base, or turn a plate into sheet metal with Convert to Sheet Metal (pick its large flat face)", "shortcut.json_error": "parameters must be JSON: {error}", "shortcut.no_command": "no command `{id}`; the text before the JSON must be a command id", "shortcut.pin": "Pin to the top of the S box", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index 86af4e4458..51cb6e5d0c 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -1366,6 +1366,7 @@ "settings.reset_intro": "次の設定はこのコンピューターでは使用できず、SolidCraft が起動できなくなるため変更されました:", "settings.reset_title": "設定が変更されました", "settings.supersampling_note": "ビューを高い解像度で描画してから縮小します。4K 未満の画面でエッジ、線、グリッドが滑らかになります。GPU の負荷が増えます(1.5× で約 2.25 倍、2× で 4 倍のピクセル)。", + "sheet.no_body": "板金ボディがありません。フランジ > Base で作成するか、板金に変換でプレートを板金に変換してください(広い平面を選択)。", "shortcut.json_error": "パラメータはJSON形式で指定してください: {error}", "shortcut.no_command": "コマンド`{id}`がありません。JSONの前のテキストはコマンドIDである必要があります", "shortcut.pin": "Sボックスの先頭にピン留め", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index 409307fe80..75ed0d74e6 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -1366,6 +1366,7 @@ "settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:", "settings.reset_title": "设置已更改", "settings.supersampling_note": "以更高分辨率绘制视图再缩小:在低于 4K 的屏幕上边、线和网格更平滑。会消耗 GPU 时间:1.5× 时约 2.25 倍像素,2× 时 4 倍。", + "sheet.no_body": "没有钣金实体:请使用法兰 > Base 创建,或使用转换为钣金将板材转换为钣金(选择其较大的平面)。", "shortcut.json_error": "参数必须采用 JSON 格式:{error}", "shortcut.no_command": "没有命令 `{id}`;JSON 前的文本必须是命令 ID", "shortcut.pin": "固定到 S 框顶部", diff --git a/crates/engine/src/cmd/sheet.rs b/crates/engine/src/cmd/sheet.rs index 9d693d3deb..fb01531518 100644 --- a/crates/engine/src/cmd/sheet.rs +++ b/crates/engine/src/cmd/sheet.rs @@ -206,6 +206,9 @@ fn flange(s: &mut Session, p: &Value) -> Result { if !matches!(position.as_str(), "inside" | "outside" | "middle") { return Err(bad(cmd, "`position` must be inside, outside or middle")); } + if s.model.state().sheets.is_empty() { + return Err(no_sheet_body(cmd)); + } let body = str_(p, "body").map(str::to_string); // The corner relief these flanges make, in place of the rule's; null is not given. let corner_relief_shape = match p.get("corner_relief_shape").filter(|v| !v.is_null()) { @@ -451,11 +454,18 @@ fn list_rules(s: &mut Session, _p: &Value) -> Result { Ok(json!({"rules": rules, "active": active})) } +fn no_sheet_body(cmd: &str) -> EngineError { + bad( + cmd, + "there is no sheet metal body: start one with Flange > Base, or turn a plate into sheet metal with Convert to Sheet Metal (pick its large flat face)", + ) +} + fn sheet_of(s: &Session, p: &Value, cmd: &str) -> Result { let st = s.model.state(); match str_(p, "body") { Some(b) => st.sheets.iter().find(|x| x.body == b).cloned().ok_or_else(|| bad(cmd, format!("`{b}` is not a sheet metal body"))), - None => st.sheets.last().cloned().ok_or_else(|| bad(cmd, "there is no sheet metal body")), + None => st.sheets.last().cloned().ok_or_else(|| no_sheet_body(cmd)), } } @@ -816,7 +826,7 @@ fn export_dxf(s: &mut Session, p: &Value) -> Result { } let sheets = &s.model.state().sheets; if sheets.is_empty() { - return Err(bad(cmd, "there is no sheet metal body")); + return Err(no_sheet_body(cmd)); } // Every file is made before the first is written. let texts = sheets diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index 174ed75922..797824ff3b 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -37,6 +37,27 @@ fn rectangle_tool(s: &mut Session, name: &str, p0: [f64; 2], p1: [f64; 2]) { run(s, "sketch.finish", json!({})); } +#[test] +fn plain_solid_sheet_operations_explain_how_to_start_sheet_metal() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 100, "width": 60, "height": 2.5})); + let before = s.doc.to_json(); + for (command, params) in [ + ("sheet.flange", json!({"type": "edge", "edges": [[50, 0, 2.5]], "height": 15})), + ("sheet.flat_pattern", json!({})), + ("sheet.export_dxf", json!({"per_body": true})), + ] { + let error = s.execute(command, ¶ms).unwrap_err(); + let crate::EngineError::BadParams { cmd, msg } = error else { panic!("{error}") }; + assert_eq!(cmd, command); + assert_eq!( + msg, + "there is no sheet metal body: start one with Flange > Base, or turn a plate into sheet metal with Convert to Sheet Metal (pick its large flat face)" + ); + assert_eq!(s.doc.to_json(), before); + } +} + #[test] fn side_edge_flanges_across_a_bend_keep_their_relief_through_rebuild_and_reopen() { use solidcraft_geom::Vec3; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 762879b809..22bea0b6f1 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2078,7 +2078,7 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut return; } ui.vertical(|ui| { - crate::ui_kit::text(ui, &error, crate::ui_kit::Text::Report("error", true)); + crate::ui_kit::text(ui, &shown_error(&error), crate::ui_kit::Text::Report("error", true)); let response = crate::ui_kit::button(ui, "Create Issue…", crate::ui_kit::Button::Normal); crate::scenario::publish_handle("dialog:create_issue", response.rect.center()); if response.clicked() { @@ -2088,6 +2088,14 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut } } +/// A refusal as the dialog shows it: without the engine's "invalid parameters for `id`: " +/// prefix (the dialog already names the command), and in the interface language when the +/// message is in the catalogs. +fn shown_error(error: &str) -> String { + let msg = error.strip_prefix("invalid parameters for `").and_then(|rest| rest.split_once("`: ")).map_or(error, |(_, msg)| msg); + crate::i18n::tr_label(msg).to_string() +} + /// Find the Boundary Fill cells again when the tool bodies changed (keeping the choices of /// cells that are still there). fn update_fill_cells(app: &SolveApp, d: &mut Dialog) { @@ -4944,6 +4952,15 @@ mod refused_ok_tests { assert_eq!(h.app.dialog.as_ref().and_then(|d| d.error.as_deref()), Some("hole depth must be positive")); } + #[test] + fn a_refusal_is_shown_without_the_engine_prefix() { + assert_eq!( + super::shown_error("invalid parameters for `sheet.flange`: there is no sheet metal body: start one with Flange > Base"), + "there is no sheet metal body: start one with Flange > Base" + ); + assert_eq!(super::shown_error("hole depth must be positive"), "hole depth must be positive"); + } + #[test] fn the_error_stays_while_the_inputs_do() { let mut h = hole_dialog(); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 33c18873bc..cc10d39814 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -500,6 +500,17 @@ mod tests { } } + #[test] + fn missing_sheet_metal_guidance_uses_the_message_catalog() { + let _scope = scope("ja"); + let message = "there is no sheet metal body: start one with Flange > Base, or turn a plate into sheet metal with Convert to Sheet Metal (pick its large flat face)"; + let error = format!("invalid parameters for `sheet.flange`: {message}"); + let translated = tr("sheet.no_body"); + assert_ne!(translated, "sheet.no_body"); + assert_ne!(translated, message); + assert_eq!(crate::friendly_error("sheet.flange", &error), format!("{}: {translated}", tr_label("Flange"))); + } + #[test] fn keys_and_engine_labels_translate_with_english_fallback() { let ja = Locale::for_language("ja"); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8c90ae6d8d..91ef3eb28b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1908,12 +1908,13 @@ pub fn command_label(id: &str) -> &str { solidcraft_engine::find_command(id).map_or(id, |c| c.label) } -/// An engine error with the command's label instead of its id: "Shell: offset must be …". The -/// label follows the interface language; the engine's sentence stays as the engine wrote it. +/// An engine error with the command's label instead of its id: "Shell: offset must be …". +/// The command label and catalogued engine messages follow the interface language. pub fn friendly_error(id: &str, e: &str) -> String { let label = i18n::tr_label(command_label(id)); let msg = e.replace(&format!("invalid parameters for `{id}`: "), "").replace(&format!("{id}: "), "").replace(&format!("`{id}`"), label); - if msg.starts_with(label) { msg } else { format!("{label}: {msg}") } + let msg = i18n::tr_label(&msg); + if msg.starts_with(label) { msg.to_string() } else { format!("{label}: {msg}") } } pub fn now_ms() -> f64 {