M8a: assembly mates — Fastened + Planar (recipe v3)

An assembly is a multi-body document. An instance is already expressible as
Transform with xf_copy=true, and a mate connector is already a CoordSys
feature, so this adds exactly one feature type: Mate.

No constraint solver. A mate rigidly transforms the body carrying connector B
so that B's frame lands on connector A's, applied in feature order like every
other feature. Chains resolve by composition; closed kinematic loops do not
converge (last mate wins) and are out of scope.

The vendored SolveSpace in src/libslic3r/slvs/ was evaluated for 3D extension
and rejected: it is built and linked but has zero callers, and SketchEngine's
solver is hand-rolled. Extending it would mean adopting a dependency to write
more code than the alternative.

- CadFeatureType::Mate appended; six fields (mate_kind, mate_cs_a, mate_cs_b,
  mate_offset, mate_angle, mate_flip) appended at the END of both cereal lists
- SNAPORCA_CAD_RECIPE_VERSION 2 -> 3; v2 blobs are rejected, as by design there
  is no migration path. Golden fixture renamed to cad_recipe_v3.bin and
  regenerated once, extended with two CoordSys + one Mate so the new fields are
  tripwired by the field-order assertions
- datum_frame() extracted from resolve_datum_coordsys() so a mate can resolve
  its connectors against the in-progress bodies vector during replay
- apply_mate dispatched early-return, so Mate is deliberately absent from
  starts_new (unreachable for that dispatch style)
- Planar: the degenerate branch splits on the sign of zB.z_target — antiparallel
  needs a 180 deg rotation about a perpendicular axis, which an earlier revision
  silently skipped, leaving the body's normal inverted
- MCP: mate command, named bare to match the other 38 methods

Drive-by: feature_type_name() was missing Mirror, ThickenSurface, SurfaceOffset,
SurfaceLoft and SurfaceFill, which reported as "Unknown" to MCP clients.

Suite 122 cases / 1741 assertions green. Note that kernel-test.sh builds only
libslic3r_tests, so McpControl.cpp is reviewed but not compiled here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-07-25 10:51:22 +02:00
co-authored by Claude Opus 5
parent 9c28be5860
commit b13ca01ccc
6 changed files with 838 additions and 47 deletions
+35
View File
@@ -71,6 +71,7 @@ const char* feature_type_name(CadFeatureType t)
case CadFeatureType::Import: return "Import";
case CadFeatureType::Boolean: return "Boolean";
case CadFeatureType::Cut: return "Cut";
case CadFeatureType::Mirror: return "Mirror";
case CadFeatureType::Axis: return "Axis";
case CadFeatureType::CoordSys: return "CoordSys";
case CadFeatureType::Helix: return "Helix";
@@ -81,6 +82,11 @@ const char* feature_type_name(CadFeatureType t)
case CadFeatureType::Rib: return "Rib";
case CadFeatureType::SurfaceExtrude: return "SurfaceExtrude";
case CadFeatureType::SurfaceRevolve: return "SurfaceRevolve";
case CadFeatureType::ThickenSurface: return "ThickenSurface";
case CadFeatureType::SurfaceOffset: return "SurfaceOffset";
case CadFeatureType::SurfaceLoft: return "SurfaceLoft";
case CadFeatureType::SurfaceFill: return "SurfaceFill";
case CadFeatureType::Mate: return "Mate";
}
return "Unknown";
}
@@ -324,6 +330,15 @@ json describe_tools()
json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 360}},
json{{"name", "axis"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}},
})}},
json{{"name", "mate"}, {"summary", "Mate two bodies: transform the moving body (cs_b) so its connector lands on the fixed one (cs_a). kind: 0=Fastened, 1=Planar."},
{"params", json::array({
json{{"name", "kind"}, {"type", "integer"}, {"default", 0}, {"description", "0=Fastened (full align), 1=Planar (normal only)"}},
json{{"name", "cs_a"}, {"type", "integer"}, {"description", "feature index of the fixed CoordSys (mate connector A)"}},
json{{"name", "cs_b"}, {"type", "integer"}, {"description", "feature index of the CoordSys on the body that moves"}},
json{{"name", "offset"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}},
json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 0}},
json{{"name", "flip"}, {"type", "boolean"}, {"default", false}},
})}},
json{{"name", "query_topology"}, {"summary", "Measured faces (centroid/normal/cylinder) and edges (length/circle) of a body."},
{"params", json::array({
json{{"name", "body"}, {"type", "integer"}, {"default", 0}},
@@ -1310,6 +1325,25 @@ json action_helix(DesignPanel* panel, const json& params)
return json{{"ok", true}, {"helix_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}};
}
json action_mate(DesignPanel* panel, const json& params)
{
if (!params.contains("cs_a")) throw std::runtime_error("mate needs 'cs_a' (CoordSys feature index)");
if (!params.contains("cs_b")) throw std::runtime_error("mate needs 'cs_b' (CoordSys feature index)");
const int kind = params.value("kind", 0);
const int cs_a = params["cs_a"].get<int>();
const int cs_b = params["cs_b"].get<int>();
const double offset = params.value("offset", 0.0);
const double angle = params.value("angle", 0.0);
const bool flip = params.value("flip", false);
CadDocument& doc = panel->mcp_doc();
doc.checkpoint();
int idx = doc.add_mate(kind, cs_a, cs_b, offset, angle, flip, "Mate");
bool ok = doc.recompute();
if (!ok) doc.undo();
panel->mcp_after_change();
return json{{"ok", ok}, {"mate_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}};
}
json action_set_variable(DesignPanel* panel, const json& params)
{
if (!params.contains("name")) throw std::runtime_error("set_variable needs 'name'");
@@ -1394,6 +1428,7 @@ std::string handle_on_main(const std::string& method, const json& params, const
if (method == "surface_offset") return rpc_result(id, action_surface_offset(panel, params));
if (method == "surface_loft") return rpc_result(id, action_surface_loft(panel, params));
if (method == "surface_fill") return rpc_result(id, action_surface_fill(panel, params));
if (method == "mate") return rpc_result(id, action_mate(panel, params));
return rpc_error(id, -32601, "Unknown method: " + method);
} catch (const Standard_Failure& ex) { // OCCT errors are NOT std::exception
return rpc_error(id, -32000, std::string("OCCT: ") + (ex.GetMessageString() ? ex.GetMessageString() : "failure"));