mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d2f97a752b
commit
94cde2af21
@@ -28,6 +28,9 @@
|
||||
#include <STEPControl_Reader.hxx>
|
||||
#include <IFSelect_ReturnStatus.hxx>
|
||||
#include <Standard_Failure.hxx>
|
||||
#include <BRepExtrema_DistShapeShape.hxx>
|
||||
#include <BRepBuilderAPI_MakeVertex.hxx>
|
||||
#include <cmath>
|
||||
|
||||
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) {
|
||||
|
||||
@@ -59,6 +59,11 @@ public:
|
||||
// already linked via Format/STEP.cpp — no new dependency. err is set on failure (empty result).
|
||||
static std::vector<TopoDS_Shape> 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,
|
||||
|
||||
+176
-19
@@ -15,6 +15,7 @@
|
||||
#include <memory>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -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<std::pair<std::vector<Vec3d>, bool>>
|
||||
chain_segments(std::vector<std::vector<Vec3d>> segs, double tol)
|
||||
{
|
||||
std::vector<std::pair<std::vector<Vec3d>, bool>> contours;
|
||||
std::vector<char> 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<Vec3d> 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<std::vector<Vec3d>> segs;
|
||||
for (TopExp_Explorer ex(sect.Shape(), TopAbs_EDGE); ex.More(); ex.Next()) {
|
||||
std::vector<Vec3d> 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<int>(), "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<int>(), "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<int>(), 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"));
|
||||
|
||||
Reference in New Issue
Block a user