PRUSA 2.7.0

This commit is contained in:
sunsets
2023-12-27 18:02:35 +08:00
parent b33112327f
commit 0a3c63dcb1
488 changed files with 92371 additions and 29443 deletions

View File

@@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests
test_aabbindirect.cpp
test_kdtreeindirect.cpp
test_arachne.cpp
test_arc_welder.cpp
test_clipper_offset.cpp
test_clipper_utils.cpp
test_color.cpp
@@ -38,8 +39,10 @@ add_executable(${_TEST_NAME}_tests
test_astar.cpp
test_anyptr.cpp
test_jump_point_search.cpp
../data/qidiparts.cpp
../data/qidiparts.hpp
test_support_spots_generator.cpp
../data/prusaparts.cpp
../data/prusaparts.hpp
test_static_map.cpp
)
if (TARGET OpenVDB::openvdb)

View File

@@ -743,4 +743,23 @@ TEST_CASE("Arachne - #10034 - Degenerated Voronoi diagram - That wasn't fixed by
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-10034-rotation-not-works.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
}
TEST_CASE("Arachne - SPE-1837 - No perimeters generated", "[ArachneNoPerimetersGeneratedSPE1837]") {
Polygon poly_0 = {
Point( 10000000, 10000000),
Point(-10000000, 10000000),
Point(-10000000, -10000000),
Point( 10000000, -10000000)
};
Polygons polygons = {poly_0};
coord_t ext_perimeter_spacing = 300000;
coord_t perimeter_spacing = 700000;
coord_t inset_count = 1;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
REQUIRE(!perimeters.empty());
}

View File

