From 2886399215df0d3210a7d6ad1f9db271626e3aa8 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 11:15:48 +0000 Subject: [PATCH] Bare lengths typed into features (commands and parameter edits) mean the design's units: they are stored with the unit written in (2 in an inch design is "2 in"); lengths with units, angles and mm designs are unchanged Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/doc/src/params.rs | 16 +++++++++++++++- crates/engine/src/cmd/features.rs | 7 +++++++ crates/engine/src/tests.rs | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/params.rs b/crates/doc/src/params.rs index 41443e15..890ac4f1 100644 --- a/crates/doc/src/params.rs +++ b/crates/doc/src/params.rs @@ -326,6 +326,18 @@ impl Document { } /// Change a parameter: a stored one, or a feature input by its name. + /// A typed length that is a bare number means the design's units: it gets that unit written + /// in (feature inputs are stored in mm otherwise). Anything else is returned unchanged. + pub fn with_design_unit(&self, e: &str, kind: Kind) -> String { + let u = self.units.trim(); + let bare = expr::is_literal(e) && e.trim().trim_start_matches(['-', '+']).trim().parse::().is_ok(); + if kind == Kind::Length && bare && !u.is_empty() && u != "mm" && expr::unit_info(u).is_some_and(|(k, _)| k == Kind::Length) { + format!("{} {u}", e.trim()) + } else { + e.to_string() + } + } + pub fn change_param(&mut self, name: &str, expr_s: &str, unit: Option<&str>, comment: Option<&str>) -> Result<()> { let hit = self.features.iter().enumerate().find_map(|(i, f)| f.param_names.iter().position(|n| n == name).map(|k| (i, k))); let Some((i, k)) = hit else { return self.set_param(name, expr_s, unit, comment) }; @@ -333,10 +345,12 @@ impl Document { return Err(DocError::Expr("expression too long".into())); } let mut next = self.clone(); + let kind = self.features.get(i).and_then(|f| f.kind.inputs().get(k).map(|x| x.2)).unwrap_or(Kind::Unitless); + let expr_s = self.with_design_unit(expr_s, kind); if let Some(f) = next.features.get_mut(i) && let Some((_, e, _)) = f.kind.inputs_mut().into_iter().nth(k) { - *e = expr_s.to_string(); + *e = expr_s.clone(); } if let Some(c) = comment { next.param_comments.insert(name.to_string(), c.to_string()); diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 4796a416..527c02fe 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -243,6 +243,13 @@ pub(super) fn add_feature(s: &mut Session, p: &Value, kind: FeatureKind) -> Resu // Commands speak world coordinates; the feature is authored in the active component. let mut kind = kind; super::component::to_active_frame(s, &mut kind); + // Bare lengths typed in an inch (or cm…) design mean that unit. + if s.doc.units.trim() != "mm" { + let doc = s.doc.clone(); + for (_, e, k) in kind.inputs_mut() { + *e = doc.with_design_unit(e, k); + } + } // Blends remember where their edges sat, to find them again after upstream edits. let (refs, names) = match &kind { FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 7451bbb0..aaaf3052 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1387,6 +1387,21 @@ fn tapped_and_clearance_holes() { assert!(rel(v2 - volume(&mut s), PI * 2.15 * 2.15 * 10.0) < 1e-3); assert!(s.execute("FusionHoleCommand", &json!({"position": [20, 25, 10], "clearance": "M7"})).is_err()); assert!(s.execute("FusionHoleCommand", &json!({"position": [20, 25, 10]})).is_err()); +/// In an inch design, a bare length typed into a feature means inches (stored with the unit); +/// lengths with units and angles are left alone. +#[test] +fn bare_lengths_take_the_design_units() { + let mut s = Session::default(); + s.doc_mut().units = "in".into(); + run(&mut s, "PrimitiveBox", json!({"length": 2, "width": "10 mm", "height": 1})); + let m = run(&mut s, "MeasureCommand", json!({})); + let max = |i: usize| m["bodies"][0]["bbox"]["max"][i].as_f64().unwrap_or(0.0); + assert!((max(0) - 50.8).abs() < 1e-9 && (max(1) - 10.0).abs() < 1e-9 && (max(2) - 25.4).abs() < 1e-9, "{m}"); + let d = s.doc.features[0].param_names[2].clone(); + run(&mut s, "ChangeParameterCommand", json!({"name": d, "expression": "2"})); + let m = run(&mut s, "MeasureCommand", json!({})); + assert!((m["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 50.8).abs() < 1e-9); + assert_eq!(s.doc.features[0].kind.inputs()[2].1, "2 in"); } /// Press Pull on a fillet's face changes its radius; on another curved face it offsets it.