From bb62f36df3915f125c7460c6c12ab88a47efe798 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 8 Jan 2020 17:10:11 +0100 Subject: [PATCH] Add tests for EigenMesh3D raycaster with hole support. Tests fail! Supports are intersecting the object when holes are added. --- src/libslic3r/SLA/Hollowing.cpp | 37 +++ src/libslic3r/SLA/Hollowing.hpp | 7 + src/libslic3r/SLAPrintSteps.cpp | 37 --- tests/sla_print/CMakeLists.txt | 5 +- tests/sla_print/sla_print_tests.cpp | 364 +------------------------- tests/sla_print/sla_raycast_tests.cpp | 61 +++++ tests/sla_print/sla_test_utils.cpp | 297 +++++++++++++++++++++ tests/sla_print/sla_test_utils.hpp | 112 ++++++++ tests/test_utils.hpp | 21 ++ 9 files changed, 541 insertions(+), 400 deletions(-) create mode 100644 tests/sla_print/sla_raycast_tests.cpp create mode 100644 tests/sla_print/sla_test_utils.cpp create mode 100644 tests/sla_print/sla_test_utils.hpp create mode 100644 tests/test_utils.hpp diff --git a/src/libslic3r/SLA/Hollowing.cpp b/src/libslic3r/SLA/Hollowing.cpp index 2b3572247..8dc2d3092 100644 --- a/src/libslic3r/SLA/Hollowing.cpp +++ b/src/libslic3r/SLA/Hollowing.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -247,4 +248,40 @@ bool DrainHole::get_intersections(const Vec3f& s, const Vec3f& dir, return true; } +void cut_drainholes(std::vector & obj_slices, + const std::vector &slicegrid, + float closing_radius, + const sla::DrainHoles & holes, + std::function thr) +{ + TriangleMesh mesh; + for (const sla::DrainHole &holept : holes) { + auto r = double(holept.radius); + auto h = double(holept.height); + sla::Contour3D hole = sla::cylinder(r, h); + Eigen::Quaterniond q; + q.setFromTwoVectors(Vec3d{0., 0., 1.}, holept.normal.cast()); + for(auto& p : hole.points) p = q * p + holept.pos.cast(); + mesh.merge(sla::to_triangle_mesh(hole)); + } + + if (mesh.empty()) return; + + mesh.require_shared_vertices(); + + TriangleMeshSlicer slicer(&mesh); + + std::vector hole_slices; + slicer.slice(slicegrid, closing_radius, &hole_slices, thr); + + if (obj_slices.size() != hole_slices.size()) + BOOST_LOG_TRIVIAL(warning) + << "Sliced object and drain-holes layer count does not match!"; + + size_t until = std::min(obj_slices.size(), hole_slices.size()); + + for (size_t i = 0; i < until; ++i) + obj_slices[i] = diff_ex(obj_slices[i], hole_slices[i]); +} + }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Hollowing.hpp b/src/libslic3r/SLA/Hollowing.hpp index ba1eb2d62..b3375ed1a 100644 --- a/src/libslic3r/SLA/Hollowing.hpp +++ b/src/libslic3r/SLA/Hollowing.hpp @@ -17,6 +17,7 @@ struct HollowingConfig double min_thickness = 2.; double quality = 0.5; double closing_distance = 0.5; + bool enabled = true; }; struct DrainHole @@ -57,6 +58,12 @@ std::unique_ptr generate_interior(const TriangleMesh &mesh, const HollowingConfig & = {}, const JobController &ctl = {}); +void cut_drainholes(std::vector & obj_slices, + const std::vector &slicegrid, + float closing_radius, + const sla::DrainHoles & holes, + std::function thr); + } } diff --git a/src/libslic3r/SLAPrintSteps.cpp b/src/libslic3r/SLAPrintSteps.cpp index e0c92a71c..0ae0e66a4 100644 --- a/src/libslic3r/SLAPrintSteps.cpp +++ b/src/libslic3r/SLAPrintSteps.cpp @@ -79,7 +79,6 @@ SLAPrint::Steps::Steps(SLAPrint *print) void SLAPrint::Steps::hollow_model(SLAPrintObject &po) { - if (!po.m_config.hollowing_enable.getBool()) { BOOST_LOG_TRIVIAL(info) << "Skipping hollowing step!"; po.m_hollowing_data.reset(); @@ -102,42 +101,6 @@ void SLAPrint::Steps::hollow_model(SLAPrintObject &po) BOOST_LOG_TRIVIAL(warning) << "Hollowed interior is empty!"; } -static void cut_drainholes(std::vector & obj_slices, - const std::vector &slicegrid, - float closing_radius, - const sla::DrainHoles & holes, - std::function thr) -{ - TriangleMesh mesh; - for (const sla::DrainHole &holept : holes) { - auto r = double(holept.radius); - auto h = double(holept.height); - sla::Contour3D hole = sla::cylinder(r, h); - Eigen::Quaterniond q; - q.setFromTwoVectors(Vec3d{0., 0., 1.}, holept.normal.cast()); - for(auto& p : hole.points) p = q * p + holept.pos.cast(); - mesh.merge(sla::to_triangle_mesh(hole)); - } - - if (mesh.empty()) return; - - mesh.require_shared_vertices(); - - TriangleMeshSlicer slicer(&mesh); - - std::vector hole_slices; - slicer.slice(slicegrid, closing_radius, &hole_slices, thr); - - if (obj_slices.size() != hole_slices.size()) - BOOST_LOG_TRIVIAL(warning) - << "Sliced object and drain-holes layer count does not match!"; - - size_t until = std::min(obj_slices.size(), hole_slices.size()); - - for (size_t i = 0; i < until; ++i) - obj_slices[i] = diff_ex(obj_slices[i], hole_slices[i]); -} - // The slicing will be performed on an imaginary 1D grid which starts from // the bottom of the bounding box created around the supported model. So // the first layer which is usually thicker will be part of the supports diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index ecc68db0a..9d47f3ae4 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -1,5 +1,8 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) -add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_print_tests.cpp) +add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp + sla_print_tests.cpp + sla_test_utils.hpp sla_test_utils.cpp + sla_raycast_tests.cpp) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 1cc959c3a..4fefeb6bb 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -2,369 +2,9 @@ #include #include -#include +#include "sla_test_utils.hpp" -// Debug -#include - -#include "libslic3r/libslic3r.h" -#include "libslic3r/Format/OBJ.hpp" -#include "libslic3r/SLAPrint.hpp" -#include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/SLA/Pad.hpp" -#include "libslic3r/SLA/SupportTreeBuilder.hpp" -#include "libslic3r/SLA/SupportTreeBuildsteps.hpp" -#include "libslic3r/SLA/SupportPointGenerator.hpp" -#include "libslic3r/SLA/Raster.hpp" -#include "libslic3r/SLA/ConcaveHull.hpp" -#include "libslic3r/MTUtils.hpp" - -#include "libslic3r/SVG.hpp" -#include "libslic3r/Format/OBJ.hpp" - -#if defined(WIN32) || defined(_WIN32) -#define PATH_SEPARATOR R"(\)" -#else -#define PATH_SEPARATOR R"(/)" -#endif - -namespace { -using namespace Slic3r; - -TriangleMesh load_model(const std::string &obj_filename) -{ - TriangleMesh mesh; - auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename; - load_obj(fpath.c_str(), &mesh); - return mesh; -} - -enum e_validity { - ASSUME_NO_EMPTY = 1, - ASSUME_MANIFOLD = 2, - ASSUME_NO_REPAIR = 4 -}; - -void check_validity(const TriangleMesh &input_mesh, - int flags = ASSUME_NO_EMPTY | ASSUME_MANIFOLD | - ASSUME_NO_REPAIR) -{ - TriangleMesh mesh{input_mesh}; - - if (flags & ASSUME_NO_EMPTY) { - REQUIRE_FALSE(mesh.empty()); - } else if (mesh.empty()) - return; // If it can be empty and it is, there is nothing left to do. - - REQUIRE(stl_validate(&mesh.stl)); - - bool do_update_shared_vertices = false; - mesh.repair(do_update_shared_vertices); - - if (flags & ASSUME_NO_REPAIR) { - REQUIRE_FALSE(mesh.needed_repair()); - } - - if (flags & ASSUME_MANIFOLD) { - mesh.require_shared_vertices(); - if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj"); - REQUIRE(mesh.is_manifold()); - } -} - -struct PadByproducts -{ - ExPolygons model_contours; - ExPolygons support_contours; - TriangleMesh mesh; -}; - -void _test_concave_hull(const Polygons &hull, const ExPolygons &polys) -{ - REQUIRE(polys.size() >=hull.size()); - - double polys_area = 0; - for (const ExPolygon &p : polys) polys_area += p.area(); - - double cchull_area = 0; - for (const Slic3r::Polygon &p : hull) cchull_area += p.area(); - - REQUIRE(cchull_area >= Approx(polys_area)); - - size_t cchull_holes = 0; - for (const Slic3r::Polygon &p : hull) - cchull_holes += p.is_clockwise() ? 1 : 0; - - REQUIRE(cchull_holes == 0); - - Polygons intr = diff(to_polygons(polys), hull); - REQUIRE(intr.empty()); -} - -void test_concave_hull(const ExPolygons &polys) { - sla::PadConfig pcfg; - - Slic3r::sla::ConcaveHull cchull{polys, pcfg.max_merge_dist_mm, []{}}; - - _test_concave_hull(cchull.polygons(), polys); - - coord_t delta = scaled(pcfg.brim_size_mm + pcfg.wing_distance()); - ExPolygons wafflex = sla::offset_waffle_style_ex(cchull, delta); - Polygons waffl = sla::offset_waffle_style(cchull, delta); - - _test_concave_hull(to_polygons(wafflex), polys); - _test_concave_hull(waffl, polys); -} - -void test_pad(const std::string & obj_filename, - const sla::PadConfig &padcfg, - PadByproducts & out) -{ - REQUIRE(padcfg.validate().empty()); - - TriangleMesh mesh = load_model(obj_filename); - - REQUIRE_FALSE(mesh.empty()); - - // Create pad skeleton only from the model - Slic3r::sla::pad_blueprint(mesh, out.model_contours); - - test_concave_hull(out.model_contours); - - REQUIRE_FALSE(out.model_contours.empty()); - - // Create the pad geometry for the model contours only - Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg); - - check_validity(out.mesh); - - auto bb = out.mesh.bounding_box(); - REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height())); -} - -void test_pad(const std::string & obj_filename, - const sla::PadConfig &padcfg = {}) -{ - PadByproducts byproducts; - test_pad(obj_filename, padcfg, byproducts); -} - -struct SupportByproducts -{ - std::string obj_fname; - std::vector slicegrid; - std::vector model_slices; - sla::SupportTreeBuilder supporttree; - TriangleMesh input_mesh; -}; - -const constexpr float CLOSING_RADIUS = 0.005f; - -void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, - const sla::SupportConfig &cfg) -{ - double gnd = stree.ground_level; - double H1 = cfg.max_solo_pillar_height_mm; - double H2 = cfg.max_dual_pillar_height_mm; - - for (const sla::Head &head : stree.heads()) { - REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET || - head.bridge_id != sla::ID_UNSET)); - } - - for (const sla::Pillar &pillar : stree.pillars()) { - if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) { - double h = pillar.height; - - if (h > H1) REQUIRE(pillar.links >= 1); - else if(h > H2) { REQUIRE(pillar.links >= 2); } - } - - REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors); - REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar); - } - - double max_bridgelen = 0.; - auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) { - Vec3d n = bridge.endp - bridge.startp; - double d = sla::distance(n); - max_brlen = std::max(d, max_brlen); - - double z = n.z(); - double polar = std::acos(z / d); - double slope = -polar + PI / 2.; - REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON); - }; - - for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen); - REQUIRE(max_bridgelen <= cfg.max_bridge_length_mm); - - max_bridgelen = 0; - for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen); - - double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope); - REQUIRE(max_bridgelen <= md); -} - -void test_supports(const std::string & obj_filename, - const sla::SupportConfig &supportcfg, - SupportByproducts & out) -{ - using namespace Slic3r; - TriangleMesh mesh = load_model(obj_filename); - - REQUIRE_FALSE(mesh.empty()); - - TriangleMeshSlicer slicer{&mesh}; - - auto bb = mesh.bounding_box(); - double zmin = bb.min.z(); - double zmax = bb.max.z(); - double gnd = zmin - supportcfg.object_elevation_mm; - auto layer_h = 0.05f; - - out.slicegrid = grid(float(gnd), float(zmax), layer_h); - slicer.slice(out.slicegrid , CLOSING_RADIUS, &out.model_slices, []{}); - - // Create the special index-triangle mesh with spatial indexing which - // is the input of the support point and support mesh generators - sla::EigenMesh3D emesh{mesh}; - - // Create the support point generator - sla::SupportPointGenerator::Config autogencfg; - autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{emesh, out.model_slices, - out.slicegrid, autogencfg, - [] {}, [](int) {}}; - - // Get the calculated support points. - std::vector support_points = point_gen.output(); - - int validityflags = ASSUME_NO_REPAIR; - - // If there is no elevation, support points shall be removed from the - // bottom of the object. - if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(support_points, zmin, - supportcfg.base_height_mm); - } else { - // Should be support points at least on the bottom of the model - REQUIRE_FALSE(support_points.empty()); - - // Also the support mesh should not be empty. - validityflags |= ASSUME_NO_EMPTY; - } - - // Generate the actual support tree - sla::SupportTreeBuilder treebuilder; - treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg}); - - check_support_tree_integrity(treebuilder, supportcfg); - - const TriangleMesh &output_mesh = treebuilder.retrieve_mesh(); - - check_validity(output_mesh, validityflags); - - // Quick check if the dimensions and placement of supports are correct - auto obb = output_mesh.bounding_box(); - - double allowed_zmin = zmin - supportcfg.object_elevation_mm; - - if (std::abs(supportcfg.object_elevation_mm) < EPSILON) - allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; - - REQUIRE(obb.min.z() >= allowed_zmin); - REQUIRE(obb.max.z() <= zmax); - - // Move out the support tree into the byproducts, we can examine it further - // in various tests. - out.obj_fname = std::move(obj_filename); - out.supporttree = std::move(treebuilder); - out.input_mesh = std::move(mesh); -} - -void test_supports(const std::string & obj_filename, - const sla::SupportConfig &supportcfg = {}) -{ - SupportByproducts byproducts; - test_supports(obj_filename, supportcfg, byproducts); -} - -void export_failed_case(const std::vector &support_slices, - const SupportByproducts &byproducts) -{ - for (size_t n = 0; n < support_slices.size(); ++n) { - const ExPolygons &sup_slice = support_slices[n]; - const ExPolygons &mod_slice = byproducts.model_slices[n]; - Polygons intersections = intersection(sup_slice, mod_slice); - - std::stringstream ss; - if (!intersections.empty()) { - ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg"; - SVG svg(ss.str()); - svg.draw(sup_slice, "green"); - svg.draw(mod_slice, "blue"); - svg.draw(intersections, "red"); - svg.Close(); - } - } - - TriangleMesh m; - byproducts.supporttree.retrieve_full_mesh(m); - m.merge(byproducts.input_mesh); - m.repair(); - m.require_shared_vertices(); - m.WriteOBJFile(byproducts.obj_fname.c_str()); -} - -void test_support_model_collision( - const std::string & obj_filename, - const sla::SupportConfig &input_supportcfg = {}) -{ - SupportByproducts byproducts; - - sla::SupportConfig supportcfg = input_supportcfg; - - // Set head penetration to a small negative value which should ensure that - // the supports will not touch the model body. - supportcfg.head_penetration_mm = -0.15; - - // TODO: currently, the tailheads penetrating into the model body do not - // respect the penetration parameter properly. No issues were reported so - // far but we should definitely fix this. - supportcfg.ground_facing_only = true; - - test_supports(obj_filename, supportcfg, byproducts); - - // Slice the support mesh given the slice grid of the model. - std::vector support_slices = - byproducts.supporttree.slice(byproducts.slicegrid, CLOSING_RADIUS); - - // The slices originate from the same slice grid so the numbers must match - - bool support_mesh_is_empty = - byproducts.supporttree.retrieve_mesh(sla::MeshType::Pad).empty() && - byproducts.supporttree.retrieve_mesh(sla::MeshType::Support).empty(); - - if (support_mesh_is_empty) - REQUIRE(support_slices.empty()); - else - REQUIRE(support_slices.size() == byproducts.model_slices.size()); - - bool notouch = true; - for (size_t n = 0; notouch && n < support_slices.size(); ++n) { - const ExPolygons &sup_slice = support_slices[n]; - const ExPolygons &mod_slice = byproducts.model_slices[n]; - - Polygons intersections = intersection(sup_slice, mod_slice); - - notouch = notouch && intersections.empty(); - } - - if (!notouch) export_failed_case(support_slices, byproducts); - - REQUIRE(notouch); -} +namespace { const char *const BELOW_PAD_TEST_OBJECTS[] = { "20mm_cube.obj", diff --git a/tests/sla_print/sla_raycast_tests.cpp b/tests/sla_print/sla_raycast_tests.cpp new file mode 100644 index 000000000..c50aa1f2f --- /dev/null +++ b/tests/sla_print/sla_raycast_tests.cpp @@ -0,0 +1,61 @@ +#include +#include + +#include +#include + +#include "sla_test_utils.hpp" + +using namespace Slic3r; + +// Create a simple scene with a 20mm cube and a big hole in the front wall +// with 5mm radius. Then shoot rays from interesting positions and see where +// they land. +TEST_CASE("Raycaster with loaded drillholes", "[sla_raycast]") +{ + // Load the cube and make it hollow. + TriangleMesh cube = load_model("20mm_cube.obj"); + sla::HollowingConfig hcfg; + std::unique_ptr cube_inside = sla::generate_interior(cube, hcfg); + REQUIRE(cube_inside); + + // Helper bb + auto boxbb = cube.bounding_box(); + + // Create the big 10mm long drainhole in the front wall. + Vec3f center = boxbb.center().cast(); + Vec3f p = {center.x(), 0., center.z()}; + Vec3f normal = {0.f, 1.f, 0.f}; + float radius = 5.f; + float hole_length = 10.; + sla::DrainHoles holes = { sla::DrainHole{p, normal, radius, hole_length} }; + + cube.merge(*cube_inside); + cube.require_shared_vertices(); + + sla::EigenMesh3D emesh{cube}; + emesh.load_holes(holes); + + Vec3d s = center.cast(); + SECTION("Fire from center, should hit the interior wall") { + auto hit = emesh.query_ray_hit(s, {0, 1., 0.}); + REQUIRE(hit.distance() == Approx(boxbb.size().x() / 2 - hcfg.min_thickness)); + } + + SECTION("Fire upward from hole center, hit distance equals the radius") { + s.y() = hcfg.min_thickness / 2; + auto hit = emesh.query_ray_hit(s, {0, 0., 1.}); + REQUIRE(hit.distance() == Approx(radius)); + } + + // Shouldn't this hit the inside wall through the hole? + SECTION("Fire from outside, hit the back side of the hole cylinder.") { + s.y() = -1.; + auto hit = emesh.query_ray_hit(s, {0, 1., 0.}); + REQUIRE(hit.distance() == Approx(hole_length + 1.f)); + } + + SECTION("Check for support tree correctness") { + test_support_model_collision("20mm_cube.obj", {}, hcfg, holes); + } +} diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp new file mode 100644 index 000000000..3da28a50e --- /dev/null +++ b/tests/sla_print/sla_test_utils.cpp @@ -0,0 +1,297 @@ +#include "sla_test_utils.hpp" + +void test_support_model_collision(const std::string &obj_filename, + const sla::SupportConfig &input_supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes) +{ + SupportByproducts byproducts; + + sla::SupportConfig supportcfg = input_supportcfg; + + // Set head penetration to a small negative value which should ensure that + // the supports will not touch the model body. + supportcfg.head_penetration_mm = -0.15; + + // TODO: currently, the tailheads penetrating into the model body do not + // respect the penetration parameter properly. No issues were reported so + // far but we should definitely fix this. + supportcfg.ground_facing_only = true; + + test_supports(obj_filename, supportcfg, hollowingcfg, drainholes, byproducts); + + // Slice the support mesh given the slice grid of the model. + std::vector support_slices = + byproducts.supporttree.slice(byproducts.slicegrid, CLOSING_RADIUS); + + // The slices originate from the same slice grid so the numbers must match + + bool support_mesh_is_empty = + byproducts.supporttree.retrieve_mesh(sla::MeshType::Pad).empty() && + byproducts.supporttree.retrieve_mesh(sla::MeshType::Support).empty(); + + if (support_mesh_is_empty) + REQUIRE(support_slices.empty()); + else + REQUIRE(support_slices.size() == byproducts.model_slices.size()); + + bool notouch = true; + for (size_t n = 0; notouch && n < support_slices.size(); ++n) { + const ExPolygons &sup_slice = support_slices[n]; + const ExPolygons &mod_slice = byproducts.model_slices[n]; + + Polygons intersections = intersection(sup_slice, mod_slice); + + notouch = notouch && intersections.empty(); + } + + /*if (!notouch) */export_failed_case(support_slices, byproducts); + + REQUIRE(notouch); +} + +void export_failed_case(const std::vector &support_slices, const SupportByproducts &byproducts) +{ + for (size_t n = 0; n < support_slices.size(); ++n) { + const ExPolygons &sup_slice = support_slices[n]; + const ExPolygons &mod_slice = byproducts.model_slices[n]; + Polygons intersections = intersection(sup_slice, mod_slice); + + std::stringstream ss; + if (!intersections.empty()) { + ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg"; + SVG svg(ss.str()); + svg.draw(sup_slice, "green"); + svg.draw(mod_slice, "blue"); + svg.draw(intersections, "red"); + svg.Close(); + } + } + + TriangleMesh m; + byproducts.supporttree.retrieve_full_mesh(m); + m.merge(byproducts.input_mesh); + m.repair(); + m.require_shared_vertices(); + m.WriteOBJFile(byproducts.obj_fname.c_str()); +} + +void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes, + SupportByproducts &out) +{ + using namespace Slic3r; + TriangleMesh mesh = load_model(obj_filename); + + REQUIRE_FALSE(mesh.empty()); + + if (hollowingcfg.enabled) { + auto inside = sla::generate_interior(mesh, hollowingcfg); + REQUIRE(inside); + mesh.merge(*inside); + mesh.require_shared_vertices(); + } + + TriangleMeshSlicer slicer{&mesh}; + + auto bb = mesh.bounding_box(); + double zmin = bb.min.z(); + double zmax = bb.max.z(); + double gnd = zmin - supportcfg.object_elevation_mm; + auto layer_h = 0.05f; + + out.slicegrid = grid(float(gnd), float(zmax), layer_h); + slicer.slice(out.slicegrid , CLOSING_RADIUS, &out.model_slices, []{}); + sla::cut_drainholes(out.model_slices, out.slicegrid, CLOSING_RADIUS, drainholes, []{}); + + // Create the special index-triangle mesh with spatial indexing which + // is the input of the support point and support mesh generators + sla::EigenMesh3D emesh{mesh}; + if (hollowingcfg.enabled) + emesh.load_holes(drainholes); + + // Create the support point generator + sla::SupportPointGenerator::Config autogencfg; + autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); + sla::SupportPointGenerator point_gen{emesh, out.model_slices, out.slicegrid, + autogencfg, [] {}, [](int) {}}; + + // Get the calculated support points. + std::vector support_points = point_gen.output(); + + int validityflags = ASSUME_NO_REPAIR; + + // If there is no elevation, support points shall be removed from the + // bottom of the object. + if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { + sla::remove_bottom_points(support_points, zmin, + supportcfg.base_height_mm); + } else { + // Should be support points at least on the bottom of the model + REQUIRE_FALSE(support_points.empty()); + + // Also the support mesh should not be empty. + validityflags |= ASSUME_NO_EMPTY; + } + + // Generate the actual support tree + sla::SupportTreeBuilder treebuilder; + treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg}); + + check_support_tree_integrity(treebuilder, supportcfg); + + const TriangleMesh &output_mesh = treebuilder.retrieve_mesh(); + + check_validity(output_mesh, validityflags); + + // Quick check if the dimensions and placement of supports are correct + auto obb = output_mesh.bounding_box(); + + double allowed_zmin = zmin - supportcfg.object_elevation_mm; + + if (std::abs(supportcfg.object_elevation_mm) < EPSILON) + allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; + + REQUIRE(obb.min.z() >= allowed_zmin); + REQUIRE(obb.max.z() <= zmax); + + // Move out the support tree into the byproducts, we can examine it further + // in various tests. + out.obj_fname = std::move(obj_filename); + out.supporttree = std::move(treebuilder); + out.input_mesh = std::move(mesh); +} + +void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, + const sla::SupportConfig &cfg) +{ + double gnd = stree.ground_level; + double H1 = cfg.max_solo_pillar_height_mm; + double H2 = cfg.max_dual_pillar_height_mm; + + for (const sla::Head &head : stree.heads()) { + REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET || + head.bridge_id != sla::ID_UNSET)); + } + + for (const sla::Pillar &pillar : stree.pillars()) { + if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) { + double h = pillar.height; + + if (h > H1) REQUIRE(pillar.links >= 1); + else if(h > H2) { REQUIRE(pillar.links >= 2); } + } + + REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors); + REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar); + } + + double max_bridgelen = 0.; + auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) { + Vec3d n = bridge.endp - bridge.startp; + double d = sla::distance(n); + max_brlen = std::max(d, max_brlen); + + double z = n.z(); + double polar = std::acos(z / d); + double slope = -polar + PI / 2.; + REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON); + }; + + for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen); + REQUIRE(max_bridgelen <= cfg.max_bridge_length_mm); + + max_bridgelen = 0; + for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen); + + double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope); + REQUIRE(max_bridgelen <= md); +} + +void test_pad(const std::string &obj_filename, const sla::PadConfig &padcfg, PadByproducts &out) +{ + REQUIRE(padcfg.validate().empty()); + + TriangleMesh mesh = load_model(obj_filename); + + REQUIRE_FALSE(mesh.empty()); + + // Create pad skeleton only from the model + Slic3r::sla::pad_blueprint(mesh, out.model_contours); + + test_concave_hull(out.model_contours); + + REQUIRE_FALSE(out.model_contours.empty()); + + // Create the pad geometry for the model contours only + Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg); + + check_validity(out.mesh); + + auto bb = out.mesh.bounding_box(); + REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height())); +} + +static void _test_concave_hull(const Polygons &hull, const ExPolygons &polys) +{ + REQUIRE(polys.size() >=hull.size()); + + double polys_area = 0; + for (const ExPolygon &p : polys) polys_area += p.area(); + + double cchull_area = 0; + for (const Slic3r::Polygon &p : hull) cchull_area += p.area(); + + REQUIRE(cchull_area >= Approx(polys_area)); + + size_t cchull_holes = 0; + for (const Slic3r::Polygon &p : hull) + cchull_holes += p.is_clockwise() ? 1 : 0; + + REQUIRE(cchull_holes == 0); + + Polygons intr = diff(to_polygons(polys), hull); + REQUIRE(intr.empty()); +} + +void test_concave_hull(const ExPolygons &polys) { + sla::PadConfig pcfg; + + Slic3r::sla::ConcaveHull cchull{polys, pcfg.max_merge_dist_mm, []{}}; + + _test_concave_hull(cchull.polygons(), polys); + + coord_t delta = scaled(pcfg.brim_size_mm + pcfg.wing_distance()); + ExPolygons wafflex = sla::offset_waffle_style_ex(cchull, delta); + Polygons waffl = sla::offset_waffle_style(cchull, delta); + + _test_concave_hull(to_polygons(wafflex), polys); + _test_concave_hull(waffl, polys); +} + +void check_validity(const TriangleMesh &input_mesh, int flags) +{ + TriangleMesh mesh{input_mesh}; + + if (flags & ASSUME_NO_EMPTY) { + REQUIRE_FALSE(mesh.empty()); + } else if (mesh.empty()) + return; // If it can be empty and it is, there is nothing left to do. + + REQUIRE(stl_validate(&mesh.stl)); + + bool do_update_shared_vertices = false; + mesh.repair(do_update_shared_vertices); + + if (flags & ASSUME_NO_REPAIR) { + REQUIRE_FALSE(mesh.needed_repair()); + } + + if (flags & ASSUME_MANIFOLD) { + mesh.require_shared_vertices(); + if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj"); + REQUIRE(mesh.is_manifold()); + } +} diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp new file mode 100644 index 000000000..dcb4934ef --- /dev/null +++ b/tests/sla_print/sla_test_utils.hpp @@ -0,0 +1,112 @@ +#ifndef SLA_TEST_UTILS_HPP +#define SLA_TEST_UTILS_HPP + +#include +#include + +// Debug +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/Format/OBJ.hpp" +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/SLA/Pad.hpp" +#include "libslic3r/SLA/SupportTreeBuilder.hpp" +#include "libslic3r/SLA/SupportTreeBuildsteps.hpp" +#include "libslic3r/SLA/SupportPointGenerator.hpp" +#include "libslic3r/SLA/Raster.hpp" +#include "libslic3r/SLA/ConcaveHull.hpp" +#include "libslic3r/MTUtils.hpp" + +#include "libslic3r/SVG.hpp" +#include "libslic3r/Format/OBJ.hpp" + +using namespace Slic3r; + +enum e_validity { + ASSUME_NO_EMPTY = 1, + ASSUME_MANIFOLD = 2, + ASSUME_NO_REPAIR = 4 +}; + +void check_validity(const TriangleMesh &input_mesh, + int flags = ASSUME_NO_EMPTY | ASSUME_MANIFOLD | + ASSUME_NO_REPAIR); + +struct PadByproducts +{ + ExPolygons model_contours; + ExPolygons support_contours; + TriangleMesh mesh; +}; + +void test_concave_hull(const ExPolygons &polys); + +void test_pad(const std::string & obj_filename, + const sla::PadConfig &padcfg, + PadByproducts & out); + +inline void test_pad(const std::string & obj_filename, + const sla::PadConfig &padcfg = {}) +{ + PadByproducts byproducts; + test_pad(obj_filename, padcfg, byproducts); +} + +struct SupportByproducts +{ + std::string obj_fname; + std::vector slicegrid; + std::vector model_slices; + sla::SupportTreeBuilder supporttree; + TriangleMesh input_mesh; +}; + +const constexpr float CLOSING_RADIUS = 0.005f; + +void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, + const sla::SupportConfig &cfg); + +void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes, + SupportByproducts &out); + +inline void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg, + SupportByproducts &out) +{ + sla::HollowingConfig hcfg; + hcfg.enabled = false; + test_supports(obj_filename, supportcfg, hcfg, {}, out); +} + +inline void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg = {}) +{ + SupportByproducts byproducts; + test_supports(obj_filename, supportcfg, byproducts); +} + +void export_failed_case(const std::vector &support_slices, + const SupportByproducts &byproducts); + + +void test_support_model_collision( + const std::string &obj_filename, + const sla::SupportConfig &input_supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes); + +inline void test_support_model_collision( + const std::string &obj_filename, + const sla::SupportConfig &input_supportcfg = {}) +{ + sla::HollowingConfig hcfg; + hcfg.enabled = false; + test_support_model_collision(obj_filename, input_supportcfg, hcfg, {}); +} + +#endif // SLA_TEST_UTILS_HPP diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp new file mode 100644 index 000000000..b129cc79f --- /dev/null +++ b/tests/test_utils.hpp @@ -0,0 +1,21 @@ +#ifndef SLIC3R_TEST_UTILS +#define SLIC3R_TEST_UTILS + +#include +#include + +#if defined(WIN32) || defined(_WIN32) +#define PATH_SEPARATOR R"(\)" +#else +#define PATH_SEPARATOR R"(/)" +#endif + +inline Slic3r::TriangleMesh load_model(const std::string &obj_filename) +{ + Slic3r::TriangleMesh mesh; + auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename; + Slic3r::load_obj(fpath.c_str(), &mesh); + return mesh; +} + +#endif // SLIC3R_TEST_UTILS