diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 374ebfb395..4b543661ea 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include // SurfaceFill: GeomAbs_C0 #include #include #include @@ -44,6 +46,7 @@ #include // multi-body: compound of bodies for display/compat #include #include // outward-normal orientation for face-extrude +#include // TopoDS::Edge for SurfaceFill #include // is_sheet_shape #include #include @@ -2076,6 +2079,44 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, result = rev.Shape(); have_body = true; break; } + case CadFeatureType::SurfaceLoft: { + std::vector profiles; + for (int ref : f.loft_profile_refs) { + if (ref < 0 || ref >= int(features.size()) + || (features[ref].type != CadFeatureType::Sketch + && features[ref].type != CadFeatureType::Project)) + continue; + profiles.push_back(build_sketch_wire(features[ref])); + } + if (profiles.size() < 2) + throw std::runtime_error("surface-loft needs 2+ valid profile sketches"); + TopoDS_Shape skin = SketchEngine::make_loft_surface(profiles, f.loft_ruled); + if (skin.IsNull()) throw std::runtime_error("surface-loft: loft failed"); + result = skin; have_body = true; + break; + } + case CadFeatureType::SurfaceFill: { + if (f.sketch_ref < 0 || f.sketch_ref >= int(features.size())) + throw std::runtime_error("surface-fill: bad sketch ref"); + const CadFeature& sk = features[f.sketch_ref]; + if (sk.type != CadFeatureType::Sketch && sk.type != CadFeatureType::Project) + throw std::runtime_error("surface-fill: ref is not a sketch"); + TopoDS_Wire wire = build_sketch_wire(sk); + if (wire.IsNull()) throw std::runtime_error("surface-fill: empty boundary"); + BRepOffsetAPI_MakeFilling fill; + int nedges = 0; + for (TopExp_Explorer ex(wire, TopAbs_EDGE); ex.More(); ex.Next()) { + fill.Add(TopoDS::Edge(ex.Current()), GeomAbs_C0); + ++nedges; + } + if (nedges == 0) throw std::runtime_error("surface-fill: boundary has no edges"); + fill.Build(); + if (!fill.IsDone()) throw std::runtime_error("surface-fill: fill failed"); + TopoDS_Shape face = fill.Shape(); + if (face.IsNull()) throw std::runtime_error("surface-fill: produced no geometry"); + result = face; 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 @@ -2855,6 +2896,7 @@ void CadDocument::route_feature(std::vector& bodies, const CadFeature& || 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::SurfaceLoft || f.type == CadFeatureType::SurfaceFill || ((f.type == CadFeatureType::Extrude || f.type == CadFeatureType::Revolve || f.type == CadFeatureType::Sweep || f.type == CadFeatureType::Loft) && f.mode == BooleanMode::New); @@ -3112,6 +3154,29 @@ int CadDocument::add_surface_revolve(int sketch_ref, double angle_deg, int axis, return int(features.size()) - 1; } +int CadDocument::add_surface_loft(const std::vector& profile_refs, bool ruled, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::SurfaceLoft; + f.name = name; + f.loft_profile_refs = profile_refs; + f.loft_ruled = ruled; + f.mode = BooleanMode::New; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_surface_fill(int sketch_ref, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::SurfaceFill; + f.name = name; + f.sketch_ref = sketch_ref; + 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) { diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index e94a1f45d8..f7854a2c1f 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, ThickenSurface, SurfaceOffset }; +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, SurfaceLoft, SurfaceFill }; enum class SketchShape { Rectangle, Circle }; enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident }; enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge }; @@ -479,6 +479,10 @@ public: // Loft through the ordered profile Sketches (each a closed wire on its own plane). int add_loft(const std::vector& profile_refs, bool ruled, BooleanMode mode, const std::string& name); + // Skin 2+ profile sketches open (no end caps) -> a sheet body. + int add_surface_loft(const std::vector& profile_refs, bool ruled, const std::string& name); + // Fill sketch sketch_ref's closed boundary wire with a smooth face -> a one-face sheet body. + int add_surface_fill(int sketch_ref, const std::string& name); int add_shell(double thickness, int face, int target_body, const std::string& name); // Grow a thin rib wall (thickness, depth) from the open Line entity `entity` inside sketch // feature `sketch_ref`, fused to `target_body`. Returns the new feature index. diff --git a/src/libslic3r/SketchEngine.cpp b/src/libslic3r/SketchEngine.cpp index 42187441e3..2b9c9c40bb 100644 --- a/src/libslic3r/SketchEngine.cpp +++ b/src/libslic3r/SketchEngine.cpp @@ -378,6 +378,24 @@ TopoDS_Shape SketchEngine::make_loft(const std::vector& profiles, b return s; } +// ponytail: sibling of make_loft that builds an open shell (sheet) instead of a solid. +TopoDS_Shape SketchEngine::make_loft_surface(const std::vector& profiles, bool ruled) +{ + if (profiles.size() < 2) + throw std::runtime_error("loft needs at least 2 profiles"); + BRepOffsetAPI_ThruSections loft(Standard_False /*shell, no end caps*/, + ruled ? Standard_True : Standard_False); + for (const TopoDS_Wire& w : profiles) { + if (w.IsNull()) throw std::runtime_error("loft: null profile wire"); + loft.AddWire(w); + } + loft.Build(); + if (!loft.IsDone()) throw std::runtime_error("loft failed"); + TopoDS_Shape s = loft.Shape(); + if (s.IsNull()) throw std::runtime_error("loft produced no shape"); + return s; +} + TopoDS_Shape SketchEngine::make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane, const TopoDS_Shape& target, double depth) { diff --git a/src/libslic3r/SketchEngine.hpp b/src/libslic3r/SketchEngine.hpp index 071125a33d..de008f22df 100644 --- a/src/libslic3r/SketchEngine.hpp +++ b/src/libslic3r/SketchEngine.hpp @@ -192,6 +192,11 @@ public: // given order. ruled=true => straight (ruled) sections; false => smooth (C2). static TopoDS_Shape make_loft(const std::vector& profiles, bool ruled); + // Skin `profiles` WITHOUT end caps -> an open shell (sheet). Same as make_loft but the + // ThruSections solid flag is false. // ponytail: a sibling instead of a bool param, so no + // existing call site changes. + static TopoDS_Shape make_loft_surface(const std::vector& profiles, bool ruled); + static TopoDS_Shape make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane, const TopoDS_Shape& target, double depth); diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index 60d0570e2e..ac7c517866 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -1070,6 +1070,35 @@ json action_surface_offset(DesignPanel* panel, const json& params) return json{{"ok", ok}, {"feature_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +json action_surface_loft(DesignPanel* panel, const json& params) +{ + if (!params.contains("profiles") || !params["profiles"].is_array()) + throw std::runtime_error("surface_loft needs 'profiles' (array of int feature indices)"); + CadDocument& doc = panel->mcp_doc(); + std::vector profiles; + for (const json& j : params["profiles"]) profiles.push_back(j.get()); + bool ruled = params.value("ruled", false); + doc.checkpoint(); + int idx = doc.add_surface_loft(profiles, ruled, "SurfaceLoft"); + 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_fill(DesignPanel* panel, const json& params) +{ + if (!params.contains("sketch")) throw std::runtime_error("surface_fill needs 'sketch' (feature index)"); + CadDocument& doc = panel->mcp_doc(); + int sketch = params["sketch"].get(); + doc.checkpoint(); + int idx = doc.add_surface_fill(sketch, "SurfaceFill"); + 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)"); @@ -1363,6 +1392,8 @@ std::string handle_on_main(const std::string& method, const json& params, const 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)); + if (method == "surface_loft") return rpc_result(id, action_surface_loft(panel, params)); + if (method == "surface_fill") return rpc_result(id, action_surface_fill(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 47533cfc75..80e945afc0 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -4752,3 +4752,125 @@ TEST_CASE("thicken-surface round-trip serialize/deserialize", "[CadDocument][sur REQUIRE_THAT(double(fy1), WithinAbs(double(oy1), 1e-6)); REQUIRE_THAT(double(fz1), WithinAbs(double(oz1), 1e-6)); } + +TEST_CASE("surface-loft makes an open shell", "[CadDocument][surface]") +{ + using Catch::Matchers::WithinAbs; + + CadDocument doc; + SketchProfile bot; + bot.points = {{-10,-10},{10,-10},{10,10},{-10,10}}; + bot.closed = true; + int s0 = doc.add_sketch_profile(bot, SketchPlane::XY(), "Bottom"); + + doc.add_plane(0 /*XY*/, 20.0, 0.0, 0, "Plane1"); + SketchPlane top = doc.resolve_datum_planes()[0].second; + SketchProfile tp; + tp.points = {{-5,-5},{5,-5},{5,5},{-5,5}}; + tp.closed = true; + int s1 = doc.add_sketch_profile(tp, top, "Top"); + + int fi = doc.add_surface_loft({s0, s1}, false, "Skin"); + REQUIRE(fi >= 0); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.bodies.size() == 1); + REQUIRE(CadDocument::is_sheet_shape(doc.bodies.back().shape)); + + // Contrast with a solid loft on the same profiles. + CadDocument doc2; + SketchProfile bot2; + bot2.points = {{-10,-10},{10,-10},{10,10},{-10,10}}; + bot2.closed = true; + int a0 = doc2.add_sketch_profile(bot2, SketchPlane::XY(), "Bottom"); + doc2.add_plane(0 /*XY*/, 20.0, 0.0, 0, "Plane1"); + SketchPlane top2 = doc2.resolve_datum_planes()[0].second; + SketchProfile tp2; + tp2.points = {{-5,-5},{5,-5},{5,5},{-5,5}}; + tp2.closed = true; + int a1 = doc2.add_sketch_profile(tp2, top2, "Top"); + doc2.add_loft({a0, a1}, false, BooleanMode::New, "SolidLoft"); + REQUIRE(doc2.recompute()); + REQUIRE(doc2.error.empty()); + REQUIRE_FALSE(CadDocument::is_sheet_shape(doc2.bodies.back().shape)); +} + +TEST_CASE("surface-fill makes a one-face sheet", "[CadDocument][surface]") +{ + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Rect"); + REQUIRE(sk >= 0); + int fi = doc.add_surface_fill(sk, "Patch"); + 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 face_count = 0; + for (TopExp_Explorer fe(doc.bodies.back().shape, TopAbs_FACE); fe.More(); fe.Next()) ++face_count; + REQUIRE(face_count >= 1); +} + +TEST_CASE("surface-loft / surface-fill bad refs safe", "[CadDocument][surface]") +{ + { + CadDocument doc; + doc.add_surface_loft({999}, false, "Bad"); + REQUIRE_FALSE(doc.recompute()); + REQUIRE_THAT(doc.error, Catch::Matchers::Contains("surface-loft")); + } + { + CadDocument doc; + doc.add_surface_fill(999, "Bad"); + REQUIRE_FALSE(doc.recompute()); + REQUIRE_THAT(doc.error, Catch::Matchers::Contains("surface-fill")); + } +} + +TEST_CASE("surface-loft round-trip serialize/deserialize", "[CadDocument][surface]") +{ + using Catch::Matchers::WithinAbs; + + CadDocument doc; + SketchProfile bot; + bot.points = {{-10,-10},{10,-10},{10,10},{-10,10}}; + bot.closed = true; + int s0 = doc.add_sketch_profile(bot, SketchPlane::XY(), "Bottom"); + doc.add_plane(0 /*XY*/, 20.0, 0.0, 0, "Plane1"); + SketchPlane top = doc.resolve_datum_planes()[0].second; + SketchProfile tp; + tp.points = {{-5,-5},{5,-5},{5,5},{-5,5}}; + tp.closed = true; + int s1 = doc.add_sketch_profile(tp, top, "Top"); + doc.add_surface_loft({s0, s1}, false, "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; + 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)); +}