URDF export for robot simulators (#719) (#942)

A design with components and joints exports as a URDF robot description (file.export to .urdf, the MCP export tool, solidcraft-cli run --out, File > Export URDF...). It writes one binary STL per component in metres, links with mass, centre of mass and inertia from material density, and maps rigid, revolute, slider, cylindrical, pin-slot, planar and ball joints to URDF joints or short chains. Loops, broken joints and loose parts are reported as warnings. Options: mesh_prefix, effort, velocity. Docs in docs/urdf.md.
This commit is contained in:
Mateusz Sych authored and GitHub committed 2026-10-13 21:52:30 -04:00
1 parent d2e409f40c
commit 17db109c5d
19 files changed
+1993 -17

No files matched your search

+18
View File
@@ -103,6 +103,24 @@ fn handshake_and_tools_list() {
assert_eq!(call(&mut s, 8, "nope/nope", json!({}))["error"]["code"], -32601);
}
/// A URDF goes through `export` with its own options (#719).
#[test]
fn export_tool_writes_a_urdf_package() {
let mut s = server();
tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}}));
let dir = std::env::temp_dir().join(format!("solidcraft-mcp-urdf-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let urdf = dir.join("robot.urdf");
let r = tool(&mut s, "export", json!({"path": urdf.to_string_lossy(), "mesh_prefix": "package://robot/meshes/"}));
assert_eq!(r["format"], "Urdf", "{r}");
assert!(std::fs::read_to_string(&urdf).unwrap().contains(r#"filename="package://robot/meshes/robot-base.stl""#));
assert!(dir.join("robot-base.stl").exists());
// effort and velocity reach file.export, which refuses a bad one.
let r = raw_tool(&mut s, "export", json!({"path": urdf.to_string_lossy(), "effort": -1}));
assert_eq!(r["result"]["isError"], json!(true), "{r}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Box → fillet → hole in one batch, then verify with measure / inspect_design and look at it.
#[test]
fn agent_builds_box_fillet_hole() {
+9 -4
View File
@@ -190,12 +190,17 @@ pub fn tool_definitions() -> Vec<Value> {
"export",
"Export",
"Export bodies to STEP (.step/.stp), 3MF (.3mf), binary STL (.stl), ASCII STL (format stla) or OBJ. The format \
comes from the extension unless given.",
comes from the extension unless given. URDF (.urdf) exports the whole assembly for robot simulators: a link per \
component placement with mass and inertia, joints from the assembly's joints, and STL meshes in metres written \
next to it (docs/urdf.md); RViz needs mesh_prefix such as package://robot/meshes/.",
obj(
json!({
"path": string("Output file"),
"format": {"type": "string", "enum": ["step", "3mf", "3mf-tagged", "stl", "stla", "obj"]},
"bodies": {"type": "array", "items": {"type": "string"}, "description": "Body names (default: all)"},
"format": {"type": "string", "enum": ["step", "3mf", "3mf-tagged", "stl", "stla", "obj", "urdf"]},
"bodies": {"type": "array", "items": {"type": "string"}, "description": "Body names (default: all; not with urdf)"},
"mesh_prefix": string("urdf: put before each mesh file name (default: none, so relative to the .urdf)"),
"effort": {"type": "number", "description": "urdf: joint effort limit, N or N m (default 1000)"},
"velocity": {"type": "number", "description": "urdf: joint velocity limit, m/s or rad/s (default 10)"},
}),
&["path"],
),
@@ -387,7 +392,7 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
p["expression"] = a.get("value").cloned().unwrap_or(Value::Null);
exec(b, "parameters.change", p)
}
"export" => exec(b, "file.export", pick(a, &["path", "format", "bodies"])),
"export" => exec(b, "file.export", pick(a, &["path", "format", "bodies", "mesh_prefix", "effort", "velocity"])),
"crash_reports" => match a.get("id") {
Some(_) => exec(b, "diagnostics.crash_report", pick(a, &["id"])),
None => exec(b, "diagnostics.crash_reports", pick(a, &["limit"])),