Unit-less parameters used as lengths read in the design's units (feature evaluation and parameter values; bare numbers stored in inputs stay mm); document.units checks what the geometry reads (Document::length_inputs) and writes the old unit (with it as the parameter's unit) into bare unit-less parameters that would move; cached results keyed on the design's units
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
e30309be0d
commit
83a10c813a
4 files changed
+72
-14
No files matched your search
+19
-1
@@ -338,6 +338,7 @@ fn fingerprint(prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_ba
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
prev.hash(&mut h);
|
||||
rolled_back.hash(&mut h);
|
||||
vals.get(DESIGN_LEN).map(|v| v.v.to_bits()).hash(&mut h);
|
||||
serde_json::to_string(f).unwrap_or_default().hash(&mut h);
|
||||
for n in param_names(f) {
|
||||
n.hash(&mut h);
|
||||
@@ -360,6 +361,7 @@ fn context_digest(doc: &Document, vals: &BTreeMap<String, Value>) -> u64 {
|
||||
for f in doc.features.iter().filter(|f| matches!(f.kind, FeatureKind::ConstructionPlane { .. })) {
|
||||
fingerprint(0, f, vals, false).hash(&mut h);
|
||||
}
|
||||
doc.units.hash(&mut h);
|
||||
serde_json::to_string(&doc.sheet).unwrap_or_default().hash(&mut h);
|
||||
serde_json::to_string(&doc.plastic).unwrap_or_default().hash(&mut h);
|
||||
serde_json::to_string(&doc.components).unwrap_or_default().hash(&mut h);
|
||||
@@ -429,7 +431,12 @@ impl Model {
|
||||
|
||||
/// Re-evaluate the document, reusing results up to the first changed feature.
|
||||
pub fn evaluate(&mut self, doc: &Document) {
|
||||
let (vals, perr) = doc.param_values();
|
||||
let (mut vals, perr) = doc.param_values();
|
||||
if let Some((Kind::Length, scale)) = expr::unit_info(doc.units.trim())
|
||||
&& scale != 1.0
|
||||
{
|
||||
vals.insert(DESIGN_LEN.into(), Value { v: scale, len: 0, ang: 0 });
|
||||
}
|
||||
self.param_errors = perr;
|
||||
let mut prev_fp = 0u64;
|
||||
let mut state = self.empty.clone();
|
||||
@@ -586,7 +593,18 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Key of the design's length unit (mm per unit) among the values (no parameter can be named
|
||||
/// so).
|
||||
pub(crate) const DESIGN_LEN: &str = "\u{1}design_len";
|
||||
|
||||
/// An expression's value. A length that comes out unit-less without being a bare number (a
|
||||
/// unit-less parameter used as a length) is in the design's units.
|
||||
fn val(vals: &BTreeMap<String, Value>, e: &str, k: Kind) -> Result<f64> {
|
||||
let scale = vals.get(DESIGN_LEN).map(|x| x.v).unwrap_or(1.0);
|
||||
if k == Kind::Length && scale != 1.0 && !expr::is_literal(e) {
|
||||
let v = expr::eval_with(e, &|n| vals.get(n).copied().ok_or_else(|| DocError::Expr(format!("unknown parameter `{n}`"))))?;
|
||||
return v.to_kind_in(k, expr::Defaults { len: scale, ..expr::Defaults::default() });
|
||||
}
|
||||
Document::eval_in(vals, e, k)
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,23 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every length input of every feature as the geometry reads it (mm; a unit-less result is
|
||||
/// in the design's units), for checking that an edit moves nothing.
|
||||
pub fn length_inputs(&self) -> Vec<Option<f64>> {
|
||||
let (vals, _) = self.param_values();
|
||||
let scale = expr::unit_info(self.units.trim()).filter(|(k, _)| *k == Kind::Length).map(|(_, s)| s).unwrap_or(1.0);
|
||||
let look = |n: &str| vals.get(n).copied().ok_or_else(|| DocError::Expr(format!("unknown parameter `{n}`")));
|
||||
self.features
|
||||
.iter()
|
||||
.flat_map(|f| f.kind.inputs())
|
||||
.filter(|(_, _, k)| *k == Kind::Length)
|
||||
.map(|(_, e, _)| {
|
||||
let d = if expr::is_literal(&e) { expr::Defaults::default() } else { expr::Defaults { len: scale, ..expr::Defaults::default() } };
|
||||
expr::eval_with(&e, &look).and_then(|v| v.to_kind_in(Kind::Length, d)).ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
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) };
|
||||
@@ -529,7 +546,16 @@ impl Document {
|
||||
for f in &self.features {
|
||||
for (name, (_, e, k)) in f.param_names.iter().zip(f.kind.inputs()) {
|
||||
if !name.is_empty() {
|
||||
v.push(ParamDef { name: name.clone(), expr: e, unit: unit_of(k).into() });
|
||||
// A bare number stored in an input is mm (the design's units are written in
|
||||
// when it is typed); anything else that comes out unit-less (a unit-less
|
||||
// parameter used as a length) is in the design's units.
|
||||
let unit = match k {
|
||||
Kind::Length if !expr::is_literal(&e) && expr::unit_info(self.units.trim()).is_some_and(|(k, _)| k == Kind::Length) => {
|
||||
self.units.trim().to_string()
|
||||
}
|
||||
_ => unit_of(k).to_string(),
|
||||
};
|
||||
v.push(ParamDef { name: name.clone(), expr: e, unit });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,14 +165,17 @@ fn document_units(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if old == u {
|
||||
return Ok(json!({ "units": u, "kept": [] }));
|
||||
}
|
||||
let (before, _) = s.doc.param_values();
|
||||
// What the geometry reads: every feature's length inputs.
|
||||
let before = s.doc.length_inputs();
|
||||
s.doc_mut().units = u.to_string();
|
||||
// Unit-less user parameters holding a bare number read it in the design's units where they
|
||||
// stand for lengths. If any value moved, those numbers keep the old unit, written out.
|
||||
let (after, _) = s.doc.param_values();
|
||||
let moved = before.iter().any(|(k, b)| after.get(k).is_none_or(|a| (a.v - b.v).abs() > 1e-9 * b.v.abs().max(1.0)));
|
||||
// stand for lengths. If any input moved, those numbers keep the old unit, written out.
|
||||
let same = |a: &[Option<f64>]| {
|
||||
a.iter().zip(&before).filter(|(x, y)| matches!((x, y), (Some(x), Some(y)) if (x - y).abs() <= 1e-9 * y.abs().max(1.0))).count()
|
||||
};
|
||||
let after = s.doc.length_inputs();
|
||||
let mut kept = Vec::new();
|
||||
if moved {
|
||||
if same(&after) < before.len() {
|
||||
let bare: Vec<(String, String)> = s
|
||||
.doc
|
||||
.params
|
||||
@@ -181,16 +184,15 @@ fn document_units(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.map(|p| (p.name.clone(), p.expr.trim().to_string()))
|
||||
.collect();
|
||||
for (name, e) in bare {
|
||||
let now = same(&s.doc.length_inputs());
|
||||
let probe = {
|
||||
let mut d = (*s.doc).clone();
|
||||
let _ = d.change_param(&name, &format!("{e} {old}"), None, None);
|
||||
d.param_values().0
|
||||
let _ = d.change_param(&name, &format!("{e} {old}"), Some(&old), None);
|
||||
same(&d.length_inputs())
|
||||
};
|
||||
// Only when writing the old unit puts the values back where they were.
|
||||
let fixes = before.iter().filter(|(k, b)| probe.get(*k).is_some_and(|a| (a.v - b.v).abs() <= 1e-9 * b.v.abs().max(1.0))).count()
|
||||
> before.iter().filter(|(k, b)| after.get(*k).is_some_and(|a| (a.v - b.v).abs() <= 1e-9 * b.v.abs().max(1.0))).count();
|
||||
if fixes {
|
||||
s.doc_mut().change_param(&name, &format!("{e} {old}"), None, None)?;
|
||||
// Only when writing the old unit puts more inputs back where they were.
|
||||
if probe > now {
|
||||
s.doc_mut().change_param(&name, &format!("{e} {old}"), Some(&old), None)?;
|
||||
kept.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1467,3 +1467,15 @@ fn booleans_stay_in_their_component() {
|
||||
assert!((vol(&mut s, "BBox") - (2000.0 - 8.0 * 6.0 * 10.0)).abs() < 1e-6, "{}", vol(&mut s, "BBox"));
|
||||
assert!(s.execute("Extrude", &json!({"sketch": "Slot", "distance": 1, "operation": "cut", "participants": ["Nope"]})).is_err());
|
||||
}
|
||||
|
||||
/// A unit-less parameter used as a length means the design's units.
|
||||
#[test]
|
||||
fn unitless_parameters_used_as_lengths_take_the_design_units() {
|
||||
let mut s = Session::default();
|
||||
s.doc_mut().units = "in".into();
|
||||
run(&mut s, "parameters.add", json!({"name": "w", "expression": "2", "unit": ""}));
|
||||
run(&mut s, "PrimitiveBox", json!({"length": "w", "width": "w * 1", "height": "10 mm"}));
|
||||
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) - 50.8).abs() < 1e-9 && (max(2) - 10.0).abs() < 1e-9, "{m}");
|
||||
}
|
||||
Reference in new issue
Block a user