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
This commit is contained in:
1 parent
90ce5261b3
commit
49bd89de13
8 files changed
+59
-8
No files matched your search
@@ -377,6 +377,10 @@ pub struct Model {
|
||||
empty: Arc<ModelState>,
|
||||
/// 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<std::sync::Mutex<EvalCache>>,
|
||||
/// Evaluate everything from scratch (no reuse; tests compare the two).
|
||||
@@ -505,6 +509,7 @@ impl Model {
|
||||
let mut out: Vec<FeatureResult> = 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Value> {
|
||||
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<Value> {
|
||||
@@ -307,7 +309,7 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
|
||||
@@ -581,7 +581,7 @@ fn press_pull(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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"))?;
|
||||
|
||||
@@ -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<Value> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -129,7 +129,8 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
"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"),
|
||||
|
||||
+1
-1
@@ -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. |
|
||||
|
||||
Reference in new issue
Block a user