mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
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:
co-authored by
Claude Opus 4.8
parent
8edc12c8f1
commit
b807be3c4a
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1379,6 +1379,112 @@ TEST_CASE("cut splits a body with a plane", "[cut]")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("mass properties: analytic cube", "[CadDocument]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
|
||||
doc.add_extrude(sk, 20.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
|
||||
auto mp = doc.body_mass_properties(0);
|
||||
REQUIRE(mp.valid);
|
||||
// Cube 20x20x20 -> V = 8000 mm^3
|
||||
REQUIRE_THAT(mp.volume, WithinRel(8000.0, 1e-6));
|
||||
// Surface area = 6 * 20^2 = 2400 mm^2
|
||||
REQUIRE_THAT(mp.surface_area, WithinRel(2400.0, 1e-6));
|
||||
// COM at geometric centre: the box centred on origin is from z=0 to z=20,
|
||||
// so centre at (0, 0, 10)
|
||||
REQUIRE_THAT(mp.center_of_mass.x(), WithinAbs(0.0, 1e-6));
|
||||
REQUIRE_THAT(mp.center_of_mass.y(), WithinAbs(0.0, 1e-6));
|
||||
REQUIRE_THAT(mp.center_of_mass.z(), WithinAbs(10.0, 1e-6));
|
||||
// Inertia sanity: symmetric cube -> diagonal terms equal, off-diagonal ~0
|
||||
double Ixx = mp.inertia[0], Iyy = mp.inertia[4], Izz = mp.inertia[8];
|
||||
REQUIRE_THAT(Ixx, WithinRel(8000.0 * (20*20 + 20*20) / 12.0, 1e-4)); // I = m/12*(b^2+h^2) about COM
|
||||
REQUIRE_THAT(Iyy, WithinRel(Ixx, 1e-4));
|
||||
REQUIRE_THAT(Izz, WithinRel(Ixx, 1e-4));
|
||||
REQUIRE_THAT(mp.inertia[1], WithinAbs(0.0, 1e-6));
|
||||
REQUIRE_THAT(mp.inertia[2], WithinAbs(0.0, 1e-6));
|
||||
REQUIRE_THAT(mp.inertia[5], WithinAbs(0.0, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("mass properties: analytic cylinder", "[CadDocument]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
CadDocument doc;
|
||||
// Circle r=10 on XY, extrude height=5 -> cylinder
|
||||
SketchEntity c;
|
||||
c.type = SketchEntity::Type::Circle;
|
||||
c.center = Vec2d(0, 0);
|
||||
c.radius = 10.0;
|
||||
int sk = doc.add_sketch_entities({c}, SketchPlane::XY(), "Circle");
|
||||
doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
|
||||
auto mp = doc.body_mass_properties(0);
|
||||
REQUIRE(mp.valid);
|
||||
double expected_vol = M_PI * 100.0 * 5.0; // pi * r^2 * h
|
||||
REQUIRE_THAT(mp.volume, WithinRel(expected_vol, 1e-4));
|
||||
// Surface: 2*pi*r^2 + 2*pi*r*h = 2*pi*100 + 2*pi*10*5 = 200*pi + 100*pi = 300*pi
|
||||
double expected_area = 2 * M_PI * 100.0 + 2 * M_PI * 10.0 * 5.0;
|
||||
REQUIRE_THAT(mp.surface_area, WithinRel(expected_area, 1e-4));
|
||||
}
|
||||
|
||||
TEST_CASE("mass properties: hollow body", "[CadDocument]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
// Solid cube
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
|
||||
doc.add_extrude(sk, 20.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
auto mp_solid = doc.body_mass_properties(0);
|
||||
REQUIRE(mp_solid.valid);
|
||||
double solid_vol = mp_solid.volume;
|
||||
|
||||
// Hollow cube with a through hole
|
||||
CadDocument doc2;
|
||||
int sk2 = doc2.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
|
||||
doc2.add_extrude(sk2, 20.0, false, BooleanMode::New, "Extrude");
|
||||
doc2.add_hole(10.0, 20.0, true, 0.0, 0.0, SketchPlane::XY(), "Hole");
|
||||
REQUIRE(doc2.recompute());
|
||||
auto mp_hollow = doc2.body_mass_properties(0);
|
||||
REQUIRE(mp_hollow.valid);
|
||||
|
||||
// Hollow volume < solid volume
|
||||
REQUIRE(mp_hollow.volume < solid_vol);
|
||||
// Hollow = solid - cylinder: V_cyl = pi*5^2*20 = 500*pi
|
||||
double expected_hollow = solid_vol - M_PI * 25.0 * 20.0;
|
||||
REQUIRE_THAT(mp_hollow.volume, WithinRel(expected_hollow, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("mass properties: invalid body index", "[CadDocument]")
|
||||
{
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
|
||||
doc.add_extrude(sk, 20.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
// Out of range -> valid=false
|
||||
auto mp = doc.body_mass_properties(99);
|
||||
REQUIRE_FALSE(mp.valid);
|
||||
REQUIRE(mp.volume == 0.0);
|
||||
// Negative index
|
||||
auto mp2 = doc.body_mass_properties(-1);
|
||||
REQUIRE_FALSE(mp2.valid);
|
||||
|
||||
// Empty document (no recompute) -> all bodies empty, index 0 out of range
|
||||
CadDocument empty;
|
||||
auto mp3 = empty.body_mass_properties(0);
|
||||
REQUIRE_FALSE(mp3.valid);
|
||||
}
|
||||
|
||||
TEST_CASE("serialize_recipe roundtrip with two bodies", "[CadDocument]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
Reference in New Issue
Block a user