diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index a9e88d7c4a..374ebfb395 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -1197,6 +1198,32 @@ int CadDocument::add_thicken(int target_body, int face, double thickness, bool f return int(features.size()) - 1; } +int CadDocument::add_thicken_surface(int target_body, double thickness, bool flip, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::ThickenSurface; + f.name = name; + f.target_body = target_body; + f.thicken_thickness = thickness; + f.thicken_flip = flip; + f.mode = BooleanMode::New; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_surface_offset(int target_body, double offset, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::SurfaceOffset; + f.name = name; + f.target_body = target_body; + f.plane_offset = offset; + f.mode = BooleanMode::New; + features.push_back(f); + return int(features.size()) - 1; +} + int CadDocument::add_project_edges(int source_body, const std::vector& edge_ids, int face, const SketchPlane& plane, const std::string& name) { @@ -2672,6 +2699,52 @@ void CadDocument::apply_thicken(std::vector& bodies, const CadFeature& bodies.push_back({solid, f.name.empty() ? std::string("Thicken") : f.name}); } +void CadDocument::apply_thicken_surface(std::vector& bodies, const CadFeature& f) const +{ + const int nb = int(bodies.size()); + if (nb == 0) throw std::runtime_error("thicken-surface: no target body"); + const int tgt = (f.target_body >= 0 && f.target_body < nb) ? f.target_body : nb - 1; + if (tgt < 0 || bodies[tgt].shape.IsNull()) throw std::runtime_error("thicken-surface: no target body"); + if (!is_sheet_shape(bodies[tgt].shape)) throw std::runtime_error("thicken-surface: target is not a sheet body"); + if (std::abs(f.thicken_thickness) < 1e-9) throw std::runtime_error("thicken-surface: thickness is zero"); + + const double off = f.thicken_flip ? -std::abs(f.thicken_thickness) + : std::abs(f.thicken_thickness); + BRepOffsetAPI_MakeThickSolid mts; + mts.MakeThickSolidBySimple(bodies[tgt].shape, off); + mts.Build(); + if (!mts.IsDone()) throw std::runtime_error("thicken-surface: failed"); + TopoDS_Shape solid = mts.Shape(); + if (solid.IsNull()) throw std::runtime_error("thicken-surface: produced no geometry"); + + { + GProp_GProps props; + BRepGProp::VolumeProperties(solid, props); + if (props.Mass() < 0.0) solid.Reverse(); + } + + bodies.push_back({solid, f.name.empty() ? std::string("ThickenSurface") : f.name}); +} + +void CadDocument::apply_surface_offset(std::vector& bodies, const CadFeature& f) const +{ + const int nb = int(bodies.size()); + if (nb == 0) throw std::runtime_error("surface-offset: no target body"); + const int tgt = (f.target_body >= 0 && f.target_body < nb) ? f.target_body : nb - 1; + if (tgt < 0 || bodies[tgt].shape.IsNull()) throw std::runtime_error("surface-offset: no target body"); + if (!is_sheet_shape(bodies[tgt].shape)) throw std::runtime_error("surface-offset: target is not a sheet body"); + const double d = f.plane_offset; + if (std::abs(d) < 1e-9) throw std::runtime_error("surface-offset: zero offset"); + + BRepOffsetAPI_MakeOffsetShape mos; + mos.PerformBySimple(bodies[tgt].shape, d); + if (!mos.IsDone()) throw std::runtime_error("surface-offset: failed"); + TopoDS_Shape off_shape = mos.Shape(); + if (off_shape.IsNull()) throw std::runtime_error("surface-offset: produced no geometry"); + + bodies.push_back({off_shape, f.name.empty() ? std::string("SurfaceOffset") : f.name}); +} + void CadDocument::apply_project(const std::vector& bodies, CadFeature& f) const { f.entities.clear(); @@ -2769,6 +2842,8 @@ void CadDocument::route_feature(std::vector& bodies, const CadFeature& if (f.type == CadFeatureType::Mirror) { apply_mirror(bodies, f); return; } // mirror body about plane if (f.type == CadFeatureType::Transform) { apply_transform(bodies, f); return; } // move/rotate body if (f.type == CadFeatureType::Thicken) { apply_thicken(bodies, f); return; } // face -> plate + if (f.type == CadFeatureType::ThickenSurface) { apply_thicken_surface(bodies, f); return; } + if (f.type == CadFeatureType::SurfaceOffset) { apply_surface_offset(bodies, f); return; } if (f.type == CadFeatureType::Project) return; // sketch-like: consumed downstream, no body // Resolve the target body: explicit target_body when valid, else the last body. const int t = (f.target_body >= 0 && f.target_body < int(bodies.size())) @@ -2779,6 +2854,7 @@ void CadDocument::route_feature(std::vector& bodies, const CadFeature& 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::ThickenSurface || f.type == CadFeatureType::SurfaceOffset || ((f.type == CadFeatureType::Extrude || f.type == CadFeatureType::Revolve || f.type == CadFeatureType::Sweep || f.type == CadFeatureType::Loft) && f.mode == BooleanMode::New); diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index f8b5d66ae7..e94a1f45d8 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, SurfaceExtrude, SurfaceRevolve }; +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, ThickenSurface, SurfaceOffset }; enum class SketchShape { Rectangle, Circle }; enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident }; enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge }; @@ -508,6 +508,10 @@ public: // Offset face `face` of `target_body` by `thickness` along its normal, producing a new // thin solid appended as a new body. flip=true offsets against the normal. int add_thicken(int target_body, int face, double thickness, bool flip, const std::string& name); + // Thicken an entire SHEET body's shell into a solid. + int add_thicken_surface(int target_body, double thickness, bool flip, const std::string& name); + // Offset a SHEET body's shell by a signed distance, producing another SHEET body. + int add_surface_offset(int target_body, double offset, 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); @@ -623,6 +627,8 @@ private: void apply_mirror(std::vector& bodies, const CadFeature& f) const; void apply_transform(std::vector& bodies, const CadFeature& f) const; void apply_thicken(std::vector& bodies, const CadFeature& f) const; + void apply_thicken_surface(std::vector& bodies, const CadFeature& f) const; + void apply_surface_offset(std::vector& bodies, const CadFeature& f) const; void apply_project(const std::vector& bodies, CadFeature& f) const; // Undo/redo stacks of feature-list snapshots. checkpoint() pushes onto m_undo and diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index d11070af12..60d0570e2e 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -1041,6 +1041,35 @@ json action_surface_revolve(DesignPanel* panel, const json& params) return json{{"ok", ok}, {"feature_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +json action_thicken_surface(DesignPanel* panel, const json& params) +{ + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no sheet body to thicken"); + int bi = target_body_arg(params, doc); + double thickness = params.value("thickness", 2.0); + bool flip = params.value("flip", false); + doc.checkpoint(); + int idx = doc.add_thicken_surface(bi, thickness, flip, "ThickenSurface"); + 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_offset(DesignPanel* panel, const json& params) +{ + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no sheet body to offset"); + int bi = target_body_arg(params, doc); + double offset = params.value("offset", 1.0); + doc.checkpoint(); + int idx = doc.add_surface_offset(bi, offset, "SurfaceOffset"); + 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)"); @@ -1332,6 +1361,8 @@ std::string handle_on_main(const std::string& method, const json& params, const 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)); + if (method == "thicken_surface") return rpc_result(id, action_thicken_surface(panel, params)); + if (method == "surface_offset") return rpc_result(id, action_surface_offset(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 0d01c9974e..47533cfc75 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -4626,3 +4626,129 @@ TEST_CASE("surface round-trip serialize/deserialize", "[CadDocument][surface]") REQUIRE_THAT(double(fy1), WithinAbs(double(oy1), 1e-6)); REQUIRE_THAT(double(fz1), WithinAbs(double(oz1), 1e-6)); } + +TEST_CASE("thicken-surface makes a solid from a sheet", "[CadDocument][surface]") +{ + using Catch::Matchers::WithinRel; + using Catch::Matchers::WithinAbs; + + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Rect"); + REQUIRE(sk >= 0); + int si = doc.add_surface_extrude(sk, 12.0, "Skin"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.bodies.size() == 1); + REQUIRE(CadDocument::is_sheet_shape(doc.bodies.back().shape)); + + Bnd_Box sheet_bb; + BRepBndLib::Add(doc.bodies.back().shape, sheet_bb); + + int ti = doc.add_thicken_surface(0, 2.0, false, "Wall"); + REQUIRE(ti == 2); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.bodies.size() == 2); + + REQUIRE(!CadDocument::is_sheet_shape(doc.bodies.back().shape)); + double vol = doc.body_mass_properties(1).volume; + REQUIRE(vol > 0); + + Bnd_Box thick_bb; + BRepBndLib::Add(doc.bodies.back().shape, thick_bb); + Standard_Real sx0, sy0, sz0, sx1, sy1, sz1; + Standard_Real tx0, ty0, tz0, tx1, ty1, tz1; + sheet_bb.Get(sx0, sy0, sz0, sx1, sy1, sz1); + thick_bb.Get(tx0, ty0, tz0, tx1, ty1, tz1); + REQUIRE_THAT(double(tx0), WithinAbs(double(sx0), 2.1)); + REQUIRE_THAT(double(tx1), WithinAbs(double(sx1), 2.1)); +} + +TEST_CASE("surface-offset creates another sheet shifted outward", "[CadDocument][surface]") +{ + using Catch::Matchers::WithinAbs; + + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Rect"); + REQUIRE(sk >= 0); + int si = doc.add_surface_extrude(sk, 12.0, "Skin"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + Bnd_Box src_bb; + BRepBndLib::Add(doc.bodies.back().shape, src_bb); + + int oi = doc.add_surface_offset(0, 1.0, "Off"); + REQUIRE(oi == 2); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.bodies.size() == 2); + REQUIRE(CadDocument::is_sheet_shape(doc.bodies.back().shape)); + + Bnd_Box off_bb; + BRepBndLib::Add(doc.bodies.back().shape, off_bb); + Standard_Real sx0, sy0, sz0, sx1, sy1, sz1; + Standard_Real ox0, oy0, oz0, ox1, oy1, oz1; + src_bb.Get(sx0, sy0, sz0, sx1, sy1, sz1); + off_bb.Get(ox0, oy0, oz0, ox1, oy1, oz1); + REQUIRE(std::abs(double(ox0) - double(sx0)) > 1e-3); + REQUIRE(std::abs(double(ox1) - double(sx1)) > 1e-3); + REQUIRE(std::abs(double(oy0) - double(sy0)) > 1e-3); + REQUIRE(std::abs(double(oy1) - double(sy1)) > 1e-3); +} + +TEST_CASE("thicken-surface on non-sheet fails", "[CadDocument][surface]") +{ + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Rect"); + REQUIRE(sk >= 0); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Solid"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE_FALSE(CadDocument::is_sheet_shape(doc.bodies.back().shape)); + + doc.add_thicken_surface(0, 2.0, false, "Bad"); + REQUIRE_FALSE(doc.recompute()); + REQUIRE_THAT(doc.error, Catch::Matchers::Contains("sheet")); +} + +TEST_CASE("thicken-surface round-trip serialize/deserialize", "[CadDocument][surface]") +{ + using Catch::Matchers::WithinAbs; + using Catch::Matchers::WithinRel; + + 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()); + doc.add_thicken_surface(0, 2.0, false, "Wall"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE_FALSE(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_FALSE(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; + Standard_Real fx0, fy0, fz0, fx1, fy1, fz1; + orig_bb.Get(ox0, oy0, oz0, ox1, oy1, oz1); + 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)); +}