double polytree_area(const Tree &tree, std::vector *out)
{
traverse_pt(tree, out);
-
+
return std::accumulate(out->begin(), out->end(), 0.0,
[](double a, const P &p) { return a + p.area(); });
}
@@ -238,7 +238,7 @@ size_t count_polys(const ExPolygons& expolys)
{
size_t c = 0;
for (auto &ep : expolys) c += ep.holes.size() + 1;
-
+
return c;
}
@@ -247,32 +247,32 @@ TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") {
Polygon unitbox;
const auto UNIT = coord_t(1. / SCALING_FACTOR);
unitbox.points = { Vec2crd{0, 0}, Vec2crd{UNIT, 0}, Vec2crd{UNIT, UNIT}, Vec2crd{0, UNIT}};
-
+
Polygon box_frame = unitbox;
box_frame.scale(20, 10);
-
+
Polygon hole_left = unitbox;
hole_left.scale(8);
hole_left.translate(UNIT, UNIT);
hole_left.reverse();
-
+
Polygon hole_right = hole_left;
hole_right.translate(UNIT * 10, 0);
-
+
Polygon inner_left = unitbox;
inner_left.scale(4);
inner_left.translate(UNIT * 3, UNIT * 3);
-
+
Polygon inner_right = inner_left;
inner_right.translate(UNIT * 10, 0);
-
+
Polygons reference = union_({box_frame, hole_left, hole_right, inner_left, inner_right});
-
+
ClipperLib::PolyTree tree = union_pt(reference);
double area_sum = box_frame.area() + hole_left.area() +
hole_right.area() + inner_left.area() +
inner_right.area();
-
+
REQUIRE(area_sum > 0);
SECTION("Traverse into Polygons WITHOUT spatial ordering") {
@@ -280,19 +280,19 @@ TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") {
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(output.size() == reference.size());
}
-
+
SECTION("Traverse into ExPolygons WITHOUT spatial ordering") {
ExPolygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(count_polys(output) == reference.size());
}
-
+
SECTION("Traverse into Polygons WITH spatial ordering") {
Polygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(output.size() == reference.size());
}
-
+
SECTION("Traverse into ExPolygons WITH spatial ordering") {
ExPolygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
diff --git a/tests/libslic3r/test_marchingsquares.cpp b/tests/libslic3r/test_marchingsquares.cpp
index fed5cc2d4d..db427a8d70 100644
--- a/tests/libslic3r/test_marchingsquares.cpp
+++ b/tests/libslic3r/test_marchingsquares.cpp
@@ -17,22 +17,111 @@
#include
#include
#include
+#include
using namespace Slic3r;
+using namespace Catch::Matchers;
-static double area(const sla::RasterBase::PixelDim &pxd)
+// Note this tests SLA/RasterToPolygons.hpp, SLA/AGGRaster.hpp, and
+// ClipperUtils.hpp at least as much as MarchingSquares.hpp.
+
+// Get the Point corresponding to a raster column and row.
+Point rstPoint(const sla::RasterGrayscaleAA& rst, const size_t c, const size_t r)
{
- return pxd.w_mm * pxd.h_mm;
+ size_t rows = rst.resolution().height_px, cols = rst.resolution().width_px;
+ auto pxd = rst.pixel_dimensions();
+ auto tr = rst.trafo();
+ coord_t width = scaled(cols * pxd.h_mm), height = scaled(rows * pxd.w_mm);
+ Point p = Point::new_scale(c * pxd.w_mm, r * pxd.h_mm);
+ // reverse the raster transformations
+ if (tr.mirror_y)
+ p.y() = height - p.y();
+ if (tr.mirror_x)
+ p.x() = width - p.x();
+ p.x() -= tr.center_x;
+ p.y() -= tr.center_y;
+ if (tr.flipXY)
+ std::swap(p.x(), p.y());
+ return p;
}
-static Slic3r::sla::RasterGrayscaleAA create_raster(
- const sla::RasterBase::Resolution &res,
- double disp_w = 100.,
- double disp_h = 100.)
+// Get the size of a raster pixel in coord_t.
+static Point rstPixel(const sla::RasterGrayscaleAA& rst)
{
- sla::RasterBase::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
-
- auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
+ auto pxd = rst.pixel_dimensions();
+ return Point::new_scale(pxd.w_mm, pxd.h_mm);
+}
+
+// Get the size of a raster in coord_t.
+static Point rstSize(const sla::RasterGrayscaleAA& rst)
+{
+ auto pxd = rst.pixel_dimensions();
+ auto res = rst.resolution();
+ return Point::new_scale(pxd.w_mm * res.width_px, pxd.h_mm * res.height_px);
+}
+
+// Get the bounding box of a raster.
+static BoundingBox rstBBox(const sla::RasterGrayscaleAA& rst)
+{
+ auto center = rst.trafo().get_center();
+ return BoundingBox(Point(0, 0) - center, rstSize(rst) - center);
+}
+
+// Get the ExPolygons directly corresponding to a raster.
+static ExPolygons rstGetPolys(sla::RasterGrayscaleAA& rst)
+{
+ size_t rows = rst.resolution().height_px, cols = rst.resolution().width_px;
+ Polygons polys;
+ for (auto r = 0; r < rows; r++) {
+ // use c0==cols as a sentinel marker for "no start column yet".
+ size_t c0 = cols;
+ for (auto c = 0; c <= cols; c++) {
+ if (c < cols && rst.read_pixel(c, r) > 128) {
+ // We have set pixels, set the c0 start column if it is not yet set.
+ if (c0 == cols)
+ c0 = c;
+ } else if (c0 < cols) {
+ // There is no pixel set, but we do have a c0 start column. Output a
+ // "row-rectangle" poly for this row between the start column c0 and
+ // the current column.
+ polys.push_back({rstPoint(rst, c0, r), rstPoint(rst, c0, r + 1), rstPoint(rst, c, r + 1), rstPoint(rst, c, r)});
+ // Make sure the poly is anti-clockwise, which it might not be
+ // depending on how rstPoint() reverses the raster transformations
+ // from (c,r) to (x,y) coordinates.
+ if (polys.back().is_clockwise())
+ polys.back().reverse();
+ // Clear the start column c0 for the next row-rectangle.
+ c0 = cols;
+ }
+ }
+ }
+ // Merge all the row-rectangle polys into contiguous raster ExPolygons.
+ return union_ex(polys);
+}
+
+// Get the length in mm of a "vector" Point.
+static double len(const Point& v) { return unscaled(v.norm()); }
+// Get the area in mm^2 of a box with corners at the origin and a Point.
+static double area(const Point& v) { return unscaled(v.x()) * unscaled(v.y()); }
+
+// Find the index of the nearest extracted ExPolygon for a reference ExPolygon.
+static int find_closest_ext(const ExPolygons& exts, ExPolygon ref)
+{
+ auto ref_center = ref.contour.bounding_box().center();
+
+ auto closest = std::min_element(exts.begin(), exts.end(), [&ref_center](auto a, auto b) {
+ auto a_center = a.contour.bounding_box().center();
+ auto b_center = b.contour.bounding_box().center();
+ return a_center.distance_to(ref_center) < b_center.distance_to(ref_center);
+ });
+ return std::distance(exts.begin(), closest);
+}
+
+static Slic3r::sla::RasterGrayscaleAA create_raster(const sla::Resolution& res, double disp_w = 100., double disp_h = 100.)
+{
+ sla::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
+
+ auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
sla::RasterBase::Trafo trafo;
trafo.center_x = bb.center().x();
trafo.center_y = bb.center().y();
@@ -43,334 +132,439 @@ static Slic3r::sla::RasterGrayscaleAA create_raster(
static ExPolygon square(double a, Point center = {0, 0})
{
ExPolygon poly;
- coord_t V = scaled(a / 2.);
-
+ coord_t V = scaled(a / 2.);
+
poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}};
poly.translate(center.x(), center.y());
-
+
return poly;
}
static ExPolygon square_with_hole(double a, Point center = {0, 0})
{
ExPolygon poly = square(a);
-
+
poly.holes.emplace_back();
- coord_t V = scaled(a / 4.);
+ coord_t V = scaled(a / 4.);
poly.holes.front().points = {{-V, V}, {V, V}, {V, -V}, {-V, -V}};
-
+
poly.translate(center.x(), center.y());
return poly;
}
-static ExPolygons circle_with_hole(double r, Point center = {0, 0}) {
-
+static ExPolygons circle_with_hole(double r, Point center = {0, 0})
+{
ExPolygon poly;
-
+
std::vector pis = linspace_vector(0., 2 * PI, 100);
-
+
coord_t rs = scaled(r);
for (double phi : pis) {
poly.contour.points.emplace_back(rs * std::cos(phi), rs * std::sin(phi));
}
-
+
poly.holes.emplace_back(poly.contour);
poly.holes.front().reverse();
- for (auto &p : poly.holes.front().points) p /= 2;
-
+ for (auto& p : poly.holes.front().points)
+ p /= 2;
+
poly.translate(center.x(), center.y());
-
+
return {poly};
}
-static const Vec2i32 W4x4 = {4, 4};
static const Vec2i32 W2x2 = {2, 2};
+static const Vec2i32 W1x1 = {1, 1};
template
-static void test_expolys(Rst && rst,
- const ExPolygons & ref,
- Vec2i32 window,
- const std::string &name = "test")
+static void test_expolys(Rst&& rst, const ExPolygons& ref, Vec2i32 window, const std::string& name = "test", bool strict = true)
{
- for (const ExPolygon &expoly : ref) rst.draw(expoly);
-
+ auto raster_bb = rstBBox(rst);
+ Point pixel_size = rstPixel(rst);
+ Point window_size{coord_t(pixel_size.x() * window.x()), coord_t(pixel_size.y() * window.y())};
+ double pixel_area = area(pixel_size);
+ double pixel_len = len(pixel_size);
+ double window_area = area(window_size);
+ double window_len = len(window_size);
+
+ for (const ExPolygon& expoly : ref)
+ rst.draw(expoly);
+
std::fstream out(name + ".png", std::ios::out);
out << rst.encode(sla::PNGRasterEncoder{});
out.close();
-
- ExPolygons extracted = sla::raster_to_polygons(rst, window);
-
- SVG svg(name + ".svg");
- svg.draw(extracted);
- svg.draw(ref, "green");
+
+ const ExPolygons bmp = rstGetPolys(rst);
+ const ExPolygons ext = sla::raster_to_polygons(rst, window);
+
+ SVG svg(name + ".svg", raster_bb);
+ svg.draw(bmp, "green");
+ if (pixel_size.x() >= scale_(0.5))
+ svg.draw_grid(raster_bb, "grey", scale_(0.05), pixel_size.x());
+ if (window_size.x() >= scale_(1.0))
+ svg.draw_grid(raster_bb, "grey", scale_(0.10), window_size.x());
+ svg.draw_outline(ref, "red", "red", scale_(0.3));
+ svg.draw_outline(ext, "blue", "blue");
svg.Close();
-
- double max_rel_err = 0.1;
- sla::RasterBase::PixelDim pxd = rst.pixel_dimensions();
- double max_abs_err = area(pxd) * scaled(1.) * scaled(1.);
-
- BoundingBox ref_bb;
- for (auto &expoly : ref) ref_bb.merge(expoly.contour.bounding_box());
-
- double max_displacement = 4. * (std::pow(pxd.h_mm, 2) + std::pow(pxd.w_mm, 2));
- max_displacement *= scaled(1.) * scaled(1.);
-
- REQUIRE(extracted.size() == ref.size());
+
+ // Note all these areas are unscaled back to mm^2.
+ double raster_area = unscaled(unscaled(area(bmp)));
+ double reference_area = unscaled(unscaled(area(ref)));
+ double extracted_area = unscaled(unscaled(area(ext)));
+
+ // Note that errors accumulate with each step going from the reference
+ // polys to the extracted polys. The rendering of the reference polys to
+ // the raster does introduce pixelization errors too. This checks for
+ // acceptable errors going from reference to raster, and raster to
+ // reference.
for (size_t i = 0; i < ref.size(); ++i) {
- REQUIRE(extracted[i].contour.is_counter_clockwise());
- REQUIRE(extracted[i].holes.size() == ref[i].holes.size());
-
- for (auto &h : extracted[i].holes) REQUIRE(h.is_clockwise());
-
- double refa = ref[i].area();
- double abs_err = std::abs(extracted[i].area() - refa);
- double rel_err = abs_err / refa;
-
- REQUIRE((rel_err <= max_rel_err || abs_err <= max_abs_err));
-
- BoundingBox bb;
- for (auto &expoly : extracted) bb.merge(expoly.contour.bounding_box());
-
- Point d = bb.center() - ref_bb.center();
- REQUIRE(double(d.transpose() * d) <= max_displacement);
+ if (ref[i].contour.size() < 20)
+ UNSCOPED_INFO("reference ref[" << i << "]: " << ref[i]);
+ }
+ CHECK_THAT(raster_area, WithinRel(reference_area, pixel_len * 0.05) || WithinAbs(reference_area, pixel_area));
+ for (size_t i = 0; i < ext.size(); ++i) {
+ if (ext[i].contour.size() < 20)
+ UNSCOPED_INFO("extracted ext[" << i << "]: " << ext[i]);
+ }
+ CHECK_THAT(extracted_area, WithinRel(raster_area, 0.05) || WithinAbs(raster_area, window_area));
+ for (auto i = 0; i < ext.size(); ++i) {
+ CHECK(ext[i].contour.is_counter_clockwise());
+ for (auto& h : ext[i].holes)
+ CHECK(h.is_clockwise());
+ }
+
+ BoundingBox ref_bb;
+ for (auto& expoly : ref)
+ ref_bb.merge(expoly.contour.bounding_box());
+ BoundingBox ext_bb;
+ for (auto& expoly : ext)
+ ext_bb.merge(expoly.contour.bounding_box());
+ CHECK(len(ext_bb.center() - ref_bb.center()) < pixel_len);
+
+ // In ambigous cases (when polygons just touch) there are multiple equally
+ // valid interpretations of the raster into polygons. Although
+ // MarchingSquares currently systematically selects the solution that
+ // breaks them into separate polygons, that might not always be true. Also,
+ // SLA/RasterToPolygons.hpp, and in particular union_ex() from
+ // ClipperUtils.hpp that it uses, can and does sometimes merge them back
+ // together. This means we cannot reliably make assertions about the
+ // extracted number of polygons and their shapes in these cases. So we skip
+ // the individual polygon checks for strict=false.
+ if (strict) {
+ CHECK(ext.size() == ref.size());
+ for (auto i = 0; i < ext.size(); ++i) {
+ auto j = find_closest_ext(ref, ext[i]);
+ INFO("Comparing ext[" << i << "] against closest ref[" << j << "]");
+ CHECK(ext[i].holes.size() == ref[j].holes.size());
+ double ext_i_area = unscaled(unscaled(ext[i].area()));
+ double ref_j_area = unscaled(unscaled(ref[j].area()));
+ CHECK_THAT(ext_i_area, WithinRel(ref_j_area, pixel_len * 0.05) || WithinAbs(ref_j_area, window_area));
+ auto ext_i_bb = ext[i].contour.bounding_box();
+ auto ref_j_bb = ref[j].contour.bounding_box();
+ CHECK(len(ext_i_bb.center() - ref_j_bb.center()) < pixel_len);
+ }
}
}
-TEST_CASE("Empty raster should result in empty polygons", "[MarchingSquares]") {
+TEST_CASE("Empty raster should result in empty polygons", "[MarchingSquares]")
+{
sla::RasterGrayscaleAAGammaPower rst{{}, {}, {}};
- ExPolygons extracted = sla::raster_to_polygons(rst);
+ ExPolygons extracted = sla::raster_to_polygons(rst);
REQUIRE(extracted.size() == 0);
}
-TEST_CASE("Marching squares directions", "[MarchingSquares]") {
- marchsq::Coord crd{1, 1};
-
- REQUIRE(step(crd, marchsq::__impl::Dir::left).r == 1);
- REQUIRE(step(crd, marchsq::__impl::Dir::left).c == 0);
-
- REQUIRE(step(crd, marchsq::__impl::Dir::down).r == 2);
- REQUIRE(step(crd, marchsq::__impl::Dir::down).c == 1);
-
- REQUIRE(step(crd, marchsq::__impl::Dir::right).r == 1);
- REQUIRE(step(crd, marchsq::__impl::Dir::right).c == 2);
-
- REQUIRE(step(crd, marchsq::__impl::Dir::up).r == 0);
- REQUIRE(step(crd, marchsq::__impl::Dir::up).c == 1);
+TEST_CASE("Marching squares directions", "[MarchingSquares]")
+{
+ using namespace marchsq;
+ Coord crd{0, 0};
+
+ __impl::step(crd, __impl::Dir::left);
+ CHECK(crd == Coord(0, -1));
+ __impl::step(crd, __impl::Dir::down);
+ CHECK(crd == Coord(1, -1));
+ __impl::step(crd, __impl::Dir::right);
+ CHECK(crd == Coord(1, 0));
+ __impl::step(crd, __impl::Dir::up);
+ CHECK(crd == Coord(0, 0));
+ __impl::step(crd, __impl::Dir::left, 7);
+ CHECK(crd == Coord(0, -7));
+ __impl::step(crd, __impl::Dir::down, 7);
+ CHECK(crd == Coord(7, -7));
+ __impl::step(crd, __impl::Dir::right, 7);
+ CHECK(crd == Coord(7, 0));
+ __impl::step(crd, __impl::Dir::up, 7);
+ CHECK(crd == Coord(0, 0));
+ __impl::step(crd, __impl::Dir::left, -3);
+ CHECK(crd == Coord(0, 3));
+ __impl::step(crd, __impl::Dir::down, -3);
+ CHECK(crd == Coord(-3, 3));
+ __impl::step(crd, __impl::Dir::right, -3);
+ CHECK(crd == Coord(-3, 0));
+ __impl::step(crd, __impl::Dir::up, -3);
+ CHECK(crd == Coord(0, 0));
}
-TEST_CASE("Fully covered raster should result in a rectangle", "[MarchingSquares]") {
+TEST_CASE("Fully covered raster should result in a rectangle", "[MarchingSquares]")
+{
auto rst = create_raster({4, 4}, 4., 4.);
-
+
ExPolygon rect = square(4);
- SECTION("Full accuracy") {
- test_expolys(rst, {rect}, W2x2, "fully_covered_full_acc");
- }
-
- SECTION("Half accuracy") {
- test_expolys(rst, {rect}, W4x4, "fully_covered_half_acc");
- }
+ SECTION("Full accuracy") { test_expolys(rst, {rect}, W1x1, "fully_covered_full_acc"); }
+
+ SECTION("Half accuracy") { test_expolys(rst, {rect}, W2x2, "fully_covered_half_acc"); }
}
-TEST_CASE("4x4 raster with one ring", "[MarchingSquares]") {
-
- sla::RasterBase::PixelDim pixdim{1, 1};
-
+TEST_CASE("4x4 raster with one ring", "[MarchingSquares]")
+{
+ sla::PixelDim pixdim{1, 1};
+
// We need one additional row and column to detect edges
sla::RasterGrayscaleAA rst{{4, 4}, pixdim, {}, agg::gamma_threshold(.5)};
-
- // Draw a triangle from individual pixels
- rst.draw(square(1., {0500000, 0500000}));
- rst.draw(square(1., {1500000, 0500000}));
- rst.draw(square(1., {2500000, 0500000}));
-
- rst.draw(square(1., {1500000, 1500000}));
- rst.draw(square(1., {2500000, 1500000}));
-
- rst.draw(square(1., {2500000, 2500000}));
-
- std::fstream out("4x4.png", std::ios::out);
- out << rst.encode(sla::PNGRasterEncoder{});
- out.close();
-
- ExPolygons extracted = sla::raster_to_polygons(rst);
-
- SVG svg("4x4.svg");
- svg.draw(extracted);
- svg.Close();
-
- REQUIRE(extracted.size() == 1);
+
+ ExPolygons one = {{{1, 1}, {3, 1}, {3, 3}, {2, 3}, {2, 2}, {1, 2}}};
+ for (ExPolygon& p : one)
+ p.scale(scaled(1.0));
+ test_expolys(rst, one, W1x1, "one_4x4");
}
-TEST_CASE("4x4 raster with two rings", "[MarchingSquares]") {
-
- sla::RasterBase::PixelDim pixdim{1, 1};
-
+TEST_CASE("10x10 raster with two rings", "[MarchingSquares]")
+{
+ sla::PixelDim pixdim{1, 1};
+
// We need one additional row and column to detect edges
- sla::RasterGrayscaleAA rst{{5, 5}, pixdim, {}, agg::gamma_threshold(.5)};
-
- SECTION("Ambiguous case with 'ac' square") {
-
- // Draw a triangle from individual pixels
- rst.draw(square(1., {3500000, 2500000}));
- rst.draw(square(1., {3500000, 3500000}));
- rst.draw(square(1., {2500000, 3500000}));
-
- rst.draw(square(1., {2500000, 1500000}));
- rst.draw(square(1., {1500000, 1500000}));
- rst.draw(square(1., {1500000, 2500000}));
-
- std::fstream out("4x4_ac.png", std::ios::out);
- out << rst.encode(sla::PNGRasterEncoder{});
- out.close();
-
- ExPolygons extracted = sla::raster_to_polygons(rst);
-
- SVG svg("4x4_ac.svg");
- svg.draw(extracted);
- svg.Close();
-
- REQUIRE(extracted.size() == 2);
+ sla::RasterGrayscaleAA rst{{10, 10}, pixdim, {}, agg::gamma_threshold(.5)};
+
+ SECTION("Ambiguous case with 'bd' square")
+ {
+ ExPolygons ac = {{{1, 1}, {3, 1}, {3, 2}, {2, 2}, {2, 3}, {1, 3}}, {{4, 4}, {2, 4}, {2, 3}, {3, 3}, {3, 2}, {4, 2}}};
+ for (ExPolygon& p : ac)
+ p.scale(scaled(2.0));
+ test_expolys(rst, ac, W1x1, "bd_10x10", false);
}
-
- SECTION("Ambiguous case with 'bd' square") {
-
- // Draw a triangle from individual pixels
- rst.draw(square(1., {3500000, 1500000}));
- rst.draw(square(1., {3500000, 2500000}));
- rst.draw(square(1., {2500000, 1500000}));
-
- rst.draw(square(1., {1500000, 2500000}));
- rst.draw(square(1., {1500000, 3500000}));
- rst.draw(square(1., {2500000, 3500000}));
-
- std::fstream out("4x4_bd.png", std::ios::out);
- out << rst.encode(sla::PNGRasterEncoder{});
- out.close();
-
- ExPolygons extracted = sla::raster_to_polygons(rst);
-
- SVG svg("4x4_bd.svg");
- svg.draw(extracted);
- svg.Close();
-
- REQUIRE(extracted.size() == 2);
+
+ SECTION("Ambiguous case with 'ac' square")
+ {
+ ExPolygons bd = {{{1, 4}, {1, 2}, {2, 2}, {2, 3}, {3, 3}, {3, 4}}, {{4, 1}, {4, 3}, {3, 3}, {3, 2}, {2, 2}, {2, 1}}};
+ for (ExPolygon& p : bd)
+ p.scale(scaled(2.0));
+ test_expolys(rst, bd, W1x1, "ac_10x10", false);
}
}
-TEST_CASE("Square with hole in the middle", "[MarchingSquares]") {
+TEST_CASE("Square with hole in the middle", "[MarchingSquares]")
+{
using namespace Slic3r;
-
+
ExPolygons inp = {square_with_hole(50.)};
-
- SECTION("Proportional raster, 1x1 mm pixel size, full accuracy") {
- test_expolys(create_raster({100, 100}, 100., 100.), inp, W2x2, "square_with_hole_proportional_1x1_mm_px_full");
+
+ SECTION("Proportional raster, 1x1 mm pixel size, full accuracy")
+ {
+ test_expolys(create_raster({100, 100}, 100., 100.), inp, W1x1, "square_with_hole_proportional_1x1_mm_px_full");
}
-
- SECTION("Proportional raster, 1x1 mm pixel size, half accuracy") {
- test_expolys(create_raster({100, 100}, 100., 100.), inp, W4x4, "square_with_hole_proportional_1x1_mm_px_half");
+
+ SECTION("Proportional raster, 1x1 mm pixel size, half accuracy")
+ {
+ test_expolys(create_raster({100, 100}, 100., 100.), inp, W2x2, "square_with_hole_proportional_1x1_mm_px_half");
}
-
- SECTION("Landscape raster, 1x1 mm pixel size, full accuracy") {
- test_expolys(create_raster({150, 100}, 150., 100.), inp, W2x2, "square_with_hole_landsc_1x1_mm_px_full");
+
+ SECTION("Landscape raster, 1x1 mm pixel size, full accuracy")
+ {
+ test_expolys(create_raster({150, 100}, 150., 100.), inp, W1x1, "square_with_hole_landsc_1x1_mm_px_full");
}
-
- SECTION("Landscape raster, 1x1 mm pixel size, half accuracy") {
- test_expolys(create_raster({150, 100}, 150., 100.), inp, W4x4, "square_with_hole_landsc_1x1_mm_px_half");
+
+ SECTION("Landscape raster, 1x1 mm pixel size, half accuracy")
+ {
+ test_expolys(create_raster({150, 100}, 150., 100.), inp, W2x2, "square_with_hole_landsc_1x1_mm_px_half");
}
-
- SECTION("Portrait raster, 1x1 mm pixel size, full accuracy") {
- test_expolys(create_raster({100, 150}, 100., 150.), inp, W2x2, "square_with_hole_portrait_1x1_mm_px_full");
+
+ SECTION("Portrait raster, 1x1 mm pixel size, full accuracy")
+ {
+ test_expolys(create_raster({100, 150}, 100., 150.), inp, W1x1, "square_with_hole_portrait_1x1_mm_px_full");
}
-
- SECTION("Portrait raster, 1x1 mm pixel size, half accuracy") {
- test_expolys(create_raster({100, 150}, 100., 150.), inp, W4x4, "square_with_hole_portrait_1x1_mm_px_half");
+
+ SECTION("Portrait raster, 1x1 mm pixel size, half accuracy")
+ {
+ test_expolys(create_raster({100, 150}, 100., 150.), inp, W2x2, "square_with_hole_portrait_1x1_mm_px_half");
}
-
- SECTION("Proportional raster, 2x2 mm pixel size, full accuracy") {
- test_expolys(create_raster({200, 200}, 100., 100.), inp, W2x2, "square_with_hole_proportional_2x2_mm_px_full");
+
+ SECTION("Proportional raster, 2x2 mm pixel size, full accuracy")
+ {
+ test_expolys(create_raster({50, 50}, 100., 100.), inp, W1x1, "square_with_hole_proportional_2x2_mm_px_full");
}
-
- SECTION("Proportional raster, 2x2 mm pixel size, half accuracy") {
- test_expolys(create_raster({200, 200}, 100., 100.), inp, W4x4, "square_with_hole_proportional_2x2_mm_px_half");
+
+ SECTION("Proportional raster, 2x2 mm pixel size, half accuracy")
+ {
+ test_expolys(create_raster({50, 50}, 100., 100.), inp, W2x2, "square_with_hole_proportional_2x2_mm_px_half");
}
-
- SECTION("Proportional raster, 0.5x0.5 mm pixel size, full accuracy") {
- test_expolys(create_raster({50, 50}, 100., 100.), inp, W2x2, "square_with_hole_proportional_0.5x0.5_mm_px_full");
+
+ SECTION("Proportional raster, 0.5x0.5 mm pixel size, full accuracy")
+ {
+ test_expolys(create_raster({200, 200}, 100., 100.), inp, W1x1, "square_with_hole_proportional_0.5x0.5_mm_px_full");
}
-
- SECTION("Proportional raster, 0.5x0.5 mm pixel size, half accuracy") {
- test_expolys(create_raster({50, 50}, 100., 100.), inp, W4x4, "square_with_hole_proportional_0.5x0.5_mm_px_half");
+
+ SECTION("Proportional raster, 0.5x0.5 mm pixel size, half accuracy")
+ {
+ test_expolys(create_raster({200, 200}, 100., 100.), inp, W2x2, "square_with_hole_proportional_0.5x0.5_mm_px_half");
}
}
-TEST_CASE("Circle with hole in the middle", "[MarchingSquares]") {
+TEST_CASE("Circle with hole in the middle", "[MarchingSquares]")
+{
using namespace Slic3r;
-
- test_expolys(create_raster({1000, 1000}), circle_with_hole(25.), W2x2, "circle_with_hole");
+
+ test_expolys(create_raster({1000, 1000}), circle_with_hole(25.), W1x1, "circle_with_hole");
}
-static void recreate_object_from_rasters(const std::string &objname, float lh) {
+static void recreate_object_from_rasters(const std::string& objname, float lh)
+{
TriangleMesh mesh = load_model(objname);
-
- auto bb = mesh.bounding_box();
+
+ auto bb = mesh.bounding_box();
Vec3f tr = -bb.center().cast();
mesh.translate(tr.x(), tr.y(), tr.z());
bb = mesh.bounding_box();
-
+
std::vector layers = slice_mesh_ex(mesh.its, grid(float(bb.min.z()) + lh, float(bb.max.z()), lh));
-
- sla::RasterBase::Resolution res{2560, 1440};
- double disp_w = 120.96;
- double disp_h = 68.04;
+
+ sla::Resolution res{2560, 1440};
+ double disp_w = 120.96;
+ double disp_h = 68.04;
#ifndef NDEBUG
size_t cntr = 0;
#endif
- for (ExPolygons &layer : layers) {
+ for (ExPolygons& layer : layers) {
auto rst = create_raster(res, disp_w, disp_h);
-
- for (ExPolygon &island : layer) {
+
+ for (ExPolygon& island : layer) {
rst.draw(island);
}
-
+
#ifndef NDEBUG
std::fstream out(objname + std::to_string(cntr) + ".png", std::ios::out);
out << rst.encode(sla::PNGRasterEncoder{});
out.close();
#endif
-
+
ExPolygons layer_ = sla::raster_to_polygons(rst);
-// float delta = scaled(std::min(rst.pixel_dimensions().h_mm,
-// rst.pixel_dimensions().w_mm)) / 2;
-
-// layer_ = expolygons_simplify(layer_, delta);
+ // float delta = scaled(std::min(rst.pixel_dimensions().h_mm,
+ // rst.pixel_dimensions().w_mm)) / 2;
+
+ // layer_ = expolygons_simplify(layer_, delta);
#ifndef NDEBUG
- SVG svg(objname + std::to_string(cntr) + ".svg", BoundingBox(Point{0, 0}, Point{scaled(disp_w), scaled(disp_h)}));
+ SVG svg(objname + std::to_string(cntr) + ".svg", rstBBox(rst));
svg.draw(layer_);
svg.draw(layer, "green");
svg.Close();
#endif
-
+
double layera = 0., layera_ = 0.;
- for (auto &p : layer) layera += p.area();
- for (auto &p : layer_) layera_ += p.area();
+ for (auto& p : layer)
+ layera += p.area();
+ for (auto& p : layer_)
+ layera_ += p.area();
#ifndef NDEBUG
std::cout << cntr++ << std::endl;
#endif
double diff = std::abs(layera_ - layera);
REQUIRE((diff <= 0.1 * layera || diff < scaled(1.) * scaled(1.)));
-
+
layer = std::move(layer_);
}
-
+
indexed_triangle_set out = slices_to_mesh(layers, bb.min.z(), double(lh), double(lh));
its_write_obj(out, "out_from_rasters.obj");
}
-TEST_CASE("Recreate object from rasters", "[SL1Import]") {
- recreate_object_from_rasters("frog_legs.obj", 0.05f);
+TEST_CASE("Recreate object from rasters", "[SL1Import]") { recreate_object_from_rasters("frog_legs.obj", 0.05f); }
+
+namespace marchsq {
+
+static constexpr float layerf = 0.20; // layer height in mm (used for z values).
+static constexpr float gsizef = 100.0; // grid size in mm (box volume side length).
+static constexpr float wsizef = 0.50; // grid window size in mm (roughly line segment length).
+static constexpr float psizef = 0.01; // raster pixel size in mm (roughly point accuracy).
+static constexpr float isoval = 0.0; // iso value threshold to use.
+static constexpr size_t wsize = std::round(wsizef / psizef);
+
+static float period = 10.0; // gyroid "wavelength" in mm (2x line spacing).
+static float freq = 2 * PI / period; // gyroid frequency in waves per mm.
+
+void set_period(float len = 10.0)
+{
+ period = len;
+ freq = 2 * PI / period;
}
+
+static size_t layer_n;
+static size_t ring_n;
+static size_t point_n;
+static size_t get_n;
+
+void reset_stats()
+{
+ layer_n = 0;
+ ring_n = 0;
+ point_n = 0;
+ get_n = 0;
+}
+
+using Rings = std::vector;
+
+template<> struct _RasterTraits
+{
+ // using Rst = Slic3r::sla::RasterGrayscaleAA;
+ // The type of pixel cell in the raster
+ using ValueType = float;
+
+ // Value at a given position
+ static float get(const size_t& layer, size_t row, size_t col)
+ {
+ get_n++;
+ const float x = col * psizef * freq;
+ const float y = row * psizef * freq;
+ const float z = layer * psizef * freq;
+
+ return sinf(x) * cosf(y) + sinf(y) * cosf(z) + sinf(z) * cosf(x);
+ }
+
+ // Number of rows and cols of the raster
+ static size_t rows(const size_t& layer) { return std::round(gsizef / psizef); }
+ static size_t cols(const size_t& layer) { return std::round(gsizef / psizef); }
+};
+
+Rings get_gyroids(size_t l)
+{
+ size_t layer = l;
+ Rings rings = execute(layer, isoval, {wsize, wsize});
+ layer_n++;
+ ring_n += rings.size();
+ for (auto r : rings)
+ point_n += r.size();
+ return rings;
+}
+
+}; // namespace marchsq
+
+void benchmark_gyroid(float period)
+{
+ marchsq::reset_stats();
+ marchsq::set_period(period);
+ INFO("grid size: " << marchsq::gsizef << "mm\nlayer height: " << marchsq::layerf << "mm\n");
+ INFO("window size: " << marchsq::wsizef << "mm\npoint size: " << marchsq::psizef << "mm\n");
+ INFO("gyroid period: " << marchsq::period << "mm\n");
+ BENCHMARK("indexed", i) { return marchsq::get_gyroids(i); };
+ INFO("output avg rings/layer: " << float(marchsq::ring_n) / float(marchsq::layer_n) << "\n");
+ INFO("output avg points/layer: " << float(marchsq::point_n) / float(marchsq::layer_n) << "\n");
+ INFO("output avg gets/layer: " << float(marchsq::get_n) / float(marchsq::layer_n) << "\n");
+
+ REQUIRE(marchsq::layer_n > 0);
+}
+
+TEST_CASE("Benchmark gyroid cube period 10.0mm", "[MarchingSquares]") { benchmark_gyroid(10.0); }
+
+TEST_CASE("Benchmark gyroid cube period 5.0mm", "[MarchingSquares]") { benchmark_gyroid(5.0); }