Files
OrcaSlicer/tests/fff_print/test_precise_seam.cpp
T
HanifKoh 84657ff11e Add Missing Includes Across the Remaining Sources and Tests (#16071)
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy

Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.

* Ignore minilzo's Config Headers in clang-tidy

lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.

* Add Missing Includes Across the Remaining Sources and Tests

Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.

* Make the GUI and Test Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.

* Keep Windows and nanosvg Setup Ahead of the Added Includes

OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.

* Add the GUI Includes the First Pass Missed

Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.

* Keep the Added Test Includes Below the NOMINMAX Guard

test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
2026-10-03 13:45:21 +08:00

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