#include #include #include #include #include #include #include "test_helpers.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/GCode/PreciseSeam.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" #include #include #include "libslic3r/Point.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include #include #include "libslic3r/libslic3r.h" #include "libslic3r/GCode/SeamPlacer.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/TriangleMesh.hpp" #include #include #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::ModifierRegionsCache cache; explicit SeamFixture(int raft_layers = 0) { // Only the layer/PrintObject context is needed; clipping uses explicit cached slices below. Test::init_print({Test::cube(20)}, print, model, {{"raft_layers", std::to_string(raft_layers)}}); REQUIRE(print.objects().size() == 1); PrintObject *object = print.get_object(0); const auto &slicing = object->slicing_parameters(); // IDs and print heights include the raft; mesh slicing heights remain object-relative. layer = object->add_layer(int(slicing.raft_layers()), 0.2, slicing.object_print_z_min + 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); ExPolygons regions; for (Polygon &slice : slices) regions.emplace_back(std::move(slice)); cache.emplace(volume, PreciseSeam::prepare_modifier_slices({std::move(regions)})); return volume; } const ModelVolume *add_regions(ModelVolumeType type, ExPolygons regions) { // Supply structured slices directly, without reconstructing holes from flat contours. const ModelVolume *volume = add(type, {}); REQUIRE(volume->is_precise_seam()); cache.at(volume) = PreciseSeam::prepare_modifier_slices({std::move(regions)}); 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()); } std::vector weak_candidate_types( const Polygon &polygon, const std::vector &segments, const std::function &painted = {}) { // Use the same coordinate conversion as production when applying prepared boundaries. // Optional painted types stand in for seam painting, which production assigns before weak zones. PrintObjectSeamData::LayerSeams candidates; candidates.perimeters.emplace_back(); auto &loop = candidates.perimeters.back(); loop.start_index = 0; for (const Point &point : polygon.points) { const Vec2f position = unscale(point).cast(); candidates.points.emplace_back(Vec3f(position.x(), position.y(), 0), loop, 0, painted ? painted(point) : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral); } loop.end_index = candidates.points.size(); bool enforced = false; PreciseSeam::apply_weak_modifiers_to_perimeter(segments, candidates, loop, enforced); std::vector types; for (const auto &candidate : candidates.points) types.push_back(candidate.type); return types; } } // namespace TEST_CASE("A simple clipped interval has consistent geometry before and after weak boundary insertion", "[PreciseSeam][SegmentExtraction]") { const bool corner = GENERATE(false, true); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Polygon cut = corner ? rectangle(-2, -2, 4, 4) : rectangle(2, -2, 8, 2); const Point expected_begin = corner ? mm(0, 4) : mm(2, 0); const Point expected_end = corner ? mm(4, 0) : mm(8, 0); const auto extracted = PreciseSeam::extract_perimeter_segments( PreciseSeam::PreparedPerimeter(perimeter), PreciseSeam::prepare_modifier_regions(ExPolygons{ExPolygon(cut)}), ModelVolumeType::PRECISE_SEAM_ENFORCED); REQUIRE(extracted.valid); REQUIRE(extracted.segments.size() == 1); CHECK(extracted.segments.front().polyline.points.front() == expected_begin); CHECK(extracted.segments.front().polyline.points.back() == expected_end); // Boundary insertion must preserve the extractor endpoints on this analytic fixture. const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {cut}); const auto applied = PreciseSeam::collect_weak_modifier_segments({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(applied.size() == 1); CHECK(applied.front().left_point == expected_begin); CHECK(applied.front().right_point == expected_end); check_square_boundary(perimeter); } TEST_CASE("Unsuccessful strong modifiers share perimeter preparation with weak processing", "[PreciseSeam][Regression]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Points original = perimeter.points; const auto *empty = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {}); const auto *distant = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(30, 30, 40, 40)}); const auto *covering = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(-2, -2, 22, 22)}); const auto *blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(2, -2, 8, 2)}); const auto *neutral = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(4, -2, 6, 2)}); const PreciseSeam::PreparedPerimeter prepared(perimeter); REQUIRE(prepared.valid); CHECK(prepared.bounds.min == mm(0, 0)); CHECK(prepared.bounds.max == mm(20, 20)); REQUIRE(prepared.line.size() == original.size() + 1); CHECK(prepared.line.points.front() == prepared.line.points.back()); const Point *line_storage = prepared.line.points.data(); PreciseSeam::PreciseSeamWarnings warnings; CHECK_FALSE(PreciseSeam::insert_strong_seam_point( {empty, distant, covering}, perimeter, prepared, fixture.layer, fixture.cache, &warnings).has_value()); CHECK(perimeter.points == original); CHECK(prepared.line.points.data() == line_storage); CHECK(warnings.full_containment.load()); // Weak receives the same preparation; neither consumer needs to reconstruct the clipping line. const auto segments = PreciseSeam::collect_weak_modifier_segments( {blocked, neutral}, perimeter, prepared, fixture.layer, fixture.cache, &warnings); REQUIRE(segments.size() == 2); // Insertion has now changed the polygon: do not use prepared for any further extraction. const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; const bool in_outer = p.y() == 0 && p.x() >= scale_(2.) && p.x() <= scale_(8.); const bool in_inner = p.x() >= scale_(4.) && p.x() <= scale_(6.); const auto expected = in_outer && !in_inner ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral; CHECK(types[i] == expected); } CHECK(warnings.failed_fragments.load() == 0); check_square_boundary(perimeter); } TEST_CASE("Structured modifier slices keep holes with their component and preserve sliced area", "[PreciseSeam][SegmentExtraction]") { SeamFixture fixture; TriangleMesh shell = Test::cube(10); TriangleMesh cavity = Test::cube(6); cavity.translate(2., 2., -1.); cavity.flip_triangles(); shell.merge(cavity); TriangleMesh island = Test::cube(2); island.translate(30., 0., 0.); shell.merge(island); // The layer intersects an annulus and a separate island, with areas 100-36 and 4 mm^2. ModelVolume *volume = fixture.modifiers.objects.front()->add_volume(shell); const bool mirrored = GENERATE(false, true); // A negative determinant must preserve exterior/hole winding and the sliced area. if (mirrored) volume->set_mirror(Vec3d(-1., 1., 1.)); PrintObject *object = fixture.print.get_object(0); PreciseSeam::ModifierRegionsCache cache; cache.emplace(volume, PreciseSeam::prepare_modifier_slices(object->slice_single_volume_regions(volume))); const auto &layers = cache.at(volume); REQUIRE(layers.size() == 1); REQUIRE(layers.front().size() == 2); size_t holes = 0; double area = 0.; for (const auto &cached_region : layers.front()) { const ExPolygon ®ion = cached_region.polygon; holes += region.holes.size(); CHECK(region.contour.is_counter_clockwise()); for (const Polygon &hole : region.holes) CHECK(hole.is_clockwise()); area += region.area(); } CHECK(holes == 1); CHECK_THAT(area / double(scale_(1.)) / double(scale_(1.)), Catch::Matchers::WithinAbs(68., 1e-4)); } TEST_CASE("Modifier slices above a raft use object layer indices for strong and weak seams", "[PreciseSeam][Regression]") { const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_BLOCKED); SeamFixture fixture(3); PrintObject *object = fixture.print.get_object(0); const auto &slicing = object->slicing_parameters(); REQUIRE(slicing.raft_layers() > 0); REQUIRE(fixture.layer->id() == slicing.raft_layers()); // The real modifier mesh intersects the lower sampled object layer, but not the upper one. Layer *upper = object->add_layer(int(slicing.raft_layers() + 1), 0.2, slicing.object_print_z_min + 4.2, 4.1); ModelVolume *modifier = fixture.modifiers.objects.front()->add_volume(Test::cube(4)); modifier->set_type(mode); const auto &slices = fixture.cache.emplace(modifier, PreciseSeam::prepare_modifier_slices(object->slice_single_volume_regions(modifier))).first->second; REQUIRE(slices.size() == 2); REQUIRE(slices[0].size() == 1); CHECK(slices[1].empty()); // Position the test perimeter relative to the transformed slice to isolate layer indexing. const BoundingBox &bounds = slices[0].front().bounds; const Point origin = bounds.min; const Polygon original(Points{origin + mm(-2, 2), origin + mm(6, 2), origin + mm(6, 10), origin + mm(-2, 10)}); Polygon perimeter = original; if (is_precise_seam_strong(mode)) { const auto seam = PreciseSeam::insert_strong_seam_point( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(seam.has_value()); const double x = mode == ModelVolumeType::PRECISE_SEAM_LEFT ? 0. : mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? 4. : 2.; CHECK(*seam == origin + mm(x, 2)); perimeter = original; CHECK_FALSE(PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), upper, fixture.cache).has_value()); } else { const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 1); CHECK(segments.front().left_point == origin + mm(0, 2)); CHECK(segments.front().right_point == origin + mm(4, 2)); const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point local = perimeter[i] - origin; const bool inside = local.y() == scale_(2.) && local.x() >= 0 && local.x() <= scale_(4.); CHECK(types[i] == (inside ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral)); } perimeter = original; CHECK(PreciseSeam::collect_weak_modifier_segments({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), upper, fixture.cache).empty()); } CHECK(perimeter.points == original.points); } 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; // Center must retain the correct source edge across several collinear edges. 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, PreciseSeam::PreparedPerimeter(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.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, PreciseSeam::PreparedPerimeter(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, PreciseSeam::PreparedPerimeter(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("Weak boundaries on the closing edge preserve candidate types", "[PreciseSeam][Regression]") { const bool reverse = GENERATE(false, true); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // Keep vertex zero fixed so the selected side remains the closing edge in both directions. if (reverse) std::reverse(perimeter.points.begin() + 1, perimeter.points.end()); const Polygon cut = reverse ? rectangle(3, -2, 9, 2) : rectangle(-2, 3, 2, 9); const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {cut}); const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 1); const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; const coord_t along = reverse ? p.x() : p.y(); const coord_t across = reverse ? p.y() : p.x(); const bool inside = across == 0 && along >= scale_(3.) && along <= scale_(9.); CHECK(types[i] == (inside ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral)); } check_square_boundary(perimeter); } TEST_CASE("Strong intersection warnings count joined segments across vertex zero", "[PreciseSeam][Regression]") { const bool extra_segment = GENERATE(false, true); const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); ExPolygons regions{ExPolygon(rectangle(-2, -2, 4, 4))}; if (extra_segment) regions.emplace_back(rectangle(8, -2, 12, 2)); // The two fragments at vertex zero form one eight-millimeter segment, longer than the extra one. const auto extracted = PreciseSeam::extract_perimeter_segments( PreciseSeam::PreparedPerimeter(perimeter), PreciseSeam::prepare_modifier_regions(regions), mode); REQUIRE(extracted.segments.size() == (extra_segment ? 2 : 1)); const auto *modifier = fixture.add_regions(mode, std::move(regions)); PreciseSeam::PreciseSeamWarnings warnings; const auto seam = PreciseSeam::insert_strong_seam_point( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == (mode == ModelVolumeType::PRECISE_SEAM_LEFT ? mm(0, 4) : mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? mm(4, 0) : mm(0, 0))); CHECK(warnings.multiple_intersections.load() == (extra_segment ? PreciseSeam::PreciseSeamWarnings::type_bit(mode) : 0u)); CHECK(warnings.failed_fragments.load() == 0); CHECK_FALSE(warnings.full_containment.load()); check_square_boundary(perimeter); } TEST_CASE("A collinear contour origin preserves strong targets and weak candidate types", "[PreciseSeam][Regression]") { const bool reverse = GENERATE(false, true); const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_BLOCKED); SeamFixture fixture; Polygon perimeter(Points{mm(10, 0), mm(20, 0), mm(20, 20), mm(0, 20), mm(0, 0)}); // Both clipping fragments have two points, with the artificial cut inside a straight side. if (reverse) std::reverse(perimeter.points.begin() + 1, perimeter.points.end()); const auto *modifier = fixture.add(mode, {rectangle(8, -2, 12, 2)}); PreciseSeam::PreciseSeamWarnings warnings; if (is_precise_seam_strong(mode)) { const auto seam = PreciseSeam::insert_strong_seam_point( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); const double x = mode == ModelVolumeType::PRECISE_SEAM_CENTER ? 10. : ((mode == ModelVolumeType::PRECISE_SEAM_LEFT) != reverse ? 8. : 12.); CHECK(*seam == mm(x, 0)); } else { const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(segments.size() == 1); const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; const bool inside = p.y() == 0 && p.x() >= scale_(8.) && p.x() <= scale_(12.); CHECK(types[i] == (inside ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral)); } } CHECK_FALSE(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.full_containment.load()); CHECK(warnings.failed_fragments.load() == 0); check_square_boundary(perimeter); } TEST_CASE("A gap on the closing edge preserves the complementary seam segment", "[PreciseSeam][Regression]") { const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_BLOCKED); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); ExPolygon region(rectangle(-2, -2, 22, 22)); region.holes.push_back(rectangle(-1, 8, 1, 12)); region.holes.back().reverse(); // The retained 76 mm arc starts and ends on edge n-1, with a four-millimeter gap between them. const auto *modifier = fixture.add_regions(mode, {region}); PreciseSeam::PreciseSeamWarnings warnings; if (is_precise_seam_strong(mode)) { const auto seam = PreciseSeam::insert_strong_seam_point( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == (mode == ModelVolumeType::PRECISE_SEAM_LEFT ? mm(0, 8) : mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? mm(0, 12) : mm(20, 10))); } else { const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(segments.size() == 1); const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; const bool gap = p.x() == 0 && p.y() > scale_(8.) && p.y() < scale_(12.); CHECK(types[i] == (gap ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral : SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked)); } } CHECK_FALSE(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.full_containment.load()); CHECK(warnings.failed_fragments.load() == 0); check_square_boundary(perimeter); } TEST_CASE("Enforced oversampling stays inside a zone that wraps around a gap", "[PreciseSeam][Regression]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); ExPolygon region(rectangle(-2, -2, 22, 22)); region.holes.push_back(rectangle(-1, 8, 1, 12)); region.holes.back().reverse(); // The zone runs from (0, 8) around the square to (0, 12); the edge after its right boundary is the gap. // Boundary helpers already keep that edge too short to split, so this guards the invariant, not a visible bug. const auto *modifier = fixture.add_regions(ModelVolumeType::PRECISE_SEAM_ENFORCED, {region}); const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 1); CHECK(segments[0].left_point == mm(0, 8)); CHECK(segments[0].right_point == mm(0, 12)); const auto types = weak_candidate_types(perimeter, segments); Points gap_points; size_t enforced_count = 0; for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; const bool gap = p.x() == 0 && p.y() > scale_(8.) && p.y() < scale_(12.); if (gap) gap_points.push_back(p); else ++enforced_count; CHECK(types[i] == (gap ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral : SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced)); } // Only the two boundary refinement helpers may lie in the gap; no oversampling points. std::sort(gap_points.begin(), gap_points.end(), [](const Point &a, const Point &b) { return a.y() < b.y(); }); CHECK(gap_points == Points{mm(0, 8.001), mm(0, 11.999)}); // The 76 mm zone itself is oversampled: far more candidates than its corners and boundaries. CHECK(enforced_count > 76. / SeamPlacer::enforcer_oversampling_distance - 10); 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 must not stop refinement before the separate segment is reached. 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, PreciseSeam::PreparedPerimeter(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 on one source edge retain insertion order and modifier priority", "[PreciseSeam][Regression]") { const bool closing_edge = GENERATE(false, true); const bool coincident = GENERATE(false, true); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Polygon outer = closing_edge ? rectangle(-2, 2, 2, 8) : rectangle(2, -2, 8, 2); const Polygon inner = coincident ? outer : (closing_edge ? rectangle(-2, 4, 2, 6) : rectangle(4, -2, 6, 2)); const auto *blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {outer}); const auto *neutral = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {inner}); // Priority order is not geometric insertion order; Neutral must win in the overlap. const auto segments = PreciseSeam::collect_weak_modifier_segments( {blocked, neutral}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 2); CHECK(segments[0].type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked); CHECK(segments[1].type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral); CHECK(segments[0].left_position.edge_index == (closing_edge ? 3 : 0)); CHECK(segments[0].right_position.edge_index == (closing_edge ? 3 : 0)); CHECK_THAT(segments[0].left_position.parameter, Catch::Matchers::WithinAbs(closing_edge ? 0.6 : 0.1, 1e-12)); CHECK_THAT(segments[0].right_position.parameter, Catch::Matchers::WithinAbs(closing_edge ? 0.9 : 0.4, 1e-12)); const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; const coord_t along = closing_edge ? p.y() : p.x(); const coord_t across = closing_edge ? p.x() : p.y(); const bool in_outer = across == 0 && along >= scale_(2.) && along <= scale_(8.); const bool in_inner = coincident ? in_outer : (across == 0 && along >= scale_(4.) && along <= scale_(6.)); const auto expected = in_outer && !in_inner ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral; CHECK(types[i] == expected); } // Ascending insertion on one edge would retrace the contour or lose pending boundaries. check_square_boundary(perimeter); } TEST_CASE("Weak zones overwrite painted candidate types inside their boundaries only", "[PreciseSeam][Regression]") { using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint; // The bottom side is painted Enforced or Blocked, the top side Blocked; the vertical sides stay Neutral. const Type painted_bottom = GENERATE(Type::Enforced, Type::Blocked); const auto zone_type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL); CAPTURE(int(painted_bottom), int(zone_type)); const Type expected_zone = zone_type == ModelVolumeType::PRECISE_SEAM_ENFORCED ? Type::Enforced : zone_type == ModelVolumeType::PRECISE_SEAM_BLOCKED ? Type::Blocked : Type::Neutral; SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // The zone covers x in [6, 14] on the bottom side only. const auto *modifier = fixture.add(zone_type, {rectangle(6, -2, 14, 2)}); const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 1); const auto painted = [painted_bottom](const Point &p) { return p.y() == 0 ? painted_bottom : p.y() == mm(0, 20).y() ? Type::Blocked : Type::Neutral; }; const auto types = weak_candidate_types(perimeter, segments, painted); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; CAPTURE(p.x(), p.y()); const bool inside = p.y() == 0 && p.x() >= mm(6, 0).x() && p.x() <= mm(14, 0).x(); // Inside the zone the weak type wins over painting, Neutral clearing it; outside, painting stays. CHECK(types[i] == (inside ? expected_zone : painted(p))); } } 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, PreciseSeam::PreparedPerimeter(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("Weak processing applies every ready interval and leaves gaps unchanged", "[PreciseSeam][SegmentExtraction]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); ExPolygon area(rectangle(1, -3, 10, 3)); area.holes.push_back(rectangle(4, -1, 7, 1)); area.holes.back().reverse(); const auto *modifier = fixture.add_regions(ModelVolumeType::PRECISE_SEAM_BLOCKED, {area, ExPolygon(rectangle(14, -3, 18, 3))}); PreciseSeam::PreciseSeamWarnings warnings; const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(segments.size() == 3); const Points starts{mm(1, 0), mm(7, 0), mm(14, 0)}; const Points ends{mm(4, 0), mm(10, 0), mm(18, 0)}; for (size_t i = 0; i < segments.size(); ++i) { CHECK(segments[i].left_point == starts[i]); CHECK(segments[i].right_point == ends[i]); require_vertex(perimeter, starts[i]); require_vertex(perimeter, ends[i]); } const auto types = weak_candidate_types(perimeter, segments); for (size_t i = 0; i < perimeter.size(); ++i) { const Point &point = perimeter[i]; // Classify the three independent intervals, including their boundary vertices. const bool inside = point.y() == 0 && ((point.x() >= starts[0].x() && point.x() <= ends[0].x()) || (point.x() >= starts[1].x() && point.x() <= ends[1].x()) || (point.x() >= starts[2].x() && point.x() <= ends[2].x())); CHECK(types[i] == (inside ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral)); } CHECK_FALSE(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.full_containment.load()); CHECK(warnings.failed_fragments.load() == 0); check_square_boundary(perimeter); } TEST_CASE("Weak priority and enforcement refinement apply on both crossed sides", "[PreciseSeam][SegmentExtraction]") { const auto high_type = GENERATE(ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL); const auto expected_high = high_type == ModelVolumeType::PRECISE_SEAM_BLOCKED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral; SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const auto *low = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(8, -2, 12, 22)}); const auto *high = fixture.add(high_type, {rectangle(10, -2, 14, 22)}); const auto segments = PreciseSeam::collect_weak_modifier_segments( {low, high}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 4); CHECK(segments[0].type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced); CHECK(segments[1].type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced); CHECK(segments[2].type == expected_high); CHECK(segments[3].type == expected_high); const auto types = weak_candidate_types(perimeter, segments); size_t enforced_counts[2] = {0, 0}; for (size_t i = 0; i < perimeter.size(); ++i) { const Point &point = perimeter[i]; const bool side = point.y() == 0 || point.y() == mm(0, 20).y(); auto expected = SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral; if (side && point.x() >= mm(8, 0).x() && point.x() <= mm(12, 0).x()) expected = SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced; if (side && point.x() >= mm(10, 0).x() && point.x() <= mm(14, 0).x()) expected = expected_high; CHECK(types[i] == expected); if (expected == SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced) ++enforced_counts[point.y() == 0 ? 0 : 1]; } // Both surviving enforced patches must contain interior candidates, not just boundaries. CHECK(enforced_counts[0] > 2); CHECK(enforced_counts[1] > 2); check_square_boundary(perimeter); } TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the whole perimeter, Blocked is skipped", "[PreciseSeam]") { const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL); CAPTURE(type); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Points original = perimeter.points; const auto *modifier = fixture.add(type, {rectangle(-2, -2, 22, 22)}); PreciseSeam::PreciseSeamWarnings warnings; const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); // Painting of both kinds, so that each type's effect on it is visible. const auto painted = [](const Point &p) { return p.x() < scale_(10.) ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced : SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked; }; const auto types = weak_candidate_types(perimeter, segments, painted); CHECK_FALSE(warnings.multiple_intersections.load()); CHECK(warnings.failed_fragments.load() == 0); if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) { // Forbidding the seam all round cannot be honoured: skipped with a warning, painting stays. CHECK(segments.empty()); CHECK(perimeter.points == original); CHECK(warnings.full_containment.load() == PreciseSeam::PreciseSeamWarnings::type_bit(type)); for (size_t i = 0; i < perimeter.size(); ++i) CHECK(types[i] == painted(perimeter[i])); return; } REQUIRE(segments.size() == 1); CHECK(segments.front().whole_perimeter); CHECK(warnings.full_containment.load() == 0); // Every candidate takes the zone's type, overriding painting. const auto expected = type == ModelVolumeType::PRECISE_SEAM_ENFORCED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral; for (size_t i = 0; i < perimeter.size(); ++i) CHECK(types[i] == expected); if (type == ModelVolumeType::PRECISE_SEAM_ENFORCED) { // As if painted green all round: every 20 mm edge is subdivided into steps of at most 0.2 mm. CHECK(perimeter.size() >= size_t(80.f / SeamPlacer::enforcer_oversampling_distance)); check_square_boundary(perimeter); } else CHECK(perimeter.points == original); // Like an unmarked perimeter: no subdivision. } TEST_CASE("Whole-perimeter weak zones take part in the usual priority order", "[PreciseSeam]") { // Volumes are listed low priority first; a later zone overwrites an earlier one. const int scenario = GENERATE(0, 1, 2); CAPTURE(scenario); SeamFixture fixture; const auto *whole_enforced = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(-2, -2, 22, 22)}); const auto *whole_neutral = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(-2, -2, 22, 22)}); const auto *whole_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(-2, -2, 22, 22)}); const auto *band_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(8, -2, 12, 2)}); const auto *band_enforced = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(8, -2, 12, 2)}); std::vector volumes; if (scenario == 0) volumes = {whole_enforced, band_blocked}; // A band forbidden inside a green perimeter. if (scenario == 1) volumes = {band_enforced, whole_neutral}; // A whole Neutral clears the lower band. if (scenario == 2) volumes = {band_enforced, whole_blocked}; // A whole Blocked is skipped; the band stays. Polygon perimeter = rectangle(0, 0, 20, 20); PreciseSeam::PreciseSeamWarnings warnings; const auto segments = PreciseSeam::collect_weak_modifier_segments( volumes, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); const auto types = weak_candidate_types(perimeter, segments); using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint; for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; CAPTURE(p.x(), p.y()); const bool in_band = p.y() == 0 && p.x() >= scale_(8.) && p.x() <= scale_(12.); const Type expected = scenario == 0 ? (in_band ? Type::Blocked : Type::Enforced) : scenario == 1 ? Type::Neutral : (in_band ? Type::Enforced : Type::Neutral); CHECK(types[i] == expected); } CHECK(warnings.full_containment.load() == (scenario == 2 ? PreciseSeam::PreciseSeamWarnings::type_bit(ModelVolumeType::PRECISE_SEAM_BLOCKED) : 0u)); check_square_boundary(perimeter); } TEST_CASE("Coverage short by a gap under 1 um is full containment while a narrow band keeps its point", "[PreciseSeam][Regression]") { // A tip pokes through the bottom edge at x = 10 mm by `depth` nm, so the perimeter line crosses it // over about 2 * depth. As a hole in a large region it leaves that much uncovered (a touch from // inside); as a separate triangle it is that much covered (a narrow band or an outside contact). const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_RIGHT); const bool gap = GENERATE(true, false); const coord_t depth = GENERATE(coord_t(2), coord_t(2000)); CAPTURE(type, gap, depth); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Points original = perimeter.points; const Point tip(mm(10, 0).x(), gap ? -depth : depth); ExPolygon region; if (gap) { region = ExPolygon(rectangle(-2, -2, 22, 22)); region.holes.push_back(Polygon(Points{tip, mm(11, 1), mm(9, 1)})); region.holes.back().reverse(); } else { region = ExPolygon(Polygon(Points{mm(9, -1), mm(11, -1), tip})); } const auto *modifier = fixture.add_regions(type, {region}); const bool micro = depth < scale_(0.001); PreciseSeam::PreciseSeamWarnings warnings; if (is_precise_seam_strong(type)) { const auto seam = PreciseSeam::insert_strong_seam_point( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); if (gap && micro) { // Skipped like an exact touch, instead of a seam at the touch or on the opposite side. CHECK_FALSE(seam.has_value()); CHECK(perimeter.points == original); } else { REQUIRE(seam.has_value()); if (!gap) // The band is processed normally: the seam lands on it. CHECK((*seam - mm(10, 0)).cast().norm() <= double(depth) + 1.); } } else { const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); const auto types = weak_candidate_types(perimeter, segments); const size_t enforced = size_t(std::count(types.begin(), types.end(), SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced)); if (gap && micro) { // Full containment like an exact touch, instead of collapsing the intended zone into one forced // point: Enforced then types the whole perimeter, subdivided as if painted green all round. REQUIRE(segments.size() == 1); CHECK(segments.front().whole_perimeter); CHECK(enforced == types.size()); CHECK(enforced > 300); } else if (gap) { REQUIRE(segments.size() == 1); CHECK(enforced > 300); // A 4 um gap is real: nearly the whole 80 mm perimeter, oversampled. } else { // The band is processed normally; below 1 um its boundaries snap into a single candidate. REQUIRE(segments.size() == 1); CHECK(enforced == (micro ? 1u : 2u)); } } // Only the sub-micron gap is full containment, and only strong types skip it with a warning; // nothing here is a binding failure. CHECK((warnings.full_containment.load() != 0) == (gap && micro && is_precise_seam_strong(type))); CHECK(warnings.failed_fragments.load() == 0); check_square_boundary(perimeter); } TEST_CASE("A modifier touching the perimeter at one point keeps the full containment policy", "[PreciseSeam][Regression]") { const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const Points original = perimeter.points; // The hole touches the perimeter only at (10, 0): no gap, so the contact must not create segments. ExPolygon region(rectangle(-2, -2, 22, 22)); region.holes.push_back(Polygon(Points{mm(10, 0), mm(12, 2), mm(10, 4), mm(8, 2)})); region.holes.back().reverse(); const auto *modifier = fixture.add_regions(type, {region}); PreciseSeam::PreciseSeamWarnings warnings; if (is_precise_seam_strong(type)) CHECK_FALSE(PreciseSeam::insert_strong_seam_point( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings).has_value()); else { const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); // Enforced and Neutral type the whole perimeter; Blocked is skipped. if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) CHECK(segments.empty()); else { REQUIRE(segments.size() == 1); CHECK(segments.front().whole_perimeter); } } // Only a whole-perimeter Enforced zone subdivides the edges. if (type != ModelVolumeType::PRECISE_SEAM_ENFORCED) CHECK(perimeter.points == original); const bool skipped = is_precise_seam_strong(type) || type == ModelVolumeType::PRECISE_SEAM_BLOCKED; CHECK(warnings.full_containment.load() == (skipped ? PreciseSeam::PreciseSeamWarnings::type_bit(type) : 0u)); CHECK_FALSE(warnings.multiple_intersections.load()); CHECK(warnings.failed_fragments.load() == 0); } TEST_CASE("Unsupported strong 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 = scenario == 3 ? fixture.add_regions(ModelVolumeType::PRECISE_SEAM_CENTER, {ExPolygon(slices[0], slices[1])}) : fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, std::move(slices)); PreciseSeam::PreciseSeamWarnings warnings; CHECK_FALSE(PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings).has_value()); CHECK(perimeter.points == original); CHECK((warnings.full_containment.load() != 0) == (scenario == 2 || scenario == 3)); CHECK_FALSE(warnings.multiple_intersections.load()); } TEST_CASE("Strong selects the longest arc rather than the longest chord", "[PreciseSeam][SegmentExtraction]") { const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT); const size_t origin = GENERATE(size_t(0), size_t(2)); const bool reverse_regions = GENERATE(false, true); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); std::rotate(perimeter.points.begin(), perimeter.points.begin() + origin, perimeter.points.end()); // The corner arc is 8 mm with a shorter chord than the other 6 mm interval. ExPolygons regions{ExPolygon(rectangle(10, -2, 16, 2)), ExPolygon(rectangle(-2, -2, 4, 4))}; if (reverse_regions) std::reverse(regions.begin(), regions.end()); const auto *modifier = fixture.add_regions(mode, std::move(regions)); PreciseSeam::PreciseSeamWarnings warnings; const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == (mode == ModelVolumeType::PRECISE_SEAM_LEFT ? mm(0, 4) : mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? mm(4, 0) : mm(0, 0))); CHECK(warnings.multiple_intersections.load()); check_square_boundary(perimeter); } TEST_CASE("Equal strong lengths compare the selected mode points", "[PreciseSeam][SegmentExtraction]") { const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_RIGHT); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // Both arcs are 6 mm: Left/Center prefer the corner; Right prefers the rear straight segment. const auto *modifier = fixture.add(mode, {rectangle(10, 18, 16, 22), rectangle(-2, 17, 3, 22)}); const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == (mode == ModelVolumeType::PRECISE_SEAM_LEFT ? mm(3, 20) : mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? mm(10, 20) : mm(0, 20))); check_square_boundary(perimeter); } TEST_CASE("Strong length comparison does not merge nearly equal lengths", "[PreciseSeam][SegmentExtraction]") { const coord_t extra = GENERATE(coord_t(0), coord_t(1)); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); Polygon second = rectangle(12, -2, 16, 2); second.points[1].x() += extra; second.points[2].x() += extra; const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_LEFT, {rectangle(2, -2, 6, 2), second}); const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == (extra == 0 ? mm(2, 0) : mm(12, 0))); } TEST_CASE("Strong priority wins over a longer segment in another modifier", "[PreciseSeam][SegmentExtraction]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const auto *high = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(2, -2, 4, 2)}); const auto *low = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(1, 18, 19, 22)}); const auto seam = PreciseSeam::insert_strong_seam_point({high, low}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == mm(3, 0)); CHECK(std::find(perimeter.points.begin(), perimeter.points.end(), mm(10, 20)) == perimeter.points.end()); } TEST_CASE("Strong continues past modifiers without usable segments", "[PreciseSeam][SegmentExtraction]") { const bool contained = GENERATE(false, true); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const auto *first = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {contained ? rectangle(-2, -2, 22, 22) : rectangle(30, 30, 40, 40)}); const auto *second = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(2, -2, 6, 2)}); PreciseSeam::PreciseSeamWarnings warnings; const auto seam = PreciseSeam::insert_strong_seam_point({first, second}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == mm(4, 0)); CHECK((warnings.full_containment.load() != 0) == contained); } TEST_CASE("Strong compares all segments of structured modifier regions", "[PreciseSeam][SegmentExtraction]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); ExPolygon area(rectangle(1, -3, 10, 3)); area.holes.push_back(rectangle(4, -1, 7, 1)); area.holes.back().reverse(); const auto *modifier = fixture.add_regions(ModelVolumeType::PRECISE_SEAM_CENTER, {area, ExPolygon(rectangle(14, -3, 18, 3))}); PreciseSeam::PreciseSeamWarnings warnings; const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == mm(16, 0)); CHECK(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.full_containment.load()); CHECK(warnings.failed_fragments.load() == 0); check_square_boundary(perimeter); } TEST_CASE("Strong tie order uses bed axes after rotating and translating the input", "[PreciseSeam][SegmentExtraction]") { const bool rotated = GENERATE(false, true); SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); Polygon modifier_region = rectangle(8, -2, 12, 22); const auto transform = [rotated](Point &p) { // Rotation is already baked into slice coordinates; translation cannot change ordering. if (rotated) p = Point(-p.y(), p.x()); p += mm(100, 200); }; for (Point &p : perimeter.points) transform(p); for (Point &p : modifier_region.points) transform(p); const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {modifier_region}); const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(seam.has_value()); CHECK(*seam == (rotated ? mm(80, 210) : mm(110, 220))); } TEST_CASE("Strong modifiers warn when another slice polygon also intersects the perimeter", "[PreciseSeam]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); // Equal lengths on the same side select the leftmost mode point, independent of slice order. 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, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == mm(4, 0)); CHECK(warnings.multiple_intersections.load()); CHECK_FALSE(warnings.full_containment.load()); check_square_boundary(perimeter); } TEST_CASE("Crossing modifiers choose the rear strong segment and keep both weak segments", "[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, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(seam.has_value()); CHECK(*seam == mm(10, 20)); } else { const auto segments = PreciseSeam::collect_weak_modifier_segments({modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); REQUIRE(segments.size() == 2); CHECK(segments[0].left_point == mm(8, 0)); CHECK(segments[0].right_point == mm(12, 0)); CHECK(segments[1].left_point == mm(12, 20)); CHECK(segments[1].right_point == mm(8, 20)); } CHECK((warnings.multiple_intersections.load() != 0) == strong); // Warn only strong, after collecting all segments. CHECK_FALSE(warnings.full_containment.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, PreciseSeam::PreparedPerimeter(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, PreciseSeam::PreparedPerimeter(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); } TEST_CASE("Volume sorting keeps strong before weak Precise Seam modifiers and the user order inside each group", "[PreciseSeam][Model]") { Model model; ModelObject *object = model.add_object(); // Enum order inside a group (Center < Right, Enforced < Neutral) is deliberately reversed here: // only the group may move a volume, the user's order inside it must survive. const auto add = [object](ModelVolumeType type) { ModelVolume *volume = object->add_volume(Test::cube(1)); volume->set_type(type); return volume; }; ModelVolume *part = add(ModelVolumeType::MODEL_PART); ModelVolume *modifier = add(ModelVolumeType::PARAMETER_MODIFIER); ModelVolume *second_part = add(ModelVolumeType::MODEL_PART); ModelVolume *weak_neutral = add(ModelVolumeType::PRECISE_SEAM_NEUTRAL); ModelVolume *strong_right = add(ModelVolumeType::PRECISE_SEAM_RIGHT); ModelVolume *weak_enforced = add(ModelVolumeType::PRECISE_SEAM_ENFORCED); ModelVolume *strong_center = add(ModelVolumeType::PRECISE_SEAM_CENTER); const ModelVolumePtrs original = object->volumes; // A full sort orders parts before modifiers; a partial one keeps parts and modifiers as they are. const bool full_sort = GENERATE(false, true); CAPTURE(full_sort); object->volumes = original; object->sort_volumes(full_sort); const ModelVolumePtrs expected_head = full_sort ? ModelVolumePtrs{part, second_part, modifier} : ModelVolumePtrs{part, modifier, second_part}; ModelVolumePtrs expected = expected_head; for (ModelVolume *volume : {strong_right, strong_center, weak_neutral, weak_enforced}) expected.push_back(volume); CHECK(object->volumes == expected); } TEST_CASE("Restoring Precise Seam positions overrides alignment only on perimeters with a strong point", "[PreciseSeam]") { std::vector layers(1); PrintObjectSeamData::LayerSeams &layer = layers.front(); // Two perimeters of three candidates each; alignment has already finalized both. for (size_t loop_idx = 0; loop_idx < 2; ++loop_idx) { layer.perimeters.emplace_back(); SeamPlacerImpl::Perimeter &loop = layer.perimeters.back(); loop.start_index = layer.points.size(); for (size_t i = 0; i < 3; ++i) layer.points.emplace_back(Vec3f(float(i), float(loop_idx), 0.f), loop, 0.f, SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral); loop.end_index = layer.points.size(); loop.finalized = true; loop.seam_index = loop.start_index; loop.final_seam_position = Vec3f(0.5f, float(loop_idx), 0.f); } SeamPlacerImpl::Perimeter &strong = layer.perimeters[0]; SeamPlacerImpl::Perimeter &plain = layer.perimeters[1]; strong.precise_seam_point = Vec3f(2.f, 0.f, 0.f); strong.precise_seam_index = 2; PreciseSeam::restore_precise_seam_positions(layers); // The strong point and its candidate index replace the aligned position. CHECK(strong.final_seam_position == Vec3f(2.f, 0.f, 0.f)); CHECK(strong.seam_index == 2); // A perimeter without a strong point keeps whatever alignment chose. CHECK(plain.final_seam_position == Vec3f(0.5f, 1.f, 0.f)); CHECK(plain.seam_index == plain.start_index); } TEST_CASE("Modifier usage marks evaluated modifiers that never reach a perimeter", "[PreciseSeam]") { SeamFixture fixture; PreciseSeam::PreciseSeamWarnings warnings; const auto checked = [&warnings](const ModelVolume *volume) { return warnings.modifier_usage.at(volume).checked.load(); }; const auto reached = [&warnings](const ModelVolume *volume) { return warnings.modifier_usage.at(volume).reached.load(); }; // Weak: all modifiers are evaluated. One stops 1 mm short of the wall, one crosses it, one contains it. const ModelVolume *short_weak = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(21, 5, 25, 10)}); const ModelVolume *crossing_weak = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(18, 5, 25, 10)}); const ModelVolume *containing_weak = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(-1, -1, 21, 21)}); for (const ModelVolume *volume : {short_weak, crossing_weak, containing_weak}) warnings.modifier_usage.try_emplace(volume); Polygon weak_perimeter = rectangle(0, 0, 20, 20); PreciseSeam::collect_weak_modifier_segments({short_weak, crossing_weak, containing_weak}, weak_perimeter, PreciseSeam::PreparedPerimeter(weak_perimeter), fixture.layer, fixture.cache, &warnings); CHECK(checked(short_weak)); CHECK_FALSE(reached(short_weak)); CHECK(reached(crossing_weak)); // Full containment reached the perimeter even though it is skipped. CHECK(reached(containing_weak)); // Strong: evaluation stops at the first modifier with a segment. The one ending 0.5 mm before the // wall is evaluated without reaching it; the one after the winner is never evaluated, so nothing is // known about it and it must not be reported. const ModelVolume *short_strong = fixture.add(ModelVolumeType::PRECISE_SEAM_LEFT, {rectangle(5, -2, 10, -0.5)}); const ModelVolume *winner = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(5, -2, 10, 2)}); const ModelVolume *shadowed = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(12, -2, 15, 2)}); for (const ModelVolume *volume : {short_strong, winner, shadowed}) warnings.modifier_usage.try_emplace(volume); Polygon strong_perimeter = rectangle(0, 0, 20, 20); REQUIRE(PreciseSeam::insert_strong_seam_point({short_strong, winner, shadowed}, strong_perimeter, PreciseSeam::PreparedPerimeter(strong_perimeter), fixture.layer, fixture.cache, &warnings).has_value()); CHECK(checked(short_strong)); CHECK_FALSE(reached(short_strong)); CHECK(reached(winner)); CHECK_FALSE(checked(shadowed)); // Unregistered modifiers are not tracked. const ModelVolume *unregistered = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(18, 5, 25, 10)}); Polygon other_perimeter = rectangle(0, 0, 20, 20); PreciseSeam::collect_weak_modifier_segments({unregistered}, other_perimeter, PreciseSeam::PreparedPerimeter(other_perimeter), fixture.layer, fixture.cache, &warnings); CHECK(warnings.modifier_usage.count(unregistered) == 0); } TEST_CASE("Weak zones type painting's oversampled candidates between their boundaries", "[PreciseSeam]") { SeamFixture fixture; Polygon perimeter = rectangle(0, 0, 20, 20); const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(8, -2, 12, 2)}); const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache); REQUIRE(segments.size() == 1); // Candidates as production builds them for a bottom edge painted green: every polygon point, then // oversampled points every enforcer_oversampling_distance towards the next one, in float arithmetic. using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint; PrintObjectSeamData::LayerSeams candidates; candidates.perimeters.emplace_back(); auto &loop = candidates.perimeters.back(); loop.start_index = 0; size_t oversampled = 0; for (size_t i = 0; i < perimeter.size(); ++i) { const Vec2f a = unscale(perimeter[i]).cast(); const Vec2f b = unscale(perimeter[(i + 1) % perimeter.size()]).cast(); const Vec3f position(a.x(), a.y(), 0.f); candidates.points.emplace_back(position, loop, 0.f, a.y() == 0.f ? Type::Enforced : Type::Neutral); if (a.y() != 0.f || b.y() != 0.f) continue; const Vec3f next(b.x(), b.y(), 0.f); const float distance = (next - position).norm(); const Vec3f direction = (next - position).normalized(); for (float step = SeamPlacer::enforcer_oversampling_distance; step < distance; step += SeamPlacer::enforcer_oversampling_distance) { candidates.points.emplace_back(position + direction * step, loop, 0.f, Type::Enforced); ++oversampled; } } loop.end_index = candidates.points.size(); bool enforced = false; PreciseSeam::apply_weak_modifiers_to_perimeter(segments, candidates, loop, enforced); // Boundaries are found among the interleaved points by exact float identity; every candidate // between them, oversampled ones included, is cleared, and painting stays outside. size_t cleared_oversampled = 0; for (const auto &candidate : candidates.points) { const Vec3f &p = candidate.position; CAPTURE(p.x(), p.y()); const bool in_zone = p.y() == 0.f && p.x() >= 8.f && p.x() <= 12.f; const Type expected = in_zone ? Type::Neutral : p.y() == 0.f ? Type::Enforced : Type::Neutral; CHECK(candidate.type == expected); if (in_zone && p.x() > 8.001f && p.x() < 11.999f) ++cleared_oversampled; } CHECK(oversampled > 0); CHECK(cleared_oversampled >= 15); // About 4 mm of 0.2 mm steps inside the zone. } namespace { // A 20 x 20 x 2 mm cube sliced at 0.2 mm, so ten layers, each with one outer wall loop. struct ExportFixture { Model model; Print print; DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); Vec3d cube_min; explicit ExportFixture(const std::string &seam_position, int raft_layers = 0) { config.set_deserialize_strict("seam_position", seam_position); config.set_deserialize_strict("seam_slope_type", "none"); // The loop then starts exactly at the seam. config.set_deserialize_strict("layer_height", "0.2"); config.set_deserialize_strict("initial_layer_print_height", "0.2"); config.set_deserialize_strict("outer_wall_line_width", "0.4"); config.set_deserialize_strict("initial_layer_line_width", "0.4"); config.set_deserialize_strict("raft_layers", std::to_string(raft_layers)); Test::init_print({make_cube(20, 20, 2)}, print, model, config); const ModelVolume *cube = model.objects.front()->volumes.front(); cube_min = cube->mesh().transformed_bounding_box(cube->get_matrix()).min; } // Places the helper's minimum corner at `min`, relative to the cube's minimum corner. void add(ModelVolumeType type, TriangleMesh mesh, const Vec3d &min, const std::string &name = "helper") { ModelVolume *volume = model.objects.front()->add_volume(std::move(mesh)); volume->set_type(type); volume->name = name; const Vec3d current = volume->mesh().transformed_bounding_box(volume->get_matrix()).min; volume->set_offset(volume->get_offset() + cube_min + min - current); } std::string export_gcode() { print.apply(model, config); return Test::gcode(print); } std::string warning() const { std::string text; for (const auto &warning : print.step_state_with_warnings(psGCodeExport).warnings) text += warning.message; return text; } }; struct OuterLoop { Vec2d seam; // Relative to the loop's centre, which is the cube's centre. BoundingBoxf bounds; }; std::vector outer_wall_loops(const std::string &gcode) { std::vector loops; bool outer_wall = false; bool in_loop = false; GCodeReader parser; parser.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { const std::string_view comment = line.comment(); if (comment.find("TYPE:") != std::string_view::npos) { outer_wall = comment.find("Outer wall") != std::string_view::npos; in_loop = false; } if (!outer_wall || !line.extruding(self) || line.dist_XY(self) == 0.f) return; if (!in_loop) { loops.push_back({Vec2d(self.x(), self.y()), {}}); loops.back().bounds.merge(loops.back().seam); in_loop = true; } loops.back().bounds.merge(Vec2d(line.new_X(self), line.new_Y(self))); }); for (OuterLoop &loop : loops) loop.seam -= loop.bounds.center(); return loops; } TriangleMesh helper_with_hole() { // 8 x 6 x 4 mm with a 4 x 3 mm hole through every layer of the cube. TriangleMesh helper = make_cube(8, 6, 4); TriangleMesh cavity = make_cube(4, 3, 3); cavity.translate(2., 1.5, 0.5); cavity.flip_triangles(); helper.merge(cavity); return helper; } } // namespace TEST_CASE("Strong modifiers place the exported seam on the side they cross", "[PreciseSeam]") { // The helper crosses the front face (y = 0) at x = 4..6 mm. The loop runs counterclockwise, so on // the front face Left is the x = 4 mm edge and Right the x = 6 mm edge. const auto [type, x] = GENERATE(table({ {ModelVolumeType::PRECISE_SEAM_CENTER, 5.}, {ModelVolumeType::PRECISE_SEAM_LEFT, 4.}, {ModelVolumeType::PRECISE_SEAM_RIGHT, 6.}})); const std::string seam_position = GENERATE(as{}, "back", "aligned", "nearest"); const int raft_layers = GENERATE(0, 2); CAPTURE(seam_position, raft_layers); ExportFixture fixture(seam_position, raft_layers); fixture.add(type, make_cube(2, 6, 4), Vec3d(4, -3, -1)); const auto loops = outer_wall_loops(fixture.export_gcode()); REQUIRE(loops.size() == 10); // Raft layers have no outer wall. for (const OuterLoop &loop : loops) { CHECK_THAT(loop.seam.x(), Catch::Matchers::WithinAbs(x - 10., 0.01)); CHECK_THAT(loop.seam.y(), Catch::Matchers::WithinAbs(loop.bounds.min.y() - loop.bounds.center().y(), 0.01)); } CHECK(fixture.warning().empty()); } TEST_CASE("Weak modifiers move the exported seam into or out of their zone", "[PreciseSeam]") { // INVALID adds no helper: a back seam then lies on the rear face (y = 20 mm). const auto type = GENERATE(ModelVolumeType::INVALID, ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED); ExportFixture fixture("back"); if (type == ModelVolumeType::PRECISE_SEAM_ENFORCED) fixture.add(type, make_cube(2, 6, 4), Vec3d(4, -3, -1)); // Front face, x = 4..6 mm. if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) fixture.add(type, make_cube(30, 6, 4), Vec3d(-5, 17, -1)); // Everything from y = 17 mm back. const auto loops = outer_wall_loops(fixture.export_gcode()); REQUIRE(loops.size() == 10); for (const OuterLoop &loop : loops) { CAPTURE(loop.seam.x(), loop.seam.y()); if (type == ModelVolumeType::PRECISE_SEAM_ENFORCED) { CHECK_THAT(loop.seam.y(), Catch::Matchers::WithinAbs(loop.bounds.min.y() - loop.bounds.center().y(), 0.01)); CHECK(loop.seam.x() >= -6.01); CHECK(loop.seam.x() <= -3.99); } else if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) CHECK(loop.seam.y() < 7.); else CHECK(loop.seam.y() > 7.); } CHECK(fixture.warning().empty()); } TEST_CASE("A strong modifier with a hole uses the left of two equal crossings", "[PreciseSeam]") { // The hole splits the front face crossing into x = 2..4 and x = 8..10 mm. The lengths tie, both // points lie equally far back, so the left one wins. ExportFixture fixture("back"); fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, helper_with_hole(), Vec3d(2, -3, -1)); const auto loops = outer_wall_loops(fixture.export_gcode()); REQUIRE(loops.size() == 10); for (const OuterLoop &loop : loops) { CHECK_THAT(loop.seam.x(), Catch::Matchers::WithinAbs(3. - 10., 0.01)); CHECK_THAT(loop.seam.y(), Catch::Matchers::WithinAbs(loop.bounds.min.y() - loop.bounds.center().y(), 0.01)); } // The multiple-intersections reason names the type. CHECK(fixture.warning().find("Seam Center") != std::string::npos); } TEST_CASE("The export warning names a modifier that misses the outer wall centreline", "[PreciseSeam]") { // The helper reaches 0.1 mm into the cube, short of the centreline 0.2 mm inside a 0.4 mm outer wall. ExportFixture fixture("back"); fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, make_cube(2, 3.1, 4), Vec3d(4, -3, -1), "short helper"); fixture.export_gcode(); CHECK(fixture.warning().find("short helper") != std::string::npos); } TEST_CASE("Full containment warns for Blocked but not for Enforced", "[PreciseSeam]") { const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED); ExportFixture fixture("back"); fixture.add(type, make_cube(30, 30, 4), Vec3d(-5, -5, -1)); fixture.export_gcode(); if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) CHECK(fixture.warning().find("Seam Blocked") != std::string::npos); else CHECK(fixture.warning().empty()); }