Files
OrcaSlicer/tests/fff_print/test_seam_placer.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

297 lines
14 KiB
C++

#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "test_helpers.hpp"
#include "libslic3r/GCode/SeamPlacer.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include <algorithm>
#include <cmath>
#include "libslic3r/Model.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <utility>
#include <cstddef>
#include "libslic3r/Point.hpp"
#include <vector>
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
namespace {
struct PipelineFixture {
Model model;
Print print;
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
explicit PipelineFixture(bool trapezoid = false)
{
auto mesh = its_make_cube(20, 20, 0.4);
if (trapezoid) {
// Opposite painted sides have deliberately different lengths: 20 mm and 6 mm.
for (auto &vertex : mesh.vertices)
if (vertex.y() == 20.0f) vertex.x() = 7.0f + 0.3f * vertex.x();
}
config.set_deserialize_strict("seam_position", "back"); // Rear needs no visibility ray tracing.
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("wall_loops", "1");
config.set_deserialize_strict("raft_layers", "0");
config.set_deserialize_strict("gcode_comments", "1"); // Match init_print so later apply calls change only the model.
Test::init_print({TriangleMesh(std::move(mesh))}, print, model, config);
}
void paint(bool all_faces)
{
auto &volume = *model.objects.front()->volumes.front();
const auto &mesh = volume.mesh();
const auto bounds = mesh.bounding_box();
TriangleSelector selector(mesh);
size_t painted = 0;
for (size_t i = 0; i < mesh.its.indices.size(); ++i) {
const auto &face = mesh.its.indices[i];
bool lower = true, upper = true;
for (int j = 0; j < 3; ++j) {
const auto &v = mesh.its.vertices[face[j]];
lower = lower && std::abs(double(v.y()) - bounds.min.y()) < 1e-6;
upper = upper && std::abs(double(v.y()) - bounds.max.y()) < 1e-6;
}
if (all_faces || lower || upper) {
selector.set_facet(int(i), EnforcerBlockerType::ENFORCER);
++painted;
}
}
REQUIRE(painted > 0);
volume.seam_facets.set(selector);
print.apply(model, config);
}
PrintObject &prepare()
{
REQUIRE(print.objects().size() == 1);
auto &object = *print.get_object(0);
object.slice(); // Real layers/regions are sufficient: each test supplies its own perimeter loops.
REQUIRE_FALSE(object.layers().empty());
REQUIRE_FALSE(object.layers().front()->regions().empty());
return object;
}
Points points_in_layer(const PrintObject &object, const std::vector<Vec2d> &xy) const
{
const auto &volume = *object.model_object()->volumes.front();
const auto minimum = volume.mesh().bounding_box().min;
const Transform3d transform = object.trafo_centered() * volume.get_matrix();
Points points;
for (const auto &point : xy) {
// add_volume centers the mesh; restore its local offset before applying the slicing transform.
const Vec3d local = minimum + Vec3d(point.x(), point.y(), 0.2);
const Vec3d placed = transform * local;
points.emplace_back(scale_(placed.x()), scale_(placed.y()));
}
return points;
}
};
void append_loop(LayerRegion &region, Points points, bool separate_paths = false)
{
// Inject deterministic external loops while keeping the real layer, region and paint-query machinery.
REQUIRE(points.size() >= 3);
points.push_back(points.front());
ExtrusionPaths paths;
if (separate_paths) {
for (size_t i = 1; i < points.size(); ++i) {
ExtrusionPath path(erExternalPerimeter, 0.08, 0.4f, 0.2f);
path.polyline = Polyline3(Polyline(Points{points[i - 1], points[i]}));
paths.push_back(std::move(path));
}
} else {
ExtrusionPath path(erExternalPerimeter, 0.08, 0.4f, 0.2f);
path.polyline = Polyline3(Polyline(std::move(points)));
paths.push_back(std::move(path));
}
region.perimeters.append(ExtrusionLoop(std::move(paths)));
}
LayerRegion &clear_first_layer(PrintObject &object)
{
Layer &layer = *object.layers().front();
for (LayerRegion *region : layer.regions()) region->perimeters.clear();
return *layer.get_region(0);
}
} // namespace
TEST_CASE("Painted seams prefer the longer candidate patch regardless of contour origin", "[SeamPlacer][Regression]")
{
const bool clockwise = GENERATE(false, true);
const bool wrapped = GENERATE(false, true);
CAPTURE(clockwise, wrapped);
PipelineFixture fixture(true);
fixture.paint(false);
PrintObject &object = fixture.prepare();
REQUIRE(object.model_object()->volumes.size() == 1);
CHECK_FALSE(object.model_object()->volumes.front()->is_precise_seam());
auto &region = clear_first_layer(object);
// A neutral loop ensures that patch indices are offsets in the layer, not zero-based local indices.
append_loop(region, fixture.points_in_layer(object, {{9, 8}, {11, 8}, {10, 10}}));
Points outline = fixture.points_in_layer(object, {{10, 20}, {7, 20}, {3.5, 10}, {0, 0}, {10, 0}, {20, 0}, {13, 20}});
if (clockwise) std::reverse(outline.begin() + 1, outline.end()); // Keep the same starting vertex.
if (!wrapped) {
const Point neutral = fixture.points_in_layer(object, {{3.5, 10}}).front();
const auto start = std::find(outline.begin(), outline.end(), neutral);
REQUIRE(start != outline.end());
std::rotate(outline.begin(), start, outline.end());
}
append_loop(region, std::move(outline));
SeamPlacer placer;
placer.init(fixture.print, [] {});
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
REQUIRE(data.perimeters.size() == 2);
const auto &perimeter = data.perimeters[1];
REQUIRE(perimeter.start_index > 0);
REQUIRE(perimeter.end_index > perimeter.start_index);
using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint;
CHECK((data.points[perimeter.start_index].type == Type::Enforced) == wrapped);
if (wrapped) CHECK(data.points[perimeter.end_index - 1].type == Type::Enforced);
const auto extremes = fixture.points_in_layer(object, {{10, 0}, {10, 20}});
const double bottom_y = unscale<double>(extremes[0].y()), top_y = unscale<double>(extremes[1].y());
size_t bottom_count = 0, top_count = 0, centers = 0;
for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) {
const auto &candidate = data.points[i];
if (candidate.type == Type::Enforced) {
// Include the small paint-radius fringe at the ends of each face.
const bool bottom = std::abs(candidate.position.y() - bottom_y) < 0.5;
const bool top = std::abs(candidate.position.y() - top_y) < 0.5;
const bool on_painted_face = bottom || top;
CAPTURE(candidate.position.x(), candidate.position.y());
CHECK(on_painted_face);
bottom_count += bottom;
top_count += top;
}
if (candidate.central_enforcer) {
++centers;
CHECK(candidate.type == Type::Enforced);
CHECK_THAT(double(candidate.position.y()), Catch::Matchers::WithinAbs(bottom_y, 0.5));
}
}
REQUIRE(top_count > 0);
REQUIRE(bottom_count > top_count);
CHECK(centers == 1); // The old wrapped-length formula instead selected the short top patch.
}
TEST_CASE("Entirely painted contours keep valid enforced seam candidates", "[SeamPlacer]")
{
PipelineFixture fixture;
fixture.paint(true);
PrintObject &object = fixture.prepare();
auto &region = clear_first_layer(object);
append_loop(region, fixture.points_in_layer(object, {{0, 0}, {20, 0}, {20, 20}, {0, 20}}));
SeamPlacer placer;
placer.init(fixture.print, [] {});
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
REQUIRE(data.perimeters.size() == 1);
const auto &perimeter = data.perimeters.front();
CHECK(perimeter.seam_index >= perimeter.start_index);
CHECK(perimeter.seam_index < perimeter.end_index);
for (const auto &candidate : data.points) {
CHECK(candidate.type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced);
CHECK_FALSE(candidate.central_enforcer); // There is no bounded patch to mark as central.
}
}
TEST_CASE("Precise Seam removes path junction duplicates but preserves separate visits", "[SeamPlacer][PreciseSeam]")
{
const bool enable_ps = GENERATE(false, true);
const bool self_touch = GENERATE(false, true);
PipelineFixture fixture;
if (enable_ps) {
auto *helper = fixture.model.objects.front()->add_volume(make_cube(1, 1, 1));
helper->set_type(ModelVolumeType::PRECISE_SEAM_NEUTRAL);
helper->set_offset(Vec3d(100, 100, 0)); // Enable normalization without intersecting the synthetic loop.
fixture.print.apply(fixture.model, fixture.config);
}
PrintObject &object = fixture.prepare();
auto &region = clear_first_layer(object);
const std::vector<Vec2d> vertices = self_touch ? std::vector<Vec2d>{{2, 2}, {10, 2}, {18, 10}, {10, 2}, {2, 18}} :
std::vector<Vec2d>{{2, 2}, {18, 2}, {18, 18}, {2, 18}};
const Points outline = fixture.points_in_layer(object, vertices);
append_loop(region, outline, true);
SeamPlacer placer;
placer.init(fixture.print, [] {});
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
REQUIRE(data.perimeters.size() == 1);
// Each separate path contributes both endpoints in ordinary mode; PS removes only adjacent copies.
REQUIRE(data.points.size() == (enable_ps ? outline.size() : 2 * outline.size()));
for (size_t i = 0; i < outline.size(); ++i) {
const Vec2f target = unscale(outline[i]).cast<float>();
const size_t input_count = std::count(outline.begin(), outline.end(), outline[i]);
// Both paths convert the same integer coordinates to float; exact identity detects duplicate copies.
const size_t actual_count = std::count_if(data.points.begin(), data.points.end(), [&](const auto &candidate) {
return candidate.position.template head<2>() == target;
});
CHECK(actual_count == (enable_ps ? input_count : 2 * input_count));
}
if (enable_ps) {
for (size_t i = 0; i < data.points.size(); ++i) {
CHECK(std::isfinite(data.points[i].local_ccw_angle));
CHECK(data.points[i].position != data.points[(i + 1) % data.points.size()].position);
}
}
}
TEST_CASE("Print apply synchronizes support and seam helpers through type changes and restored models", "[SeamPlacer][PreciseSeam][Print]")
{
const int changed = GENERATE(0, 1, 2); // Support only, seam only, or both including cross-family switches.
PipelineFixture fixture;
auto *model_object = fixture.model.objects.front();
auto *support = model_object->add_volume(make_cube(1, 1, 1));
support->set_type(ModelVolumeType::SUPPORT_BLOCKER);
auto *seam = model_object->add_volume(make_cube(1, 1, 1));
seam->set_type(ModelVolumeType::PRECISE_SEAM_CENTER);
fixture.print.apply(fixture.model, fixture.config);
REQUIRE(fixture.print.objects().size() == 1);
const PrintObject *original_print_object = fixture.print.objects().front();
const ModelVolume *original_part = original_print_object->model_object()->volumes.front();
const Model before(fixture.model); // A restored model snapshot preserves IDs, as the apply path requires.
if (changed == 0 || changed == 2) {
support->set_type(changed == 2 ? ModelVolumeType::PRECISE_SEAM_LEFT : ModelVolumeType::SUPPORT_ENFORCER);
support->set_offset(Vec3d(3, 4, 0));
}
if (changed == 1 || changed == 2) {
seam->set_type(changed == 2 ? ModelVolumeType::SUPPORT_BLOCKER : ModelVolumeType::PRECISE_SEAM_RIGHT);
seam->set_offset(Vec3d(-3, 2, 0));
}
if (changed == 2) std::swap(model_object->volumes[1], model_object->volumes[2]);
const auto check_applied = [&](const Model &expected) {
REQUIRE(fixture.print.objects().size() == 1);
// Helper-only changes should preserve the print object and its unaffected printable volume.
CHECK(fixture.print.objects().front() == original_print_object);
const auto &actual = fixture.print.objects().front()->model_object()->volumes;
const auto &wanted = expected.objects.front()->volumes;
REQUIRE(actual.size() == wanted.size());
CHECK(actual.front() == original_part);
for (size_t i = 0; i < wanted.size(); ++i) {
CAPTURE(changed, i);
CHECK(actual[i]->id() == wanted[i]->id());
CHECK(actual[i]->type() == wanted[i]->type());
CHECK(actual[i]->get_matrix().isApprox(wanted[i]->get_matrix(), 1e-9));
}
};
fixture.print.apply(fixture.model, fixture.config);
check_applied(fixture.model);
fixture.print.apply(before, fixture.config);
check_applied(before);
fixture.print.apply(fixture.model, fixture.config);
check_applied(fixture.model);
}