From 9283d2b5bb2e9925caabf28fb8a829b6774da05a Mon Sep 17 00:00:00 2001 From: Jonas Wrede Date: Tue, 13 Oct 2026 08:39:19 +0200 Subject: [PATCH] feat: feature replies list the bodies the feature created (#525) Fixes #374. Feature replies gain created_bodies: the bodies the new feature made, apart from the ones it changed or that were already there. Documented in the execute tool description and docs/mcp.md. --- crates/engine/src/cmd/features.rs | 4 +++- crates/engine/src/cmd/file.rs | 4 ++-- crates/engine/src/tests.rs | 16 +++++++++++++++- crates/mcp/src/tools.rs | 3 ++- docs/mcp.md | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 89ff8b60..1f23f5d6 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -472,8 +472,10 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) -> } let st = s.model.state(); let bodies: Vec<&str> = st.bodies.iter().map(|b| b.name.as_str()).collect(); + // The bodies this feature made, apart from the ones it changed or that were there (#374). + let created: Vec<&str> = st.bodies.iter().filter(|b| b.feature == id).map(|b| b.name.as_str()).collect(); let fname = s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default(); - Ok(json!({"feature": id, "name": fname, "bodies": bodies, "ms": r.map(|r| r.ms)})) + Ok(json!({"feature": id, "name": fname, "bodies": bodies, "created_bodies": created, "ms": r.map(|r| r.ms)})) } fn extrude(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index fca53a7a..d1258380 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -78,7 +78,7 @@ fn add_step(s: &mut Session, path: &str, name: Option<&str>) -> Result { let st = s.model.state(); let bodies: Vec<&str> = st.bodies.iter().filter(|b| b.feature == id).map(|b| b.name.as_str()).collect(); let fname = s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default(); - Ok(json!({"path": path, "feature": id, "name": fname, "bodies": bodies, "warnings": sf.warnings})) + Ok(json!({"path": path, "feature": id, "name": fname, "bodies": bodies, "created_bodies": bodies, "warnings": sf.warnings})) } /// Add a MeshImport feature for a 3MF or STL file. @@ -98,7 +98,7 @@ fn add_mesh(s: &mut Session, path: &str, name: Option<&str>) -> Result { let st = s.model.state(); let bodies: Vec<&str> = st.bodies.iter().filter(|b| b.feature == id).map(|b| b.name.as_str()).collect(); let fname = s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default(); - Ok(json!({"path": path, "feature": id, "name": fname, "bodies": bodies, "warnings": mf.warnings})) + Ok(json!({"path": path, "feature": id, "name": fname, "bodies": bodies, "created_bodies": bodies, "warnings": mf.warnings})) } fn insert_mesh(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index d2ab3f76..960a2b0d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2839,7 +2839,6 @@ fn a_profile_added_later_is_named_after_body_name() { /// The reply names the bodies the feature created, apart from the ones already there (#374). #[test] -#[ignore = "fails until #374 is fixed"] fn extrude_reply_lists_the_bodies_it_created() { let mut s = Session::default(); run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [0, 50, 0], "operation": "new_body", "body_name": "Base"})); @@ -2938,3 +2937,18 @@ fn tapered_extrude_of_a_profile_mixing_lines_and_an_arc() { let expect = a0 * h + p * t * h * h / 2.0 + k * t * t * h.powi(3) / 3.0; assert!(rel(volume(&mut s), expect) < 1e-3, "{} vs {expect}", volume(&mut s)); } + +/// A feature that joins into a body creates none, and its reply says so; a primitive's reply +/// names the body it made (#374). +#[test] +fn feature_replies_list_only_the_bodies_they_created() { + let mut s = Session::default(); + let r = run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Base"})); + assert_eq!(r["created_bodies"], json!(["Base"]), "{r}"); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Boss"})); + run(&mut s, "sketch.circle.center", json!({"center": [5, 5], "radius": 2})); + run(&mut s, "sketch.finish", json!({})); + let r = run(&mut s, "solid.extrude", json!({"sketch": "Boss", "distance": "15", "operation": "join"})); + assert_eq!(r["created_bodies"], json!([]), "{r}"); + assert_eq!(r["bodies"], json!(["Base"]), "{r}"); +} diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 0c7378af..7536f18f 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -62,7 +62,8 @@ pub fn tool_definitions() -> Vec { the command's result; failures leave the design unchanged. Sketch commands that add geometry return \ created: {points, curves, constraints, dimensions}: the ids of what they added, stable and not reused \ after a delete (undo rewinds the counters with the design), usable right away \ - in sketch.dimension, sketch.constraint.* and sketch.delete.", + in sketch.dimension, sketch.constraint.* and sketch.delete. Feature commands return the feature's id \ + and name, every body (bodies) and the bodies the feature created (created_bodies).", obj( json!({ "command": string("Command id (see list_commands)"), diff --git a/docs/mcp.md b/docs/mcp.md index 67db4d73..b3152415 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -50,7 +50,7 @@ Claude Desktop (`claude_desktop_config.json`), bridged to a running app: | Tool | Arguments | What it does | |---|---|---| | `list_commands` | `filter?` | The command catalog: id, label, tab/panel, shortcut, parameter docs, enabled now. `filter` matches id, label, tab and panel. | -| `execute` | `command`, `params?` | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. | +| `execute` | `command`, `params?` | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. A feature command replies with the feature (`feature`, `name`), every body (`bodies`) and the bodies it created (`created_bodies`). | | `batch` | `commands: [{command, params}]`, `rollback?` (default true) | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`) and every result before it. | | `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. |