mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 14:20:58 +00:00
* 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.
295 lines
14 KiB
C++
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);
|
|
}
|