mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
M7a: surface bodies — SurfaceExtrude + SurfaceRevolve (open shells)
Two body-producing features that emit an open shell instead of a capped solid: SurfaceExtrude (prism of a sketch wire, no end caps) and SurfaceRevolve (revolve of a wire about an in-plane axis, no caps). Each appends a new sheet body whose TopoDS_Shape has no TopAbs_SOLID. Purely additive: two enum values appended at the end of CadFeatureType, reusing existing serialized fields (sketch_ref/distance, revolve_angle/revolve_axis). No new cereal fields, recipe stays v2, golden fixture unchanged. is_sheet_shape() derives sheet-ness from the OCCT shape type (bodies are not serialized). MCP surface_extrude/surface_revolve added as pure additions. Suite 99 cases / 1474 assertions green. 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
15ea0a813f
commit
01e474e17c
@@ -25,6 +25,8 @@
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
#include <BRepPrimAPI_MakeCylinder.hxx>
|
||||
#include <BRepPrimAPI_MakeCone.hxx>
|
||||
#include <BRepPrimAPI_MakePrism.hxx>
|
||||
#include <BRepPrimAPI_MakeRevol.hxx>
|
||||
#include <BRepCheck_Analyzer.hxx>
|
||||
#include <BRepLib.hxx>
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
@@ -36,10 +38,12 @@
|
||||
#include <GCE2d_MakeSegment.hxx>
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopAbs.hxx>
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Compound.hxx> // multi-body: compound of bodies for display/compat
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <TopAbs_Orientation.hxx> // outward-normal orientation for face-extrude
|
||||
#include <TopExp_Explorer.hxx> // is_sheet_shape
|
||||
#include <gp_Circ.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
#include <gp_Ax3.hxx>
|
||||
@@ -2012,6 +2016,39 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CadFeatureType::SurfaceExtrude: {
|
||||
if (f.sketch_ref < 0 || f.sketch_ref >= int(features.size()))
|
||||
throw std::runtime_error("surface-extrude: bad sketch ref");
|
||||
const CadFeature& sk = features[f.sketch_ref];
|
||||
if (sk.type != CadFeatureType::Sketch && sk.type != CadFeatureType::Project)
|
||||
throw std::runtime_error("surface-extrude: ref is not a sketch");
|
||||
TopoDS_Wire wire = build_sketch_wire(sk);
|
||||
if (wire.IsNull()) throw std::runtime_error("surface-extrude: empty profile");
|
||||
gp_Dir nrm(sk.plane.normal.x(), sk.plane.normal.y(), sk.plane.normal.z());
|
||||
gp_Vec v(nrm.XYZ() * f.distance);
|
||||
TopoDS_Shape shell = BRepPrimAPI_MakePrism(wire, v, false, true).Shape();
|
||||
if (shell.IsNull()) throw std::runtime_error("surface-extrude: prism failed");
|
||||
result = shell; have_body = true;
|
||||
break;
|
||||
}
|
||||
case CadFeatureType::SurfaceRevolve: {
|
||||
if (f.sketch_ref < 0 || f.sketch_ref >= int(features.size()))
|
||||
throw std::runtime_error("surface-revolve: bad sketch ref");
|
||||
const CadFeature& sk = features[f.sketch_ref];
|
||||
if (sk.type != CadFeatureType::Sketch && sk.type != CadFeatureType::Project)
|
||||
throw std::runtime_error("surface-revolve: ref is not a sketch");
|
||||
TopoDS_Wire wire = build_sketch_wire(sk);
|
||||
if (wire.IsNull()) throw std::runtime_error("surface-revolve: empty profile");
|
||||
const Vec3d& adir = (f.revolve_axis == 1) ? sk.plane.y_axis : sk.plane.x_axis;
|
||||
gp_Pnt o(sk.plane.origin.x(), sk.plane.origin.y(), sk.plane.origin.z());
|
||||
gp_Dir xd(adir.x(), adir.y(), adir.z());
|
||||
gp_Ax1 axis(o, xd);
|
||||
const double ang = f.revolve_angle * M_PI / 180.0;
|
||||
BRepPrimAPI_MakeRevol rev(wire, axis, ang, false);
|
||||
if (!rev.IsDone()) throw std::runtime_error("surface-revolve: revolve failed");
|
||||
result = rev.Shape(); have_body = true;
|
||||
break;
|
||||
}
|
||||
case CadFeatureType::Sweep: {
|
||||
const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size())
|
||||
&& (features[f.sketch_ref].type == CadFeatureType::Sketch
|
||||
@@ -2741,6 +2778,7 @@ void CadDocument::route_feature(std::vector<CadBody>& bodies, const CadFeature&
|
||||
// mutates the target body in place.
|
||||
const bool starts_new = bodies.empty()
|
||||
|| f.type == CadFeatureType::Import // an imported solid is always its own base body
|
||||
|| f.type == CadFeatureType::SurfaceExtrude || f.type == CadFeatureType::SurfaceRevolve
|
||||
|| ((f.type == CadFeatureType::Extrude || f.type == CadFeatureType::Revolve
|
||||
|| f.type == CadFeatureType::Sweep || f.type == CadFeatureType::Loft)
|
||||
&& f.mode == BooleanMode::New);
|
||||
@@ -2973,4 +3011,35 @@ GeometryEngine::MassProps CadDocument::body_mass_properties(int body_index) cons
|
||||
return GeometryEngine::mass_properties(bodies[body_index].shape);
|
||||
}
|
||||
|
||||
int CadDocument::add_surface_extrude(int sketch_ref, double distance, const std::string& name)
|
||||
{
|
||||
CadFeature f;
|
||||
f.type = CadFeatureType::SurfaceExtrude;
|
||||
f.name = name;
|
||||
f.sketch_ref = sketch_ref;
|
||||
f.distance = distance;
|
||||
f.mode = BooleanMode::New;
|
||||
features.push_back(f);
|
||||
return int(features.size()) - 1;
|
||||
}
|
||||
|
||||
int CadDocument::add_surface_revolve(int sketch_ref, double angle_deg, int axis, const std::string& name)
|
||||
{
|
||||
CadFeature f;
|
||||
f.type = CadFeatureType::SurfaceRevolve;
|
||||
f.name = name;
|
||||
f.sketch_ref = sketch_ref;
|
||||
f.revolve_angle = angle_deg;
|
||||
f.revolve_axis = axis;
|
||||
f.mode = BooleanMode::New;
|
||||
features.push_back(f);
|
||||
return int(features.size()) - 1;
|
||||
}
|
||||
|
||||
// ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway.
|
||||
bool CadDocument::is_sheet_shape(const TopoDS_Shape& s)
|
||||
{
|
||||
return !TopExp_Explorer(s, TopAbs_SOLID).More();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project, DeleteFace, Rib };
|
||||
enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project, DeleteFace, Rib, SurfaceExtrude, SurfaceRevolve };
|
||||
enum class SketchShape { Rectangle, Circle };
|
||||
enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident };
|
||||
enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge };
|
||||
@@ -510,6 +510,8 @@ public:
|
||||
int add_thicken(int target_body, int face, double thickness, bool flip, const std::string& name);
|
||||
int add_delete_face(int target_body, const std::vector<int>& faces,
|
||||
const std::string& name);
|
||||
int add_surface_extrude(int sketch_ref, double distance, const std::string& name);
|
||||
int add_surface_revolve(int sketch_ref, double angle_deg, int axis, const std::string& name);
|
||||
// Datum plane: derived from base (0=XY/1=XZ/2=YZ/3+N=Nth earlier datum), offset
|
||||
// along its normal, optional tilt about a base axis. Produces no solid.
|
||||
int add_plane(int base, double offset, double angle_tilt, int axis,
|
||||
@@ -553,6 +555,9 @@ public:
|
||||
|
||||
GeometryEngine::MassProps body_mass_properties(int body_index) const;
|
||||
|
||||
// ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway.
|
||||
static bool is_sheet_shape(const TopoDS_Shape& s); // true if TopExp finds no TopAbs_SOLID
|
||||
|
||||
// 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
|
||||
|
||||
@@ -79,6 +79,8 @@ const char* feature_type_name(CadFeatureType t)
|
||||
case CadFeatureType::Project: return "Project";
|
||||
case CadFeatureType::DeleteFace: return "DeleteFace";
|
||||
case CadFeatureType::Rib: return "Rib";
|
||||
case CadFeatureType::SurfaceExtrude: return "SurfaceExtrude";
|
||||
case CadFeatureType::SurfaceRevolve: return "SurfaceRevolve";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -311,6 +313,17 @@ json describe_tools()
|
||||
json{{"name", "depth"}, {"type", "number"}, {"unit", "mm"}, {"default", 10}, {"min", 0.01}},
|
||||
json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body"}},
|
||||
})}},
|
||||
json{{"name", "surface_extrude"}, {"summary", "Extrude a sketch wire with no end caps -> an open sheet body."},
|
||||
{"params", json::array({
|
||||
json{{"name", "sketch"}, {"type", "integer"}, {"description", "sketch feature index"}},
|
||||
json{{"name", "distance"},{"type", "number"}, {"unit", "mm"}, {"default", 10}, {"min", 0.01}},
|
||||
})}},
|
||||
json{{"name", "surface_revolve"}, {"summary", "Revolve a sketch wire with no caps -> an open sheet body."},
|
||||
{"params", json::array({
|
||||
json{{"name", "sketch"}, {"type", "integer"}, {"description", "sketch feature index"}},
|
||||
json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 360}},
|
||||
json{{"name", "axis"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}},
|
||||
})}},
|
||||
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}},
|
||||
@@ -999,6 +1012,35 @@ json action_rib(DesignPanel* panel, const json& params)
|
||||
return json{{"ok", ok}, {"rib_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
json action_surface_extrude(DesignPanel* panel, const json& params)
|
||||
{
|
||||
if (!params.contains("sketch")) throw std::runtime_error("surface_extrude needs 'sketch' (feature index)");
|
||||
CadDocument& doc = panel->mcp_doc();
|
||||
int sketch = params["sketch"].get<int>();
|
||||
double distance = params.value("distance", 10.0);
|
||||
doc.checkpoint();
|
||||
int idx = doc.add_surface_extrude(sketch, distance, "SurfaceExtrude");
|
||||
bool ok = doc.recompute();
|
||||
if (!ok) doc.undo();
|
||||
panel->mcp_after_change();
|
||||
return json{{"ok", ok}, {"feature_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
json action_surface_revolve(DesignPanel* panel, const json& params)
|
||||
{
|
||||
if (!params.contains("sketch")) throw std::runtime_error("surface_revolve needs 'sketch' (feature index)");
|
||||
CadDocument& doc = panel->mcp_doc();
|
||||
int sketch = params["sketch"].get<int>();
|
||||
double angle = params.value("angle", 360.0);
|
||||
int axis = params.value("axis", 0);
|
||||
doc.checkpoint();
|
||||
int idx = doc.add_surface_revolve(sketch, angle, axis, "SurfaceRevolve");
|
||||
bool ok = doc.recompute();
|
||||
if (!ok) doc.undo();
|
||||
panel->mcp_after_change();
|
||||
return json{{"ok", ok}, {"feature_index", idx}, {"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)");
|
||||
@@ -1288,6 +1330,8 @@ std::string handle_on_main(const std::string& method, const json& params, const
|
||||
if (method == "helix") return rpc_result(id, action_helix(panel, params));
|
||||
if (method == "set_variable") return rpc_result(id, action_set_variable(panel, params));
|
||||
if (method == "set_feature_expr") return rpc_result(id, action_set_feature_expr(panel, params));
|
||||
if (method == "surface_extrude") return rpc_result(id, action_surface_extrude(panel, params));
|
||||
if (method == "surface_revolve") return rpc_result(id, action_surface_revolve(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"));
|
||||
|
||||
@@ -4532,3 +4532,97 @@ TEST_CASE("parametric recipe round-trips through serialize/deserialize", "[CadDo
|
||||
REQUIRE_THAT(ny1, WithinAbs(oy1, 1e-6));
|
||||
REQUIRE_THAT(nz1, WithinAbs(oz1, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("surface-extrude makes an open shell", "[CadDocument][surface]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Rect");
|
||||
REQUIRE(sk >= 0);
|
||||
int fi = doc.add_surface_extrude(sk, 12.0, "Skin");
|
||||
REQUIRE(fi == 1);
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(doc.bodies.size() == 1);
|
||||
REQUIRE(CadDocument::is_sheet_shape(doc.bodies.back().shape));
|
||||
|
||||
// A rectangle skin has 4 side faces (no end caps).
|
||||
int face_count = 0, solid_count = 0;
|
||||
for (TopExp_Explorer fe(doc.bodies.back().shape, TopAbs_FACE); fe.More(); fe.Next()) ++face_count;
|
||||
for (TopExp_Explorer se(doc.bodies.back().shape, TopAbs_SOLID); se.More(); se.Next()) ++solid_count;
|
||||
REQUIRE(face_count >= 1);
|
||||
REQUIRE(solid_count == 0);
|
||||
REQUIRE(doc.display_mesh.facets_count() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("surface-revolve makes an open shell", "[CadDocument][surface]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
CadDocument doc;
|
||||
// A small rectangle offset from the axis: u=10..15, v=0..5.
|
||||
SketchProfile sp;
|
||||
sp.points = {{10,0},{15,0},{15,5},{10,5}};
|
||||
sp.closed = true;
|
||||
const int sk = doc.add_sketch_profile(sp, SketchPlane::XY(), "Profile");
|
||||
REQUIRE(sk >= 0);
|
||||
int fi = doc.add_surface_revolve(sk, 360, 0, "Rev");
|
||||
REQUIRE(fi == 1);
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(doc.bodies.size() == 1);
|
||||
REQUIRE(CadDocument::is_sheet_shape(doc.bodies.back().shape));
|
||||
|
||||
int solid_count = 0;
|
||||
for (TopExp_Explorer se(doc.bodies.back().shape, TopAbs_SOLID); se.More(); se.Next()) ++solid_count;
|
||||
REQUIRE(solid_count == 0);
|
||||
REQUIRE(doc.display_mesh.facets_count() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("surface-extrude bad ref safe", "[CadDocument][surface]")
|
||||
{
|
||||
CadDocument doc;
|
||||
int fi = doc.add_surface_extrude(999, 10, "Bad");
|
||||
REQUIRE(fi == 0);
|
||||
REQUIRE_FALSE(doc.recompute());
|
||||
REQUIRE_THAT(doc.error, Catch::Matchers::Contains("surface-extrude"));
|
||||
}
|
||||
|
||||
TEST_CASE("surface round-trip serialize/deserialize", "[CadDocument][surface]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Rect");
|
||||
doc.add_surface_extrude(sk, 12.0, "Skin");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(CadDocument::is_sheet_shape(doc.bodies.back().shape));
|
||||
|
||||
size_t orig_nb = doc.bodies.size();
|
||||
Bnd_Box orig_bb;
|
||||
BRepBndLib::Add(doc.bodies.back().shape, orig_bb);
|
||||
|
||||
std::string blob = doc.serialize_recipe();
|
||||
REQUIRE_FALSE(blob.empty());
|
||||
|
||||
CadDocument fresh;
|
||||
REQUIRE(fresh.deserialize_recipe(blob));
|
||||
REQUIRE(fresh.error.empty());
|
||||
REQUIRE(fresh.bodies.size() == orig_nb);
|
||||
REQUIRE(CadDocument::is_sheet_shape(fresh.bodies.back().shape));
|
||||
|
||||
Bnd_Box fresh_bb;
|
||||
BRepBndLib::Add(fresh.bodies.back().shape, fresh_bb);
|
||||
Standard_Real ox0, oy0, oz0, ox1, oy1, oz1;
|
||||
orig_bb.Get(ox0, oy0, oz0, ox1, oy1, oz1);
|
||||
Standard_Real fx0, fy0, fz0, fx1, fy1, fz1;
|
||||
fresh_bb.Get(fx0, fy0, fz0, fx1, fy1, fz1);
|
||||
REQUIRE_THAT(double(fx0), WithinAbs(double(ox0), 1e-6));
|
||||
REQUIRE_THAT(double(fy0), WithinAbs(double(oy0), 1e-6));
|
||||
REQUIRE_THAT(double(fz0), WithinAbs(double(oz0), 1e-6));
|
||||
REQUIRE_THAT(double(fx1), WithinAbs(double(ox1), 1e-6));
|
||||
REQUIRE_THAT(double(fy1), WithinAbs(double(oy1), 1e-6));
|
||||
REQUIRE_THAT(double(fz1), WithinAbs(double(oz1), 1e-6));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user