From af2f09fac20de2a532d490229dbcd294ba0bd1a5 Mon Sep 17 00:00:00 2001 From: Raymond Thurman Date: Thu, 15 Oct 2026 00:20:02 -0500 Subject: [PATCH] A new empty design takes Units of new designs (#1283) The empty design shown at launch, New on a design that is still empty, file.new and the blank design left when the last tab closes all take the Units of new designs preference. Opened files and the sample part keep their own units. Fixes #1205. Co-authored-by: raythurman2386 --- apps/solidcraft-web/src/main.rs | 3 ++ apps/solidcraft/src/main.rs | 5 +++ crates/ui-egui/src/documents.rs | 66 ++++++++++++++++++++++++++++++--- crates/ui-egui/src/lib.rs | 4 ++ crates/ui-egui/src/prefs.rs | 2 + 5 files changed, 74 insertions(+), 6 deletions(-) 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();