#include #include #include #include #include #include #include "test_helpers.hpp" #include "libslic3r/GCode/PreciseSeam.hpp" #include #include "libslic3r/Point.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" #include #include #include "libslic3r/libslic3r.h" #include "libslic3r/GCode/SeamPlacer.hpp" #include using namespace Slic3r; namespace { Point mm(double x, double y) { return Point(scale_(x), scale_(y)); } Polygon rectangle(double x0, double y0, double x1, double y1) { // Counterclockwise contours match the modifier-slice cache contract. return Polygon(Points{mm(x0, y0), mm(x1, y0), mm(x1, y1), mm(x0, y1)}); } struct SeamFixture { Model model; Print print; Model modifiers; Layer *layer = nullptr; PreciseSeam::ModifierSlicesCache cache; SeamFixture() { // Only the layer/PrintObject context is needed; clipping uses explicit cached slices below. Test::init_print({Test::cube(20)}, print, model, {{"raft_layers", "0"}}); REQUIRE(print.objects().size() == 1); PrintObject *object = print.get_object(0); layer = object->add_layer(int(object->slicing_parameters().raft_layers()), 0.2, 0.2, 0.1); modifiers.add_object(); } const ModelVolume *add(ModelVolumeType type, Polygons slices) { // These volumes own cache keys; mesh slicing is deliberately outside this geometry fixture. ModelVolume *volume = modifiers.objects.front()->add_volume(Test::cube(1)); volume->set_type(type); cache.emplace(volume, std::vector{std::move(slices)}); return volume; } }; void check_square_boundary(const Polygon &polygon) { // Wrong insertion edges can retrace a side without changing area: check length as well. CHECK_THAT(unscale(polygon.length()), Catch::Matchers::WithinAbs(80.0, 0.00001)); for (const Point &p : polygon.points) { CAPTURE(p.x(), p.y()); CHECK(p.x() >= scale_(0)); CHECK(p.x() <= scale_(20)); CHECK(p.y() >= scale_(0)); CHECK(p.y() <= scale_(20)); const bool on_boundary = p.x() == 0 || p.x() == scale_(20) || p.y() == 0 || p.y() == scale_(20); CHECK(on_boundary); } } void require_vertex(const Polygon &polygon, const Point &point) { // Integer coordinates make the micron transition helpers exact on these axis-aligned edges. CAPTURE(point.x(), point.y()); REQUIRE(std::find(polygon.points.begin(), polygon.points.end(), point) != polygon.points.end()); } } // namespace TEST_CASE("Strong seam modes select the requested location on a clipped side", "[PreciseSeam]") { const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT); SeamFixture fixture; // Clipper may collapse all three original collinear edges into one. Polygon perimeter(Points{mm(0, 0), mm(2, 0), mm(4, 0), mm(8, 0), mm(20, 0), mm(20, 20), mm(0, 20)}); const ModelVolume *modifier = fixture.add(mode, {rectangle(1, -2, 13, 2)}); PreciseSeam::PreciseSeamWarnings warnings; const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); const double expected_x = mode == ModelVolumeType::PRECISE_SEAM_LEFT ? 1.0 : mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? 13.0 : 7.0; CHECK(*seam == mm(expected_x, 0)); require_vertex(perimeter, mm(expected_x - 0.001, 0)); require_vertex(perimeter, mm(expected_x + 0.001, 0)); check_square_boundary(perimeter); CHECK_FALSE(warnings.through_body.load()); CHECK_FALSE(warnings.full_containment.load()); CHECK_FALSE(warnings.multiple_intersections.load()); } TEST_CASE("Center seams preserve the edge order at vertices and across the contour origin", "[PreciseSeam]") { const bool wrap = GENERATE(false, true); SeamFixture fixture; Polygon perimeter(Points{mm(0, 0), mm(4, 0), mm(20, 0), mm(20, 20), mm(0, 20)}); // Symmetric cuts put the arc midpoint exactly on an existing vertex, including vertex zero. const Polygon cut = wrap ? rectangle(-2, -2, 4, 4) : rectangle(1, -2, 7, 2); const ModelVolume *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {cut}); const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == (wrap ? mm(0, 0) : mm(4, 0))); require_vertex(perimeter, wrap ? mm(0, 0.001) : mm(3.999, 0)); require_vertex(perimeter, wrap ? mm(0.001, 0) : mm(4.001, 0)); check_square_boundary(perimeter); } TEST_CASE("Center seams land on the closing edge", "[PreciseSeam]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(-2, 3, 2, 9)}); const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == mm(0, 6)); require_vertex(perimeter, mm(0, 5.999)); require_vertex(perimeter, mm(0, 6.001)); check_square_boundary(perimeter); } TEST_CASE("Coincident weak boundaries do not prevent later boundary refinement", "[PreciseSeam][Regression]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // Duplicate boundaries used to stall the reverse cursor before reaching the separate segment. const auto *a = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(2, -2, 6, 2)}); const auto *b = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(2, -2, 6, 2)}); const auto *c = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(10, -2, 14, 2)}); const auto segments = PreciseSeam::collect_weak_modifier_segments({c, b, a}, perimeter, fixture.layer, fixture.cache); REQUIRE(segments.size() == 3); for (double x : {1.999, 6.001, 9.999, 14.001}) require_vertex(perimeter, mm(x, 0)); check_square_boundary(perimeter); } TEST_CASE("Weak boundaries sharing a vertex refine both sides", "[PreciseSeam]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const auto *a = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(2, -2, 6, 2)}); const auto *b = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(6, -2, 10, 2)}); const auto segments = PreciseSeam::collect_weak_modifier_segments({a, b}, perimeter, fixture.layer, fixture.cache); REQUIRE(segments.size() == 2); require_vertex(perimeter, mm(5.999, 0)); require_vertex(perimeter, mm(6.001, 0)); check_square_boundary(perimeter); } TEST_CASE("Unsupported modifier sections are skipped with the appropriate warning", "[PreciseSeam]") { const int scenario = GENERATE(0, 1, 2, 3); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Points original = perimeter.points; Polygons slices; if (scenario == 0) slices = {rectangle(30, 30, 40, 40)}; // Disjoint bounds. if (scenario == 1) slices = {rectangle(2, 2, 4, 4)}; // Wholly inside; no common boundary. if (scenario == 2) slices = {rectangle(-2, -2, 22, 22)}; // Contains the entire perimeter. if (scenario == 3) { Polygon hole = rectangle(2, 2, 4, 4); hole.reverse(); slices = {rectangle(-2, -2, 22, 22), hole}; } const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, std::move(slices)); PreciseSeam::PreciseSeamWarnings warnings; CHECK_FALSE(PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings).has_value()); CHECK(perimeter.points == original); CHECK(warnings.full_containment.load() == (scenario == 2)); CHECK(warnings.multiply_connected.load() == (scenario == 3)); CHECK_FALSE(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.through_body.load()); } TEST_CASE("Strong modifiers warn when another slice polygon also intersects the perimeter", "[PreciseSeam]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // One helper has two disconnected sections; only its first section supplies the seam. const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(2, -2, 6, 2), rectangle(12, -2, 16, 2)}); PreciseSeam::PreciseSeamWarnings warnings; const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == mm(4, 0)); CHECK(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.through_body.load()); CHECK_FALSE(warnings.full_containment.load()); CHECK_FALSE(warnings.multiply_connected.load()); check_square_boundary(perimeter); } TEST_CASE("Modifiers crossing the entire body raise a through body warning", "[PreciseSeam]") { const bool strong = GENERATE(false, true); CAPTURE(strong); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // The strip exits on opposite sides, leaving two exterior pieces. None of the clipped // vertices matches a square corner, so this also exercises the general segment-extraction path. const auto type = strong ? ModelVolumeType::PRECISE_SEAM_CENTER : ModelVolumeType::PRECISE_SEAM_BLOCKED; const auto *modifier = fixture.add(type, {rectangle(8, -2, 12, 22)}); PreciseSeam::PreciseSeamWarnings warnings; if (strong) { const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(seam->x() == mm(10, 0).x()); // Either boundary segment may be encountered first by the clipping traversal. const bool on_crossed_side = seam->y() == 0 || seam->y() == mm(0, 20).y(); CHECK(on_crossed_side); } else { const auto segments = PreciseSeam::collect_weak_modifier_segments({modifier}, perimeter, fixture.layer, fixture.cache, &warnings); REQUIRE_FALSE(segments.empty()); } CHECK(warnings.through_body.load()); CHECK_FALSE(warnings.multiple_intersections.load()); // The clipped strip is one polygon. CHECK_FALSE(warnings.full_containment.load()); CHECK_FALSE(warnings.multiply_connected.load()); check_square_boundary(perimeter); } TEST_CASE("Modifier hierarchy keeps strong order and applies the highest weak priority last", "[PreciseSeam]") { const auto high_type = GENERATE(ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL, ModelVolumeType::PRECISE_SEAM_ENFORCED); const auto expected_type = high_type == ModelVolumeType::PRECISE_SEAM_BLOCKED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : high_type == ModelVolumeType::PRECISE_SEAM_NEUTRAL ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral : SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced; SeamFixture fixture; const auto *strong_a = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(1, -2, 3, 2)}); const auto *strong_b = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(11, -2, 13, 2)}); const auto *high = fixture.add(high_type, {rectangle(2, -2, 6, 2)}); const auto low_type = high_type == ModelVolumeType::PRECISE_SEAM_ENFORCED ? ModelVolumeType::PRECISE_SEAM_BLOCKED : ModelVolumeType::PRECISE_SEAM_ENFORCED; const auto *low = fixture.add(low_type, {rectangle(2, -2, 6, 2)}); std::vector strong, weak; bool has_strong = false; PreciseSeam::init_precise_seam_data(strong, weak, has_strong, fixture.modifiers.objects.front()); REQUIRE(has_strong); CHECK(strong == std::vector{strong_a, strong_b}); CHECK(weak == std::vector{low, high}); Polygon perimeter = rectangle(0, 0, 20, 20); const auto seam = PreciseSeam::insert_strong_seam_point(strong, perimeter, fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == mm(2, 0)); perimeter = rectangle(0, 0, 20, 20); const auto segments = PreciseSeam::collect_weak_modifier_segments(weak, perimeter, fixture.layer, fixture.cache); REQUIRE(segments.size() == 2); PrintObjectSeamData::LayerSeams result; result.perimeters.emplace_back(); auto &loop = result.perimeters.back(); // Include a preceding candidate to exercise nonzero global layer indices. result.points.emplace_back(Vec3f(-1, -1, 0), loop, 0, SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral); loop.start_index = 1; for (const Point &p : perimeter.points) { // Match production's double-to-float conversion: weak boundary lookup uses exact equality. const Vec2f position = unscale(p).cast(); result.points.emplace_back(Vec3f(position.x(), position.y(), 0), loop, 0, SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral); } loop.end_index = result.points.size(); bool enforced = false; PreciseSeam::apply_weak_modifiers_to_perimeter(segments, result, loop, enforced); size_t patch_count = 0; for (size_t i = loop.start_index; i < loop.end_index; ++i) { const auto &candidate = result.points[i]; // Axis-aligned input and interpolation keep y exactly zero; this classifies, rather than measures, the patch. const bool in_patch = candidate.position.y() == 0 && candidate.position.x() >= 2 && candidate.position.x() <= 6; CHECK(candidate.type == (in_patch ? expected_type : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral)); if (in_patch) ++patch_count; } CHECK(patch_count >= 2); if (high_type == ModelVolumeType::PRECISE_SEAM_ENFORCED) { // Four millimetres of enforcement must be subdivided, not just marked at its endpoints. CHECK(enforced); CHECK(patch_count >= size_t(4.0f / SeamPlacer::enforcer_oversampling_distance)); } CHECK(result.points.front().type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral); }