Files
OrcaSlicer/tests/fff_print/test_precise_seam.cpp
T
harrierpigeonandClaude Fable 5.1 9dbd0307f8 Add the includes clang-tidy's include cleaner asks for on the belt files
The clang-tidy job on #14394 fails on 129 misc-include-cleaner findings:
the belt sources and tests use std::, Eigen, Point/PrintConfig and
BeltBrim symbols without including the header that provides them, which
only compiled because the precompiled header supplied it. Every include
the job names is added, in each file's existing include style ("../" in
the GCode/ and Support/ subdirectories, quoted libslic3r/ paths in the
GUI and tests). No code changes.

Verified with scripts/clang_tidy_diff.py -p build-tidy --base eb5b9a77b9
(SLIC3R_PCH=OFF compile database, clang-tidy 22.1.8): no findings left.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6Kg5igmmMU2YLoK6HrsWV
2026-10-05 20:43:18 -05:00

1474 lines
79 KiB
C++

#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "test_helpers.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/GCode/PreciseSeam.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp"
#include <algorithm>
#include <functional>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PrintConfig.hpp"
#include <vector>
#include <utility>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCode/SeamPlacer.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/ExPolygon.hpp"
#include <cstddef>
#include <string>
#include <string_view>
#include <cstdlib>
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<double>(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<SeamPlacerImpl::EnforcedBlockedSeamPoint> weak_candidate_types(
const Polygon &polygon, const std::vector<PreciseSeam::WeakModifierSegment> &segments,
const std::function<SeamPlacerImpl::EnforcedBlockedSeamPoint(const Point &)> &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<float>();
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<SeamPlacerImpl::EnforcedBlockedSeamPoint> 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 &region = 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<const ModelVolume*> 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<double>().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<const ModelVolume*> 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<const ModelVolume*>{strong_a, strong_b});
CHECK(weak == std::vector<const ModelVolume*>{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<float>();
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<PrintObjectSeamData::LayerSeams> 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<float>();
const Vec2f b = unscale(perimeter[(i + 1) % perimeter.size()]).cast<float>();
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<OuterLoop> outer_wall_loops(const std::string &gcode)
{
std::vector<OuterLoop> 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, double>({
{ModelVolumeType::PRECISE_SEAM_CENTER, 5.},
{ModelVolumeType::PRECISE_SEAM_LEFT, 4.},
{ModelVolumeType::PRECISE_SEAM_RIGHT, 6.}}));
const std::string seam_position = GENERATE(as<std::string>{}, "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());
}
TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object was sliced in", "[PreciseSeam][belt]")
{
Model model;
Print print;
Test::init_print({Test::cube(20)}, print, model, {
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "belt_slice_rotation_global", 1 },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "skirt_loops", 0 },
// Keep the first layer's islands the raw slice, like the modifier's.
{ "elefant_foot_compensation", 0 },
});
print.process();
const PrintObject *object = print.get_object(0);
REQUIRE(object->layer_count() > 0);
// A modifier with the object's own mesh and placement must slice to the object's own islands
// on every layer. Sliced without the belt rotation it would give 20 mm squares on the lower
// layers and nothing above 20 mm, while the tilted cube reaches about 28 mm.
Model modifiers;
ModelVolume *modifier = modifiers.add_object()->add_volume(*model.objects.front()->volumes.front());
modifier->set_type(ModelVolumeType::PRECISE_SEAM_BLOCKED);
const std::vector<ExPolygons> slices = object->slice_single_volume_regions(modifier);
REQUIRE(slices.size() == object->layer_count());
const double tolerance = scale_(0.05) * scale_(20.);
for (size_t i = 0; i < slices.size(); ++i) {
CAPTURE(i, object->get_layer(int(i))->slice_z);
const ExPolygons &islands = object->get_layer(int(i))->lslices;
double object_area = 0., modifier_area = 0.;
for (const ExPolygon &island : islands)
object_area += island.area();
for (const ExPolygon &region : slices[i])
modifier_area += region.area();
CHECK(std::abs(modifier_area - object_area) < tolerance);
CHECK(get_extents(slices[i]).inflated(scale_(0.05)).contains(get_extents(islands)));
}
}