diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 5f5d86aa8..dced5c02a 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -134,12 +134,30 @@ inline bool is_approx(const Point &p1, const Point &p2, coord_t epsilon = coord_ return d.x() < epsilon && d.y() < epsilon; } +inline bool is_approx(const Vec2f &p1, const Vec2f &p2, float epsilon = float(EPSILON)) +{ + Vec2f d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon; +} + inline bool is_approx(const Vec2d &p1, const Vec2d &p2, double epsilon = EPSILON) { Vec2d d = (p2 - p1).cwiseAbs(); return d.x() < epsilon && d.y() < epsilon; } +inline bool is_approx(const Vec3f &p1, const Vec3f &p2, float epsilon = float(EPSILON)) +{ + Vec3f d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon && d.z() < epsilon; +} + +inline bool is_approx(const Vec3d &p1, const Vec3d &p2, double epsilon = EPSILON) +{ + Vec3d d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon && d.z() < epsilon; +} + namespace int128 { // Exact orientation predicate, // returns +1: CCW, 0: collinear, -1: CW. diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index 16d289d9c..5cd97522d 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -593,6 +593,16 @@ TriangleMesh TriangleMesh::convex_hull_3d() const return output_mesh; } +std::vector TriangleMesh::slice(const std::vector &z) +{ + // convert doubles to floats + std::vector z_f(z.begin(), z.end()); + TriangleMeshSlicer mslicer(this); + std::vector layers; + mslicer.slice(z_f, 0.0004f, &layers, [](){}); + return layers; +} + void TriangleMesh::require_shared_vertices() { BOOST_LOG_TRIVIAL(trace) << "TriangleMeshSlicer::require_shared_vertices - start"; @@ -1861,7 +1871,8 @@ void TriangleMeshSlicer::cut(float z, TriangleMesh* upper, TriangleMesh* lower) } // Generate the vertex list for a cube solid of arbitrary size in X/Y/Z. -TriangleMesh make_cube(double x, double y, double z) { +TriangleMesh make_cube(double x, double y, double z) +{ Vec3d pv[8] = { Vec3d(x, y, 0), Vec3d(x, 0, 0), Vec3d(0, 0, 0), Vec3d(0, y, 0), Vec3d(x, y, z), Vec3d(0, y, z), @@ -1878,7 +1889,8 @@ TriangleMesh make_cube(double x, double y, double z) { Pointf3s vertices(&pv[0], &pv[0]+8); TriangleMesh mesh(vertices ,facets); - return mesh; + mesh.repair(); + return mesh; } // Generate the mesh for a cylinder and return it, using @@ -1922,7 +1934,9 @@ TriangleMesh make_cylinder(double r, double h, double fa) facets.emplace_back(Vec3crd(id, 2, 3)); facets.emplace_back(Vec3crd(id, id - 1, 2)); - return TriangleMesh(std::move(vertices), std::move(facets)); + TriangleMesh mesh(std::move(vertices), std::move(facets)); + mesh.repair(); + return mesh; } // Generates mesh for a sphere centered about the origin, using the generated angle @@ -1978,7 +1992,9 @@ TriangleMesh make_sphere(double radius, double fa) k2 = k2_next; } } - return TriangleMesh(std::move(vertices), std::move(facets)); + TriangleMesh mesh(std::move(vertices), std::move(facets)); + mesh.repair(); + return mesh; } } diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 86ca1625e..9c9f82040 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -58,8 +58,14 @@ public: BoundingBoxf3 bounding_box() const; // Returns the bbox of this TriangleMesh transformed by the given transformation BoundingBoxf3 transformed_bounding_box(const Transform3d &trafo) const; + // Return the size of the mesh in coordinates. + Vec3d size() const { return stl.stats.size.cast(); } + /// Return the center of the related bounding box. + Vec3d center() const { return this->bounding_box().center(); } // Returns the convex hull of this TriangleMesh TriangleMesh convex_hull_3d() const; + // Slice this mesh at the provided Z levels and return the vector + std::vector slice(const std::vector& z); void reset_repair_stats(); bool needed_repair() const; void require_shared_vertices(); diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index ee7e3fd1e..bd5623a22 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(${_TEST_NAME}_tests test_data.cpp test_data.hpp test_flow.cpp + test_trianglemesh.cpp ) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp index 729e72626..a8d3d0d03 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_data.cpp @@ -210,7 +210,7 @@ std::shared_ptr init_print(std::initializer_list meshes, Slic3r config->apply(*_config); const char* v {std::getenv("SLIC3R_TESTS_GCODE")}; - auto tests_gcode {(v == nullptr ? ""s : std::string(v))}; + auto tests_gcode {(v == nullptr ? "" : std::string(v))}; if (tests_gcode != "") config->set_key_value("gcode_comments", new ConfigOptionBool(true)); @@ -240,9 +240,9 @@ std::shared_ptr init_print(std::initializer_list meshes, Sl config->apply(*_config); const char* v {std::getenv("SLIC3R_TESTS_GCODE")}; - auto tests_gcode {(v == nullptr ? ""s : std::string(v))}; + auto tests_gcode {(v == nullptr ? "" : std::string(v))}; - if (tests_gcode != ""s) + if (tests_gcode != "") config->set_key_value("gcode_comments", new ConfigOptionBool(true)); std::shared_ptr print { std::make_shared() }; @@ -279,7 +279,7 @@ Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh) { Slic3r::Model result; ModelObject *object = result.add_object(); - object->name += model_name + ".stl"s; + object->name += model_name + ".stl"; object->add_volume(_mesh); object->add_instance(); return result; @@ -294,3 +294,26 @@ void add_testmesh_to_model(Slic3r::Model& result, const std::string& model_name, } } } // namespace Slic3r::Test + +#include + +SCENARIO("init_print functionality") { + GIVEN("A default config") { + std::shared_ptr config(Slic3r::DynamicPrintConfig::new_from_defaults()); + std::stringstream gcode; + WHEN("init_print is called with a single mesh.") { + Slic3r::Model model; + auto print = Slic3r::Test::init_print({ Slic3r::Test::TestMesh::cube_20x20x20 }, model, config, true); + gcode.clear(); + THEN("One mesh/printobject is in the resulting Print object.") { + REQUIRE(print->objects().size() == 1); + } + THEN("print->process() doesn't crash.") { + REQUIRE_NOTHROW(print->process()); + } + THEN("Export gcode functions outputs text.") { + REQUIRE(!Slic3r::Test::gcode(print).empty()); + } + } + } +} diff --git a/tests/fff_print/test_data.hpp b/tests/fff_print/test_data.hpp index 77c2eea3c..e67e400f2 100644 --- a/tests/fff_print/test_data.hpp +++ b/tests/fff_print/test_data.hpp @@ -56,7 +56,7 @@ TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0); /// Templated function to see if two values are equivalent (+/- epsilon) template -bool _equiv(const T& a, const T& b) { return abs(a - b) < Slic3r::Geometry::epsilon; } +bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; } template bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; } diff --git a/tests/fff_print/test_trianglemesh.cpp b/tests/fff_print/test_trianglemesh.cpp new file mode 100644 index 000000000..8e099db75 --- /dev/null +++ b/tests/fff_print/test_trianglemesh.cpp @@ -0,0 +1,433 @@ +#include + +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/libslic3r.h" + +#include +#include +#include + +//#include "test_options.hpp" +#include "test_data.hpp" + +using namespace Slic3r; +using namespace std; + +SCENARIO( "TriangleMesh: Basic mesh statistics") { + GIVEN( "A 20mm cube, built from constexpr std::array" ) { + std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + TriangleMesh cube(vertices, facets); + cube.repair(); + + THEN( "Volume is appropriate for 20mm square cube.") { + REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2); + } + + THEN( "Vertices array matches input.") { + for (auto i = 0U; i < cube.its.vertices.size(); i++) { + REQUIRE(cube.its.vertices.at(i) == vertices.at(i).cast()); + } + for (auto i = 0U; i < vertices.size(); i++) { + REQUIRE(vertices.at(i).cast() == cube.its.vertices.at(i)); + } + } + THEN( "Vertex count matches vertex array size.") { + REQUIRE(cube.facets_count() == facets.size()); + } + + THEN( "Facet array matches input.") { + for (auto i = 0U; i < cube.its.indices.size(); i++) { + REQUIRE(cube.its.indices.at(i) == facets.at(i)); + } + + for (auto i = 0U; i < facets.size(); i++) { + REQUIRE(facets.at(i) == cube.its.indices.at(i)); + } + } + THEN( "Facet count matches facet array size.") { + REQUIRE(cube.facets_count() == facets.size()); + } + +#if 0 + THEN( "Number of normals is equal to the number of facets.") { + REQUIRE(cube.normals().size() == facets.size()); + } +#endif + + THEN( "center() returns the center of the object.") { + REQUIRE(cube.center() == Vec3d(10.0,10.0,10.0)); + } + + THEN( "Size of cube is (20,20,20)") { + REQUIRE(cube.size() == Vec3d(20,20,20)); + } + + } + GIVEN( "A 20mm cube with one corner on the origin") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + + TriangleMesh cube(vertices, facets); + cube.repair(); + + THEN( "Volume is appropriate for 20mm square cube.") { + REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2); + } + + THEN( "Vertices array matches input.") { + for (auto i = 0U; i < cube.its.vertices.size(); i++) { + REQUIRE(cube.its.vertices.at(i) == vertices.at(i).cast()); + } + for (auto i = 0U; i < vertices.size(); i++) { + REQUIRE(vertices.at(i).cast() == cube.its.vertices.at(i)); + } + } + THEN( "Vertex count matches vertex array size.") { + REQUIRE(cube.facets_count() == facets.size()); + } + + THEN( "Facet array matches input.") { + for (auto i = 0U; i < cube.its.indices.size(); i++) { + REQUIRE(cube.its.indices.at(i) == facets.at(i)); + } + + for (auto i = 0U; i < facets.size(); i++) { + REQUIRE(facets.at(i) == cube.its.indices.at(i)); + } + } + THEN( "Facet count matches facet array size.") { + REQUIRE(cube.facets_count() == facets.size()); + } + +#if 0 + THEN( "Number of normals is equal to the number of facets.") { + REQUIRE(cube.normals().size() == facets.size()); + } +#endif + + THEN( "center() returns the center of the object.") { + REQUIRE(cube.center() == Vec3d(10.0,10.0,10.0)); + } + + THEN( "Size of cube is (20,20,20)") { + REQUIRE(cube.size() == Vec3d(20,20,20)); + } + } +} + +SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") { + GIVEN( "A 20mm cube with one corner on the origin") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + TriangleMesh cube(vertices, facets); + cube.repair(); + + WHEN( "The cube is scaled 200% uniformly") { + cube.scale(2.0); + THEN( "The volume is equivalent to 40x40x40 (all dimensions increased by 200%") { + REQUIRE(abs(cube.volume() - 40.0*40.0*40.0) < 1e-2); + } + } + WHEN( "The resulting cube is scaled 200% in the X direction") { + cube.scale(Vec3d(2.0, 1, 1)); + THEN( "The volume is doubled.") { + REQUIRE(abs(cube.volume() - 2*20.0*20.0*20.0) < 1e-2); + } + THEN( "The X coordinate size is 200%.") { + REQUIRE(cube.its.vertices.at(0).x() == 40.0); + } + } + + WHEN( "The cube is scaled 25% in the X direction") { + cube.scale(Vec3d(0.25, 1, 1)); + THEN( "The volume is 25% of the previous volume.") { + REQUIRE(abs(cube.volume() - 0.25*20.0*20.0*20.0) < 1e-2); + } + THEN( "The X coordinate size is 25% from previous.") { + REQUIRE(cube.its.vertices.at(0).x() == 5.0); + } + } + + WHEN( "The cube is rotated 45 degrees.") { + cube.rotate_z(float(M_PI / 4.)); + THEN( "The X component of the size is sqrt(2)*20") { + REQUIRE(abs(cube.size().x() - sqrt(2.0)*20) < 1e-2); + } + } + + WHEN( "The cube is translated (5, 10, 0) units with a Vec3f ") { + cube.translate(Vec3f(5.0, 10.0, 0.0)); + THEN( "The first vertex is located at 25, 30, 0") { + REQUIRE(cube.its.vertices.at(0) == Vec3f(25.0, 30.0, 0.0)); + } + } + + WHEN( "The cube is translated (5, 10, 0) units with 3 doubles") { + cube.translate(5.0, 10.0, 0.0); + THEN( "The first vertex is located at 25, 30, 0") { + REQUIRE(cube.its.vertices.at(0) == Vec3f(25.0, 30.0, 0.0)); + } + } + WHEN( "The cube is translated (5, 10, 0) units and then aligned to origin") { + cube.translate(5.0, 10.0, 0.0); + cube.align_to_origin(); + THEN( "The third vertex is located at 0,0,0") { + REQUIRE(cube.its.vertices.at(2) == Vec3f(0.0, 0.0, 0.0)); + } + } + } +} + +SCENARIO( "TriangleMesh: slice behavior.") { + GIVEN( "A 20mm cube with one corner on the origin") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + TriangleMesh cube(vertices, facets); + cube.repair(); + + WHEN("Cube is sliced with z = [0+EPSILON,2,4,8,6,8,10,12,14,16,18,20]") { + std::vector z { 0+EPSILON,2,4,8,6,8,10,12,14,16,18,20 }; + auto result {cube.slice(z)}; + THEN( "The correct number of polygons are returned per layer.") { + for (auto i = 0U; i < z.size(); i++) { + REQUIRE(result.at(i).size() == 1); + } + } + THEN( "The area of the returned polygons is correct.") { + for (auto i = 0U; i < z.size(); i++) { + REQUIRE(result.at(i).at(0).area() == 20.0*20/(std::pow(SCALING_FACTOR,2))); + } + } + } + } + GIVEN( "A STL with an irregular shape.") { + const std::vector vertices {Vec3d(0,0,0),Vec3d(0,0,20),Vec3d(0,5,0),Vec3d(0,5,20),Vec3d(50,0,0),Vec3d(50,0,20),Vec3d(15,5,0),Vec3d(35,5,0),Vec3d(15,20,0),Vec3d(50,5,0),Vec3d(35,20,0),Vec3d(15,5,10),Vec3d(50,5,20),Vec3d(35,5,10),Vec3d(35,20,10),Vec3d(15,20,10)}; + const std::vector facets {Vec3crd(0,1,2),Vec3crd(2,1,3),Vec3crd(1,0,4),Vec3crd(5,1,4),Vec3crd(0,2,4),Vec3crd(4,2,6),Vec3crd(7,6,8),Vec3crd(4,6,7),Vec3crd(9,4,7),Vec3crd(7,8,10),Vec3crd(2,3,6),Vec3crd(11,3,12),Vec3crd(7,12,9),Vec3crd(13,12,7),Vec3crd(6,3,11),Vec3crd(11,12,13),Vec3crd(3,1,5),Vec3crd(12,3,5),Vec3crd(5,4,9),Vec3crd(12,5,9),Vec3crd(13,7,10),Vec3crd(14,13,10),Vec3crd(8,15,10),Vec3crd(10,15,14),Vec3crd(6,11,8),Vec3crd(8,11,15),Vec3crd(15,11,13),Vec3crd(14,15,13)}; + + TriangleMesh cube(vertices, facets); + cube.repair(); + WHEN(" a top tangent plane is sliced") { + auto slices {cube.slice({5.0, 10.0})}; + THEN( "its area is included") { + REQUIRE(slices.at(0).at(0).area() > 0); + REQUIRE(slices.at(1).at(0).area() > 0); + } + } + WHEN(" a model that has been transformed is sliced") { + cube.mirror_z(); + auto slices {cube.slice({-5.0, -10.0})}; + THEN( "it is sliced properly (mirrored bottom plane area is included)") { + REQUIRE(slices.at(0).at(0).area() > 0); + REQUIRE(slices.at(1).at(0).area() > 0); + } + } + } +} + +SCENARIO( "make_xxx functions produce meshes.") { + GIVEN("make_cube() function") { + WHEN("make_cube() is called with arguments 20,20,20") { + TriangleMesh cube = make_cube(20,20,20); + THEN("The resulting mesh has one and only one vertex at 0,0,0") { + auto verts {cube.its.vertices}; + REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 0; } ) == 1); + } + THEN("The mesh volume is 20*20*20") { + REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2); + } + THEN("The resulting mesh is in the repaired state.") { + REQUIRE(cube.repaired == true); + } + THEN("There are 12 facets.") { + REQUIRE(cube.its.indices.size() == 12); + } + } + } + GIVEN("make_cylinder() function") { + WHEN("make_cylinder() is called with arguments 10,10, PI / 3") { + TriangleMesh cyl = make_cylinder(10, 10, PI / 243.0); + double angle = (2*PI / floor(2*PI / (PI / 243.0))); + THEN("The resulting mesh has one and only one vertex at 0,0,0") { + auto verts {cyl.its.vertices}; + REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 0; } ) == 1); + } + THEN("The resulting mesh has one and only one vertex at 0,0,10") { + auto verts {cyl.its.vertices}; + REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 10; } ) == 1); + } + THEN("Resulting mesh has 2 + (2*PI/angle * 2) vertices.") { + REQUIRE(cyl.its.vertices.size() == (2 + ((2*PI/angle)*2))); + } + THEN("Resulting mesh has 2*PI/angle * 4 facets") { + REQUIRE(cyl.its.indices.size() == (2*PI/angle)*4); + } + THEN("The resulting mesh is in the repaired state.") { + REQUIRE(cyl.repaired == true); + } + THEN( "The mesh volume is approximately 10pi * 10^2") { + REQUIRE(abs(cyl.volume() - (10.0 * M_PI * std::pow(10,2))) < 1); + } + } + } + + GIVEN("make_sphere() function") { + WHEN("make_sphere() is called with arguments 10, PI / 3") { + TriangleMesh sph = make_sphere(10, PI / 243.0); + double angle = (2.0*PI / floor(2.0*PI / (PI / 243.0))); + THEN("Resulting mesh has one point at 0,0,-10 and one at 0,0,10") { + const std::vector &verts = sph.its.vertices; + REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return is_approx(t, Vec3f(0.f, 0.f, 10.f)); } ) == 1); + REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return is_approx(t, Vec3f(0.f, 0.f, -10.f)); } ) == 1); + } + THEN("The resulting mesh is in the repaired state.") { + REQUIRE(sph.repaired == true); + } + THEN( "The mesh volume is approximately 4/3 * pi * 10^3") { + REQUIRE(abs(sph.volume() - (4.0/3.0 * M_PI * std::pow(10,3))) < 1); // 1% tolerance? + } + } + } +} + +SCENARIO( "TriangleMesh: split functionality.") { + GIVEN( "A 20mm cube with one corner on the origin") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + + TriangleMesh cube(vertices, facets); + cube.repair(); + WHEN( "The mesh is split into its component parts.") { + auto meshes {cube.split()}; + THEN(" The bounding box statistics are propagated to the split copies") { + REQUIRE(meshes.size() == 1); + REQUIRE((meshes.at(0)->bounding_box() == cube.bounding_box())); + } + } + } + GIVEN( "Two 20mm cubes, each with one corner on the origin, merged into a single TriangleMesh") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + + TriangleMesh cube(vertices, facets); + cube.repair(); + TriangleMesh cube2(vertices, facets); + cube2.repair(); + + cube.merge(cube2); + cube.repair(); + WHEN( "The combined mesh is split") { + auto meshes {cube.split()}; + THEN( "Two meshes are in the output vector.") { + REQUIRE(meshes.size() == 2); + } + } + } +} + +SCENARIO( "TriangleMesh: Mesh merge functions") { + GIVEN( "Two 20mm cubes, each with one corner on the origin") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + + TriangleMesh cube(vertices, facets); + cube.repair(); + TriangleMesh cube2(vertices, facets); + cube2.repair(); + + WHEN( "The two meshes are merged") { + cube.merge(cube2); + cube.repair(); + THEN( "There are twice as many facets in the merged mesh as the original.") { + REQUIRE(cube.stl.stats.number_of_facets == 2 * cube2.stl.stats.number_of_facets); + } + } + } +} + +SCENARIO( "TriangleMeshSlicer: Cut behavior.") { + GIVEN( "A 20mm cube with one corner on the origin") { + const std::vector vertices { Vec3d(20,20,0), Vec3d(20,0,0), Vec3d(0,0,0), Vec3d(0,20,0), Vec3d(20,20,20), Vec3d(0,20,20), Vec3d(0,0,20), Vec3d(20,0,20) }; + const std::vector facets { Vec3crd(0,1,2), Vec3crd(0,2,3), Vec3crd(4,5,6), Vec3crd(4,6,7), Vec3crd(0,4,7), Vec3crd(0,7,1), Vec3crd(1,7,6), Vec3crd(1,6,2), Vec3crd(2,6,5), Vec3crd(2,5,3), Vec3crd(4,0,3), Vec3crd(4,3,5) }; + + TriangleMesh cube(vertices, facets); + cube.repair(); + WHEN( "Object is cut at the bottom") { + TriangleMesh upper {}; + TriangleMesh lower {}; + TriangleMeshSlicer slicer(&cube); + slicer.cut(0, &upper, &lower); + THEN("Upper mesh has all facets except those belonging to the slicing plane.") { + REQUIRE(upper.facets_count() == 12); + } + THEN("Lower mesh has no facets.") { + REQUIRE(lower.facets_count() == 0); + } + } + WHEN( "Object is cut at the center") { + TriangleMesh upper {}; + TriangleMesh lower {}; + TriangleMeshSlicer slicer(&cube); + slicer.cut(10, &upper, &lower); + THEN("Upper mesh has 2 external horizontal facets, 3 facets on each side, and 6 facets on the triangulated side (2 + 12 + 6).") { + REQUIRE(upper.facets_count() == 2+12+6); + } + THEN("Lower mesh has 2 external horizontal facets, 3 facets on each side, and 6 facets on the triangulated side (2 + 12 + 6).") { + REQUIRE(lower.facets_count() == 2+12+6); + } + } + } +} +#ifdef TEST_PERFORMANCE +TEST_CASE("Regression test for issue #4486 - files take forever to slice") { + TriangleMesh mesh; + auto config {Slic3r::Config::new_from_defaults()}; + mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/100_000.stl"); + mesh.repair(); + + config->set("layer_height", 500); + config->set("first_layer_height", 250); + config->set("nozzle_diameter", 500); + + Slic3r::Model model; + auto print {Slic3r::Test::init_print({mesh}, model, config)}; + + print->status_cb = [] (int ln, const std::string& msg) { Slic3r::Log::info("Print") << ln << " " << msg << "\n";}; + + std::future fut = std::async([&print] () { print->process(); }); + std::chrono::milliseconds span {120000}; + bool timedout {false}; + if(fut.wait_for(span) == std::future_status::timeout) { + timedout = true; + } + REQUIRE(timedout == false); + +} +#endif // TEST_PERFORMANCE + +#ifdef BUILD_PROFILE +TEST_CASE("Profile test for issue #4486 - files take forever to slice") { + TriangleMesh mesh; + auto config {Slic3r::Config::new_from_defaults()}; + mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl"); + mesh.repair(); + + config->set("layer_height", 500); + config->set("first_layer_height", 250); + config->set("nozzle_diameter", 500); + config->set("fill_density", "5%"); + + Slic3r::Model model; + auto print {Slic3r::Test::init_print({mesh}, model, config)}; + + print->status_cb = [] (int ln, const std::string& msg) { Slic3r::Log::info("Print") << ln << " " << msg << "\n";}; + + print->process(); + + REQUIRE(true); + +} +#endif //BUILD_PROFILE