From a606dfe00af823df5c6f72f4d4eee430760acb54 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 25 Jul 2026 04:03:52 +0200 Subject: [PATCH] =?UTF-8?q?M5b:=20rib=20=E2=80=94=20thin=20stiffening=20wa?= =?UTF-8?q?ll=20grown=20from=20an=20open=20sketch=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q --- src/libslic3r/CadDocument.cpp | 47 ++++++++++++ src/libslic3r/CadDocument.hpp | 22 ++++-- src/slic3r/GUI/McpControl.cpp | 30 ++++++++ tests/data/cad_recipe_v2.bin | Bin 29525 -> 30269 bytes tests/libslic3r/test_caddocument.cpp | 106 +++++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 0838eb45eb..0eefc5f765 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -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& 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) diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index 3c6668191b..009592a82b 100644 --- a/src/libslic3r/CadDocument.hpp +++ b/src/libslic3r/CadDocument.hpp @@ -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 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 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 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& 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. diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index 466c0eea07..1391be1a0a 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -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 entity = params["entity"].get(); + 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)); diff --git a/tests/data/cad_recipe_v2.bin b/tests/data/cad_recipe_v2.bin index 331f6fcd71b697c8966aebc6873287a51cf43d75..472bfaeecaf6ced634f8fcff970fe8f89c85bff0 100644 GIT binary patch delta 557 zcmccmjB)Q9#tmzjCv!6Kb2>19fr`WCHOvdqg?!mDgx+ys2=(*LN7tw)G#6dywCDnK zp=?PEja;%A8kZ|z*yy3M2yE8m3P#?=@6=h)eDY2kq5~wod9pq;SRTZgJXv38^BUui z=nk1{J`Zl*WF>py%|X^@-~yYK?C+y%V02!NF0{f8;yeM$h^6h9Y~mQ1Bv&1^O-j<6PnAsIa+K1^JZZwAor9kkXx?^ zl(kS@#Ju^F1`Eq(K3yR9xB)xM=4U2Ag4J>!^XA<)kC``ba{v+>U6wO%F7N;vpy2~l v>=FQE{Rsvt*%Us9d2?Dc&`{ZUAop_866VeQX+T3gvw%t%^MI_~g;^{BM}k1! diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index e60a61ff9e..907783749a 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -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 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 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 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]")