diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index fe8b0f71d..633549b42 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -194,6 +194,8 @@ add_library(libslic3r STATIC SLA/SLARaster.cpp SLA/SLARasterWriter.hpp SLA/SLARasterWriter.cpp + SLA/ConcaveHull.hpp + SLA/ConcaveHull.cpp ) encoding_check(libslic3r) diff --git a/src/libslic3r/SLA/ConcaveHull.cpp b/src/libslic3r/SLA/ConcaveHull.cpp new file mode 100644 index 000000000..dff061721 --- /dev/null +++ b/src/libslic3r/SLA/ConcaveHull.cpp @@ -0,0 +1,171 @@ +#include "ConcaveHull.hpp" +#include +#include +#include "SLASpatIndex.hpp" +#include + +namespace Slic3r { +namespace sla { + +inline Vec3d to_vec3(const Vec2crd &v2) { return {double(v2(X)), double(v2(Y)), 0.}; } +inline Vec3d to_vec3(const Vec2d &v2) { return {v2(X), v2(Y), 0.}; } +inline Vec2crd to_vec2(const Vec3d &v3) { return {coord_t(v3(X)), coord_t(v3(Y))}; } + +Point ConcaveHull::centroid(const Points &pp) +{ + Point c; + switch(pp.size()) { + case 0: break; + case 1: c = pp.front(); break; + case 2: c = (pp[0] + pp[1]) / 2; break; + default: { + auto MAX = std::numeric_limits::max(); + auto MIN = std::numeric_limits::min(); + Point min = {MAX, MAX}, max = {MIN, MIN}; + + for(auto& p : pp) { + if(p(0) < min(0)) min(0) = p(0); + if(p(1) < min(1)) min(1) = p(1); + if(p(0) > max(0)) max(0) = p(0); + if(p(1) > max(1)) max(1) = p(1); + } + c(0) = min(0) + (max(0) - min(0)) / 2; + c(1) = min(1) + (max(1) - min(1)) / 2; + break; + } + } + + return c; +} + +// As it shows, the current offset_ex in ClipperUtils hangs if used in jtRound +// mode +ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths, + coord_t delta, + ClipperLib::JoinType jointype) +{ + using ClipperLib::ClipperOffset; + using ClipperLib::etClosedPolygon; + using ClipperLib::Paths; + using ClipperLib::Path; + + ClipperOffset offs; + offs.ArcTolerance = scaled(0.01); + + for (auto &p : paths) + // If the input is not at least a triangle, we can not do this algorithm + if(p.size() < 3) { + BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!"; + return {}; + } + + offs.AddPaths(paths, jointype, etClosedPolygon); + + Paths result; + offs.Execute(result, static_cast(delta)); + + return result; +} + +Points ConcaveHull::calculate_centroids() const +{ + // We get the centroids of all the islands in the 2D slice + Points centroids = reserve_vector(m_polys.size()); + std::transform(m_polys.begin(), m_polys.end(), + std::back_inserter(centroids), + [this](const Polygon &poly) { return centroid(poly); }); + + return centroids; +} + +void ConcaveHull::merge_polygons() { m_polys = get_contours(union_ex(m_polys)); } + +void ConcaveHull::add_connector_rectangles(const Points ¢roids, + coord_t max_dist, + ThrowOnCancel thr) +{ + // Centroid of the centroids of islands. This is where the additional + // connector sticks are routed. + Point cc = centroid(centroids); + + PointIndex ctrindex; + unsigned idx = 0; + for(const Point &ct : centroids) ctrindex.insert(to_vec3(ct), idx++); + + m_polys.reserve(m_polys.size() + centroids.size()); + + idx = 0; + for (const Point &c : centroids) { + thr(); + + double dx = c.x() - cc.x(), dy = c.y() - cc.y(); + double l = std::sqrt(dx * dx + dy * dy); + double nx = dx / l, ny = dy / l; + + const Point &ct = centroids[idx]; + + std::vector result = ctrindex.nearest(to_vec3(ct), 2); + + double dist = max_dist; + for (const PointIndexEl &el : result) + if (el.second != idx) { + dist = Line(to_vec2(el.first), ct).length(); + break; + } + + idx++; + + if (dist >= max_dist) return; + + Polygon r; + r.points.reserve(3); + r.points.emplace_back(cc); + + Point n(scaled(nx), scaled(ny)); + r.points.emplace_back(c + Point(n.y(), -n.x())); + r.points.emplace_back(c + Point(-n.y(), n.x())); + offset(r, scaled(1.)); + + m_polys.emplace_back(r); + } +} + +ConcaveHull::ConcaveHull(const Polygons &polys, double mergedist, ThrowOnCancel thr) +{ + if(polys.empty()) return; + + m_polys = polys; + merge_polygons(); + + if(m_polys.size() == 1) return; + + Points centroids = calculate_centroids(); + + add_connector_rectangles(centroids, scaled(mergedist), thr); + + merge_polygons(); +} + +ExPolygons ConcaveHull::to_expolygons() const +{ + auto ret = reserve_vector(m_polys.size()); + for (const Polygon &p : m_polys) ret.emplace_back(ExPolygon(p)); + return ret; +} + +ExPolygons offset_waffle_style_ex(const ConcaveHull &hull, coord_t delta) +{ + ClipperLib::Paths paths = Slic3rMultiPoints_to_ClipperPaths(hull.polygons()); + paths = fast_offset(paths, 2 * delta, ClipperLib::jtRound); + paths = fast_offset(paths, -delta, ClipperLib::jtRound); + ExPolygons ret = ClipperPaths_to_Slic3rExPolygons(paths); + for (ExPolygon &p : ret) p.holes = {}; + return ret; +} + +Polygons offset_waffle_style(const ConcaveHull &hull, coord_t delta) +{ + return to_polygons(offset_waffle_style_ex(hull, delta)); +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/ConcaveHull.hpp b/src/libslic3r/SLA/ConcaveHull.hpp new file mode 100644 index 000000000..94e16d77c --- /dev/null +++ b/src/libslic3r/SLA/ConcaveHull.hpp @@ -0,0 +1,53 @@ +#ifndef CONCAVEHULL_HPP +#define CONCAVEHULL_HPP + +#include + +namespace Slic3r { +namespace sla { + +inline Polygons get_contours(const ExPolygons &poly) +{ + Polygons ret; ret.reserve(poly.size()); + for (const ExPolygon &p : poly) ret.emplace_back(p.contour); + + return ret; +} + +using ThrowOnCancel = std::function; + +/// A fake concave hull that is constructed by connecting separate shapes +/// with explicit bridges. Bridges are generated from each shape's centroid +/// to the center of the "scene" which is the centroid calculated from the shape +/// centroids (a star is created...) +class ConcaveHull { + Polygons m_polys; + + static Point centroid(const Points& pp); + + static inline Point centroid(const Polygon &poly) { return poly.centroid(); } + + Points calculate_centroids() const; + + void merge_polygons(); + + void add_connector_rectangles(const Points ¢roids, + coord_t max_dist, + ThrowOnCancel thr); +public: + + ConcaveHull(const ExPolygons& polys, double merge_dist, ThrowOnCancel thr) + : ConcaveHull{to_polygons(polys), merge_dist, thr} {} + + ConcaveHull(const Polygons& polys, double mergedist, ThrowOnCancel thr); + + const Polygons & polygons() const { return m_polys; } + + ExPolygons to_expolygons() const; +}; + +ExPolygons offset_waffle_style_ex(const ConcaveHull &ccvhull, coord_t delta); +Polygons offset_waffle_style(const ConcaveHull &polys, coord_t delta); + +}} // namespace Slic3r::sla +#endif // CONCAVEHULL_HPP diff --git a/src/libslic3r/SLA/SLAPad.cpp b/src/libslic3r/SLA/SLAPad.cpp index 71f8b1c7f..7cd9eb4e4 100644 --- a/src/libslic3r/SLA/SLAPad.cpp +++ b/src/libslic3r/SLA/SLAPad.cpp @@ -1,6 +1,7 @@ #include "SLAPad.hpp" #include "SLABoilerPlate.hpp" #include "SLASpatIndex.hpp" +#include "ConcaveHull.hpp" #include "boost/log/trivial.hpp" #include "SLABoostAdapter.hpp" @@ -206,36 +207,6 @@ Contour3D inline straight_walls(const Polygon &plate, return walls(plate, plate, lo_z, hi_z, .0 /*offset_diff*/, thr); } -// As it shows, the current offset_ex in ClipperUtils hangs if used in jtRound -// mode -ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths, - coord_t delta, - ClipperLib::JoinType jointype) -{ - using ClipperLib::ClipperOffset; - using ClipperLib::etClosedPolygon; - using ClipperLib::Paths; - using ClipperLib::Path; - - ClipperOffset offs; - offs.ArcTolerance = scaled(0.01); - - for (auto &p : paths) - // If the input is not at least a triangle, we can not do this algorithm - if(p.size() < 3) { - BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!"; - return {}; - } - - offs.AddPaths(paths, jointype, etClosedPolygon); - - Paths result; - offs.Execute(result, static_cast(delta)); - - return result; -} - - // Function to cut tiny connector cavities for a given polygon. The input poly // will be offsetted by "padding" and small rectangle shaped cavities will be // inserted along the perimeter in every "stride" distance. The stick rectangles @@ -322,158 +293,15 @@ ExPolygons breakstick_holes(const ExPolygons &input, Args...args) return ret; } -/// A fake concave hull that is constructed by connecting separate shapes -/// with explicit bridges. Bridges are generated from each shape's centroid -/// to the center of the "scene" which is the centroid calculated from the shape -/// centroids (a star is created...) -class ConcaveHull { - Polygons m_polys; +static inline coord_t get_waffle_offset(const PadConfig &c) +{ + return scaled(c.brim_size_mm + c.wing_distance()); +} - Point centroid(const Points& pp) const - { - Point c; - switch(pp.size()) { - case 0: break; - case 1: c = pp.front(); break; - case 2: c = (pp[0] + pp[1]) / 2; break; - default: { - auto MAX = std::numeric_limits::max(); - auto MIN = std::numeric_limits::min(); - Point min = {MAX, MAX}, max = {MIN, MIN}; - - for(auto& p : pp) { - if(p(0) < min(0)) min(0) = p(0); - if(p(1) < min(1)) min(1) = p(1); - if(p(0) > max(0)) max(0) = p(0); - if(p(1) > max(1)) max(1) = p(1); - } - c(0) = min(0) + (max(0) - min(0)) / 2; - c(1) = min(1) + (max(1) - min(1)) / 2; - break; - } - } - - return c; - } - - inline Point centroid(const Polygon &poly) const { return poly.centroid(); } - - Points calculate_centroids() const - { - // We get the centroids of all the islands in the 2D slice - Points centroids = reserve_vector(m_polys.size()); - std::transform(m_polys.begin(), m_polys.end(), - std::back_inserter(centroids), - [this](const Polygon &poly) { return centroid(poly); }); - - return centroids; - } - - void merge_polygons() { m_polys = union_(m_polys); } - - void add_connector_rectangles(const Points ¢roids, - coord_t max_dist, - ThrowOnCancel thr) - { - namespace bgi = boost::geometry::index; - using PointIndexElement = std::pair; - using PointIndex = bgi::rtree>; - - // Centroid of the centroids of islands. This is where the additional - // connector sticks are routed. - Point cc = centroid(centroids); - - PointIndex ctrindex; - unsigned idx = 0; - for(const Point &ct : centroids) - ctrindex.insert(std::make_pair(ct, idx++)); - - m_polys.reserve(m_polys.size() + centroids.size()); - - idx = 0; - for (const Point &c : centroids) { - thr(); - - double dx = c.x() - cc.x(), dy = c.y() - cc.y(); - double l = std::sqrt(dx * dx + dy * dy); - double nx = dx / l, ny = dy / l; - - const Point &ct = centroids[idx]; - - std::vector result; - ctrindex.query(bgi::nearest(ct, 2), std::back_inserter(result)); - - double dist = max_dist; - for (const PointIndexElement &el : result) - if (el.second != idx) { - dist = Line(el.first, ct).length(); - break; - } - - idx++; - - if (dist >= max_dist) return; - - Polygon r; - r.points.reserve(3); - r.points.emplace_back(cc); - - Point d(scaled(nx), scaled(ny)); - r.points.emplace_back(c + Point(-d.y(), d.x())); - r.points.emplace_back(c + Point(d.y(), -d.x())); - offset(r, scaled(1.)); - - m_polys.emplace_back(r); - } - } - -public: - - ConcaveHull(const ExPolygons& polys, double merge_dist, ThrowOnCancel thr) - : ConcaveHull{to_polygons(polys), merge_dist, thr} {} - - ConcaveHull(const Polygons& polys, double mergedist, ThrowOnCancel thr) - { - if(polys.empty()) return; - - m_polys = polys; - merge_polygons(); - - if(m_polys.size() == 1) return; - - Points centroids = calculate_centroids(); - - add_connector_rectangles(centroids, scaled(mergedist), thr); - - merge_polygons(); - } - - // const Polygons & polygons() const { return m_polys; } - - ExPolygons to_expolygons() const - { - auto ret = reserve_vector(m_polys.size()); - for (const Polygon &p : m_polys) ret.emplace_back(ExPolygon(p)); - return ret; - } - - void offset_waffle_style(coord_t delta) { - ClipperLib::Paths paths = Slic3rMultiPoints_to_ClipperPaths(m_polys); - paths = fast_offset(paths, 2 * delta, ClipperLib::jtRound); - paths = fast_offset(paths, -delta, ClipperLib::jtRound); - m_polys = ClipperPaths_to_Slic3rPolygons(paths); - } - - static inline coord_t get_waffle_offset(const PadConfig &c) - { - return scaled(c.brim_size_mm + c.wing_distance()); - } - - static inline double get_merge_distance(const PadConfig &c) - { - return 2. * (1.8 * c.wall_thickness_mm) + c.max_merge_dist_mm; - } -}; +static inline double get_merge_distance(const PadConfig &c) +{ + return 2. * (1.8 * c.wall_thickness_mm) + c.max_merge_dist_mm; +} // Part of the pad configuration that is used for 3D geometry generation struct PadConfig3D { @@ -591,7 +419,7 @@ public: scaled(cfg.embed_object.object_gap_mm), ClipperLib::jtMiter, 1); - ConcaveHull fullcvh = + ExPolygons fullcvh = wafflized_concave_hull(support_blueprint, model_bp_offs, cfg, thr); auto model_bp_sticks = @@ -600,7 +428,7 @@ public: cfg.embed_object.stick_width_mm, cfg.embed_object.stick_penetration_mm); - ExPolygons fullpad = diff_ex(fullcvh.to_expolygons(), model_bp_sticks); + ExPolygons fullpad = diff_ex(fullcvh, model_bp_sticks); remove_redundant_parts(fullpad); @@ -619,7 +447,7 @@ private: // Create the wafflized pad around all object in the scene. This pad doesnt // have any holes yet. - ConcaveHull wafflized_concave_hull(const ExPolygons &supp_bp, + ExPolygons wafflized_concave_hull(const ExPolygons &supp_bp, const ExPolygons &model_bp, const PadConfig &cfg, ThrowOnCancel thr) @@ -629,10 +457,8 @@ private: for (auto &ep : supp_bp) allin.emplace_back(ep.contour); for (auto &ep : model_bp) allin.emplace_back(ep.contour); - ConcaveHull ret{allin, ConcaveHull::get_merge_distance(cfg), thr}; - ret.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg)); - - return ret; + ConcaveHull cchull{allin, get_merge_distance(cfg), thr}; + return offset_waffle_style_ex(cchull, get_waffle_offset(cfg)); } // To remove parts of the pad skeleton which do not host any supports @@ -663,10 +489,9 @@ public: for (auto &ep : support_blueprint) outer.emplace_back(ep.contour); for (auto &ep : model_blueprint) outer.emplace_back(ep.contour); - ConcaveHull ochull{outer, ConcaveHull::get_merge_distance(cfg), thr}; + ConcaveHull ochull{outer, get_merge_distance(cfg), thr}; - ochull.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg)); - outer = ochull.to_expolygons(); + outer = offset_waffle_style_ex(ochull, get_waffle_offset(cfg)); } }; @@ -861,7 +686,7 @@ std::string PadConfig::validate() const if (brim_size_mm < MIN_BRIM_SIZE_MM || bottom_offset() > brim_size_mm + wing_distance() || - ConcaveHull::get_waffle_offset(*this) <= MIN_BRIM_SIZE_MM) + get_waffle_offset(*this) <= MIN_BRIM_SIZE_MM) return L("Pad brim size is too small for the current configuration."); return ""; diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp index b92e44dbd..e953cc136 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp @@ -175,7 +175,7 @@ class SupportTreeBuildsteps { // A spatial index to easily find strong pillars to connect to. PillarIndex m_pillar_index; - + // When bridging heads to pillars... TODO: find a cleaner solution ccr::BlockingMutex m_bridge_mutex; diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 74b1ed8b1..f41fd3200 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -17,6 +17,7 @@ #include "libslic3r/SLA/SLASupportTreeBuildsteps.hpp" #include "libslic3r/SLA/SLAAutoSupports.hpp" #include "libslic3r/SLA/SLARaster.hpp" +#include "libslic3r/SLA/ConcaveHull.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/SVG.hpp" @@ -79,6 +80,43 @@ struct PadByproducts 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) @@ -92,6 +130,8 @@ void test_pad(const std::string & obj_filename, // 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 @@ -257,7 +297,7 @@ void export_failed_case(const std::vector &support_slices, 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"; @@ -268,7 +308,7 @@ void export_failed_case(const std::vector &support_slices, svg.Close(); } } - + TriangleMesh m; byproducts.supporttree.retrieve_full_mesh(m); m.merge(byproducts.input_mesh); @@ -288,7 +328,7 @@ void test_support_model_collision( // 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. @@ -305,7 +345,7 @@ void test_support_model_collision( 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 @@ -320,7 +360,7 @@ void test_support_model_collision( notouch = notouch && intersections.empty(); } - + if (!notouch) export_failed_case(support_slices, byproducts); REQUIRE(notouch); @@ -359,12 +399,12 @@ template void test_pairhash() const I Ibits = int(sizeof(I) * CHAR_BIT); const II IIbits = int(sizeof(II) * CHAR_BIT); - + int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; if (std::is_signed::value) bits -= 1; - const I Imin = std::is_signed::value ? -I(std::pow(2., bits)) : 0; + const I Imin = 0; const I Imax = I(std::pow(2., bits) - 1); - + std::uniform_int_distribution dis(Imin, Imax); for (size_t i = 0; i < nums;) {