@@ -0,0 +1,511 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <random>
#include <libslic3r/Geometry/ArcWelder.hpp>
#include <libslic3r/Geometry/Circle.hpp>
#include <libslic3r/SVG.hpp>
#include <libslic3r/libslic3r.h>
using namespace Slic3r;
TEST_CASE("arc basics", "[ArcWelder]") {
using namespace Slic3r::Geometry;
WHEN("arc from { 2000.f, 1000.f } to { 1000.f, 2000.f }") {
Vec2f p1{ 2000.f, 1000.f };
Vec2f p2{ 1000.f, 2000.f };
float r{ 1000.f };
const double s = 1000.f / sqrt(2.);
THEN("90 degrees arc, CCW") {
Vec2f c = ArcWelder::arc_center(p1, p2, r, true);
Vec2f m = ArcWelder::arc_middle_point(p1, p2, r, true);
REQUIRE(is_approx(c, Vec2f{ 1000.f, 1000.f }));
REQUIRE(ArcWelder::arc_angle(p1, p2, r) == Approx(0.5 * M_PI));
REQUIRE(ArcWelder::arc_length(p1, p2, r) == Approx(r * 0.5 * M_PI).epsilon(0.001));
REQUIRE(is_approx(m, Vec2f{ 1000.f + s, 1000.f + s }, 0.001f));
}
THEN("90 degrees arc, CW") {
Vec2f c = ArcWelder::arc_center(p1, p2, r, false);
Vec2f m = ArcWelder::arc_middle_point(p1, p2, r, false);
REQUIRE(is_approx(c, Vec2f{ 2000.f, 2000.f }));
REQUIRE(is_approx(m, Vec2f{ 2000.f - s, 2000.f - s }, 0.001f));
}
THEN("270 degrees arc, CCW") {
Vec2f c = ArcWelder::arc_center(p1, p2, - r, true);
Vec2f m = ArcWelder::arc_middle_point(p1, p2, - r, true);
REQUIRE(is_approx(c, Vec2f{ 2000.f, 2000.f }));
REQUIRE(ArcWelder::arc_angle(p1, p2, - r) == Approx(1.5 * M_PI));
REQUIRE(ArcWelder::arc_length(p1, p2, - r) == Approx(r * 1.5 * M_PI).epsilon(0.001));
REQUIRE(is_approx(m, Vec2f{ 2000.f + s, 2000.f + s }, 0.001f));
}
THEN("270 degrees arc, CW") {
Vec2f c = ArcWelder::arc_center(p1, p2, - r, false);
Vec2f m = ArcWelder::arc_middle_point(p1, p2, - r, false);
REQUIRE(is_approx(c, Vec2f{ 1000.f, 1000.f }));
REQUIRE(is_approx(m, Vec2f{ 1000.f - s, 1000.f - s }, 0.001f));
}
}
WHEN("arc from { 1707.11f, 1707.11f } to { 1000.f, 2000.f }") {
Vec2f p1{ 1707.11f, 1707.11f };
Vec2f p2{ 1000.f, 2000.f };
float r{ 1000.f };
Vec2f center1 = Vec2f{ 1000.f, 1000.f };
// Center on the other side of the CCW arch.
Vec2f center2 = center1 + 2. * (0.5 * (p1 + p2) - center1);
THEN("45 degrees arc, CCW") {
Vec2f c = ArcWelder::arc_center(p1, p2, r, true);
REQUIRE(is_approx(c, center1, 1.f));
REQUIRE(ArcWelder::arc_angle(p1, p2, r) == Approx(0.25 * M_PI));
REQUIRE(ArcWelder::arc_length(p1, p2, r) == Approx(r * 0.25 * M_PI).epsilon(0.001));
}
THEN("45 degrees arc, CW") {
Vec2f c = ArcWelder::arc_center(p1, p2, r, false);
REQUIRE(is_approx(c, center2, 1.f));
}
THEN("315 degrees arc, CCW") {
Vec2f c = ArcWelder::arc_center(p1, p2, - r, true);
REQUIRE(is_approx(c, center2, 1.f));
REQUIRE(ArcWelder::arc_angle(p1, p2, - r) == Approx((2. - 0.25) * M_PI));
REQUIRE(ArcWelder::arc_length(p1, p2, - r) == Approx(r * (2. - 0.25) * M_PI).epsilon(0.001));
}
THEN("315 degrees arc, CW") {
Vec2f c = ArcWelder::arc_center(p1, p2, - r, false);
REQUIRE(is_approx(c, center1, 1.f));
}
}
WHEN("arc from { 1866.f, 1500.f } to { 1000.f, 2000.f }") {
Vec2f p1{ 1866.f, 1500.f };
Vec2f p2{ 1000.f, 2000.f };
float r{ 1000.f };
Vec2f center1 = Vec2f{ 1000.f, 1000.f };
// Center on the other side of the CCW arch.
Vec2f center2 = center1 + 2. * (0.5 * (p1 + p2) - center1);
THEN("60 degrees arc, CCW") {
Vec2f c = ArcWelder::arc_center(p1, p2, r, true);
REQUIRE(is_approx(c, center1, 1.f));
REQUIRE(is_approx(ArcWelder::arc_angle(p1, p2, r), float(M_PI / 3.), 0.001f));
REQUIRE(ArcWelder::arc_length(p1, p2, r) == Approx(r * M_PI / 3.).epsilon(0.001));
}
THEN("60 degrees arc, CW") {
Vec2f c = ArcWelder::arc_center(p1, p2, r, false);
REQUIRE(is_approx(c, center2, 1.f));
}
THEN("300 degrees arc, CCW") {
Vec2f c = ArcWelder::arc_center(p1, p2, - r, true);
REQUIRE(is_approx(c, center2, 1.f));
REQUIRE(is_approx(ArcWelder::arc_angle(p1, p2, - r), float((2. - 1./3.) * M_PI), 0.001f));
REQUIRE(ArcWelder::arc_length(p1, p2, - r) == Approx(r * (2. - 1. / 3.) * M_PI).epsilon(0.001));
}
THEN("300 degrees arc, CW") {
Vec2f c = ArcWelder::arc_center(p1, p2, - r, false);
REQUIRE(is_approx(c, center1, 1.f));
}
}
}
TEST_CASE("arc discretization", "[ArcWelder]") {
using namespace Slic3r::Geometry;
WHEN("arc from { 2, 1 } to { 1, 2 }") {
const Point p1 = Point::new_scale(2., 1.);
const Point p2 = Point::new_scale(1., 2.);
const Point center = Point::new_scale(1., 1.);
const float radius = scaled<float>(1.);
const float resolution = scaled<float>(0.002);
auto test = [center, resolution, radius](const Point &p1, const Point &p2, const float r, const bool ccw) {
Vec2f c = ArcWelder::arc_center(p1.cast<float>(), p2.cast<float>(), r, ccw);
REQUIRE((p1.cast<float>() - c).norm() == Approx(radius));
REQUIRE((c - center.cast<float>()).norm() == Approx(0.));
Points pts = ArcWelder::arc_discretize(p1, p2, r, ccw, resolution);
REQUIRE(pts.size() >= 2);
REQUIRE(pts.front() == p1);
REQUIRE(pts.back() == p2);
for (const Point &p : pts)
REQUIRE(std::abs((p.cast<double>() - c.cast<double>()).norm() - double(radius)) < double(resolution + SCALED_EPSILON));
};
THEN("90 degrees arc, CCW") {
test(p1, p2, radius, true);
}
THEN("270 degrees arc, CCW") {
test(p2, p1, - radius, true);
}
THEN("90 degrees arc, CW") {
test(p2, p1, radius, false);
}
THEN("270 degrees arc, CW") {
test(p1, p2, - radius, false);
}
}
}
void test_arc_fit_variance(const Point &p1, const Point &p2, const float r, const float r_fit, const bool ccw, const Points::const_iterator begin, const Points::const_iterator end)
{
using namespace Slic3r::Geometry;
double variance = ArcWelder::arc_fit_variance(p1, p2, r, ccw, begin, end);
double variance_fit = ArcWelder::arc_fit_variance(p1, p2, r_fit, ccw, begin, end);
REQUIRE(variance_fit < variance);
};
void test_arc_fit_max_deviation(const Point &p1, const Point &p2, const float r, const float r_fit, const bool ccw, const Points::const_iterator begin, const Points::const_iterator end)
{
using namespace Slic3r::Geometry;
double max_deviation = ArcWelder::arc_fit_max_deviation(p1, p2, r, ccw, begin, end);
double max_deviation_fit = ArcWelder::arc_fit_max_deviation(p1, p2, r_fit, ccw, begin, end);
REQUIRE(std::abs(max_deviation_fit) < std::abs(max_deviation));
};
void test_arc_fit(const Point &p1, const Point &p2, const float r, const float r_fit, const bool ccw, const Points::const_iterator begin, const Points::const_iterator end)
{
test_arc_fit_variance(p1, p2, r, r_fit, ccw, begin, end);
test_arc_fit_max_deviation(p1, p2, r, r_fit, ccw, begin, end);
};
TEST_CASE("arc fitting", "[ArcWelder]") {
using namespace Slic3r::Geometry;
WHEN("arc from { 2, 1 } to { 1, 2 }") {
const Point p1 = Point::new_scale(2., 1.);
const Point p2 = Point::new_scale(1., 2.);
const Point center = Point::new_scale(1., 1.);
const float radius = scaled<float>(1.);
const float resolution = scaled<float>(0.002);
auto test = [center, resolution](const Point &p1, const Point &p2, const float r, const bool ccw) {
Points pts = ArcWelder::arc_discretize(p1, p2, r, ccw, resolution);
ArcWelder::Path path = ArcWelder::fit_path(pts, resolution + SCALED_EPSILON, ArcWelder::default_scaled_resolution);
REQUIRE(path.size() == 2);
REQUIRE(path.front().point == p1);
REQUIRE(path.front().radius == 0.f);
REQUIRE(path.back().point == p2);
REQUIRE(path.back().ccw() == ccw);
test_arc_fit(p1, p2, r, path.back().radius, true, pts.begin(), pts.end());
};
THEN("90 degrees arc, CCW is fitted") {
test(p1, p2, radius, true);
}
THEN("270 degrees arc, CCW is fitted") {
test(p2, p1, - radius, true);
}
THEN("90 degrees arc, CW is fitted") {
test(p2, p1, radius, false);
}
THEN("270 degrees arc, CW is fitted") {
test(p1, p2, - radius, false);
}
}
WHEN("arc from { 2, 1 } to { 1, 2 }, another arc from { 2, 1 } to { 0, 2 }, tangentially connected") {
const Point p1 = Point::new_scale(2., 1.);
const Point p2 = Point::new_scale(1., 2.);
const Point p3 = Point::new_scale(0., 3.);
const Point center1 = Point::new_scale(1., 1.);
const Point center2 = Point::new_scale(1., 3.);
const float radius = scaled<float>(1.);
const float resolution = scaled<float>(0.002);
auto test = [center1, center2, resolution](const Point &p1, const Point &p2, const Point &p3, const float r, const bool ccw) {
Points pts = ArcWelder::arc_discretize(p1, p2, r, ccw, resolution);
size_t num_pts1 = pts.size();
{
Points pts2 = ArcWelder::arc_discretize(p2, p3, - r, ! ccw, resolution);
REQUIRE(pts.back() == pts2.front());
pts.insert(pts.end(), std::next(pts2.begin()), pts2.end());
}
ArcWelder::Path path = ArcWelder::fit_path(pts, resolution + SCALED_EPSILON, ArcWelder::default_scaled_resolution);
REQUIRE(path.size() == 3);
REQUIRE(path.front().point == p1);
REQUIRE(path.front().radius == 0.f);
REQUIRE(path[1].point == p2);
REQUIRE(path[1].ccw() == ccw);
REQUIRE(path.back().point == p3);
REQUIRE(path.back().ccw() == ! ccw);
test_arc_fit(p1, p2, r, path[1].radius, ccw, pts.begin(), pts.begin() + num_pts1);
test_arc_fit(p2, p3, - r, path.back().radius, ! ccw, pts.begin() + num_pts1 - 1, pts.end());
};
THEN("90 degrees arches, CCW are fitted") {
test(p1, p2, p3, radius, true);
}
THEN("270 degrees arc, CCW is fitted") {
test(p3, p2, p1, -radius, true);
}
THEN("90 degrees arc, CW is fitted") {
test(p3, p2, p1, radius, false);
}
THEN("270 degrees arc, CW is fitted") {
test(p1, p2, p3, -radius, false);
}
}
}
TEST_CASE("least squares arc fitting, interpolating end points", "[ArcWelder]") {
using namespace Slic3r::Geometry;
// Generate bunch of random arches.
const coord_t max_coordinate = scaled<coord_t>(sqrt(250. - 1.));
static constexpr const double min_radius = scaled<double>(0.01);
static constexpr const double max_radius = scaled<double>(250.);
// static constexpr const double deviation = scaled<double>(0.5);
static constexpr const double deviation = scaled<double>(0.1);
// Seeded with a fixed seed, to be repeatable.
std::mt19937 rng(867092346);
std::uniform_int_distribution<int32_t> coord_sampler(0, int32_t(max_coordinate));
std::uniform_real_distribution<double> angle_sampler(0.001, 2. * M_PI - 0.001);
std::uniform_real_distribution<double> radius_sampler(min_radius, max_radius);
std::uniform_int_distribution<int> num_samples_sampler(1, 100);
auto test_arc_fitting = [&rng, &coord_sampler, &num_samples_sampler, &angle_sampler, &radius_sampler]() {
auto sample_point = [&rng, &coord_sampler]() {
return Vec2d(coord_sampler(rng), coord_sampler(rng));
};
// Start and end point of the arc:
Vec2d center_pos = sample_point();
double angle0 = angle_sampler(rng);
double angle = angle_sampler(rng);
double radius = radius_sampler(rng);
Vec2d v1 = Eigen::Rotation2D(angle0) * Vec2d(1., 0.);
Vec2d v2 = Eigen::Rotation2D(angle0 + angle) * Vec2d(1., 0.);
Vec2d start_pos = center_pos + radius * v1;
Vec2d end_pos = center_pos + radius * v2;
std::vector<Vec2d> samples;
size_t num_samples = num_samples_sampler(rng);
for (size_t i = 0; i < num_samples; ++ i) {
double sample_r = sqrt(std::uniform_real_distribution<double>(sqr(std::max(0., radius - deviation)), sqr(radius + deviation))(rng));
double sample_a = std::uniform_real_distribution<double>(0., angle)(rng);
Vec2d pt = center_pos + Eigen::Rotation2D(angle0 + sample_a) * Vec2d(sample_r, 0.);
samples.emplace_back(pt);
assert((pt - center_pos).norm() > radius - deviation - SCALED_EPSILON);
assert((pt - center_pos).norm() < radius + deviation + SCALED_EPSILON);
}
// Vec2d new_center = ArcWelder::arc_fit_center_algebraic_ls(start_pos, end_pos, center_pos, samples.begin(), samples.end());
THEN("Center is fitted correctly") {
std::optional<Vec2d> new_center_opt = ArcWelder::arc_fit_center_gauss_newton_ls(start_pos, end_pos, center_pos, samples.begin(), samples.end(), 10);
REQUIRE(new_center_opt);
if (new_center_opt) {
Vec2d new_center = *new_center_opt;
double new_radius = (new_center - start_pos).norm();
double total_deviation = 0;
double new_total_deviation = 0;
for (const Vec2d &s : samples) {
total_deviation += sqr((s - center_pos).norm() - radius);
new_total_deviation += sqr((s - new_center).norm() - radius);
}
total_deviation /= double(num_samples);
new_total_deviation /= double(num_samples);
REQUIRE(new_total_deviation <= total_deviation);
#if 0
double dist = (center_pos - new_center).norm();
printf("Radius: %lf, Angle: %lf deg, Samples: %d, Dist: %lf\n", unscaled<double>(radius), 180. * angle / M_PI, int(num_samples), unscaled<double>(dist));
// REQUIRE(is_approx(center_pos, new_center, deviation));
if (dist > scaled<double>(1.)) {
static int irun = 0;
char path[2048];
sprintf(path, "d:\\temp\\debug\\circle-fit-%d.svg", irun++);
Eigen::AlignedBox<double, 2> bbox(start_pos, end_pos);
for (const Vec2d& sample : samples)
bbox.extend(sample);
bbox.extend(center_pos);
Slic3r::SVG svg(path, BoundingBox(bbox.min().cast<coord_t>(), bbox.max().cast<coord_t>()).inflated(bbox.sizes().maxCoeff() * 0.05));
Points arc_src;
for (size_t i = 0; i <= 1000; ++i)
arc_src.emplace_back((center_pos + Eigen::Rotation2D(angle0 + double(i) * angle / 1000.) * Vec2d(radius, 0.)).cast<coord_t>());
svg.draw(Polyline(arc_src));
Points arc_new;
double new_arc_angle = ArcWelder::arc_angle(start_pos, end_pos, (new_center - start_pos).norm());
for (size_t i = 0; i <= 1000; ++i)
arc_new.emplace_back((new_center + Eigen::Rotation2D(double(i) * new_arc_angle / 1000.) * (start_pos - new_center)).cast<coord_t>());
svg.draw(Polyline(arc_new), "magenta");
svg.draw(Point(start_pos.cast<coord_t>()), "blue");
svg.draw(Point(end_pos.cast<coord_t>()), "blue");
svg.draw(Point(center_pos.cast<coord_t>()), "blue");
for (const Vec2d &sample : samples)
svg.draw(Point(sample.cast<coord_t>()), "red");
svg.draw(Point(new_center.cast<coord_t>()), "magenta");
}
if (!is_approx(center_pos, new_center, scaled<double>(5.))) {
printf("Failed\n");
}
#endif
} else {
printf("Fitting failed\n");
}
}
};
WHEN("Generating a random arc and randomized arc samples") {
for (size_t i = 0; i < 1000; ++ i)
test_arc_fitting();
}
}
TEST_CASE("arc wedge test", "[ArcWelder]") {
using namespace Slic3r::Geometry;
WHEN("test point inside wedge, arc from { 2, 1 } to { 1, 2 }") {
const int64_t s = 1000000;
const Vec2i64 p1{ 2 * s, s };
const Vec2i64 p2{ s, 2 * s };
const Vec2i64 center{ s, s };
const int64_t radius{ s };
auto test = [center](
// Arc data
const Vec2i64 &p1, const Vec2i64 &p2, const int64_t r, const bool ccw,
// Test data
const Vec2i64 &ptest, const bool ptest_inside) {
const Vec2d c = ArcWelder::arc_center(p1.cast<double>(), p2.cast<double>(), double(r), ccw);
REQUIRE(is_approx(c, center.cast<double>()));
REQUIRE(ArcWelder::inside_arc_wedge(p1, p2, center, r > 0, ccw, ptest) == ptest_inside);
REQUIRE(ArcWelder::inside_arc_wedge(p1.cast<double>(), p2.cast<double>(), double(r), ccw, ptest.cast<double>()) == ptest_inside);
};
auto test_quadrants = [center, test](
// Arc data
const Vec2i64 &p1, const Vec2i64 &p2, const int64_t r, const bool ccw,
// Test data
const Vec2i64 &ptest1, const bool ptest_inside1,
const Vec2i64 &ptest2, const bool ptest_inside2,
const Vec2i64 &ptest3, const bool ptest_inside3,
const Vec2i64 &ptest4, const bool ptest_inside4) {
test(p1, p2, r, ccw, ptest1 + center, ptest_inside1);
test(p1, p2, r, ccw, ptest2 + center, ptest_inside2);
test(p1, p2, r, ccw, ptest3 + center, ptest_inside3);
test(p1, p2, r, ccw, ptest4 + center, ptest_inside4);
};
THEN("90 degrees arc, CCW") {
test_quadrants(p1, p2, radius, true,
Vec2i64{ s, s }, true,
Vec2i64{ s, - s }, false,
Vec2i64{ - s, s }, false,
Vec2i64{ - s, - s }, false);
}
THEN("270 degrees arc, CCW") {
test_quadrants(p2, p1, -radius, true,
Vec2i64{ s, s }, false,
Vec2i64{ s, - s }, true,
Vec2i64{ - s, s }, true,
Vec2i64{ - s, - s }, true);
}
THEN("90 degrees arc, CW") {
test_quadrants(p2, p1, radius, false,
Vec2i64{ s, s }, true,
Vec2i64{ s, - s }, false,
Vec2i64{ - s, s }, false,
Vec2i64{ - s, - s }, false);
}
THEN("270 degrees arc, CW") {
test_quadrants(p1, p2, -radius, false,
Vec2i64{ s, s }, false,
Vec2i64{ s, - s }, true,
Vec2i64{ - s, s }, true,
Vec2i64{ - s, - s }, true);
}
}
}
#if 0
// For quantization
//#include <libslic3r/GCode/GCodeWriter.hpp>
TEST_CASE("arc quantization", "[ArcWelder]") {
using namespace Slic3r::Geometry;
WHEN("generating a bunch of random arches") {
static constexpr const size_t len = 100000;
static constexpr const coord_t max_coordinate = scaled<coord_t>(250.);
static constexpr const float max_radius = scaled<float>(250.);
ArcWelder::Segments path;
path.reserve(len + 1);
// Seeded with a fixed seed, to be repeatable.
std::mt19937 rng(987432690);
// Generate bunch of random arches.
std::uniform_int_distribution<int32_t> coord_sampler(0, int32_t(max_coordinate));
std::uniform_real_distribution<float> radius_sampler(- max_radius, max_radius);
std::uniform_int_distribution<int> orientation_sampler(0, 1);
path.push_back({ Point{coord_sampler(rng), coord_sampler(rng)}, 0, ArcWelder::Orientation::CCW });
for (size_t i = 0; i < len; ++ i) {
ArcWelder::Segment seg { Point{coord_sampler(rng), coord_sampler(rng)}, radius_sampler(rng), orientation_sampler(rng) ? ArcWelder::Orientation::CCW : ArcWelder::Orientation::CW };
while ((seg.point.cast<double>() - path.back().point.cast<double>()).norm() > 2. * std::abs(seg.radius))
seg = { Point{coord_sampler(rng), coord_sampler(rng)}, radius_sampler(rng), orientation_sampler(rng) ? ArcWelder::Orientation::CCW : ArcWelder::Orientation::CW };
path.push_back(seg);
}
// Run the test, quantize coordinates and radius, find the maximum error of quantization comparing the two arches.
struct ArcEval {
double error;
double radius;
double angle;
};
std::vector<ArcEval> center_errors_R;
center_errors_R.reserve(len);
std::vector<double> center_errors_R_exact;
center_errors_R_exact.reserve(len);
std::vector<double> center_errors_IJ;
center_errors_IJ.reserve(len);
for (size_t i = 0; i < len; ++ i) {
// Source arc:
const Vec2d start_point = unscaled<double>(path[i].point);
const Vec2d end_point = unscaled<double>(path[i + 1].point);
const double radius = unscaled<double>(path[i + 1].radius);
const bool ccw = path[i + 1].ccw();
const Vec2d center = ArcWelder::arc_center(start_point, end_point, radius, ccw);
{
const double d1 = (start_point - center).norm();
const double d2 = (end_point - center).norm();
const double dx = (end_point - start_point).norm();
assert(std::abs(d1 - std::abs(radius)) < EPSILON);
assert(std::abs(d2 - std::abs(radius)) < EPSILON);
}
// Quantized arc:
const Vec2d start_point_quantized { GCodeFormatter::quantize_xyzf(start_point.x()), GCodeFormatter::quantize_xyzf(start_point.y()) };
const Vec2d end_point_quantized { GCodeFormatter::quantize_xyzf(end_point .x()), GCodeFormatter::quantize_xyzf(end_point .y()) };
const double radius_quantized { GCodeFormatter::quantize_xyzf(radius) };
const Vec2d center_quantized { GCodeFormatter::quantize_xyzf(center .x()), GCodeFormatter::quantize_xyzf(center .y()) };
// Evaulate maximum error for the quantized arc given by the end points and radius.
const Vec2d center_from_quantized = ArcWelder::arc_center(start_point_quantized, end_point_quantized, radius, ccw);
const Vec2d center_reconstructed = ArcWelder::arc_center(start_point_quantized, end_point_quantized, radius_quantized, ccw);
#if 0
center_errors_R.push_back({
std::abs((center_reconstructed - center).norm()),
radius,
ArcWelder::arc_angle(start_point, end_point, radius) * 180. / M_PI
});
if (center_errors_R.back().error > 0.15)
printf("Fuj\n");
#endif
center_errors_R_exact.emplace_back(std::abs((center_from_quantized - center).norm()));
if (center_errors_R_exact.back() > 0.15)
printf("Fuj\n");
center_errors_IJ.emplace_back(std::abs((center_quantized - center).norm()));
if (center_errors_IJ.back() > 0.15)
printf("Fuj\n");
// Adjust center of the arc to the quantized end points.
Vec2d third_point = ArcWelder::arc_middle_point(start_point, end_point, radius, ccw);
double third_point_radius = (third_point - center).norm();
assert(std::abs(third_point_radius - std::abs(radius)) < EPSILON);
std::optional<Vec2d> center_adjusted = try_circle_center(start_point_quantized, end_point_quantized, third_point, EPSILON);
//assert(center_adjusted);
if (center_adjusted) {
const double radius_adjusted = (center_adjusted.value() - start_point_quantized).norm() * (radius > 0 ? 1. : -1.);
const double radius_adjusted_quantized = GCodeFormatter::quantize_xyzf(radius_adjusted);
// Evaulate maximum error for the quantized arc given by the end points and radius.
const Vec2f center_reconstructed = ArcWelder::arc_center(start_point_quantized.cast<float>(), end_point_quantized.cast<float>(), float(radius_adjusted_quantized), ccw);
double rtest = std::abs(radius_adjusted_quantized);
double d1 = std::abs((center_reconstructed - start_point.cast<float>()).norm() - rtest);
double d2 = std::abs((center_reconstructed - end_point.cast<float>()).norm() - rtest);
double d3 = std::abs((center_reconstructed - third_point.cast<float>()).norm() - rtest);
double d = std::max(d1, std::max(d2, d3));
center_errors_R.push_back({
d,
radius,
ArcWelder::arc_angle(start_point, end_point, radius) * 180. / M_PI
});
} else {
printf("Adjusted circle is collinear.\n");
}
}
std::sort(center_errors_R.begin(), center_errors_R.end(), [](auto l, auto r) { return l.error > r.error; });
std::sort(center_errors_R_exact.begin(), center_errors_R_exact.end(), [](auto l, auto r) { return l > r; });
std::sort(center_errors_IJ.begin(), center_errors_IJ.end(), [](auto l, auto r) { return l > r; });
printf("Maximum error R: %lf\n", center_errors_R.back().error);
printf("Maximum error R exact: %lf\n", center_errors_R_exact.back());
printf("Maximum error IJ: %lf\n", center_errors_IJ.back());
}
}
#endif

