From 49bd89de13d420e16109376f2d84e06b6c3bbf90 Mon Sep 17 00:00:00 2001 From: CypherNaugh_0x <9931495+CypherNaught-0x@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:00:32 +0200 Subject: [PATCH] Parameter and feature edits report the features they restored from the evaluation cache: setting a parameter back (or a timeline.edit back) reused the earlier results for the same inputs, so the geometry changed back but recomputed said 0, which read as "nothing rebuilt". The evaluator now counts cache hits as last_restored, and parameters.change, timeline.edit, timeline.redefine and solid.press_pull on a fillet or chamfer face report it as restored next to recomputed (params docs, the MCP set_parameter description and docs/mcp.md say what each counts). Tests: setting a parameter back and a feature edit back report restored, in the evaluator and through the commands --- crates/doc/src/eval.rs | 7 +++++++ crates/doc/src/tests.rs | 20 ++++++++++++++++++++ crates/engine/src/cmd/edit.rs | 10 ++++++---- crates/engine/src/cmd/features.rs | 2 +- crates/engine/src/cmd/parameters.rs | 4 +++- crates/engine/src/cmd/parameters_tests.rs | 19 +++++++++++++++++++ crates/mcp/src/tools.rs | 3 ++- docs/mcp.md | 2 +- 8 files changed, 59 insertions(+), 8 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index fcf41553..a7229d47 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -377,6 +377,10 @@ pub struct Model { empty: Arc, /// Features recomputed by the last `evaluate`. pub last_recomputed: usize, + /// Features whose result the last `evaluate` took from the cache instead of recomputing: the + /// same feature on the same inputs was evaluated before (an edit set back, undo, redo, a + /// configuration row). Their geometry did change since the previous evaluation. + pub last_restored: usize, /// Results by inputs, shared with this model's copies. pub cache: Arc>, /// Evaluate everything from scratch (no reuse; tests compare the two). @@ -505,6 +509,7 @@ impl Model { let mut out: Vec = Vec::with_capacity(doc.features.len()); let mut reuse = !self.no_cache; let mut recomputed = 0; + let mut restored = 0; let marker = doc.marker.unwrap_or(usize::MAX); let ctx = context_digest(doc, &vals); for (i, f) in doc.features.iter().enumerate() { @@ -569,6 +574,7 @@ impl Model { if !self.no_cache && let Some(c) = self.cache.lock().ok().and_then(|mut m| m.get(key)) { + restored += 1; if c.error.is_none() { state = c.state.clone(); } @@ -652,6 +658,7 @@ impl Model { } self.results = out; self.last_recomputed = recomputed; + self.last_restored = restored; } } diff --git a/crates/doc/src/tests.rs b/crates/doc/src/tests.rs index a11dd964..b74f6da1 100644 --- a/crates/doc/src/tests.rs +++ b/crates/doc/src/tests.rs @@ -81,6 +81,26 @@ fn parametric_plate_reevaluates() { assert!(rel(measure(&m.state().bodies[0].body).unwrap().volume, 50.0 * 30.0 * 10.0) < 1e-9); } +/// Setting a parameter back takes the earlier results from the cache: nothing is recomputed, but +/// the evaluation reports the features it restored (their geometry did change back). +#[test] +fn set_back_reports_restored_features() { + let (mut doc, _) = plate_doc(); + let mut m = Model::new(); + m.evaluate(&doc); + assert_eq!((m.last_recomputed, m.last_restored), (2, 0)); + doc.set_param("width", "50 mm", None, None).unwrap(); + m.evaluate(&doc); + assert_eq!((m.last_recomputed, m.last_restored), (2, 0)); + doc.set_param("width", "40 mm", None, None).unwrap(); + m.evaluate(&doc); + assert_eq!((m.last_recomputed, m.last_restored), (0, 2)); + assert!(rel(measure(&m.state().bodies[0].body).unwrap().volume, 24000.0) < 1e-9); + // No change: nothing recomputed and nothing restored. + m.evaluate(&doc); + assert_eq!((m.last_recomputed, m.last_restored), (0, 0)); +} + #[test] fn box_fillet_cut_timeline() { let (mut doc, _) = plate_doc(); diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 64bd563e..08eb8695 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -28,8 +28,10 @@ pub static COMMANDS: &[CommandSpec] = &[ .noundo() .params("feature: id|name → features deleted with it, and features that would fail without it"), CommandSpec::new("timeline.suppress", "Suppress Feature", suppress).params("feature: id|name, suppressed?: bool (default toggles)"), - CommandSpec::new("timeline.edit", "Edit Feature", edit_feature) - .params("feature: id|name, set: {fields to change, e.g. {\"extent\": {\"distance\": \"30\"}}}"), + CommandSpec::new("timeline.edit", "Edit Feature", edit_feature).params( + "feature: id|name, set: {fields to change, e.g. {\"extent\": {\"distance\": \"30\"}}} → recomputed: features rebuilt, \ + restored: features whose earlier result for the same inputs was reused", + ), CommandSpec::new("select.set", "Select", select_set) .noundo() .params("items: [{type: body|edge|face|feature|sketch_curve|sketch_point|profile, …}], add?: bool"), @@ -204,7 +206,7 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result { if let Some(e) = s.model.result(id).and_then(|r| r.error.clone()) { return Err(EngineError::Other(e)); } - Ok(json!({"feature": id, "recomputed": s.model.last_recomputed})) + Ok(json!({"feature": id, "recomputed": s.model.last_recomputed, "restored": s.model.last_restored})) } fn select_set(s: &mut Session, p: &Value) -> Result { @@ -307,7 +309,7 @@ fn redefine(s: &mut Session, p: &Value) -> Result { if let Some(e) = s.model.result(id).and_then(|r| r.error.clone()) { return Err(EngineError::Other(e)); } - Ok(json!({"feature": id, "recomputed": s.model.last_recomputed})) + Ok(json!({"feature": id, "recomputed": s.model.last_recomputed, "restored": s.model.last_restored})) } fn dependents(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index dd7890be..fbdb8848 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -581,7 +581,7 @@ fn press_pull(s: &mut Session, p: &Value) -> Result { let new = format!("({old}) + ({distance})"); let r = super::find_command("timeline.edit").ok_or_else(|| bad(cmd, "timeline.edit"))?; let out = (r.run)(s, &json!({"feature": f.id, "set": {key: new}}))?; - return Ok(json!({"feature": f.id, "edited": f.name, key: new, "recomputed": out.get("recomputed")})); + return Ok(json!({"feature": f.id, "edited": f.name, key: new, "recomputed": out.get("recomputed"), "restored": out.get("restored")})); } // Another curved face moves along its normal. let r = super::find_command("solid.offset_face").ok_or_else(|| bad(cmd, "solid.offset_face"))?; diff --git a/crates/engine/src/cmd/parameters.rs b/crates/engine/src/cmd/parameters.rs index 82b8759d..5437627f 100644 --- a/crates/engine/src/cmd/parameters.rs +++ b/crates/engine/src/cmd/parameters.rs @@ -12,7 +12,8 @@ use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("parameters.change", "Change Parameters", change_param).at("SOLID", "MODIFY").icon("params").params( "name, expression (number or text), unit?: mm|cm|m|in|ft|deg|rad|\"\" (new parameters default to their value's unit), comment?, \ - favorite?: bool, new_name?: rename (references follow); or delete: name", + favorite?: bool, new_name?: rename (references follow); or delete: name → recomputed: features rebuilt, \ + restored: features whose earlier result for the same inputs was reused (e.g. a value set back)", ), CommandSpec::new("parameters.list", "List Parameters", list).noundo().params( "favorites?: bool (only favourites), filter?: all|user|sketch|feature|favorites → parameters (name, expression, unit, value in its unit, \ @@ -107,6 +108,7 @@ fn change_param(s: &mut Session, p: &Value) -> Result { out["value"] = json!(vals.get(&name).map(|v| v.v)); out["display_value"] = json!(s.doc.param_display_value(&vals, &name)); out["recomputed"] = json!(s.model.last_recomputed); + out["restored"] = json!(s.model.last_restored); out["errors"] = json!(failing(s)); Ok(out) } diff --git a/crates/engine/src/cmd/parameters_tests.rs b/crates/engine/src/cmd/parameters_tests.rs index 18983408..db8ae158 100644 --- a/crates/engine/src/cmd/parameters_tests.rs +++ b/crates/engine/src/cmd/parameters_tests.rs @@ -67,6 +67,25 @@ fn width_follows_an_expression() { assert!(e.contains("expected a length, got an angle"), "{e}"); } +/// Setting a parameter back rebuilds nothing (the earlier results are reused) and says so: the +/// features it restored are reported next to `recomputed`, so `recomputed: 0` is not mistaken +/// for "nothing changed". timeline.edit reports the same. +#[test] +fn setting_a_parameter_back_reports_restored_features() { + let mut s = Session::default(); + block(&mut s); + let r = run(&mut s, "parameters.change", json!({"name": "len", "expression": "100 mm"})); + assert_eq!((r["recomputed"].as_u64(), r["restored"].as_u64()), (Some(2), Some(0)), "{r}"); + let r = run(&mut s, "parameters.change", json!({"name": "len", "expression": "60 mm"})); + assert_eq!((r["recomputed"].as_u64(), r["restored"].as_u64()), (Some(0), Some(2)), "{r}"); + assert!(rel(volume(&mut s), 40.0 * 30.0 * 10.0) < 1e-9); + let r = run(&mut s, "timeline.edit", json!({"feature": "Extrude1", "set": {"extent": {"distance": "20"}}})); + assert_eq!((r["recomputed"].as_u64(), r["restored"].as_u64()), (Some(1), Some(0)), "{r}"); + let r = run(&mut s, "timeline.edit", json!({"feature": "Extrude1", "set": {"extent": {"distance": "10"}}})); + assert_eq!((r["recomputed"].as_u64(), r["restored"].as_u64()), (Some(0), Some(1)), "{r}"); + assert!(rel(volume(&mut s), 40.0 * 30.0 * 10.0) < 1e-9); +} + #[test] fn feature_inputs_are_named_and_editable() { let mut s = Session::default(); diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index c307b284..0d1e993a 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -129,7 +129,8 @@ pub fn tool_definitions() -> Vec { "set_parameter", "Set a parameter", "Create or change a user parameter and re-evaluate the timeline. `value` is a number (mm) or an expression \ - (\"40 mm\", \"width * 2\", \"30 deg\"). Returns the new value, the features recomputed and any features that now fail.", + (\"40 mm\", \"width * 2\", \"30 deg\"). Returns the new value, the features recomputed (`recomputed`), the features whose earlier \ + result for the same inputs was reused instead (`restored`, e.g. after setting a value back) and any features that now fail.", obj( json!({ "name": string("Parameter name"), diff --git a/docs/mcp.md b/docs/mcp.md index c6a996c0..3aec985e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -54,7 +54,7 @@ Claude Desktop (`claude_desktop_config.json`), bridged to a running app: | `inspect_design` | `measure?` (default true), `sketches?` (default true) | Parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible), sketches (curves, points, constraints, profiles, DOF, solve status, visible), selection, undo depth. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`). | | `measure` | `body?` | Volume (mm³), area (mm²), centre of mass, bbox and topology counts per body, with totals. | | `body_topology` | `body` | Edges (index, midpoint, length, ends) and faces (index, area, centroid, normal). An edge midpoint is the `[x,y,z]` that picks that edge for fillet and chamfer. | -| `set_parameter` | `name`, `value` (number or expression), `unit?`, `comment?` | Create or change a user parameter and recompute; reports features that now fail. | +| `set_parameter` | `name`, `value` (number or expression), `unit?`, `comment?` | Create or change a user parameter and recompute; reports `recomputed` (features rebuilt), `restored` (features whose earlier result for the same inputs was reused, e.g. a value set back) and features that now fail. | | `screenshot` | `view?` (iso, top, front, back, bottom, left, right, home, fit), `path?`, `width?`, `height?`, `source?` (window, model) | A PNG image content block. Headless: CPU render fitted to the model. Bridged: the app window (default) or a model render with the app's camera. | | `export` | `path`, `format?` (step, 3mf, stl, stla, obj), `bodies?` | STEP, 3MF or mesh export. | | `undo`, `redo` | — | Undo / redo one change. |