Parameters: move user parameters up and down (#1323)

Fixes #1187. New parameters.move {name, before|after} reorders user parameters (the design's own params order, so it is saved with no format change, and undoable); the Parameters dialog's user rows get up/down buttons. Label in every catalog. I merged main in. Checked on main + this branch: cargo check --workspace, the engine parameters tests, the UI parameter_order test, and docs/features.md regenerates unchanged.
This commit is contained in:
DuBearte authored and GitHub committed 2026-10-15 06:34:16 -04:00
1 parent d7a4a29ce3
commit 4dc638109b
14 files changed
+152 -11

No files matched your search

+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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": "パラメータを一覧表示",
+1
View File
@@ -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": "매개 변수 목록",
+1
View File
@@ -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": "Список параметров",
+1
View File
@@ -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": "Список параметрів",
+1
View File
@@ -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": "列出参数",
+19
View File
@@ -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<usize> {
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}`")));
+15
View File
@@ -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<Value> {
Ok(json!({"name": name, "favorite": on}))
}
fn move_param(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
let cmd = "parameters.comment";
let name = param_name(cmd, p)?.to_string();
+49
View File
@@ -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<String> {
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);
}
+24 -6
View File
@@ -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 => {
+30
View File
@@ -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<String> {
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());
}
+7 -5
View File
@@ -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 <id> '<json>'`, 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 <id> '<json>'`, 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` | – |