diff --git a/apps/solidcraft-web/src/main.rs b/apps/solidcraft-web/src/main.rs index 2d0f83e6..ac6fd240 100644 --- a/apps/solidcraft-web/src/main.rs +++ b/apps/solidcraft-web/src/main.rs @@ -465,6 +465,9 @@ mod web { app.session.mark_saved(); } if !query().contains("sample") { + // The empty design takes Units of new designs (#1205). The sample + // part is built in millimetres and is left that way. + solidcraft_ui_egui::prefs::apply_new_design(&mut app); let rev = app.session.revision; app.home.show(rev); } diff --git a/apps/solidcraft/src/main.rs b/apps/solidcraft/src/main.rs index ee4162fb..b64fe928 100644 --- a/apps/solidcraft/src/main.rs +++ b/apps/solidcraft/src/main.rs @@ -500,6 +500,11 @@ fn main() -> eframe::Result { for f in &files { app.open_path_async(f); } + // No file and no sample: the empty design takes Units of new designs (#1205). + // An opened file and the sample part keep the units they were saved or built with. + if files.is_empty() && !sample { + solidcraft_ui_egui::prefs::apply_new_design(&mut app); + } // No design given: the start page (not when driven over the control channel, whose // clients expect the design view; `ui.home` shows it there). if files.is_empty() && !sample && control_port.is_none() { diff --git a/crates/ui-egui/src/documents.rs b/crates/ui-egui/src/documents.rs index cacae7ad..7ab22a72 100644 --- a/crates/ui-egui/src/documents.rs +++ b/crates/ui-egui/src/documents.rs @@ -180,27 +180,37 @@ pub fn adopt(app: &mut SolveApp, session: Session) { } } -/// A new, empty design in its own tab. +/// A new, empty design in its own tab. One that is already empty keeps its tab and still +/// takes Units of new designs (#1205): a launch starts on that empty design, so returning +/// here used to leave it in millimetres. pub fn new_design(app: &mut SolveApp) { if app.file_picker_busy() { return; } app.home.open = false; if active_is_blank(app) { + crate::prefs::apply_new_design(app); return; } if let Err(e) = open_tab(app, Session::default()) { app.set_status(e, true); + } else { + crate::prefs::apply_new_design(app); } - crate::prefs::apply_new_design(app); } -/// Before opening a file: a fresh tab unless the active design is blank. +/// Before opening a file: a fresh tab unless the active design is blank. The empty design, +/// new or already showing, takes Units of new designs so a cancelled open is not millimetres +/// when the preference says otherwise. pub fn prepare_open(app: &mut SolveApp) { - if !active_is_blank(app) - && let Err(e) = open_tab(app, Session::default()) - { + if active_is_blank(app) { + crate::prefs::apply_new_design(app); + return; + } + if let Err(e) = open_tab(app, Session::default()) { app.set_status(e, true); + } else { + crate::prefs::apply_new_design(app); } } @@ -226,6 +236,7 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) { app.tool = None; app.session.replace_design(Session::default()); app.session.revision = rev + 1; + crate::prefs::apply_new_design(app); app.cam = Camera::default(); app.fit_view(); put_view(app, DocView::default()); @@ -788,6 +799,49 @@ mod tests { assert!(!a.session.auto_project, "a design adopted over a blank one"); } + /// #1205: Units of new designs apply to the empty design a launch shows, to New while that + /// design is still empty, to `file.new`, and to the blank design left when the last tab closes. + /// A design that already has features keeps its units. + #[test] + fn new_designs_use_the_preferred_units_including_a_blank_one() { + let mut a = app(); + a.preferences.default_units = "in".into(); + assert_eq!(a.session.doc.units, "mm"); + // What a launch with no file does after reading preferences. + crate::prefs::apply_new_design(&mut a); + assert_eq!(a.session.doc.units, "in"); + assert!(!a.session.is_dirty()); + assert!(active_is_blank(&a)); + let tabs = a.docs.count(); + new_design(&mut a); + assert_eq!(a.docs.count(), tabs, "New on an empty design does not open another tab"); + assert_eq!(a.session.doc.units, "in"); + assert!(!a.session.is_dirty()); + // Geometry already there is left alone. New then opens a tab in the preferred units. + a.run("solid.box", json!({"length": 25.4, "width": 25.4, "height": 25.4})).unwrap(); + assert_eq!(a.session.doc.units, "in"); + a.preferences.default_units = "ft".into(); + new_design(&mut a); + assert_eq!(a.docs.count(), tabs + 1); + assert_eq!(a.session.doc.units, "ft"); + assert!(a.session.doc.features.is_empty()); + assert!(!a.session.is_dirty()); + a.run("file.new", json!({"name": "From command"})).unwrap(); + assert_eq!(a.session.doc.units, "ft"); + assert_eq!(a.session.doc.name, "From command"); + assert!(!a.session.is_dirty()); + // Closing every tab leaves a blank design in the preferred units. + assert_eq!(a.docs.count(), 2); + let active = a.docs.active; + close(&mut a, active, true); + assert_eq!(a.docs.count(), 1); + assert!(!a.session.doc.features.is_empty(), "the other tab still has its part"); + close(&mut a, 0, true); + assert!(active_is_blank(&a)); + assert_eq!(a.session.doc.units, "ft"); + assert!(!a.session.is_dirty()); + } + /// A design built elsewhere replaces a blank one and gets a tab otherwise. #[test] fn adopt_reuses_a_blank_design() { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a7e383f7..ee5bfe71 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -582,6 +582,10 @@ impl SolveApp { if !(id == "view.navigate" && v.get("continuous").and_then(Value::as_bool) == Some(true)) { self.session.echo(language.format("ui.command_done", &[("command", label)])); } + if id == "file.new" { + // `file.new` starts from millimetres. Units of new designs still apply (#1205). + prefs::apply_new_design(self); + } if matches!(id, "file.new" | "doc.open" | "doc.recover") { render_workspace::reset_document(self); self.sketch_choices = sketch_families::Choices::default(); diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 871efa91..4c30b043 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -516,6 +516,8 @@ pub fn apply(app: &mut SolveApp, ctx: &egui::Context) { } /// A new, empty design takes the preferred units (not an undo step, not a change to save). +/// Callers: a launch with no file, New on a design that is already empty, `file.new`, and the +/// blank design left when the last tab closes. A design that already has features is unchanged. pub fn apply_new_design(app: &mut SolveApp) { if app.session.doc.features.is_empty() && app.session.doc.units != app.preferences.default_units { let saved = !app.session.is_dirty();