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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK

* contributors: add Richard Sauer (aussieboer61)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Richard Sauer
2026-10-24 01:54:51 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 7db656fdc7
commit fde62a39ab
9 changed files with 86 additions and 3 deletions
+3 -2
View File
@@ -163,7 +163,7 @@ pub fn specs() -> Vec<CommandSpec> {
.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<Value> {
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()) }))
}
+1
View File
@@ -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).
+4
View File
@@ -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,
+3
View File
@@ -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<Value> {
"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(
+51
View File
@@ -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));
}