M5c: pattern-on-curve — replicate a body along a sketch curve

Extend CadFeatureType::Pattern (no new enum) with a curve mode: when
pattern_curve_sketch >= 0 it takes precedence over linear/circular. The guide
entity is sampled at equal-parameter points via a file-local sample_entity_2d()
(Line lerp, Arc angle-lerp, cubic-BSpline Bernstein, p0->p1 fallback), and each
seed copy is translated by (P_i - P_0) and fused. Two serialized fields
(pattern_curve_sketch/pattern_curve_entity) appended to both symmetric cereal
lists (version stays 2, golden fixture regenerated 30269->30517). MCP:
pattern_on_curve. 3 new [CadDocument][pattern] tests; suite 89/1395.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-07-25 04:15:31 +02:00
co-authored by Claude Opus 4.8
parent a606dfe00a
commit aa30575369
5 changed files with 205 additions and 2 deletions
+92
View File
@@ -1125,6 +1125,98 @@ TEST_CASE("pattern replicates a body linearly and circularly", "[CadDocument]")
}
}
TEST_CASE("pattern-on-curve: copies land on a line and bbox spans the curve length", "[CadDocument][pattern]")
{
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
using namespace Slic3r;
CadDocument doc;
// Seed body: 4x4x4 box at the origin via sketch+extrude.
int s0 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 4, 4, 0, "Seed");
doc.add_extrude(s0, 4.0, false, BooleanMode::New, "E");
// Guide sketch: one Line entity from (0,0) to (30,0) on XY.
std::vector<SketchEntity> guide = {
{SketchEntity::Type::Line, Vec2d(0, 0), Vec2d(30, 0)},
};
int gs = doc.add_sketch_entities(guide, SketchPlane::XY(), "Guide");
doc.add_pattern_on_curve(4, gs, 0, 0, "OnCurve");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
REQUIRE(doc.display_mesh.facets_count() > 0);
auto bb = doc.display_mesh.bounding_box();
double x_extent = bb.max.x() - bb.min.x();
// 4 copies: at x=0, x=10, x=20, x=30. The seed is a 4x4 box centred at origin,
// so the overall X span is from -2 to 32 = 34 mm.
REQUIRE_THAT(x_extent, WithinAbs(34.0, 2.0));
}
TEST_CASE("pattern-on-curve: bad refs are safe", "[CadDocument][pattern]")
{
using namespace Slic3r;
CadDocument doc;
int s0 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 4, 4, 0, "Seed");
doc.add_extrude(s0, 4.0, false, BooleanMode::New, "E");
// Bad sketch ref -> error.
doc.add_pattern_on_curve(3, 999, 0, 0, "Bad");
REQUIRE_FALSE(doc.recompute());
REQUIRE_FALSE(doc.error.empty());
REQUIRE(doc.error.find("pattern") != std::string::npos);
}
TEST_CASE("pattern-on-curve: round-trip through serialize/deserialize", "[CadDocument][pattern]")
{
using Catch::Matchers::WithinRel;
using namespace Slic3r;
CadDocument doc;
int s0 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 4, 4, 0, "Seed");
doc.add_extrude(s0, 4.0, false, BooleanMode::New, "E");
std::vector<SketchEntity> guide = {
{SketchEntity::Type::Line, Vec2d(0, 0), Vec2d(30, 0)},
};
int gs = doc.add_sketch_entities(guide, SketchPlane::XY(), "Guide");
doc.add_pattern_on_curve(4, gs, 0, 0, "OnCurve");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
auto orig_bb = doc.display_mesh.bounding_box();
size_t orig_n = doc.bodies.size();
std::string blob = doc.serialize_recipe();
REQUIRE_FALSE(blob.empty());
CadDocument fresh;
REQUIRE(fresh.deserialize_recipe(blob));
REQUIRE(fresh.recompute());
REQUIRE(fresh.error.empty());
REQUIRE(fresh.bodies.size() == orig_n);
auto fresh_bb = fresh.display_mesh.bounding_box();
REQUIRE_THAT(orig_bb.min.x(), WithinRel(fresh_bb.min.x(), 1e-6));
REQUIRE_THAT(orig_bb.max.x(), WithinRel(fresh_bb.max.x(), 1e-6));
// Verify the deserialized field values.
bool found = false;
for (const auto& f : fresh.features) {
if (f.name == "OnCurve") {
REQUIRE(f.pattern_curve_sketch == gs);
REQUIRE(f.pattern_curve_entity == 0);
found = true;
break;
}
}
REQUIRE(found);
}
TEST_CASE("thread standards table carries correct ISO/UTS measures", "[CadDocument]")
{
using namespace Slic3r;