From fde62a39ab36c6870fc7eaf05d4c6767e23401fb Mon Sep 17 00:00:00 2001 From: Richard Sauer <60841910+aussieboer61@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:54:51 +1000 Subject: [PATCH] MEASUREINIT: choose the units of new drawings, kept in the profile (#1792) * MEASUREINIT: choose the units of new drawings, kept in the profile NEW, QNEW and the first drawing at startup were always imperial unless a template or {"metric": true} was given, and nothing kept a metric choice across restarts. MEASUREINIT (0 imperial, the default; 1 metric) is now a profile variable: NEW without `metric` or a template follows it, the app's startup drawing follows it, and Options > Files shows it as "Units of new drawings". QNEWTEMPLATE and an explicit `metric` still win. Part of #1182. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK * contributors: add Richard Sauer (aussieboer61) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK --------- Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: Brandon Thomas --- apps/cadcraft/src/main.rs | 4 ++- contributors/people.toml | 4 +++ crates/engine/src/cmd/file.rs | 5 +-- crates/engine/src/cmd/options.rs | 1 + crates/engine/src/lib.rs | 4 +++ crates/engine/src/sysvars.rs | 3 ++ crates/engine/tests/measureinit.rs | 51 ++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/uk.tsv | 4 +++ crates/ui-egui/src/options/dialog.rs | 13 +++++++ 9 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 crates/engine/tests/measureinit.rs diff --git a/apps/cadcraft/src/main.rs b/apps/cadcraft/src/main.rs index 157294ac..50131191 100644 --- a/apps/cadcraft/src/main.rs +++ b/apps/cadcraft/src/main.rs @@ -357,7 +357,9 @@ fn main() -> eframe::Result { app.open_path(f); } if app.session.docs.is_empty() { - app.session.new_drawing(false); + // In the units the profile asks for (MEASUREINIT, restored above). + let metric = app.session.settings.measureinit; + app.session.new_drawing(metric); } let ctx = cc.egui_ctx.clone(); let desktop_theme = desktop_theme::DesktopTheme::start(move || ctx.request_repaint()); diff --git a/contributors/people.toml b/contributors/people.toml index 121a5d6f..d9b96ae7 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -80,3 +80,7 @@ sync_github_name = true real_name = "Rodrigo Gressler" display_name = "Rodrigo" sync_github_name = false + +[people.aussieboer61] +real_name = "Richard Sauer" +display_name = "Richard Sauer" diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index b6998f42..3bb9f870 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -163,7 +163,7 @@ pub fn specs() -> Vec { .menu(&["File", "New Drawing..."]) .key("Cmd+N") .alias(&["qnew"]) - .params("{metric?: bool, template?: path to a .dwt/.dxf template (\".\" for none; default QNEWTEMPLATE)}") + .params("{metric?: bool (default MEASUREINIT), template?: path to a .dwt/.dxf template (\".\" for none; default QNEWTEMPLATE)}") .enabled(always) .noundo(), CommandSpec::new("open", "Open...", run_open) @@ -252,7 +252,8 @@ fn run_new(s: &mut Session, p: &Value) -> Result { if let Some(r) = template::run_new(s, p)? { return Ok(r); } - let i = s.new_drawing(bool_or(p, "metric", false)); + let metric = s.settings.measureinit; + let i = s.new_drawing(bool_or(p, "metric", metric)); Ok(json!({ "index": i, "title": s.docs.get(i).map(|d| d.title.clone()) })) } diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs index 15bdf5bf..9dbeeddb 100644 --- a/crates/engine/src/cmd/options.rs +++ b/crates/engine/src/cmd/options.rs @@ -52,6 +52,7 @@ pub const PROFILE_VARS: &[&str] = &[ "PICKADD", "PICKDRAG", "MAXARRAY", + "MEASUREINIT", ]; /// The most profile entries [`restore`] reads (a saved profile is untrusted input). diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 9435e429..ab9c0285 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -359,6 +359,9 @@ pub struct Settings { pub gridmajor: u32, /// ZOOMFACTOR: how far one mouse wheel step zooms (3..=100). pub zoomfactor: u32, + /// MEASUREINIT: the units of a new drawing made without a template (NEW, QNEW and the first + /// drawing at startup): `false` (0, the default) imperial, `true` (1) metric. + pub measureinit: bool, /// -VIEW sEttings > Layer snapshot: named views save the layer settings with them. pub view_layer_snapshot: bool, pub dynmode: bool, @@ -477,6 +480,7 @@ impl Default for Settings { gridunit: Vec2::new(0.5, 0.5), gridmajor: 5, zoomfactor: 60, + measureinit: false, view_layer_snapshot: true, dynmode: true, dyndigrip: 31, diff --git a/crates/engine/src/sysvars.rs b/crates/engine/src/sysvars.rs index 89bde769..08f68465 100644 --- a/crates/engine/src/sysvars.rs +++ b/crates/engine/src/sysvars.rs @@ -64,6 +64,7 @@ const SESSION_VARS: &[&str] = &[ "CURSORSIZE", "MAXARRAY", "ZOOMFACTOR", + "MEASUREINIT", "LASTPOINT", "FONTALT", "FONTFALLBACK", @@ -140,6 +141,7 @@ pub fn get(s: &Session, name: &str) -> Option { "CURSORSIZE" => json!(st.cursorsize), "MAXARRAY" => json!(st.maxarray), "ZOOMFACTOR" => json!(st.zoomfactor), + "MEASUREINIT" => json!(i32::from(st.measureinit)), "LASTPOINT" => json!([s.last_point.x, s.last_point.y, 0.0]), // Held in radians (`$ANGBASE` in DXF is in degrees); reported in degrees, like POLARANG. "ANGBASE" => json!(s.doc().ok()?.header.f64("ANGBASE", 0.0).to_degrees()), @@ -355,6 +357,7 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> { "CURSORSIZE" => st.cursorsize = as_f64(v).ok_or_else(bad)?.clamp(1.0, 100.0), "MAXARRAY" => st.maxarray = as_f64(v).ok_or_else(bad)?.clamp(100.0, 10_000_000.0) as u64, "ZOOMFACTOR" => st.zoomfactor = as_i(v).filter(|z| (3..=100).contains(z)).ok_or_else(bad)? as u32, + "MEASUREINIT" => st.measureinit = small(v, 1).ok_or_else(bad)? == 1, "FONTALT" => cadcraft_fonts::ttf::set_font_alt(v.as_str().ok_or_else(bad)?), "FONTFALLBACK" => cadcraft_fonts::ttf::set_fallback_fonts(v.as_str().ok_or_else(bad)?), "SAVETIME" => crate::autosave::set_savetime( diff --git a/crates/engine/tests/measureinit.rs b/crates/engine/tests/measureinit.rs new file mode 100644 index 00000000..8ba4713a --- /dev/null +++ b/crates/engine/tests/measureinit.rs @@ -0,0 +1,51 @@ +//! MEASUREINIT: the units of a new drawing made without a template (issue #1182). It is a profile +//! setting, so a metric choice survives restarts; an explicit `metric` or template still wins. + +use cadcraft_engine::Session; +use serde_json::json; + +fn var(s: &mut Session, name: &str) -> serde_json::Value { + s.execute("getvar", &json!({ "name": name })).unwrap()["value"].clone() +} + +#[test] +fn imperial_by_default() { + let mut s = Session::empty(); + assert_eq!(var(&mut s, "MEASUREINIT"), json!(0)); + s.execute("new", &json!({})).unwrap(); + assert_eq!(var(&mut s, "INSUNITS"), json!(1)); + assert_eq!(var(&mut s, "MEASUREMENT"), json!(0)); +} + +#[test] +fn new_follows_measureinit() { + let mut s = Session::empty(); + s.execute("options", &json!({ "MEASUREINIT": 1 })).unwrap(); + s.execute("new", &json!({})).unwrap(); + assert_eq!(var(&mut s, "INSUNITS"), json!(4)); + assert_eq!(var(&mut s, "MEASUREMENT"), json!(1)); + assert_eq!(var(&mut s, "DIMSTYLE"), json!("ISO-25")); + // An explicit choice still wins. + s.execute("new", &json!({ "metric": false })).unwrap(); + assert_eq!(var(&mut s, "INSUNITS"), json!(1)); +} + +#[test] +fn only_zero_or_one() { + let mut s = Session::empty(); + for bad in [json!(2), json!(-1), json!("metric")] { + assert!(s.execute("options", &json!({ "MEASUREINIT": bad })).is_err(), "{bad}"); + } + assert_eq!(var(&mut s, "MEASUREINIT"), json!(0)); +} + +#[test] +fn kept_in_the_profile() { + let mut s = Session::empty(); + s.execute("options", &json!({ "MEASUREINIT": 1 })).unwrap(); + let saved = cadcraft_engine::cmd::options::profile(&s); + assert_eq!(saved["MEASUREINIT"], json!(1)); + let mut restarted = Session::empty(); + cadcraft_engine::cmd::options::restore(&mut restarted, &saved); + assert_eq!(var(&mut restarted, "MEASUREINIT"), json!(1)); +} diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 73c0f3c6..70ec74cd 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -980,6 +980,10 @@ Reset keeps the interface language. Скидання не змінює мову інтерфейсу. Reset to Defaults Скинути до типових Save new drawings as Зберігати нові креслення як + Units of new drawings Одиниці нових креслень + Imperial (inches) Імперські (дюйми) + Metric (millimetres) Метричні (міліметри) + New and the first drawing at startup use these units unless a default template is set (QNEWTEMPLATE). Команда «Створити» і перше креслення під час запуску використовують ці одиниці, якщо не задано типовий шаблон (QNEWTEMPLATE). Screen magnifiers, screen readers and recorders follow the system cursor. Екранні лупи, програми читання з екрана та запису стежать за системним курсором. Select objects first, then choose a command Спочатку вибирати об'єкти, потім команду Selection Вибір diff --git a/crates/ui-egui/src/options/dialog.rs b/crates/ui-egui/src/options/dialog.rs index 66367038..7c0a22af 100644 --- a/crates/ui-egui/src/options/dialog.rs +++ b/crates/ui-egui/src/options/dialog.rs @@ -233,6 +233,18 @@ fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map) { } ui.end_row(); + ui.label(crate::tl!("Units of new drawings")); + ui.horizontal(|ui| { + let metric = app.session.settings.measureinit; + if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric { + changes.insert("MEASUREINIT".into(), json!(0)); + } + if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric { + changes.insert("MEASUREINIT".into(), json!(1)); + } + }); + ui.end_row(); + ui.label(crate::tl!("Automatic save every")); let mut minutes = cadcraft_engine::autosave::savetime(); let max = cadcraft_engine::autosave::MAX_SAVETIME; @@ -258,6 +270,7 @@ fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map) { } ui.end_row(); }); + hint(ui, crate::tl!("New and the first drawing at startup use these units unless a default template is set (QNEWTEMPLATE).")); hint(ui, crate::tl!("A drawing opened from a file keeps that file's format.")); hint(ui, crate::tl!("Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off.")); }