diff --git a/assets/locales/de.json b/assets/locales/de.json index b369b424..76960a33 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -600,6 +600,7 @@ "cmd.parameters_delete": "Parameter löschen", "cmd.parameters_export": "Parameter exportieren", "cmd.parameters_favorite": "Favorisierter Parameter", + "cmd.parameters_move": "Parameter verschieben", "cmd.parameters_graph": "Parametergraph", "cmd.parameters_import": "Parameter importieren", "cmd.parameters_list": "Parameter auflisten", diff --git a/assets/locales/en.json b/assets/locales/en.json index 43a426f8..f8810357 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -151,6 +151,7 @@ "cmd.parameters_delete": "Delete Parameter", "cmd.parameters_export": "Export Parameters", "cmd.parameters_favorite": "Favourite Parameter", + "cmd.parameters_move": "Move Parameter", "cmd.parameters_graph": "Parameter Graph", "cmd.parameters_import": "Import Parameters", "cmd.parameters_list": "List Parameters", diff --git a/assets/locales/fr.json b/assets/locales/fr.json index bf42e9bf..a2decd8c 100644 --- a/assets/locales/fr.json +++ b/assets/locales/fr.json @@ -141,6 +141,7 @@ "cmd.parameters_delete": "Supprimer le paramètre", "cmd.parameters_export": "Exporter les paramètres", "cmd.parameters_favorite": "Paramètre favori", + "cmd.parameters_move": "Déplacer le paramètre", "cmd.parameters_graph": "Graphe des paramètres", "cmd.parameters_import": "Importer des paramètres", "cmd.parameters_list": "Lister les paramètres", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index 2f8dac30..d5b04f5b 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -151,6 +151,7 @@ "cmd.parameters_delete": "パラメータを削除", "cmd.parameters_export": "パラメータをエクスポート", "cmd.parameters_favorite": "お気に入りパラメータ", + "cmd.parameters_move": "パラメータを移動", "cmd.parameters_graph": "パラメータ グラフ", "cmd.parameters_import": "パラメータをインポート", "cmd.parameters_list": "パラメータを一覧表示", diff --git a/assets/locales/ko.json b/assets/locales/ko.json index debb6431..f0e72e3f 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -600,6 +600,7 @@ "cmd.parameters_delete": "매개 변수 삭제", "cmd.parameters_export": "매개 변수 내보내기", "cmd.parameters_favorite": "즐겨찾는 매개 변수", + "cmd.parameters_move": "매개 변수 이동", "cmd.parameters_graph": "매개 변수 그래프", "cmd.parameters_import": "매개 변수 가져오기", "cmd.parameters_list": "매개 변수 목록", diff --git a/assets/locales/ru.json b/assets/locales/ru.json index 509e91d3..1cb1c169 100644 --- a/assets/locales/ru.json +++ b/assets/locales/ru.json @@ -140,6 +140,7 @@ "cmd.parameters_delete": "Удалить параметр", "cmd.parameters_export": "Экспортировать параметры", "cmd.parameters_favorite": "Избранный параметр", + "cmd.parameters_move": "Переместить параметр", "cmd.parameters_graph": "График параметра", "cmd.parameters_import": "Импортировать параметры", "cmd.parameters_list": "Список параметров", diff --git a/assets/locales/uk.json b/assets/locales/uk.json index d296994c..9c572138 100644 --- a/assets/locales/uk.json +++ b/assets/locales/uk.json @@ -141,6 +141,7 @@ "cmd.parameters_delete": "Видалити параметр", "cmd.parameters_export": "Експортувати параметри", "cmd.parameters_favorite": "Обраний параметр", + "cmd.parameters_move": "Перемістити параметр", "cmd.parameters_graph": "Граф параметрів", "cmd.parameters_import": "Імпортувати параметри", "cmd.parameters_list": "Список параметрів", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index 656a0459..478f2171 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -151,6 +151,7 @@ "cmd.parameters_delete": "删除参数", "cmd.parameters_export": "导出参数", "cmd.parameters_favorite": "收藏参数", + "cmd.parameters_move": "移动参数", "cmd.parameters_graph": "参数关系图", "cmd.parameters_import": "导入参数", "cmd.parameters_list": "列出参数", diff --git a/crates/doc/src/params.rs b/crates/doc/src/params.rs index ae18f4a0..0115397c 100644 --- a/crates/doc/src/params.rs +++ b/crates/doc/src/params.rs @@ -352,6 +352,25 @@ impl Document { rows } + /// Move user parameter `name` to just before (or with `after`, just after) user parameter + /// `target`: the order the Parameters dialog lists them in. Nothing else changes. + pub fn move_user_param(&mut self, name: &str, target: &str, after: bool) -> Result<()> { + fn user(params: &[crate::Param], n: &str) -> Option { + params.iter().position(|p| p.name == n && !p.model) + } + let (Some(from), Some(_)) = (user(&self.params, name), user(&self.params, target)) else { + let missing = if user(&self.params, name).is_none() { name } else { target }; + return Err(DocError::Unknown(format!("user parameter `{missing}`"))); + }; + if name == target { + return Err(DocError::Invalid("a parameter cannot move next to itself".into())); + } + let moved = self.params.remove(from); + let at = user(&self.params, target).map_or(self.params.len(), |i| if after { i + 1 } else { i }); + self.params.insert(at, moved); + Ok(()) + } + pub fn set_favorite(&mut self, name: &str, on: bool) -> Result<()> { if !self.name_taken(name) { return Err(DocError::Unknown(format!("parameter `{name}`"))); diff --git a/crates/engine/src/cmd/parameters.rs b/crates/engine/src/cmd/parameters.rs index aa3c2147..47756fe7 100644 --- a/crates/engine/src/cmd/parameters.rs +++ b/crates/engine/src/cmd/parameters.rs @@ -24,6 +24,8 @@ pub static COMMANDS: &[CommandSpec] = &[ .params("name, new_name (expressions, feature inputs and dimensions that use it follow)"), CommandSpec::new("parameters.delete", "Delete Parameter", delete).params("name (refused while something uses it; the error lists the users)"), CommandSpec::new("parameters.favorite", "Favourite Parameter", favorite).params("name, favorite?: bool (default true)"), + CommandSpec::new("parameters.move", "Move Parameter", move_param) + .params("name, before | after: another user parameter; the user parameter is listed just before or after it"), CommandSpec::new("parameters.comment", "Comment Parameter", comment).params("name, comment"), CommandSpec::new("parameters.users", "Parameter Users", users) .noundo() @@ -179,6 +181,19 @@ fn favorite(s: &mut Session, p: &Value) -> Result { Ok(json!({"name": name, "favorite": on})) } +fn move_param(s: &mut Session, p: &Value) -> Result { + let cmd = "parameters.move"; + let name = param_name(cmd, p)?.to_string(); + let (target, after) = match (str_(p, "before"), str_(p, "after")) { + (Some(t), None) => (t.to_string(), false), + (None, Some(t)) => (t.to_string(), true), + _ => return Err(bad(cmd, "give one of `before` or `after`: another user parameter")), + }; + s.doc_mut().move_user_param(&name, &target, after)?; + let order: Vec<&str> = s.doc.params.iter().filter(|q| !q.model).map(|q| q.name.as_str()).collect(); + Ok(json!({"name": name, "order": order})) +} + fn comment(s: &mut Session, p: &Value) -> Result { let cmd = "parameters.comment"; let name = param_name(cmd, p)?.to_string(); diff --git a/crates/engine/src/cmd/parameters_tests.rs b/crates/engine/src/cmd/parameters_tests.rs index a4b54aee..1b9601e4 100644 --- a/crates/engine/src/cmd/parameters_tests.rs +++ b/crates/engine/src/cmd/parameters_tests.rs @@ -544,3 +544,52 @@ fn switching_configurations_back_reuses_the_later_features() { assert!(rel(volume(&mut s), 40.0 * 30.0 * 10.0) < 1e-9); assert!(hits(&s) > before, "{r}"); } + +/// The user parameters' names in the order the Parameters dialog lists them. +fn user_order(s: &mut Session) -> Vec { + let v = run(s, "parameters.list", json!({"filter": "user"})); + v["parameters"].as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap().to_string()).collect() +} + +#[test] +fn user_parameters_reorder_with_undo() { + // #1187: move a user parameter before or after another; nothing else changes. + let mut s = Session::default(); + for (n, e) in [("a", "1 mm"), ("b", "2 mm"), ("c", "a + b")] { + run(&mut s, "parameters.add", json!({"name": n, "expression": e})); + } + assert_eq!(user_order(&mut s), ["a", "b", "c"]); + let r = run(&mut s, "parameters.move", json!({"name": "c", "before": "a"})); + assert_eq!(r["order"], json!(["c", "a", "b"])); + assert_eq!(user_order(&mut s), ["c", "a", "b"]); + run(&mut s, "parameters.move", json!({"name": "c", "after": "a"})); + assert_eq!(user_order(&mut s), ["a", "c", "b"]); + // Values and references are untouched; the order is kept in the design file. + let list = run(&mut s, "parameters.list", json!({"filter": "user"})); + let c = list["parameters"].as_array().unwrap().iter().find(|r| r["name"] == "c").unwrap().clone(); + assert_eq!(c["expression"], "a + b"); + assert!((c["value"].as_f64().unwrap() - 3.0).abs() < 1e-9, "{c}"); + let back = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()); + let names: Vec<&str> = back.doc.params.iter().filter(|p| !p.model).map(|p| p.name.as_str()).collect(); + assert_eq!(names, ["a", "c", "b"]); + run(&mut s, "edit.undo", json!({})); + assert_eq!(user_order(&mut s), ["c", "a", "b"]); +} + +#[test] +fn moving_a_parameter_refuses_bad_targets() { + let mut s = Session::default(); + run(&mut s, "parameters.add", json!({"name": "a", "expression": "1 mm"})); + run(&mut s, "parameters.add", json!({"name": "b", "expression": "2 mm"})); + let before = s.doc.to_json(); + for p in [ + json!({"name": "a", "before": "a"}), + json!({"name": "a", "before": "nope"}), + json!({"name": "nope", "before": "a"}), + json!({"name": "a"}), + json!({"name": "a", "before": "b", "after": "b"}), + ] { + err(&mut s, "parameters.move", p); + } + assert_eq!(s.doc.to_json(), before); +} diff --git a/crates/ui-egui/src/params_dialog.rs b/crates/ui-egui/src/params_dialog.rs index 906d102b..d2cc46e9 100644 --- a/crates/ui-egui/src/params_dialog.rs +++ b/crates/ui-egui/src/params_dialog.rs @@ -226,12 +226,30 @@ fn table(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut ParamsDialog, rows: &[&R ui.label(RichText::new(h).color(t.text_dim).small()); } ui.end_row(); - for r in rows { - // Favourite star. - let star = if r.favorite { RichText::new("★").color(t.accent) } else { RichText::new("☆").color(t.text_dim) }; - if ui.add(egui::Button::new(star).frame(false)).on_hover_text(crate::i18n::tr_label("Favourite")).clicked() { - d.error = app.run("parameters.favorite", json!({"name": r.name, "favorite": !r.favorite})).err(); - } + for (i, r) in rows.iter().enumerate() { + ui.horizontal(|ui| { + // Favourite star. + let star = if r.favorite { RichText::new("★").color(t.accent) } else { RichText::new("☆").color(t.text_dim) }; + if ui.add(egui::Button::new(star).frame(false)).on_hover_text(crate::i18n::tr_label("Favourite")).clicked() { + d.error = app.run("parameters.favorite", json!({"name": r.name, "favorite": !r.favorite})).err(); + } + // Up and down past the neighbouring user parameter (#1187), in the user table only: + // the favourites table lists favourites first whatever their order. + if salt == "user" { + for (glyph, tip, neighbour, side) in + [("▲", "Move up", i.checked_sub(1).and_then(|j| rows.get(j)), "before"), ("▼", "Move down", rows.get(i + 1), "after")] + { + let button = egui::Button::new(RichText::new(glyph).small().color(t.text_dim)).frame_when_inactive(false); + let resp = ui.add_enabled(neighbour.is_some(), button).on_hover_text(crate::i18n::tr_label(tip)); + crate::scenario::publish_handle(&format!("param:{}:{}", side, r.name), resp.rect.center()); + if resp.clicked() + && let Some(n) = neighbour + { + d.error = app.run("parameters.move", json!({"name": r.name, side: n.name})).err(); + } + } + } + }); // Name: double-click to rename. match &mut d.renaming { Some((old, new)) if *old == r.name => { diff --git a/crates/ui-egui/tests/parameter_order.rs b/crates/ui-egui/tests/parameter_order.rs new file mode 100644 index 00000000..57834441 --- /dev/null +++ b/crates/ui-egui/tests/parameter_order.rs @@ -0,0 +1,30 @@ +//! The Parameters dialog's ▲ / ▼ buttons reorder user parameters (#1187). +use serde_json::json; +use solidcraft_ui_egui::scenario::Harness; + +fn order(h: &mut Harness) -> Vec { + let v = h.app.session.execute("parameters.list", &json!({"filter": "user"})).unwrap(); + v["parameters"].as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap().to_string()).collect() +} + +#[test] +fn arrows_move_user_parameters_up_and_down() { + let mut h = Harness::new(); + for (n, e) in [("a", "1 mm"), ("b", "2 mm"), ("c", "3 mm")] { + h.app.session.execute("parameters.add", &json!({"name": n, "expression": e})).unwrap(); + } + h.step(&json!({"widget":"Parameters"})).unwrap(); + assert!(h.app.params.is_some()); + h.frames(3); + h.run(&json!([{"click":{"handle":"param:after:a"}}])).unwrap(); + h.frames(2); + assert_eq!(order(&mut h), ["b", "a", "c"]); + h.run(&json!([{"click":{"handle":"param:before:c"}}])).unwrap(); + h.frames(2); + assert_eq!(order(&mut h), ["b", "c", "a"]); + // The first row cannot move up, the last cannot move down. + h.run(&json!([{"click":{"handle":"param:before:b"}}, {"click":{"handle":"param:after:a"}}])).unwrap(); + h.frames(2); + assert_eq!(order(&mut h), ["b", "c", "a"]); + assert!(h.app.params.as_ref().unwrap().error.is_none()); +} diff --git a/docs/features.md b/docs/features.md index d624bc61..8c4a2335 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,6 +1,6 @@ # Features -SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **365 commands.** Planned (registered, not built yet): 4. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec ''`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md). +SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **367 commands.** Planned (registered, not built yet): 4. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec ''`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md). ## Sketch (99) @@ -315,7 +315,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm | Measure (I) | `inspect.measure` | SOLID › INSPECT | | Section Analysis | `inspect.section` | SOLID › INSPECT | -## Files (7) +## Files (8) | Command | Id | Toolbar | |---|---|---| @@ -325,7 +325,8 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm | Save (Ctrl+S) | `file.save` | – | | Save As | `file.save_as` | – | | Export | `file.export` | – | -| Save As Mesh | `file.save_mesh` | – | +| Save As Mesh | `file.save_mesh` | MESH › MODIFY | +| Installed Slicers | `file.slicers` | – | ## Documents (4) @@ -462,7 +463,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm | Command | Id | Toolbar | |---|---|---| -| Convert Mesh | `mesh.convert` | MESH › MODIFY | +| Convert Mesh | `mesh.convert` | – | ## `model.*` (3) @@ -491,7 +492,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm | Move Occurrence | `occurrence.move` | – | | Ground | `occurrence.ground` | – | -## `parameters.*` (12) +## `parameters.*` (13) | Command | Id | Toolbar | |---|---|---| @@ -501,6 +502,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm | Rename Parameter | `parameters.rename` | – | | Delete Parameter | `parameters.delete` | – | | Favourite Parameter | `parameters.favorite` | – | +| Move Parameter | `parameters.move` | – | | Comment Parameter | `parameters.comment` | – | | Parameter Users | `parameters.users` | – | | Parameter Graph | `parameters.graph` | – |