mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
M7c: SurfaceLoft + SurfaceFill (open skins from profiles / a boundary)
The two remaining ways to create a sheet body. SurfaceLoft skins 2+ profile sketches without end caps via a new SketchEngine::make_loft_surface — a sibling of make_loft with the ThruSections solid flag false, so no existing call site changes. SurfaceFill patches a single closed boundary wire into a smooth face with BRepOffsetAPI_MakeFilling, adding each boundary edge as a C0 constraint. Purely additive: two enum values appended to CadFeatureType, reusing the existing loft_profile_refs/loft_ruled and sketch_ref fields. No new cereal fields, recipe stays v2, golden fixture unchanged (30773). MCP surface_loft/surface_fill added as pure additions. Suite 107 cases / 1553 assertions green, including a test contrasting the open skin against the solid loft of the same profiles. 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
2da75d28ca
commit
9c28be5860
@@ -19,6 +19,7 @@
|
||||
#include <BRepOffsetAPI_MakePipeShell.hxx>
|
||||
#include <BRepOffsetAPI_MakeThickSolid.hxx>
|
||||
#include <BRepOffsetAPI_MakeOffsetShape.hxx>
|
||||
#include <BRepOffsetAPI_MakeFilling.hxx>
|
||||
#include <BRepOffsetAPI_DraftAngle.hxx>
|
||||
#include <Bnd_Box.hxx>
|
||||
#include <BRepBndLib.hxx>
|
||||
@@ -36,6 +37,7 @@
|
||||
#include <Geom_CylindricalSurface.hxx>
|
||||
#include <Geom_ConicalSurface.hxx>
|
||||
#include <Geom2d_TrimmedCurve.hxx>
|
||||
#include <GeomAbs_Shape.hxx> // SurfaceFill: GeomAbs_C0
|
||||
#include <GCE2d_MakeSegment.hxx>
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
@@ -44,6 +46,7 @@
|
||||
#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 <TopoDS.hxx> // TopoDS::Edge for SurfaceFill
|
||||
#include <TopExp_Explorer.hxx> // is_sheet_shape
|
||||
#include <gp_Circ.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
@@ -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<TopoDS_Wire> 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<CadBody>& 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<int>& 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)
|
||||
{
|
||||
|
||||
@@ -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<int>& 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<int>& 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.
|
||||
|
||||
@@ -378,6 +378,24 @@ TopoDS_Shape SketchEngine::make_loft(const std::vector<TopoDS_Wire>& 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<TopoDS_Wire>& 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)
|
||||
{
|
||||
|
||||
@@ -192,6 +192,11 @@ public:
|
||||
// given order. ruled=true => straight (ruled) sections; false => smooth (C2).
|
||||
static TopoDS_Shape make_loft(const std::vector<TopoDS_Wire>& 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<TopoDS_Wire>& profiles, bool ruled);
|
||||
|
||||
static TopoDS_Shape make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
const TopoDS_Shape& target, double depth);
|
||||
|
||||
|
||||
@@ -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<int> profiles;
|
||||
for (const json& j : params["profiles"]) profiles.push_back(j.get<int>());
|
||||
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<int>();
|
||||
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"));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user