diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 14286ab56b..a9e88d7c4a 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -36,10 +38,12 @@ #include #include #include +#include #include #include // multi-body: compound of bodies for display/compat #include #include // outward-normal orientation for face-extrude +#include // is_sheet_shape #include #include #include @@ -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& 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 diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index 404ef24111..f8b5d66ae7 100644 --- a/src/libslic3r/CadDocument.hpp +++ b/src/libslic3r/CadDocument.hpp @@ -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& 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 diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index 77608d7a1d..d11070af12 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -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(); + 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(); + 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")); diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index f3c190206b..0d01c9974e 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -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)); +}