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.
210 lines
6.8 KiB
C++
210 lines
6.8 KiB
C++
#include <algorithm>
|
|
#include <boost/rational.hpp>
|
|
#include <boost/multiprecision/fwd.hpp>
|
|
#include <catch2/catch_all.hpp>
|
|
#include <vector>
|
|
#include "libnest2d/libnest2d.hpp"
|
|
#include "libnest2d/geometry_traits.hpp"
|
|
#include "libnest2d/backends/libslic3r/geometries.hpp"
|
|
#include "libnest2d/common.hpp"
|
|
#include <type_traits>
|
|
#include <cstdlib>
|
|
#include "libnest2d/placers/nfpplacer.hpp"
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <iterator>
|
|
#include "libnest2d/utils/rotcalipers.hpp"
|
|
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include "libnest2d_test_utils.hpp"
|
|
#include "printer_parts.hpp"
|
|
|
|
using namespace libnest2d;
|
|
|
|
namespace {
|
|
|
|
using Catch::Matchers::WithinAbs;
|
|
using Catch::Matchers::WithinRel;
|
|
|
|
// Geometry values round-trip through floating point, so compare with a small
|
|
// tolerance that works both near and away from zero.
|
|
void require_close(double value, double expected) {
|
|
REQUIRE_THAT(value, WithinRel(expected, 1e-9) || WithinAbs(expected, 1e-9));
|
|
}
|
|
|
|
// The printer parts as nestable items, computed once.
|
|
const std::vector<Item> &prusa_parts() {
|
|
static const std::vector<Item> parts = [] {
|
|
std::vector<Item> ret;
|
|
ret.reserve(PRINTER_PART_POLYGONS.size());
|
|
for (auto &inp : PRINTER_PART_POLYGONS) {
|
|
auto inp_cpy = inp;
|
|
if (ClosureTypeV<PathImpl> == Closure::OPEN)
|
|
inp_cpy.points.pop_back();
|
|
if constexpr (!is_clockwise<PathImpl>())
|
|
std::reverse(inp_cpy.begin(), inp_cpy.end());
|
|
ret.emplace_back(inp_cpy);
|
|
}
|
|
return ret;
|
|
}();
|
|
return parts;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Degree and radian conversion round-trips", "[Geometry]") {
|
|
Degrees deg(180);
|
|
Radians rad(deg);
|
|
|
|
require_close(rad, Pi);
|
|
require_close(deg, 180);
|
|
require_close(Degrees(rad), 180);
|
|
require_close(rad, Radians(deg));
|
|
require_close(Degrees(rad), deg);
|
|
REQUIRE(rad == deg);
|
|
}
|
|
|
|
TEST_CASE("Segment angle to the X axis", "[Geometry]") {
|
|
auto quadrant = [](Point to) { return Degrees(Segment({0, 0}, to).angleToXaxis()); };
|
|
|
|
REQUIRE(quadrant({12, -10}) > 270); REQUIRE(quadrant({12, -10}) < 360);
|
|
REQUIRE(quadrant({12, 10}) > 0); REQUIRE(quadrant({12, 10}) < 90);
|
|
REQUIRE(quadrant({-12, 10}) > 90); REQUIRE(quadrant({-12, 10}) < 180);
|
|
REQUIRE(quadrant({-12, -10}) > 180); REQUIRE(quadrant({-12, -10}) < 270);
|
|
|
|
require_close(quadrant({1, 0}), 0);
|
|
require_close(quadrant({0, 1}), 90);
|
|
require_close(quadrant({-1, 0}), 180);
|
|
require_close(quadrant({0, -1}), 270);
|
|
}
|
|
|
|
TEST_CASE("Point to segment distance", "[Geometry]") {
|
|
Point p2 = {10, 0};
|
|
Segment seg({0, 0}, {10, 10});
|
|
|
|
auto check = [](TCompute<Coord> val, TCompute<Coord> expected) {
|
|
if (std::is_floating_point<TCompute<Coord>>::value)
|
|
require_close(double(val), double(expected));
|
|
else
|
|
REQUIRE(val == expected);
|
|
};
|
|
|
|
auto h = pointlike::horizontalDistance(p2, seg);
|
|
REQUIRE(h.second);
|
|
check(h.first, 10);
|
|
|
|
auto v = pointlike::verticalDistance(p2, seg);
|
|
REQUIRE(v.second);
|
|
check(v.first, -10);
|
|
|
|
v = pointlike::verticalDistance(Point{10, 20}, seg);
|
|
REQUIRE(v.second);
|
|
check(v.first, 10);
|
|
|
|
Point p4 = {80, 0};
|
|
Segment seg2({0, 0}, {0, 40});
|
|
|
|
h = pointlike::horizontalDistance(p4, seg2);
|
|
REQUIRE(h.second);
|
|
check(h.first, 80);
|
|
|
|
v = pointlike::verticalDistance(p4, seg2);
|
|
REQUIRE_FALSE(v.second); // the point does not project onto the segment
|
|
}
|
|
|
|
TEST_CASE("Item area", "[Geometry]") {
|
|
require_close(RectangleItem(10, 10).area(), 100);
|
|
require_close(RectangleItem(100, 100).area(), 10000);
|
|
|
|
Item item = {
|
|
{61, 97}, {70, 151}, {176, 151}, {189, 138},
|
|
{189, 59}, {70, 59}, {61, 77}, {61, 97}
|
|
};
|
|
REQUIRE(std::abs(shapelike::area(item.transformedShape())) > 0);
|
|
}
|
|
|
|
TEST_CASE("Point inside polygon", "[Geometry]") {
|
|
RectangleItem rect(10, 10);
|
|
|
|
REQUIRE(rect.isInside(Point{1, 1}));
|
|
REQUIRE(rect.isInside(Point{3, 3}));
|
|
REQUIRE_FALSE(rect.isInside(Point{11, 11}));
|
|
REQUIRE_FALSE(rect.isInside(Point{11, 12}));
|
|
}
|
|
|
|
TEST_CASE("Bounding circle of the printer parts", "[Geometry]") {
|
|
PolygonImpl p = {{{0, 10}, {10, 0}, {0, -10}, {0, 10}}, {}};
|
|
Circle c = placers::boundingCircle(p);
|
|
|
|
require_close(getX(c.center()), 0);
|
|
require_close(getY(c.center()), 0);
|
|
require_close(c.radius(), 10);
|
|
|
|
shapelike::translate(p, PointImpl{10, 10});
|
|
c = placers::boundingCircle(p);
|
|
require_close(getX(c.center()), 10);
|
|
require_close(getY(c.center()), 10);
|
|
require_close(c.radius(), 10);
|
|
|
|
for (auto &part : prusa_parts()) {
|
|
c = placers::boundingCircle(part.transformedShape());
|
|
REQUIRE_FALSE(std::isnan(c.radius()));
|
|
for (auto v : shapelike::contour(part.transformedShape())) {
|
|
auto d = pointlike::distance(v, c.center());
|
|
if (d > c.radius())
|
|
REQUIRE(std::abs(1.0 - d / c.radius()) <= 1e-3); // on the circle
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Convex hull of a printer part", "[Geometry]") {
|
|
PathImpl poly = PRINTER_PART_POLYGONS[0];
|
|
auto chull = sl::convexHull(poly);
|
|
|
|
REQUIRE(chull.size() == poly.size()); // the part is already convex
|
|
}
|
|
|
|
namespace {
|
|
|
|
using Unit = int64_t;
|
|
using Ratio = boost::rational<boost::multiprecision::int128_t>;
|
|
|
|
// Reference minimum-area bounding box, found by brute force over every edge
|
|
// direction, to validate the rotating-calipers implementation.
|
|
long double ref_min_area_box(const PolygonImpl &p) {
|
|
long double min_area = std::numeric_limits<long double>::max();
|
|
|
|
auto update_min = [&](const Point &a, const Point &b) {
|
|
PolygonImpl rotated = p;
|
|
sl::rotate(rotated, -Segment(a, b).angleToXaxis());
|
|
min_area = std::min(min_area, cast<long double>(sl::area(sl::boundingBox(rotated))));
|
|
};
|
|
|
|
auto it = sl::cbegin(p), itx = std::next(it);
|
|
while (itx != sl::cend(p)) { update_min(*it, *itx); ++it; ++itx; }
|
|
update_min(*std::prev(sl::cend(p)), *sl::cbegin(p));
|
|
|
|
return min_area;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Minimum-area bounding box via rotating calipers", "[Geometry]") {
|
|
const long double tolerance = 500e6l;
|
|
|
|
for (const PathImpl &part : PRINTER_PART_POLYGONS) {
|
|
auto area = cast<long double>(minAreaBoundingBox<PathImpl, Unit, Ratio>(part).area());
|
|
REQUIRE(std::abs(ref_min_area_box(PolygonImpl(part)) - area) < tolerance);
|
|
}
|
|
|
|
for (PathImpl part : STEGOSAUR_POLYGONS) {
|
|
std::reverse(part.begin(), part.end());
|
|
PolygonImpl poly(removeCollinearPoints<PathImpl, PointImpl, Unit>(part, 1000000));
|
|
auto area = cast<long double>(minAreaBoundingBox<PolygonImpl, Unit, Ratio>(poly).area());
|
|
REQUIRE(std::abs(ref_min_area_box(poly) - area) < tolerance);
|
|
}
|
|
}
|