View File

@@ -23,7 +23,8 @@ TEST_CASE("Cut character from surface", "[]")
Transform3d tr = Transform3d::Identity();
tr.translate(Vec3d(0., 0., -z_depth));
tr.scale(Emboss::SHAPE_SCALE);
double text_shape_scale = 0.001; // Emboss.cpp --> SHAPE_SCALE
tr.scale(text_shape_scale);
Emboss::OrthoProject cut_projection(tr, Vec3d(0., 0., z_depth));
auto object = its_make_cube(782 - 49 + 50, 724 + 10 + 50, 5);
@@ -158,7 +159,7 @@ TEST_CASE("CutSurface in 3mf", "[Emboss]")
FontProp fp = tc.style.prop;
ExPolygons shapes = Emboss::text2shapes(ff, tc.text.c_str(), fp);
double shape_scale = Emboss::get_shape_scale(fp, *ff.font_file);
double shape_scale = Emboss::get_text_shape_scale(fp, *ff.font_file);
Emboss::OrthoProject projection = create_projection_for_cut(
cut_projection_tr, shape_scale, get_extents(shapes), z_range);

View File

@@ -195,15 +195,17 @@ TEST_CASE("Visualize glyph from font", "[Emboss]")
#endif // VISUALIZE
#include "test_utils.hpp"
#include "nanosvg/nanosvg.h" // load SVG file
#include "libslic3r/NSVGUtils.hpp"
#include <nanosvg/nanosvg.h> // load SVG file
#include <libslic3r/NSVGUtils.hpp>
#include <libslic3r/IntersectionPoints.hpp>
ExPolygons heal_and_check(const Polygons &polygons)
{
Pointfs intersections_prev = intersection_points(polygons);
IntersectionsLines intersections_prev = get_intersections(polygons);
Points polygons_points = to_points(polygons);
Points duplicits_prev = collect_duplicates(polygons_points);
ExPolygons shape = Emboss::heal_shape(polygons);
auto [shape, success] = Emboss::heal_polygons(polygons);
CHECK(success);
// Is default shape for unhealabled shape?
bool is_default_shape =
@@ -213,7 +215,7 @@ ExPolygons heal_and_check(const Polygons &polygons)
shape.front().holes.front().points.size() == 4 ;
CHECK(!is_default_shape);
Pointfs intersections = intersection_points(shape);
IntersectionsLines intersections = get_intersections(shape);
Points shape_points = to_points(shape);
Points duplicits = collect_duplicates(shape_points);
//{
@@ -244,7 +246,9 @@ void scale(Polygons &polygons, double multiplicator) {
Polygons load_polygons(const std::string &svg_file) {
std::string file_path = TEST_DATA_DIR PATH_SEPARATOR + svg_file;
NSVGimage *image = nsvgParseFromFile(file_path.c_str(), "px", 96.0f);
Polygons polygons = NSVGUtils::to_polygons(image);
NSVGLineParams param{1000};
param.scale = 10.;
Polygons polygons = to_polygons(*image, param);
nsvgDelete(image);
return polygons;
}
@@ -258,7 +262,8 @@ TEST_CASE("Heal of 'i' in ALIENATO.TTF", "[Emboss]")
auto a = heal_and_check(polygons);
Polygons scaled_shape = polygons; // copy
scale(scaled_shape, 1 / Emboss::SHAPE_SCALE);
double text_shape_scale = 0.001; // Emboss.cpp --> SHAPE_SCALE
scale(scaled_shape, 1 / text_shape_scale);
auto b = heal_and_check(scaled_shape);
// different scale
@@ -282,12 +287,37 @@ TEST_CASE("Heal of 'm' in Allura_Script.ttf", "[Emboss]")
auto a = heal_and_check(polygons);
}
#include "libslic3r/NSVGUtils.hpp"
TEST_CASE("Heal of svg contour overlap", "[Emboss]") {
std::string svg_file = "contour_neighbor.svg";
auto image = nsvgParseFromFile(TEST_DATA_DIR PATH_SEPARATOR + svg_file, "mm");
NSVGLineParams param(1e10);
ExPolygonsWithIds shapes = create_shape_with_ids(*image, param);
Polygons polygons;
for (ExPolygonsWithId &shape : shapes)
polygons.push_back(shape.expoly.front().contour);
auto a = heal_and_check(polygons);
}
// Input contour is extracted from case above "contour_neighbor.svg" with trouble shooted scale
TEST_CASE("Heal of overlaping contour", "[Emboss]"){
// Extracted from svg:
Points contour{{2228926, 1543620}, {745002, 2065101}, {745002, 2065094}, {744990, 2065094}, {684487, 1466338},
{510999, 908378}, {236555, 403250}, {-126813, -37014}, {-567074, -400382}, {-1072201, -674822},
{-567074, -400378}, {-126813, -37010}, {236555, 403250}, {510999, 908382}, {684487, 1466346},
{744990, 2065105}, {-2219648, 2073234}, {-2228926, -908814}, {-1646879, -2073235}};
ExPolygons shapes = {ExPolygon{contour}};
CHECK(Emboss::heal_expolygons(shapes));
}
TEST_CASE("Heal of points close to line", "[Emboss]")
{
std::string file_name = "points_close_to_line.svg";
std::string file_path = TEST_DATA_DIR PATH_SEPARATOR + file_name;
NSVGimage *image = nsvgParseFromFile(file_path.c_str(), "px", 96.0f);
Polygons polygons = NSVGUtils::to_polygons(image);
NSVGLineParams param{1000};
param.scale = 1.;
Polygons polygons = to_polygons(*image, param);
nsvgDelete(image);
REQUIRE(polygons.size() == 1);
Polygon polygon = polygons.front();
@@ -309,16 +339,19 @@ The other kids at school nicknamed him Ix,\n\
which in the language of Betelgeuse Five translates as\t\n\
\"boy who is not able satisfactorily to explain what a Hrung is,\n\
nor why it should choose to collapse on Betelgeuse Seven\".";
float line_height = 10.f, depth = 2.f;
float line_height = 10.f;
auto font = Emboss::create_font_file(font_path.c_str());
REQUIRE(font != nullptr);
Emboss::FontFileWithCache ffwc(std::move(font));
FontProp fp{line_height, depth};
ExPolygons shapes = Emboss::text2shapes(ffwc, text.c_str(), fp);
FontProp fp{line_height};
auto was_canceled = []() { return false; };
ExPolygons shapes = Emboss::text2shapes(ffwc, text.c_str(), fp, was_canceled);
REQUIRE(!shapes.empty());
float depth = 2.f;
Emboss::ProjectZ projection(depth);
indexed_triangle_set its = Emboss::polygons2model(shapes, projection);
CHECK(!its.indices.empty());
@@ -468,7 +501,8 @@ TEST_CASE("Cut surface", "[]")
Transform3d tr = Transform3d::Identity();
tr.translate(Vec3d(0., 0., -z_depth));
tr.scale(Emboss::SHAPE_SCALE);
double text_shape_scale = 0.001; // Emboss.cpp --> SHAPE_SCALE
tr.scale(text_shape_scale);
Emboss::OrthoProject cut_projection(tr, Vec3d(0., 0., z_depth));
auto object = its_make_cube(782 - 49 + 50, 724 + 10 + 50, 5);
@@ -491,7 +525,7 @@ TEST_CASE("Cut surface", "[]")
#include <sstream>
#include <cereal/cereal.hpp>
#include <cereal/archives/binary.hpp>
TEST_CASE("UndoRedo serialization", "[Emboss]")
TEST_CASE("UndoRedo TextConfiguration serialization", "[Emboss]")
{
TextConfiguration tc;
tc.text = "Dovede-li se člověk zasmát sám sobě, nevyjde ze smíchu po celý život.";
@@ -500,11 +534,12 @@ TEST_CASE("UndoRedo serialization", "[Emboss]")
es.path = "Simply the best";
es.type = EmbossStyle::Type::file_path;
FontProp &fp = es.prop;
fp.angle = 100.;
fp.distance = 10.;
fp.char_gap = 1;
fp.use_surface = true;
tc.fix_3mf_tr = Transform3d::Identity();
fp.char_gap = 3;
fp.line_gap = 7;
fp.boldness = 2.3f;
fp.skew = 4.5f;
fp.collection_number = 13;
fp.size_in_mm= 6.7f;
std::stringstream ss; // any stream can be used
{
@@ -520,7 +555,45 @@ TEST_CASE("UndoRedo serialization", "[Emboss]")
}
CHECK(tc.style == tc_loaded.style);
CHECK(tc.text == tc_loaded.text);
CHECK(tc.fix_3mf_tr.has_value() == tc_loaded.fix_3mf_tr.has_value());
}
#include "libslic3r/EmbossShape.hpp"
TEST_CASE("UndoRedo EmbossShape serialization", "[Emboss]")
{
EmbossShape emboss;
emboss.shapes_with_ids = {{0, {{{0, 0}, {10, 0}, {10, 10}, {0, 10}}, {{5, 5}, {6, 5}, {6, 6}, {5, 6}}}}};
emboss.scale = 2.;
emboss.projection.depth = 5.;
emboss.projection.use_surface = true;
emboss.fix_3mf_tr = Transform3d::Identity();
emboss.svg_file = EmbossShape::SvgFile{};
emboss.svg_file->path = "Everything starts somewhere, though many physicists disagree.\
But people have always been dimly aware of the problem with the start of things.\
They wonder how the snowplough driver gets to work,\
or how the makers of dictionaries look up the spelling of words.";
emboss.svg_file->path_in_3mf = "Všechno někde začíná, i když mnoho fyziků nesouhlasí.\
Ale lidé si vždy jen matně uvědomovali problém se začátkem věcí.\
Zajímalo je, jak se řidič sněžného pluhu dostane do práce\
nebo jak tvůrci slovníků vyhledávají pravopis slov.";
emboss.svg_file->file_data = std::make_unique<std::string>("cite: Terry Pratchett");
std::stringstream ss; // any stream can be used
{
cereal::BinaryOutputArchive oarchive(ss); // Create an output archive
oarchive(emboss);
} // archive goes out of scope, ensuring all contents are flushed
EmbossShape emboss_loaded;
{
cereal::BinaryInputArchive iarchive(ss); // Create an input archive
iarchive(emboss_loaded);
}
CHECK(emboss.shapes_with_ids.front().expoly == emboss_loaded.shapes_with_ids.front().expoly);
CHECK(emboss.scale == emboss_loaded.scale);
CHECK(emboss.projection.depth == emboss_loaded.projection.depth);
CHECK(emboss.projection.use_surface == emboss_loaded.projection.use_surface);
CHECK(emboss.svg_file->path == emboss_loaded.svg_file->path);
CHECK(emboss.svg_file->path_in_3mf == emboss_loaded.svg_file->path_in_3mf);
}
@@ -780,7 +853,7 @@ using MyMesh = Slic3r::MeshBoolean::cgal2::CGALMesh;
// Second Idea
// Store original its inside of text configuration[optional]
// Cause problem with next editation of object -> cut, simplify, Netfabb, Hollow, ...(transform original vertices)
// Cause problem with next editation of object -> cut, simplify, repair by WinSDK, Hollow, ...(transform original vertices)
TEST_CASE("Emboss extrude cut", "[Emboss-Cut]")
{
std::string font_path = get_font_filepath();

View File

@@ -65,3 +65,99 @@ SCENARIO("Basics", "[ExPolygon]") {
}
}
}
#include <sstream>
#include <cereal/cereal.hpp>
#include <cereal/archives/binary.hpp>
#include "libslic3r/ExPolygonSerialize.hpp"
TEST_CASE("Serialization of expolygons", "[ExPolygon, Cereal, serialization]")
{
ExPolygons expolys{{
// expolygon 1 - without holes
{{0,0}, {10,0}, {10,10}, {0,10}}, // contour
// expolygon 2 - with rect 1px hole
{{{0,0}, {10,0}, {10,10}, {0,10}},
{{5, 5}, {6, 5}, {6, 6}, {5, 6}}}
}};
std::stringstream ss; // any stream can be used
{
cereal::BinaryOutputArchive oarchive(ss); // Create an output archive
oarchive(expolys);
} // archive goes out of scope, ensuring all contents are flushed
std::string data = ss.str();
CHECK(!data.empty());
ExPolygons expolys_loaded;
{
cereal::BinaryInputArchive iarchive(ss); // Create an input archive
iarchive(expolys_loaded);
}
CHECK(expolys == expolys_loaded);
}
#include <cereal/archives/json.hpp>
#include <regex>
// It is used to serialize expolygons into 3mf.
TEST_CASE("Serialization of expolygons to string", "[ExPolygon, Cereal, serialization]")
{
ExPolygons expolys{{
// expolygon 1 - without holes
{{0,0}, {10,0}, {10,10}, {0,10}}, // contour
// expolygon 2 - with rect 1px hole
{{{0,0}, {10,0}, {10,10}, {0,10}},
{{5, 5}, {6, 5}, {6, 6}, {5, 6}}}
}};
std::stringstream ss_out; // any stream can be used
{
cereal::JSONOutputArchive oarchive(ss_out); // Create an output archive
oarchive(expolys);
} // archive goes out of scope, ensuring all contents are flushed
//Simplify text representation of expolygons
std::string data = ss_out.str();
// Data contain this JSON string
//{
// "value0": [
// {
// "value0": {
// "value0":
// [{"value0": 0, "value1": 0}, {"value0": 10, "value1": 0}, {"value0": 10, "value1": 10}, {"value0": 0, "value1": 10}]
// },
// "value1": []
// },
// {
// "value0": {
// "value0":
// [{"value0": 0, "value1": 0}, {"value0": 10, "value1": 0}, {"value0": 10, "value1": 10}, {"value0": 0, "value1": 10}]
// },
// "value1": [{
// "value0":
// [{"value0": 5, "value1": 5}, {"value0": 6, "value1": 5}, {"value0": 6, "value1": 6}, {"value0": 5, "value1": 6}]
// }]
// }
// ]
//}
// Change JSON named object to JSON arrays(without name)
// RegEx for wihitespace = "[ \t\r\n\v\f]"
std::regex r("\"value[0-9]+\":|[ \t\r\n\v\f]");
std::string data_short = std::regex_replace(data, r , "");
std::replace(data_short.begin(), data_short.end(), '{', '[');
std::replace(data_short.begin(), data_short.end(), '}', ']');
CHECK(!data_short.empty());
// Cereal acceptable string
// [[[[[[0,0],[10,0],[10,10],[0,10]]],[]],[[[[0,0],[10,0],[10,10],[0,10]]],[[[[5,5],[6,5],[6,6],[5,6]]]]]]]
std::stringstream ss_in(data_short);
ExPolygons expolys_loaded;
{
cereal::JSONInputArchive iarchive(ss_in); // Create an input archive
iarchive(expolys_loaded);
}
CHECK(expolys == expolys_loaded);
}

View File

@@ -84,16 +84,16 @@ TEST_CASE("Line::perpendicular_to", "[Geometry]") {
TEST_CASE("Polygon::contains works properly", "[Geometry]"){
// this test was failing on Windows (GH #1950)
Slic3r::Polygon polygon(Points({
Point(207802834,-57084522),
Point(196528149,-37556190),
Point(173626821,-25420928),
Point(171285751,-21366123),
Point(118673592,-21366123),
Point(116332562,-25420928),
Point(93431208,-37556191),
Point(82156517,-57084523),
Point(129714478,-84542120),
Point(160244873,-84542120)
{207802834,-57084522},
{196528149,-37556190},
{173626821,-25420928},
{171285751,-21366123},
{118673592,-21366123},
{116332562,-25420928},
{93431208,-37556191},
{82156517,-57084523},
{129714478,-84542120},
{160244873,-84542120}
}));
Point point(95706562, -57294774);
REQUIRE(polygon.contains(point));
@@ -196,6 +196,40 @@ TEST_CASE("Offseting a line generates a polygon correctly", "[Geometry]"){
REQUIRE(area.area() == Slic3r::Polygon(Points({Point(10,5),Point(20,5),Point(20,15),Point(10,15)})).area());
}
SCENARIO("Circle Fit, 3 points", "[Geometry]") {
WHEN("Three points make a circle") {
double s1 = scaled<double>(1.);
THEN("circle_center(): A center point { 0, 0 } is returned") {
Vec2d center = Geometry::circle_center(Vec2d{ s1, 0. }, Vec2d{ 0, s1 }, Vec2d{ -s1, 0. }, SCALED_EPSILON);
REQUIRE(is_approx(center, Vec2d(0, 0)));
}
THEN("circle_center(): A center point { 0, 0 } is returned for points in reverse") {
Vec2d center = Geometry::circle_center(Vec2d{ -s1, 0. }, Vec2d{ 0, s1 }, Vec2d{ s1, 0. }, SCALED_EPSILON);
REQUIRE(is_approx(center, Vec2d(0, 0)));
}
THEN("try_circle_center(): A center point { 0, 0 } is returned") {
std::optional<Vec2d> center = Geometry::try_circle_center(Vec2d{ s1, 0. }, Vec2d{ 0, s1 }, Vec2d{ -s1, 0. }, SCALED_EPSILON);
REQUIRE(center);
REQUIRE(is_approx(*center, Vec2d(0, 0)));
}
THEN("try_circle_center(): A center point { 0, 0 } is returned for points in reverse") {
std::optional<Vec2d> center = Geometry::try_circle_center(Vec2d{ -s1, 0. }, Vec2d{ 0, s1 }, Vec2d{ s1, 0. }, SCALED_EPSILON);
REQUIRE(center);
REQUIRE(is_approx(*center, Vec2d(0, 0)));
}
}
WHEN("Three points are collinear") {
double s1 = scaled<double>(1.);
THEN("circle_center(): A center point { 2, 0 } is returned") {
Vec2d center = Geometry::circle_center(Vec2d{ s1, 0. }, Vec2d{ 2. * s1, 0. }, Vec2d{ 3. * s1, 0. }, SCALED_EPSILON);
REQUIRE(is_approx(center, Vec2d(2. * s1, 0)));
}
THEN("try_circle_center(): Fails for collinear points") {
std::optional<Vec2d> center = Geometry::try_circle_center(Vec2d{ s1, 0. }, Vec2d{ 2. * s1, 0. }, Vec2d{ 3. * s1, 0. }, SCALED_EPSILON);
REQUIRE(! center);
}
}
}
SCENARIO("Circle Fit, TaubinFit with Newton's method", "[Geometry]") {
GIVEN("A vector of Vec2ds arranged in a half-circle with approximately the same distance R from some point") {
Vec2d expected_center(-6, 0);
@@ -288,6 +322,53 @@ SCENARIO("Circle Fit, TaubinFit with Newton's method", "[Geometry]") {
}
}
SCENARIO("Circle Fit, least squares by decomposition or by solving normal equation", "[Geometry]") {
auto test_circle_fit = [](const Geometry::Circled &circle, const Vec2d &center, const double radius) {
THEN("A center point matches.") {
REQUIRE(is_approx(circle.center, center));
}
THEN("Radius matches") {
REQUIRE(is_approx(circle.radius, radius));
}
};
GIVEN("A vector of Vec2ds arranged in a half-circle with approximately the same distance R from some point") {
const Vec2d expected_center(-6., 0.);
const double expected_radius = 6.;
Vec2ds sample{Vec2d(6.0, 0), Vec2d(5.1961524, 3), Vec2d(3 ,5.1961524), Vec2d(0, 6.0), Vec2d(3, 5.1961524), Vec2d(-5.1961524, 3), Vec2d(-6.0, 0)};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Vec2d &a) { return a + expected_center; });
WHEN("Circle fit is called on the entire array, least squares SVD") {
test_circle_fit(Geometry::circle_linear_least_squares_svd(sample), expected_center, expected_radius);
}
WHEN("Circle fit is called on the first four points, least squares SVD") {
test_circle_fit(Geometry::circle_linear_least_squares_svd(Vec2ds(sample.cbegin(), sample.cbegin() + 4)), expected_center, expected_radius);
}
WHEN("Circle fit is called on the middle four points, least squares SVD") {
test_circle_fit(Geometry::circle_linear_least_squares_svd(Vec2ds(sample.cbegin() + 2, sample.cbegin() + 6)), expected_center, expected_radius);
}
WHEN("Circle fit is called on the entire array, least squares QR decomposition") {
test_circle_fit(Geometry::circle_linear_least_squares_qr(sample), expected_center, expected_radius);
}
WHEN("Circle fit is called on the first four points, least squares QR decomposition") {
test_circle_fit(Geometry::circle_linear_least_squares_qr(Vec2ds(sample.cbegin(), sample.cbegin() + 4)), expected_center, expected_radius);
}
WHEN("Circle fit is called on the middle four points, least squares QR decomposition") {
test_circle_fit(Geometry::circle_linear_least_squares_qr(Vec2ds(sample.cbegin() + 2, sample.cbegin() + 6)), expected_center, expected_radius);
}
WHEN("Circle fit is called on the entire array, least squares by normal equations") {
test_circle_fit(Geometry::circle_linear_least_squares_normal(sample), expected_center, expected_radius);
}
WHEN("Circle fit is called on the first four points, least squares by normal equations") {
test_circle_fit(Geometry::circle_linear_least_squares_normal(Vec2ds(sample.cbegin(), sample.cbegin() + 4)), expected_center, expected_radius);
}
WHEN("Circle fit is called on the middle four points, least squares by normal equations") {
test_circle_fit(Geometry::circle_linear_least_squares_normal(Vec2ds(sample.cbegin() + 2, sample.cbegin() + 6)), expected_center, expected_radius);
}
}
}
TEST_CASE("smallest_enclosing_circle_welzl", "[Geometry]") {
// Some random points in plane.
Points pts {
@@ -310,6 +391,7 @@ SCENARIO("Path chaining", "[Geometry]") {
GIVEN("A path") {
Points points = { Point(26,26),Point(52,26),Point(0,26),Point(26,52),Point(26,0),Point(0,52),Point(52,52),Point(52,0) };
THEN("Chained with no diagonals (thus 26 units long)") {
// if chain_points() works correctly, these points should be joined with no diagonal paths
std::vector<Points::size_type> indices = chain_points(points);
for (Points::size_type i = 0; i + 1 < indices.size(); ++ i) {
double dist = (points.at(indices.at(i)).cast<double>() - points.at(indices.at(i+1)).cast<double>()).norm();

View File

@@ -5,6 +5,24 @@
using namespace Slic3r;
SCENARIO("Simplify polyne, template", "[Polyline]")
{
Points polyline{ {0,0}, {1000,0}, {2000,0}, {2000,1000}, {2000,2000}, {1000,2000}, {0,2000}, {0,1000}, {0,0} };
WHEN("simplified with Douglas-Peucker with back inserter") {
Points out;
douglas_peucker<int64_t>(polyline.begin(), polyline.end(), std::back_inserter(out), 10, [](const Point &p) { return p; });
THEN("simplified correctly") {
REQUIRE(out == Points{ {0,0}, {2000,0}, {2000,2000}, {0,2000}, {0,0} });
}
}
WHEN("simplified with Douglas-Peucker in place") {
Points out{ polyline };
out.erase(douglas_peucker<int64_t>(out.begin(), out.end(), out.begin(), 10, [](const Point &p) { return p; }), out.end());
THEN("simplified correctly") {
REQUIRE(out == Points{ {0,0}, {2000,0}, {2000,2000}, {0,2000}, {0,0} });
}
}
}
SCENARIO("Simplify polyline", "[Polyline]")
{
GIVEN("polyline 1") {

View File

@@ -0,0 +1,98 @@
#include <catch2/catch.hpp>
#include <string_view>
#include "libslic3r/StaticMap.hpp"
TEST_CASE("Empty static map should be possible to create and should be empty", "[StaticMap]")
{
using namespace Slic3r;
static const constexpr StaticSet EmptySet;
static const constexpr auto EmptyMap = make_staticmap<int, int>();
constexpr bool is_map_empty = EmptyMap.empty();
constexpr bool is_set_empty = EmptySet.empty();
REQUIRE(is_map_empty);
REQUIRE(is_set_empty);
}
TEST_CASE("StaticSet should derive it's type from the initializer", "[StaticMap]") {
using namespace Slic3r;
static const constexpr StaticSet iOneSet = { 1 };
static constexpr size_t iOneSetSize = iOneSet.size();
REQUIRE(iOneSetSize == 1);
static const constexpr StaticSet iManySet = { 1, 3, 5, 80, 40 };
static constexpr size_t iManySetSize = iManySet.size();
REQUIRE(iManySetSize == 5);
}
TEST_CASE("StaticMap should derive it's type using make_staticmap", "[StaticMap]") {
using namespace Slic3r;
static const constexpr auto ciOneMap = make_staticmap<char, int>({
{'a', 1},
});
static constexpr size_t ciOneMapSize = ciOneMap.size();
static constexpr bool ciOneMapValid = query(ciOneMap, 'a').value_or(0) == 1;
REQUIRE(ciOneMapSize == 1);
REQUIRE(ciOneMapValid);
static const constexpr auto ciManyMap = make_staticmap<char, int>({
{'a', 1}, {'b', 2}, {'A', 10}
});
static constexpr size_t ciManyMapSize = ciManyMap.size();
static constexpr bool ciManyMapValid =
query(ciManyMap, 'a').value_or(0) == 1 &&
query(ciManyMap, 'b').value_or(0) == 2 &&
query(ciManyMap, 'A').value_or(0) == 10 &&
!contains(ciManyMap, 'B') &&
!query(ciManyMap, 'c').has_value();
REQUIRE(ciManyMapSize == 3);
REQUIRE(ciManyMapValid);
for (auto &[k, v] : ciManyMap) {
auto val = query(ciManyMap, k);
REQUIRE(val.has_value());
REQUIRE(*val == v);
}
}
TEST_CASE("StaticSet should be able to find contained values", "[StaticMap]")
{
using namespace Slic3r;
using namespace std::string_view_literals;
auto cmp = [](const char *a, const char *b) constexpr {
return std::string_view{a} < std::string_view{b};
};
static constexpr StaticSet CStrSet = {cmp, "One", "Two", "Three"};
static constexpr StaticSet StringSet = {"One"sv, "Two"sv, "Three"sv};
static constexpr bool CStrSetValid = query(CStrSet, "One").has_value() &&
contains(CStrSet, "Two") &&
contains(CStrSet, "Three") &&
!contains(CStrSet, "one") &&
!contains(CStrSet, "two") &&
!contains(CStrSet, "three");
static constexpr bool StringSetValid = contains(StringSet, "One"sv) &&
contains(StringSet, "Two"sv) &&
contains(StringSet, "Three"sv) &&
!contains(StringSet, "one"sv) &&
!contains(StringSet, "two"sv) &&
!contains(StringSet, "three"sv);
REQUIRE(CStrSetValid);
REQUIRE(StringSetValid);
REQUIRE(CStrSet.size() == 3);
REQUIRE(StringSet.size() == 3);
}

View File

@@ -0,0 +1,163 @@
#include "libslic3r/Point.hpp"
#include <catch2/catch.hpp>
#include <libslic3r/SupportSpotsGenerator.hpp>
using namespace Slic3r;
using namespace SupportSpotsGenerator;
TEST_CASE("Numerical integral calculation compared with exact solution.", "[SupportSpotsGenerator]") {
const float width = 10;
const float height = 20;
const Polygon polygon = {
scaled(Vec2f{-width / 2, -height / 2}),
scaled(Vec2f{width / 2, -height / 2}),
scaled(Vec2f{width / 2, height / 2}),
scaled(Vec2f{-width / 2, height / 2})
};
const Integrals integrals{{polygon}};
CHECK(integrals.area == Approx(width * height));
CHECK(integrals.x_i.x() == Approx(0));
CHECK(integrals.x_i.y() == Approx(0));
CHECK(integrals.x_i_squared.x() == Approx(std::pow(width, 3) * height / 12));
CHECK(integrals.x_i_squared.y() == Approx(width * std::pow(height, 3) / 12));
}
TEST_CASE("Moment values and ratio check.", "[SupportSpotsGenerator]") {
const float width = 40;
const float height = 2;
// Moments are calculated at centroid.
// Polygon centroid must not be (0, 0).
const Polygon polygon = {
scaled(Vec2f{0, 0}),
scaled(Vec2f{width, 0}),
scaled(Vec2f{width, height}),
scaled(Vec2f{0, height})
};
const Integrals integrals{{polygon}};
const Vec2f x_axis{1, 0};
const float x_axis_moment = compute_second_moment(integrals, x_axis);
const Vec2f y_axis{0, 1};
const float y_axis_moment = compute_second_moment(integrals, y_axis);
const float moment_ratio = std::pow(width / height, 2);
// Ensure the object transaltion has no effect.
CHECK(x_axis_moment == Approx(width * std::pow(height, 3) / 12));
CHECK(y_axis_moment == Approx(std::pow(width, 3) * height / 12));
// If the object is "wide" the y axis moments should be large compared to x axis moment.
CHECK(y_axis_moment / x_axis_moment == Approx(moment_ratio));
}
TEST_CASE("Moments calculation for rotated axis.", "[SupportSpotsGenerator]") {
Polygon polygon = {
scaled(Vec2f{6.362284076172198, 138.9674202217155}),
scaled(Vec2f{97.48779843751677, 106.08136606617076}),
scaled(Vec2f{135.75221821532384, 66.84428834668765}),
scaled(Vec2f{191.5308049852741, 45.77905628725614}),
scaled(Vec2f{182.7525148049201, 74.01799041087513}),
scaled(Vec2f{296.83210979283473, 196.80022572637228}),
scaled(Vec2f{215.16434429179148, 187.45715418834143}),
scaled(Vec2f{64.64574271229334, 284.293883209721}),
scaled(Vec2f{110.76507036894843, 174.35633141113783}),
scaled(Vec2f{77.56229640885199, 189.33057746591336})
};
Integrals integrals{{polygon}};
// Meassured counterclockwise from (1, 0)
const float angle = 1.432f;
Vec2f axis{std::cos(angle), std::sin(angle)};
float moment_calculated_then_rotated = compute_second_moment(
integrals,
axis
);
// We want to rotate the object clockwise by angle to align the axis with (1, 0)
// Method .rotate is counterclockwise for positive angle
polygon.rotate(-angle);
Integrals integrals_rotated{{polygon}};
float moment_rotated_polygon = compute_second_moment(
integrals_rotated,
Vec2f{1, 0}
);
// Up to 0.1% accuracy
CHECK_THAT(moment_calculated_then_rotated, Catch::Matchers::WithinRel(moment_rotated_polygon, 0.001f));
}
struct ObjectPartFixture {
const Polyline polyline{
Point{scaled(Vec2f{0, 0})},
Point{scaled(Vec2f{1, 0})},
};
const float width = 0.1f;
bool connected_to_bed = true;
coordf_t print_head_z = 0.2;
coordf_t layer_height = 0.2;
ExtrusionAttributes attributes;
ExtrusionEntityCollection collection;
std::vector<const ExtrusionEntityCollection*> extrusions{};
Polygon expected_polygon{
Point{scaled(Vec2f{0, -width / 2})},
Point{scaled(Vec2f{1, -width / 2})},
Point{scaled(Vec2f{1, width / 2})},
Point{scaled(Vec2f{0, width / 2})}
};
ObjectPartFixture() {
attributes.width = width;
const ExtrusionPath path{polyline, attributes};
collection.append(path);
extrusions.push_back(&collection);
}
};
TEST_CASE_METHOD(ObjectPartFixture, "Constructing ObjectPart using extrusion collections", "[SupportSpotsGenerator]") {
ObjectPart part{
extrusions,
connected_to_bed,
print_head_z,
layer_height,
std::nullopt
};
Integrals expected{{expected_polygon}};
CHECK(part.connected_to_bed == true);
Vec3f volume_centroid{part.volume_centroid_accumulator / part.volume};
CHECK(volume_centroid.x() == Approx(0.5));
CHECK(volume_centroid.y() == Approx(0));
CHECK(volume_centroid.z() == Approx(layer_height / 2));
CHECK(part.sticking_area == Approx(expected.area));
CHECK(part.sticking_centroid_accumulator.x() == Approx(expected.x_i.x()));
CHECK(part.sticking_centroid_accumulator.y() == Approx(expected.x_i.y()));
CHECK(part.sticking_second_moment_of_area_accumulator.x() == Approx(expected.x_i_squared.x()));
CHECK(part.sticking_second_moment_of_area_accumulator.y() == Approx(expected.x_i_squared.y()));
CHECK(part.sticking_second_moment_of_area_covariance_accumulator == Approx(expected.xy).margin(1e-6));
CHECK(part.volume == Approx(layer_height * width));
}
TEST_CASE_METHOD(ObjectPartFixture, "Constructing ObjectPart with brim", "[SupportSpotsGenerator]") {
float brim_width = 1;
Polygons brim = get_brim(ExPolygon{expected_polygon}, BrimType::btOuterOnly, brim_width);
ObjectPart part{
extrusions,
connected_to_bed,
print_head_z,
layer_height,
brim
};
CHECK(part.sticking_area == Approx((1 + 2*brim_width) * (width + 2*brim_width)));
}