CAD mass properties: volume / area / centre of mass / inertia

Port of snaporca c2821af783. New GeometryEngine::mass_properties over BRepGProp
(separate VolumeProperties/SurfaceProperties), bounds-checked
CadDocument::body_mass_properties, and a mass_properties MCP method. Query-only:
no CadFeature, no serialization, no version change. Analytic tests
(WithinRel/WithinAbs): cube 8000/2400/COM(0,0,10)/inertia 533333, cylinder
500pi/300pi, hollow = solid-500pi, invalid index -> valid=false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-07-23 23:18:42 +02:00
co-authored by Claude Opus 4.8
parent 8edc12c8f1
commit b807be3c4a
6 changed files with 169 additions and 0 deletions
+6
View File
@@ -1852,4 +1852,10 @@ bool CadDocument::export_step(const std::string& path,
return true;
}
GeometryEngine::MassProps CadDocument::body_mass_properties(int body_index) const
{
if (body_index < 0 || body_index >= int(bodies.size())) return {};
return GeometryEngine::mass_properties(bodies[body_index].shape);
}
} // namespace Slic3r
+2
View File
@@ -371,6 +371,8 @@ public:
const std::vector<Transform3d>& body_xforms,
std::string& err) const;
GeometryEngine::MassProps body_mass_properties(int body_index) const;
// Undo/redo of the feature recipe (Onshape-style Ctrl+Z). The caller marks a
// user-action boundary by calling checkpoint() BEFORE the mutation(s) for that
// action (add/delete/move/replace, or a direct features edit). undo()/redo() then
+27
View File
@@ -465,6 +465,33 @@ GeometryEngine::surface_deviation(const TopoDS_Shape& candidate,
return d;
}
GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& shape)
{
MassProps p;
if (shape.IsNull()) return p;
try {
GProp_GProps vprops;
BRepGProp::VolumeProperties(shape, vprops);
double mass = vprops.Mass();
if (std::abs(mass) < 1e-30) return p;
p.volume = std::abs(mass);
p.center_of_mass = Vec3d(vprops.CentreOfMass().X(), vprops.CentreOfMass().Y(), vprops.CentreOfMass().Z());
gp_Mat mat = vprops.MatrixOfInertia();
p.inertia = {{
mat(1,1), mat(1,2), mat(1,3),
mat(2,1), mat(2,2), mat(2,3),
mat(3,1), mat(3,2), mat(3,3),
}};
GProp_GProps sprops;
BRepGProp::SurfaceProperties(shape, sprops);
p.surface_area = sprops.Mass();
p.valid = true;
} catch (const Standard_Failure&) {
// leave valid = false
}
return p;
}
std::string GeometryEngine::primitive_name(PrimitiveType type)
{
switch (type) {
+9
View File
@@ -94,6 +94,15 @@ public:
double merge_angle_deg,
MeshBrepStats& stats);
struct MassProps {
double volume{0.0};
double surface_area{0.0};
Vec3d center_of_mass{Vec3d::Zero()};
std::array<double, 9> inertia{};
bool valid{false};
};
static MassProps mass_properties(const TopoDS_Shape& shape);
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,
+19
View File
@@ -187,6 +187,10 @@ json describe_tools()
json{{"name", "a"}, {"type", "object"}},
json{{"name", "b"}, {"type", "object"}},
})}},
json{{"name", "mass_properties"}, {"summary", "Volume / surface area / centre of mass / inertia tensor of a body."},
{"params", json::array({
json{{"name", "body"}, {"type", "integer"}, {"default", 0}},
})}},
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}},
@@ -367,6 +371,20 @@ json measure(DesignPanel* panel, const json& params)
return r;
}
json mass_properties(DesignPanel* panel, const json& params)
{
const TopoDS_Shape& shape = body_shape(panel, params);
auto mp = GeometryEngine::mass_properties(shape);
if (!mp.valid) throw std::runtime_error("mass properties could not be computed (null/empty shape)");
return json{
{"volume", mp.volume},
{"surface_area", mp.surface_area},
{"center_of_mass", json::array({mp.center_of_mass.x(), mp.center_of_mass.y(), mp.center_of_mass.z()})},
{"inertia", mp.inertia},
{"valid", mp.valid},
};
}
// 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,
@@ -785,6 +803,7 @@ std::string handle_on_main(const std::string& method, const json& params, const
if (method == "describe_scene") return rpc_result(id, describe_scene(panel));
if (method == "query_topology") return rpc_result(id, query_topology(panel, params));
if (method == "measure") return rpc_result(id, measure(panel, params));
if (method == "mass_properties") return rpc_result(id, mass_properties(panel, params));
if (method == "slice_body") return rpc_result(id, slice_body(panel, params));
if (method == "import_step") return rpc_result(id, import_step(panel, params));
if (method == "import_mesh") return rpc_result(id, import_mesh(panel, params));