mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
M5b: rib — thin stiffening wall grown from an open sketch line
New CadFeatureType::Rib (appended). A straight open Line entity in a sketch is offset ±thickness/2 along its in-plane perpendicular into a thin rectangle, extruded rib_depth along the sketch-plane normal, and fused to the target body. Line-only for now (ponytail; polyline/arc ribs are a later extension) — a non-line entity fails cleanly at recompute. Four serialized fields (rib_sketch_ref/rib_entity/rib_thickness/rib_depth) appended to both symmetric cereal lists (version stays 2, golden fixture regenerated 29525->30269). MCP: rib. 3 new [CadDocument][rib] tests; suite 86/1376. 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
0bbf22ceae
commit
a606dfe00a
@@ -748,6 +748,21 @@ int CadDocument::add_shell(double thickness, int face, int target_body, const st
|
||||
return int(features.size()) - 1;
|
||||
}
|
||||
|
||||
int CadDocument::add_rib(int sketch_ref, int entity, double thickness, double depth,
|
||||
int target_body, const std::string& name)
|
||||
{
|
||||
CadFeature f;
|
||||
f.type = CadFeatureType::Rib;
|
||||
f.name = name;
|
||||
f.rib_sketch_ref = sketch_ref;
|
||||
f.rib_entity = entity;
|
||||
f.rib_thickness = thickness;
|
||||
f.rib_depth = depth;
|
||||
f.target_body = target_body;
|
||||
features.push_back(f);
|
||||
return int(features.size()) - 1;
|
||||
}
|
||||
|
||||
int CadDocument::add_delete_face(int target_body, const std::vector<int>& faces,
|
||||
const std::string& name)
|
||||
{
|
||||
@@ -1784,6 +1799,38 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CadFeatureType::Rib: {
|
||||
if (!have_body) throw std::runtime_error("rib needs a body");
|
||||
if (f.rib_sketch_ref < 0 || f.rib_sketch_ref >= (int)features.size())
|
||||
throw std::runtime_error("rib: bad sketch ref");
|
||||
const CadFeature& sk = features[f.rib_sketch_ref];
|
||||
if (sk.type != CadFeatureType::Sketch)
|
||||
throw std::runtime_error("rib: ref is not a sketch");
|
||||
if (f.rib_entity < 0 || f.rib_entity >= (int)sk.entities.size())
|
||||
throw std::runtime_error("rib: bad entity");
|
||||
const SketchEntity& ln = sk.entities[f.rib_entity];
|
||||
if (ln.type != SketchEntity::Type::Line)
|
||||
throw std::runtime_error("rib: entity must be a line"); // ponytail: line-only for now
|
||||
// Thin rectangle centred on the line, in the sketch plane: offset both endpoints by
|
||||
// +/-thickness/2 along the in-plane perpendicular of the line direction.
|
||||
const SketchPlane& pl = sk.plane;
|
||||
Vec2d a = ln.p0, b = ln.p1;
|
||||
Vec2d dir = (b - a); double L = dir.norm();
|
||||
if (L < 1e-9) throw std::runtime_error("rib: degenerate line");
|
||||
dir /= L;
|
||||
Vec2d perp(-dir.y(), dir.x());
|
||||
double h = f.rib_thickness * 0.5;
|
||||
Vec2d q0 = a + perp*h, q1 = b + perp*h, q2 = b - perp*h, q3 = a - perp*h;
|
||||
auto w3 = [&](const Vec2d& p){ Vec3d w = pl.to_world(p); return gp_Pnt(w.x(),w.y(),w.z()); };
|
||||
BRepBuilderAPI_MakePolygon poly(w3(q0), w3(q1), w3(q2), w3(q3), Standard_True);
|
||||
if (!poly.IsDone()) throw std::runtime_error("rib: profile failed");
|
||||
TopoDS_Shape wall = SketchEngine::make_extrude(poly.Wire(), pl, f.rib_depth, false, 0.0);
|
||||
if (wall.IsNull()) throw std::runtime_error("rib: extrude failed");
|
||||
BRepAlgoAPI_Fuse fuse(result, wall);
|
||||
if (!fuse.IsDone()) throw std::runtime_error("rib: fuse failed");
|
||||
result = fuse.Shape();
|
||||
break;
|
||||
}
|
||||
case CadFeatureType::Fillet:
|
||||
if (!have_body) throw std::runtime_error("fillet needs a body");
|
||||
if (f.dressup_edge >= 0)
|
||||
|
||||
@@ -17,7 +17,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 };
|
||||
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 SketchShape { Rectangle, Circle };
|
||||
enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident };
|
||||
enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge };
|
||||
@@ -276,6 +276,12 @@ struct CadFeature {
|
||||
// healed via BRepAlgoAPI_Defeaturing.
|
||||
std::vector<int> delete_faces;
|
||||
|
||||
// Rib: a thin wall grown from an open sketch line, fused to the body.
|
||||
int rib_sketch_ref{-1}; // feature index of the Sketch holding the profile
|
||||
int rib_entity{-1}; // index of the open Line entity within that sketch
|
||||
double rib_thickness{2}; // wall thickness (mm), centred on the line
|
||||
double rib_depth{10}; // extrude distance along the sketch-plane normal (mm)
|
||||
|
||||
template<class Archive>
|
||||
void save(Archive& ar) const {
|
||||
std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string();
|
||||
@@ -308,7 +314,8 @@ struct CadFeature {
|
||||
project_source_body, project_edges, project_face,
|
||||
delete_faces,
|
||||
hole_style, hole_cbore_diameter, hole_cbore_depth,
|
||||
hole_csink_diameter, hole_csink_angle, hole_standard);
|
||||
hole_csink_diameter, hole_csink_angle, hole_standard,
|
||||
rib_sketch_ref, rib_entity, rib_thickness, rib_depth);
|
||||
}
|
||||
template<class Archive>
|
||||
void load(Archive& ar) {
|
||||
@@ -340,9 +347,10 @@ struct CadFeature {
|
||||
thicken_face, thicken_thickness, thicken_flip,
|
||||
cut_face_body, cut_face,
|
||||
project_source_body, project_edges, project_face,
|
||||
delete_faces,
|
||||
hole_style, hole_cbore_diameter, hole_cbore_depth,
|
||||
hole_csink_diameter, hole_csink_angle, hole_standard);
|
||||
delete_faces,
|
||||
hole_style, hole_cbore_diameter, hole_cbore_depth,
|
||||
hole_csink_diameter, hole_csink_angle, hole_standard,
|
||||
rib_sketch_ref, rib_entity, rib_thickness, rib_depth);
|
||||
imported_solid = brep_from_string(brep);
|
||||
}
|
||||
};
|
||||
@@ -449,6 +457,10 @@ public:
|
||||
int add_loft(const std::vector<int>& profile_refs, bool ruled, BooleanMode mode,
|
||||
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.
|
||||
int add_rib(int sketch_ref, int entity, double thickness, double depth,
|
||||
int target_body, const std::string& name);
|
||||
int add_draft(double angle, int face, int target_body, const std::string& name);
|
||||
// Boolean between two existing bodies. op reuses BooleanMode (Add=union, Cut=subtract,
|
||||
// Intersect=common; New invalid). target survives, tool is consumed unless keep_tool.
|
||||
|
||||
@@ -77,6 +77,8 @@ const char* feature_type_name(CadFeatureType t)
|
||||
case CadFeatureType::Transform: return "Transform";
|
||||
case CadFeatureType::Thicken: return "Thicken";
|
||||
case CadFeatureType::Project: return "Project";
|
||||
case CadFeatureType::DeleteFace: return "DeleteFace";
|
||||
case CadFeatureType::Rib: return "Rib";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -294,6 +296,14 @@ json describe_tools()
|
||||
json{{"name", "ent_b"}, {"type", "integer"}, {"description", "second entity index within the sketch"}},
|
||||
json{{"name", "end_b"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}, {"description", "0 = start/p0 side, 1 = end/p1 side"}},
|
||||
})}},
|
||||
json{{"name", "rib"}, {"summary", "Grow a thin rib wall (stiffener) from an open Line sketch entity, fused to a body."},
|
||||
{"params", json::array({
|
||||
json{{"name", "sketch"}, {"type", "integer"}, {"description", "sketch feature index holding the open line"}},
|
||||
json{{"name", "entity"}, {"type", "integer"}, {"description", "entity index of the open Line within the sketch"}},
|
||||
json{{"name", "thickness"}, {"type", "number"}, {"unit", "mm"}, {"default", 2}, {"min", 0.01}},
|
||||
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", "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}},
|
||||
@@ -944,6 +954,25 @@ json action_shell(DesignPanel* panel, const json& params)
|
||||
return json{{"ok", ok}, {"shell_index", s}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
json action_rib(DesignPanel* panel, const json& params)
|
||||
{
|
||||
if (!params.contains("sketch")) throw std::runtime_error("rib needs 'sketch' (feature index)");
|
||||
if (!params.contains("entity")) throw std::runtime_error("rib needs 'entity' (entity index)");
|
||||
CadDocument& doc = panel->mcp_doc();
|
||||
if (doc.bodies.empty()) throw std::runtime_error("no body to rib");
|
||||
int sketch = params["sketch"].get<int>();
|
||||
int entity = params["entity"].get<int>();
|
||||
double thickness = params.value("thickness", 2.0);
|
||||
double depth = params.value("depth", 10.0);
|
||||
int bi = target_body_arg(params, doc);
|
||||
doc.checkpoint();
|
||||
int idx = doc.add_rib(sketch, entity, thickness, depth, bi, "Rib");
|
||||
bool ok = doc.recompute();
|
||||
if (!ok) doc.undo();
|
||||
panel->mcp_after_change();
|
||||
return json{{"ok", ok}, {"rib_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)");
|
||||
@@ -1182,6 +1211,7 @@ std::string handle_on_main(const std::string& method, const json& params, const
|
||||
if (method == "boolean") return rpc_result(id, action_boolean(panel, params));
|
||||
if (method == "pattern") return rpc_result(id, action_pattern(panel, params));
|
||||
if (method == "shell") return rpc_result(id, action_shell(panel, params));
|
||||
if (method == "rib") return rpc_result(id, action_rib(panel, params));
|
||||
if (method == "draft") return rpc_result(id, action_draft(panel, params));
|
||||
if (method == "mirror") return rpc_result(id, action_mirror(panel, params));
|
||||
if (method == "transform") return rpc_result(id, action_transform(panel, params));
|
||||
|
||||
Binary file not shown.
@@ -3809,6 +3809,112 @@ TEST_CASE("golden recipe v1 still deserialises", "[CadDocument]")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rib tests (M5b) ---
|
||||
|
||||
TEST_CASE("rib adds material to a box", "[CadDocument][rib]")
|
||||
{
|
||||
using Catch::Matchers::WithinRel;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
|
||||
// Build a box: 40x40x10 extruded on XY -> z=[0,10]
|
||||
int sk_box = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(),
|
||||
40, 40, 10, "Box");
|
||||
doc.add_extrude(sk_box, 10.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
double Vbox = double(doc.display_mesh.volume());
|
||||
REQUIRE(Vbox > 0.0);
|
||||
|
||||
// Sketch a single open Line across the box footprint, on the same XY plane.
|
||||
// Line from (5,20) to (35,20), centred in Y but off-centre in X.
|
||||
std::vector<SketchEntity> ents = {
|
||||
{SketchEntity::Type::Line, Vec2d(5, 20), Vec2d(35, 20)},
|
||||
};
|
||||
int sk_rib = doc.add_sketch_entities(ents, SketchPlane::XY(), "RibLine");
|
||||
REQUIRE(sk_rib >= 0);
|
||||
|
||||
int fi = doc.add_rib(sk_rib, 0, 3.0, 12.0, 0, "Rib");
|
||||
REQUIRE(fi >= 0);
|
||||
REQUIRE(doc.features[fi].type == CadFeatureType::Rib);
|
||||
|
||||
bool ok = doc.recompute();
|
||||
REQUIRE(ok);
|
||||
REQUIRE(doc.error.empty());
|
||||
// The rib fused extra material -> volume must be strictly larger.
|
||||
double Vrib = double(doc.display_mesh.volume());
|
||||
REQUIRE(Vrib > Vbox);
|
||||
|
||||
// A rib with a bad sketch ref must fail cleanly, not crash.
|
||||
CadDocument bad;
|
||||
bad.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Box");
|
||||
bad.add_extrude(0, 10.0, false, BooleanMode::New, "E");
|
||||
REQUIRE(bad.recompute());
|
||||
bad.add_rib(999, 0, 3.0, 10.0, 0, "BadRib");
|
||||
REQUIRE_FALSE(bad.recompute());
|
||||
}
|
||||
|
||||
TEST_CASE("rib non-line entity rejected safely", "[CadDocument][rib]")
|
||||
{
|
||||
using Catch::Matchers::Contains;
|
||||
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Box");
|
||||
doc.add_extrude(sk, 10.0, false, BooleanMode::New, "E");
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
// Sketch with a Circle entity (not a Line)
|
||||
std::vector<SketchEntity> ents = {
|
||||
{SketchEntity::Type::Circle, Vec2d(0, 0), Vec2d(0, 0), Vec2d(0, 0), 5.0},
|
||||
};
|
||||
int sk2 = doc.add_sketch_entities(ents, SketchPlane::XY(), "CircleSketch");
|
||||
doc.add_rib(sk2, 0, 2.0, 10.0, 0, "BadRib");
|
||||
|
||||
REQUIRE_FALSE(doc.recompute());
|
||||
REQUIRE_FALSE(doc.error.empty());
|
||||
REQUIRE_THAT(doc.error, Catch::Matchers::Contains("rib"));
|
||||
}
|
||||
|
||||
TEST_CASE("rib round-trip serialization", "[CadDocument][rib]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 40, 40, 10, "Box");
|
||||
doc.add_extrude(sk, 10.0, false, BooleanMode::New, "E");
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
std::vector<SketchEntity> ents = {
|
||||
{SketchEntity::Type::Line, Vec2d(5, 20), Vec2d(35, 20)},
|
||||
};
|
||||
int sk2 = doc.add_sketch_entities(ents, SketchPlane::XY(), "RibLine");
|
||||
doc.add_rib(sk2, 0, 3.0, 12.0, 0, "Rib");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
|
||||
size_t nb = doc.bodies.size();
|
||||
double Vdoc = double(doc.display_mesh.volume());
|
||||
|
||||
auto blob = doc.serialize_recipe();
|
||||
REQUIRE_FALSE(blob.empty());
|
||||
|
||||
CadDocument fresh;
|
||||
REQUIRE(fresh.deserialize_recipe(blob));
|
||||
REQUIRE(fresh.bodies.size() == nb);
|
||||
double Vfresh = double(fresh.display_mesh.volume());
|
||||
REQUIRE_THAT(Vfresh, WithinAbs(Vdoc, 1e-6));
|
||||
|
||||
// Find the rib feature and check its fields survived.
|
||||
const CadFeature* rf = nullptr;
|
||||
for (const auto& f : fresh.features) {
|
||||
if (f.type == CadFeatureType::Rib) { rf = &f; break; }
|
||||
}
|
||||
REQUIRE(rf != nullptr);
|
||||
REQUIRE_THAT(rf->rib_thickness, WithinAbs(3.0, 1e-9));
|
||||
REQUIRE_THAT(rf->rib_depth, WithinAbs(12.0, 1e-9));
|
||||
}
|
||||
|
||||
// --- Bridge tests (M3c) ---
|
||||
|
||||
TEST_CASE("bridge two collinear lines", "[CadDocument][bridge]")
|
||||
|
||||
Reference in New Issue
Block a user