From 94cde2af211df067bf42def08860e01fc73daee5 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Tue, 30 Jun 2026 17:52:08 +0200 Subject: [PATCH] MCP slice 5: body-targeted fillet/chamfer, ordered slice contours, surface-deviation validate, widen Build - fillet/chamfer accept optional `body` (sets target_body; edge id resolved against THAT body) - slice_body chains section segments into ordered contours, each flagged closed/open - validate_against adds surface_deviation (one-sided Hausdorff via BRepExtrema_DistShapeShape) - new actions pattern/shell/draft; extrude gains end=blind|symmetric|two_sided|through_all|up_to_face + taper/flip/distance2 - GeometryEngine::surface_deviation helper (byte-identical to snaporca; Catch2 [Deviation] test lives in snaporca, whose test_geometry.cpp carries the CAD suite) Product code byte-identical to snaporca (md5 match). Both forks build clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q --- src/libslic3r/GeometryEngine.cpp | 28 +++++ src/libslic3r/GeometryEngine.hpp | 5 + src/slic3r/GUI/McpControl.cpp | 195 ++++++++++++++++++++++++++++--- 3 files changed, 209 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/GeometryEngine.cpp b/src/libslic3r/GeometryEngine.cpp index 58f35f2671..ce9d083051 100644 --- a/src/libslic3r/GeometryEngine.cpp +++ b/src/libslic3r/GeometryEngine.cpp @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include namespace Slic3r { @@ -275,6 +278,31 @@ TriangleMesh GeometryEngine::tessellate(const TopoDS_Shape& shape, TriangleMesh result; result.from_stl(stl); return result; } +GeometryEngine::Deviation +GeometryEngine::surface_deviation(const TopoDS_Shape& candidate, + const TopoDS_Shape& reference, + double linear_deflection) +{ + Deviation d; + if (candidate.IsNull() || reference.IsNull()) return d; + TriangleMesh mesh = tessellate(candidate, linear_deflection, 0.5); + const auto& verts = mesh.its.vertices; + if (verts.empty()) return d; + double sum = 0.0, sumsq = 0.0; + int n = 0; + for (const auto& v : verts) { + gp_Pnt p(v.x(), v.y(), v.z()); + BRepExtrema_DistShapeShape dss(BRepBuilderAPI_MakeVertex(p).Vertex(), reference); + if (!dss.IsDone() || dss.NbSolution() < 1) continue; + double dist = dss.Value(); + d.max_mm = std::max(d.max_mm, dist); + sum += dist; sumsq += dist * dist; ++n; + } + d.sample_count = n; + if (n > 0) { d.mean_mm = sum / n; d.rms_mm = std::sqrt(sumsq / n); } + return d; +} + std::string GeometryEngine::primitive_name(PrimitiveType type) { switch (type) { diff --git a/src/libslic3r/GeometryEngine.hpp b/src/libslic3r/GeometryEngine.hpp index 6032993001..305658c4b5 100644 --- a/src/libslic3r/GeometryEngine.hpp +++ b/src/libslic3r/GeometryEngine.hpp @@ -59,6 +59,11 @@ public: // already linked via Format/STEP.cpp — no new dependency. err is set on failure (empty result). static std::vector read_step_solids(const std::string& path, std::string& err); + struct Deviation { double max_mm{0}; double mean_mm{0}; double rms_mm{0}; int sample_count{0}; }; + static Deviation surface_deviation(const TopoDS_Shape& candidate, + const TopoDS_Shape& reference, + double linear_deflection = 0.5); + static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces = FaceGroup::All); static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius, diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index d1c7d61811..622848676f 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -86,18 +87,18 @@ std::string rpc_error(const json& id, int code, const std::string& msg) json describe_tools() { - // Hand-written descriptor for slice 1. The bridge turns this into MCP tool - // schemas; later slices grow this list (ideally from the kernel directly). + // Hand-written descriptor. The bridge turns this into MCP tool schemas; later + // slices grow this list (ideally from the kernel directly). return json{ {"app", "SnapOrca CAD"}, {"protocol", "jsonrpc-2.0"}, - {"slice", 1}, + {"slice", 5}, {"tools", json::array({ json{{"name", "describe_tools"}, {"summary", "List callable tools and their parameters."}, {"params", json::array()}}, json{{"name", "describe_scene"}, {"summary", "Feature tree + per-body bounding boxes of the Design document."}, {"params", json::array()}}, - json{{"name", "extrude"}, {"summary", "Extrude a profile to a depth. Give an explicit closed `profile` (list of [x,y]) or default to a centred width x height rectangle."}, + json{{"name", "extrude"}, {"summary", "Extrude a profile to a depth. Give an explicit closed `profile` (list of [x,y]) or default to a centred width x height rectangle. End conditions match Onshape."}, {"params", json::array({ json{{"name", "width"}, {"type", "number"}, {"unit", "mm"}, {"default", 20}, {"min", 0.01}}, json{{"name", "height"}, {"type", "number"}, {"unit", "mm"}, {"default", 20}, {"min", 0.01}}, @@ -105,6 +106,11 @@ json describe_tools() json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, json{{"name", "profile"}, {"type", "array"}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}}, json{{"name", "boolean"}, {"type", "string"}, {"enum", json::array({"new", "union", "subtract", "intersect"})}, {"default", "new"}}, + json{{"name", "end"}, {"type", "string"}, {"enum", json::array({"blind", "symmetric", "two_sided", "through_all", "up_to_face"})}, {"default", "blind"}}, + json{{"name", "distance2"},{"type", "number"}, {"unit", "mm"}, {"description", "second-side depth when end=two_sided"}}, + json{{"name", "up_to_face"},{"type", "integer"}, {"description", "target face id (query_topology on the last body) when end=up_to_face"}}, + json{{"name", "taper"}, {"type", "number"}, {"unit", "deg"}, {"default", 0}, {"description", "draft/taper of the side wall"}}, + json{{"name", "flip"}, {"type", "boolean"}, {"default", false}}, })}}, json{{"name", "revolve"}, {"summary", "Revolve a profile about a plane axis. Give an explicit `profile` offset from the axis (or a rectangle) — angle degrees about axis 0=plane X / 1=plane Y."}, {"params", json::array({ @@ -117,15 +123,17 @@ json describe_tools() json{{"name", "profile"}, {"type", "array"}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}}, json{{"name", "boolean"}, {"type", "string"}, {"enum", json::array({"new", "union", "subtract", "intersect"})}, {"default", "new"}}, })}}, - json{{"name", "fillet"}, {"summary", "Round a measured edge of the current body (edge id from query_topology)."}, + json{{"name", "fillet"}, {"summary", "Round a measured edge of a body (edge id from query_topology on that body)."}, {"params", json::array({ json{{"name", "edge"}, {"type", "integer"}}, json{{"name", "radius"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}}, + json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}}, })}}, - json{{"name", "chamfer"}, {"summary", "Chamfer a measured edge of the current body (edge id from query_topology)."}, + json{{"name", "chamfer"}, {"summary", "Chamfer a measured edge of a body (edge id from query_topology on that body)."}, {"params", json::array({ json{{"name", "edge"}, {"type", "integer"}}, json{{"name", "distance"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}}, + json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}}, })}}, json{{"name", "hole"}, {"summary", "Drill a circular hole into the current body at (x,y) on a plane."}, {"params", json::array({ @@ -144,6 +152,28 @@ json describe_tools() json{{"name", "keep_tool"}, {"type", "boolean"}, {"default", false}}, json{{"name", "tolerance"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, })}}, + json{{"name", "pattern"}, {"summary", "Replicate a body: linear (count along a plane axis at spacing) or circular (count over an angle about the plane normal)."}, + {"params", json::array({ + json{{"name", "circular"}, {"type", "boolean"}, {"default", false}}, + json{{"name", "count"}, {"type", "integer"}, {"default", 3}, {"min", 1}}, + json{{"name", "spacing"}, {"type", "number"}, {"unit", "mm"}, {"default", 10}, {"description", "linear step"}}, + json{{"name", "dir"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}, {"description", "linear axis: 0=plane X, 1=plane Y"}}, + json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 360}, {"description", "circular total sweep"}}, + json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, + json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body"}}, + })}}, + json{{"name", "shell"}, {"summary", "Hollow a body to a wall thickness (inward); optionally leave one face open."}, + {"params", json::array({ + json{{"name", "thickness"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}}, + json{{"name", "face"}, {"type", "integer"}, {"description", "face id to leave open (query_topology); omit for a closed hollow"}}, + json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body"}}, + })}}, + json{{"name", "draft"}, {"summary", "Taper a body face by an angle about its base (pull direction +Z)."}, + {"params", json::array({ + json{{"name", "face"}, {"type", "integer"}, {"description", "face id to draft (query_topology)"}}, + json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 5}}, + json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body"}}, + })}}, 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}}, @@ -154,7 +184,7 @@ json describe_tools() json{{"name", "a"}, {"type", "object"}}, json{{"name", "b"}, {"type", "object"}}, })}}, - json{{"name", "slice_body"}, {"summary", "Cross-section of a body by a base plane at an offset (sections-as-evidence); returns world polylines."}, + json{{"name", "slice_body"}, {"summary", "Cross-section of a body by a base plane at an offset (sections-as-evidence); returns ordered world contours, each flagged closed/open."}, {"params", json::array({ json{{"name", "body"}, {"type", "integer"}, {"default", 0}}, json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, @@ -164,7 +194,7 @@ json describe_tools() {"params", json::array({ json{{"name", "path"}, {"type", "string"}}, })}}, - json{{"name", "validate_against"}, {"summary", "Volume + bbox/centroid deviation of a body vs a reference {step:path|body:id} (the RE acceptance metric)."}, + json{{"name", "validate_against"}, {"summary", "Volume + bbox/centroid + surface deviation (max/mean/rms mm) of a body vs a reference {step:path|body:id} (the RE acceptance metric)."}, {"params", json::array({ json{{"name", "body"}, {"type", "integer"}, {"default", 0}}, json{{"name", "reference"}, {"type", "object"}}, @@ -322,6 +352,40 @@ json measure(DesignPanel* panel, const json& params) return r; } +// Chain raw section segments (each a sampled-edge polyline) into ordered contours by joining +// endpoints within tol. Grows the tail; when the tail is stuck, reverses the contour and grows +// the other end. A contour is closed when its two ends meet. OCCT section vertices are exact, +// so a small absolute tol suffices. +std::vector, bool>> +chain_segments(std::vector> segs, double tol) +{ + std::vector, bool>> contours; + std::vector used(segs.size(), 0); + auto near = [&](const Vec3d& a, const Vec3d& b) { return (a - b).norm() <= tol; }; + for (size_t i = 0; i < segs.size(); ++i) { + if (used[i] || segs[i].size() < 2) continue; + used[i] = 1; + std::vector c = segs[i]; + for (int side = 0; side < 2; ) { // grow tail; reverse once when stuck + bool grew = false; + for (size_t j = 0; j < segs.size(); ++j) { + if (used[j] || segs[j].size() < 2) continue; + if (near(c.back(), segs[j].front())) { + c.insert(c.end(), segs[j].begin() + 1, segs[j].end()); used[j] = 1; grew = true; + } else if (near(c.back(), segs[j].back())) { + for (auto it = segs[j].rbegin() + 1; it != segs[j].rend(); ++it) c.push_back(*it); + used[j] = 1; grew = true; + } + } + if (grew) { side = 0; continue; } + std::reverse(c.begin(), c.end()); ++side; // try the other end + } + bool closed = c.size() > 2 && near(c.front(), c.back()); + contours.emplace_back(std::move(c), closed); + } + return contours; +} + // sections-as-evidence: cross-section of a body by a named base plane at an offset. json slice_body(DesignPanel* panel, const json& params) { @@ -338,18 +402,24 @@ json slice_body(DesignPanel* panel, const json& params) sect.Approximation(Standard_True); sect.Build(); if (!sect.IsDone()) throw std::runtime_error("section failed"); - // ponytail: return raw section segments (one polyline per edge), unordered. Chain into - // loops later if a downstream step needs ordered contours. - json polylines = json::array(); + std::vector> segs; for (TopExp_Explorer ex(sect.Shape(), TopAbs_EDGE); ex.More(); ex.Next()) { std::vector pts = GeometryEngine::sample_edge_world(TopoDS::Edge(ex.Current())); - if (pts.size() < 2) continue; - json pl = json::array(); - for (const Vec3d& p : pts) pl.push_back(vec3(p)); - polylines.push_back(std::move(pl)); + if (pts.size() >= 2) segs.push_back(std::move(pts)); + } + const int raw = int(segs.size()); + auto contours = chain_segments(std::move(segs), 1e-3); + json jcont = json::array(); + int closed_n = 0; + for (auto& pc : contours) { + if (pc.second) ++closed_n; + json pts = json::array(); + for (const Vec3d& p : pc.first) pts.push_back(vec3(p)); + jcont.push_back(json{{"closed", pc.second}, {"points", std::move(pts)}}); } return json{{"body", params.value("body", 0)}, {"plane", plane_name}, {"offset", offset}, - {"segment_count", int(polylines.size())}, {"polylines", std::move(polylines)}}; + {"segment_count", raw}, {"contour_count", int(contours.size())}, + {"closed_count", closed_n}, {"contours", std::move(jcont)}}; } // --- Build: bring a reference part in (Import STEP as native B-rep bodies) ------ @@ -420,12 +490,17 @@ json validate_against(DesignPanel* panel, const json& params) double dv = b.volume > 0 ? (a.volume - b.volume) / b.volume * 100.0 : 0.0; Vec3d coff = a.centroid - b.centroid; Vec3d dmin = a.bmin - b.bmin, dmax = a.bmax - b.bmax; + // Surface-level deviation (one-sided Hausdorff, candidate vertices -> reference solid): + // catches local shape error that matching volume + bbox can hide. + GeometryEngine::Deviation dev = GeometryEngine::surface_deviation(cand, ref); return json{ {"volume", a.volume}, {"volume_reference", b.volume}, {"volume_delta_pct", dv}, {"centroid_offset", vec3(coff)}, {"centroid_offset_mm", coff.norm()}, {"bbox", json{{"min", vec3(a.bmin)}, {"max", vec3(a.bmax)}}}, {"bbox_reference", json{{"min", vec3(b.bmin)}, {"max", vec3(b.bmax)}}}, {"bbox_delta", json{{"min", vec3(dmin)}, {"max", vec3(dmax)}}}, + {"surface_deviation", json{{"max_mm", dev.max_mm}, {"mean_mm", dev.mean_mm}, + {"rms_mm", dev.rms_mm}, {"samples", dev.sample_count}}}, }; } @@ -448,10 +523,22 @@ json action_extrude(DesignPanel* panel, const json& params) int s = has_prof ? doc.add_sketch_profile(prof, pl, "Sketch") : doc.add_sketch(SketchShape::Rectangle, pl, w, h, 0.0, "Sketch"); int e = doc.add_extrude(s, d, /*symmetric*/false, mode, "Extrude"); + // End condition (Onshape parity). apply reads extrude_end directly; the kernel `symmetric` + // bool is unused, so set the field here. up_to_face id comes from query_topology on the + // target (last) body. taper_deg lofts the side wall; flip negates the direction. + const std::string end = params.value("end", std::string("blind")); + CadFeature& fe = doc.features[e]; + fe.flip = params.value("flip", false); + fe.taper_deg = params.value("taper", 0.0); + if (end == "symmetric") fe.extrude_end = ExtrudeEnd::Symmetric; + else if (end == "two_sided") { fe.extrude_end = ExtrudeEnd::TwoSided; fe.distance2 = params.value("distance2", d); } + else if (end == "through_all") fe.extrude_end = ExtrudeEnd::ThroughAll; + else if (end == "up_to_face") { fe.extrude_end = ExtrudeEnd::UpToFace; fe.up_to_face = params.value("up_to_face", -1); } + else fe.extrude_end = ExtrudeEnd::Blind; bool ok = doc.recompute(); if (!ok) doc.undo(); panel->mcp_after_change(); - return json{{"ok", ok}, {"sketch_index", s}, {"extrude_index", e}, + return json{{"ok", ok}, {"sketch_index", s}, {"extrude_index", e}, {"end", end}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } @@ -482,6 +569,16 @@ json action_revolve(DesignPanel* panel, const json& params) {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +// Resolve an optional explicit body target. Default (no `body`, or <0) = last body, which is +// what the kernel picks anyway. Validated BEFORE any checkpoint so a bad index throws clean. +int target_body_arg(const json& params, const CadDocument& doc) +{ + int bi = params.value("body", -1); + if (bi >= int(doc.bodies.size())) + throw std::runtime_error("body index out of range (have " + std::to_string(doc.bodies.size()) + ")"); + return bi; // <0 -> kernel uses the last body +} + json action_fillet(DesignPanel* panel, const json& params) { if (!params.contains("edge")) throw std::runtime_error("fillet needs 'edge' (id from query_topology)"); @@ -489,12 +586,15 @@ json action_fillet(DesignPanel* panel, const json& params) if (radius <= 0) throw std::runtime_error("radius must be > 0"); CadDocument& doc = panel->mcp_doc(); if (doc.bodies.empty()) throw std::runtime_error("no body to fillet"); + int bi = target_body_arg(params, doc); doc.checkpoint(); int f = doc.add_fillet(radius, params["edge"].get(), "Fillet"); + if (bi >= 0) doc.features[f].target_body = bi; // edge id resolved against THIS body's shape bool ok = doc.recompute(); if (!ok) doc.undo(); panel->mcp_after_change(); - return json{{"ok", ok}, {"fillet_index", f}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; + return json{{"ok", ok}, {"fillet_index", f}, {"body", bi < 0 ? int(doc.bodies.size()) - 1 : bi}, + {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } json action_chamfer(DesignPanel* panel, const json& params) @@ -504,12 +604,15 @@ json action_chamfer(DesignPanel* panel, const json& params) if (dist <= 0) throw std::runtime_error("distance must be > 0"); CadDocument& doc = panel->mcp_doc(); if (doc.bodies.empty()) throw std::runtime_error("no body to chamfer"); + int bi = target_body_arg(params, doc); doc.checkpoint(); int c = doc.add_chamfer(dist, params["edge"].get(), "Chamfer"); + if (bi >= 0) doc.features[c].target_body = bi; // edge id resolved against THIS body's shape bool ok = doc.recompute(); if (!ok) doc.undo(); panel->mcp_after_change(); - return json{{"ok", ok}, {"chamfer_index", c}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; + return json{{"ok", ok}, {"chamfer_index", c}, {"body", bi < 0 ? int(doc.bodies.size()) - 1 : bi}, + {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } json action_hole(DesignPanel* panel, const json& params) @@ -551,6 +654,57 @@ json action_boolean(DesignPanel* panel, const json& params) return json{{"ok", ok}, {"boolean_index", b}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +json action_pattern(DesignPanel* panel, const json& params) +{ + const bool circular = params.value("circular", false); + const int count = params.value("count", 3); + const double spacing = params.value("spacing", 10.0); // linear step (mm) + const int dir = params.value("dir", 0); // 0 = plane X, 1 = plane Y + const double angle = params.value("angle", 360.0); // circular total sweep (deg) + if (count < 1) throw std::runtime_error("count must be >= 1"); + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no body to pattern"); + int bi = target_body_arg(params, doc); + doc.checkpoint(); + int p = doc.add_pattern(circular, count, spacing, dir, angle, bi, "Pattern"); + doc.features[p].plane = plane_from(params, doc); // axis (circular) / step dirs (linear) + bool ok = doc.recompute(); + if (!ok) doc.undo(); + panel->mcp_after_change(); + return json{{"ok", ok}, {"pattern_index", p}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; +} + +json action_shell(DesignPanel* panel, const json& params) +{ + const double thickness = params.value("thickness", 1.0); + if (thickness <= 0) throw std::runtime_error("thickness must be > 0"); + const int face = params.value("face", -1); // face id to leave open (-1 = closed hollow) + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no body to shell"); + int bi = target_body_arg(params, doc); + doc.checkpoint(); + int s = doc.add_shell(thickness, face, bi, "Shell"); + bool ok = doc.recompute(); + if (!ok) doc.undo(); + panel->mcp_after_change(); + return json{{"ok", ok}, {"shell_index", s}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; +} + +json action_draft(DesignPanel* panel, const json& params) +{ + if (!params.contains("face")) throw std::runtime_error("draft needs 'face' (id from query_topology)"); + const double angle = params.value("angle", 5.0); + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no body to draft"); + int bi = target_body_arg(params, doc); + doc.checkpoint(); + int d = doc.add_draft(angle, params["face"].get(), bi, "Draft"); + bool ok = doc.recompute(); + if (!ok) doc.undo(); + panel->mcp_after_change(); + return json{{"ok", ok}, {"draft_index", d}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; +} + // Dispatch one parsed request ON THE MAIN THREAD. Returns a JSON-RPC reply string. std::string handle_on_main(const std::string& method, const json& params, const json& id) { @@ -572,6 +726,9 @@ std::string handle_on_main(const std::string& method, const json& params, const if (method == "chamfer") return rpc_result(id, action_chamfer(panel, params)); if (method == "hole") return rpc_result(id, action_hole(panel, params)); if (method == "boolean") return rpc_result(id, action_boolean(panel, params)); + if (method == "pattern") return rpc_result(id, action_pattern(panel, params)); + if (method == "shell") return rpc_result(id, action_shell(panel, params)); + if (method == "draft") return rpc_result(id, action_draft(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"));