mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
M7b: thicken-from-surface + surface offset
Two features bridging sheet bodies back to solids and to other sheets: ThickenSurface feeds a whole sheet shell to MakeThickSolidBySimple (the same OCCT recipe the face-level Thicken already uses) and appends the result as a solid; SurfaceOffset offsets a sheet's shell along its normals via MakeOffsetShape::PerformBySimple, keeping it open. Both refuse a non-sheet target with a clear error. Purely additive: two enum values appended to CadFeatureType, reusing the existing target_body / thicken_thickness / thicken_flip / plane_offset fields. No new cereal fields, recipe stays v2, golden fixture unchanged (30773). MCP thicken_surface/surface_offset added as pure additions. Suite 103 cases / 1520 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
01e474e17c
commit
2da75d28ca
@@ -18,6 +18,7 @@
|
||||
#include <BRepOffsetAPI_MakePipe.hxx>
|
||||
#include <BRepOffsetAPI_MakePipeShell.hxx>
|
||||
#include <BRepOffsetAPI_MakeThickSolid.hxx>
|
||||
#include <BRepOffsetAPI_MakeOffsetShape.hxx>
|
||||
#include <BRepOffsetAPI_DraftAngle.hxx>
|
||||
#include <Bnd_Box.hxx>
|
||||
#include <BRepBndLib.hxx>
|
||||
@@ -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<int>& edge_ids, int face,
|
||||
const SketchPlane& plane, const std::string& name)
|
||||
{
|
||||
@@ -2672,6 +2699,52 @@ void CadDocument::apply_thicken(std::vector<CadBody>& bodies, const CadFeature&
|
||||
bodies.push_back({solid, f.name.empty() ? std::string("Thicken") : f.name});
|
||||
}
|
||||
|
||||
void CadDocument::apply_thicken_surface(std::vector<CadBody>& 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<CadBody>& 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<CadBody>& bodies, CadFeature& f) const
|
||||
{
|
||||
f.entities.clear();
|
||||
@@ -2769,6 +2842,8 @@ void CadDocument::route_feature(std::vector<CadBody>& 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<CadBody>& 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);
|
||||
|
||||
@@ -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<int>& 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<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_transform(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_thicken(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_thicken_surface(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_surface_offset(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_project(const std::vector<CadBody>& bodies, CadFeature& f) const;
|
||||
|
||||
// Undo/redo stacks of feature-list snapshots. checkpoint() pushes onto m_undo and
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user