Browse Source

Ported test_trianglemesh from upstream slic3r, thanks @lordofhyphens

bubnikv 5 years ago
parent
commit
c99e7cb0df

+ 18 - 0
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.

+ 20 - 4
src/libslic3r/TriangleMesh.cpp

@@ -593,6 +593,16 @@ TriangleMesh TriangleMesh::convex_hull_3d() const
     return output_mesh;
 }
 
+std::vector<ExPolygons> TriangleMesh::slice(const std::vector<double> &z)
+{
+    // convert doubles to floats
+    std::vector<float> z_f(z.begin(), z.end());
+    TriangleMeshSlicer mslicer(this);
+    std::vector<ExPolygons> 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;
 }
 
 }

+ 6 - 0
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<double>(); }
+    /// 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<ExPolygons> slice(const std::vector<double>& z);
     void reset_repair_stats();
     bool needed_repair() const;
     void require_shared_vertices();

+ 1 - 0
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")

+ 27 - 4
tests/fff_print/test_data.cpp

@@ -210,7 +210,7 @@ std::shared_ptr<Print> init_print(std::initializer_list<TestMesh> 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<Print> init_print(std::initializer_list<TriangleMesh> 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> print { std::make_shared<Slic3r::Print>() };
@@ -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 <catch2/catch.hpp>
+
+SCENARIO("init_print functionality") {
+	GIVEN("A default config") {
+		std::shared_ptr<Slic3r::DynamicPrintConfig> 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());
+			}
+		}
+	}
+}

+ 1 - 1
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 <typename T>
-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 <typename T>
 bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }

+ 433 - 0
tests/fff_print/test_trianglemesh.cpp

@@ -0,0 +1,433 @@
+#include <catch2/catch.hpp>
+
+#include "libslic3r/TriangleMesh.hpp"
+#include "libslic3r/Point.hpp"
+#include "libslic3r/Config.hpp"
+#include "libslic3r/Model.hpp"
+#include "libslic3r/libslic3r.h"
+
+#include <algorithm>
+#include <future>
+#include <chrono>
+
+//#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<Vec3d> 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<Vec3crd> 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<float>());
+            }
+            for (auto i = 0U; i < vertices.size(); i++) {
+                REQUIRE(vertices.at(i).cast<float>() == 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<Vec3d> 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<Vec3crd> 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<float>());
+            }
+            for (auto i = 0U; i < vertices.size(); i++) {
+                REQUIRE(vertices.at(i).cast<float>() == 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<Vec3d> 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<Vec3crd> 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<Vec3d> 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<Vec3crd> 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<double> 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<Vec3d> 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<Vec3crd> 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<stl_vertex> &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<Vec3d> 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<Vec3crd> 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<Vec3d> 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<Vec3crd> 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<Vec3d> 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<Vec3crd> 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<Vec3d> 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<Vec3crd> 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<void> 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