#include #include #include #include #include "ConcaveHull.hpp" #include "boost/log/trivial.hpp" #include "ClipperUtils.hpp" #include "Tesselate.hpp" #include "MTUtils.hpp" #include "TriangulateWall.hpp" // For debugging: // #include // #include #include "SVG.hpp" #include "I18N.hpp" #include //! macro used to mark string used at localization, //! return same string #define L(s) Slic3r::I18N::translate(s) namespace Slic3r { namespace sla { namespace { Contour3D walls( const Polygon &lower, const Polygon &upper, double lower_z_mm, double upper_z_mm) { Wall w = triangulate_wall(lower, upper, lower_z_mm, upper_z_mm); Contour3D ret; ret.points = std::move(w.first); ret.faces3 = std::move(w.second); return ret; } // Same as walls() but with identical higher and lower polygons. Contour3D inline straight_walls(const Polygon &plate, double lo_z, double hi_z) { return walls(plate, plate, lo_z, hi_z); } // 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 // will have a with about "stick_width". The input dimensions are in world // measure, not the scaled clipper units. void breakstick_holes(Points& pts, double padding, double stride, double stick_width, double penetration) { if(stride <= EPSILON || stick_width <= EPSILON || padding <= EPSILON) return; // SVG svg("bridgestick_plate.svg"); // svg.draw(poly); // The connector stick will be a small rectangle with dimensions // stick_width x (penetration + padding) to have some penetration // into the input polygon. Points out; out.reserve(2 * pts.size()); // output polygon points // stick bottom and right edge dimensions double sbottom = scaled(stick_width); double sright = scaled(penetration + padding); // scaled stride distance double sstride = scaled(stride); double t = 0; // process pairs of vertices as an edge, start with the last and // first point for (size_t i = pts.size() - 1, j = 0; j < pts.size(); i = j, ++j) { // Get vertices and the direction vectors const Point &a = pts[i], &b = pts[j]; Vec2d dir = b.cast() - a.cast(); double nrm = dir.norm(); dir /= nrm; Vec2d dirp(-dir(Y), dir(X)); // Insert start point out.emplace_back(a); // dodge the start point, do not make sticks on the joins while (t < sbottom) t += sbottom; double tend = nrm - sbottom; while (t < tend) { // insert the stick on the polygon perimeter // calculate the stick rectangle vertices and insert them // into the output. Point p1 = a + (t * dir).cast(); Point p2 = p1 + (sright * dirp).cast(); Point p3 = p2 + (sbottom * dir).cast(); Point p4 = p3 + (sright * -dirp).cast(); out.insert(out.end(), {p1, p2, p3, p4}); // continue along the perimeter t += sstride; } t = t - nrm; // Insert edge endpoint out.emplace_back(b); } // move the new points out.shrink_to_fit(); pts.swap(out); } template ExPolygons breakstick_holes(const ExPolygons &input, Args...args) { ExPolygons ret = input; for (ExPolygon &p : ret) { breakstick_holes(p.contour.points, args...); for (auto &h : p.holes) breakstick_holes(h.points, args...); } return ret; } 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; } // Part of the pad configuration that is used for 3D geometry generation struct PadConfig3D { double thickness, height, wing_height, slope; explicit PadConfig3D(const PadConfig &cfg2d) : thickness{cfg2d.wall_thickness_mm} , height{cfg2d.full_height()} , wing_height{cfg2d.wall_height_mm} , slope{cfg2d.wall_slope} {} inline double bottom_offset() const { return (thickness + wing_height) / std::tan(slope); } }; // Outer part of the skeleton is used to generate the waffled edges of the pad. // Inner parts will not be waffled or offsetted. Inner parts are only used if // pad is generated around the object and correspond to holes and inner polygons // in the model blueprint. struct PadSkeleton { ExPolygons inner, outer; }; PadSkeleton divide_blueprint(const ExPolygons &bp) { ClipperLib::PolyTree ptree = union_pt(bp); PadSkeleton ret; ret.inner.reserve(size_t(ptree.Total())); ret.outer.reserve(size_t(ptree.Total())); for (ClipperLib::PolyTree::PolyNode *node : ptree.Childs) { ExPolygon poly(ClipperPath_to_Slic3rPolygon(node->Contour)); for (ClipperLib::PolyTree::PolyNode *child : node->Childs) { poly.holes.emplace_back( ClipperPath_to_Slic3rPolygon(child->Contour)); traverse_pt(child->Childs, &ret.inner); } ret.outer.emplace_back(poly); } return ret; } // A helper class for storing polygons and maintaining a spatial index of their // bounding boxes. class Intersector { BoxIndex m_index; ExPolygons m_polys; public: // Add a new polygon to the index void add(const ExPolygon &ep) { m_polys.emplace_back(ep); m_index.insert(BoundingBox{ep}, unsigned(m_index.size())); } // Check an arbitrary polygon for intersection with the indexed polygons bool intersects(const ExPolygon &poly) { // Create a suitable query bounding box. auto bb = poly.contour.bounding_box(); std::vector qres = m_index.query(bb, BoxIndex::qtIntersects); // Now check intersections on the actual polygons (not just the boxes) bool is_overlap = false; auto qit = qres.begin(); while (!is_overlap && qit != qres.end()) is_overlap = is_overlap || poly.overlaps(m_polys[(qit++)->second]); return is_overlap; } }; // This dummy intersector to implement the "force pad everywhere" feature struct DummyIntersector { inline void add(const ExPolygon &) {} inline bool intersects(const ExPolygon &) { return true; } }; template class _AroundPadSkeleton : public PadSkeleton { // A spatial index used to be able to efficiently find intersections of // support polygons with the model polygons. _Intersector m_intersector; public: _AroundPadSkeleton(const ExPolygons &support_blueprint, const ExPolygons &model_blueprint, const PadConfig & cfg, ThrowOnCancel thr) { // We need to merge the support and the model contours in a special // way in which the model contours have to be substracted from the // support contours. The pad has to have a hole in which the model can // fit perfectly (thus the substraction -- diff_ex). Also, the pad has // to be eliminated from areas where there is no need for a pad, due // to missing supports. add_supports_to_index(support_blueprint); auto model_bp_offs = offset_ex(model_blueprint, scaled(cfg.embed_object.object_gap_mm), ClipperLib::jtMiter, 1); ExPolygons fullcvh = wafflized_concave_hull(support_blueprint, model_bp_offs, cfg, thr); auto model_bp_sticks = breakstick_holes(model_bp_offs, cfg.embed_object.object_gap_mm, cfg.embed_object.stick_stride_mm, cfg.embed_object.stick_width_mm, cfg.embed_object.stick_penetration_mm); ExPolygons fullpad = diff_ex(fullcvh, model_bp_sticks); PadSkeleton divided = divide_blueprint(fullpad); remove_redundant_parts(divided.outer); remove_redundant_parts(divided.inner); outer = std::move(divided.outer); inner = std::move(divided.inner); } private: // Add the support blueprint to the search index to be queried later void add_supports_to_index(const ExPolygons &supp_bp) { for (auto &ep : supp_bp) m_intersector.add(ep); } // Create the wafflized pad around all object in the scene. This pad doesnt // have any holes yet. ExPolygons wafflized_concave_hull(const ExPolygons &supp_bp, const ExPolygons &model_bp, const PadConfig &cfg, ThrowOnCancel thr) { auto allin = reserve_vector(supp_bp.size() + model_bp.size()); for (auto &ep : supp_bp) allin.emplace_back(ep.contour); for (auto &ep : model_bp) allin.emplace_back(ep.contour); 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 void remove_redundant_parts(ExPolygons &parts) { auto endit = std::remove_if(parts.begin(), parts.end(), [this](const ExPolygon &p) { return !m_intersector.intersects(p); }); parts.erase(endit, parts.end()); } }; using AroundPadSkeleton = _AroundPadSkeleton; using BrimPadSkeleton = _AroundPadSkeleton; class BelowPadSkeleton : public PadSkeleton { public: BelowPadSkeleton(const ExPolygons &support_blueprint, const ExPolygons &model_blueprint, const PadConfig & cfg, ThrowOnCancel thr) { outer.reserve(support_blueprint.size() + model_blueprint.size()); for (auto &ep : support_blueprint) outer.emplace_back(ep.contour); for (auto &ep : model_blueprint) outer.emplace_back(ep.contour); ConcaveHull ochull{outer, get_merge_distance(cfg), thr}; outer = offset_waffle_style_ex(ochull, get_waffle_offset(cfg)); } }; // Offset the contour only, leave the holes untouched template ExPolygon offset_contour_only(const ExPolygon &poly, coord_t delta, Args...args) { ExPolygons tmp = offset_ex(poly.contour, float(delta), args...); if (tmp.empty()) return {}; Polygons holes = poly.holes; for (auto &h : holes) h.reverse(); tmp = diff_ex(to_polygons(tmp), holes); if (tmp.empty()) return {}; return tmp.front(); } bool add_cavity(Contour3D &pad, ExPolygon &top_poly, const PadConfig3D &cfg, ThrowOnCancel thr) { auto logerr = []{BOOST_LOG_TRIVIAL(error)<<"Could not create pad cavity";}; double wing_distance = cfg.wing_height / std::tan(cfg.slope); coord_t delta_inner = -scaled(cfg.thickness + wing_distance); coord_t delta_middle = -scaled(cfg.thickness); ExPolygon inner_base = offset_contour_only(top_poly, delta_inner); ExPolygon middle_base = offset_contour_only(top_poly, delta_middle); if (inner_base.empty() || middle_base.empty()) { logerr(); return false; } ExPolygons pdiff = diff_ex(top_poly, middle_base.contour); if (pdiff.size() != 1) { logerr(); return false; } top_poly = pdiff.front(); double z_min = -cfg.wing_height, z_max = 0; pad.merge(walls(inner_base.contour, middle_base.contour, z_min, z_max)); thr(); pad.merge(triangulate_expolygon_3d(inner_base, z_min, NORMALS_UP)); return true; } Contour3D create_outer_pad_geometry(const ExPolygons & skeleton, const PadConfig3D &cfg, ThrowOnCancel thr) { Contour3D ret; for (const ExPolygon &pad_part : skeleton) { ExPolygon top_poly{pad_part}; ExPolygon bottom_poly = offset_contour_only(pad_part, -scaled(cfg.bottom_offset())); if (bottom_poly.empty()) continue; thr(); double z_min = -cfg.height, z_max = 0; ret.merge(walls(top_poly.contour, bottom_poly.contour, z_max, z_min)); if (cfg.wing_height > 0. && add_cavity(ret, top_poly, cfg, thr)) z_max = -cfg.wing_height; for (auto &h : bottom_poly.holes) ret.merge(straight_walls(h, z_max, z_min)); ret.merge(triangulate_expolygon_3d(bottom_poly, z_min, NORMALS_DOWN)); ret.merge(triangulate_expolygon_3d(top_poly, NORMALS_UP)); } return ret; } Contour3D create_inner_pad_geometry(const ExPolygons & skeleton, const PadConfig3D &cfg, ThrowOnCancel thr) { Contour3D ret; double z_max = 0., z_min = -cfg.height; for (const ExPolygon &pad_part : skeleton) { thr(); ret.merge(straight_walls(pad_part.contour, z_max, z_min)); for (auto &h : pad_part.holes) ret.merge(straight_walls(h, z_max, z_min)); ret.merge(triangulate_expolygon_3d(pad_part, z_min, NORMALS_DOWN)); ret.merge(triangulate_expolygon_3d(pad_part, z_max, NORMALS_UP)); } return ret; } Contour3D create_pad_geometry(const PadSkeleton &skelet, const PadConfig & cfg, ThrowOnCancel thr) { #ifndef NDEBUG SVG svg("pad_skeleton.svg"); svg.draw(skelet.outer, "green"); svg.draw(skelet.inner, "blue"); svg.Close(); #endif PadConfig3D cfg3d(cfg); return create_outer_pad_geometry(skelet.outer, cfg3d, thr) .merge(create_inner_pad_geometry(skelet.inner, cfg3d, thr)); } Contour3D create_pad_geometry(const ExPolygons &supp_bp, const ExPolygons &model_bp, const PadConfig & cfg, ThrowOnCancel thr) { PadSkeleton skelet; if (cfg.embed_object.enabled) { if (cfg.embed_object.everywhere) skelet = BrimPadSkeleton(supp_bp, model_bp, cfg, thr); else skelet = AroundPadSkeleton(supp_bp, model_bp, cfg, thr); } else skelet = BelowPadSkeleton(supp_bp, model_bp, cfg, thr); return create_pad_geometry(skelet, cfg, thr); } } // namespace void pad_blueprint(const TriangleMesh & mesh, ExPolygons & output, const std::vector &heights, ThrowOnCancel thrfn) { if (mesh.empty()) return; TriangleMeshSlicer slicer(&mesh); auto out = reserve_vector(heights.size()); slicer.slice(heights, SlicingMode::Regular, 0.f, &out, thrfn); size_t count = 0; for(auto& o : out) count += o.size(); // Unification is expensive, a simplify also speeds up the pad generation auto tmp = reserve_vector(count); for(ExPolygons& o : out) for(ExPolygon& e : o) { auto&& exss = e.simplify(scaled(0.1)); for(ExPolygon& ep : exss) tmp.emplace_back(std::move(ep)); } ExPolygons utmp = union_ex(tmp); for(auto& o : utmp) { auto&& smp = o.simplify(scaled(0.1)); output.insert(output.end(), smp.begin(), smp.end()); } } void pad_blueprint(const TriangleMesh &mesh, ExPolygons & output, float h, float layerh, ThrowOnCancel thrfn) { float gnd = float(mesh.bounding_box().min(Z)); std::vector slicegrid = grid(gnd, gnd + h, layerh); pad_blueprint(mesh, output, slicegrid, thrfn); } void create_pad(const ExPolygons &sup_blueprint, const ExPolygons &model_blueprint, TriangleMesh & out, const PadConfig & cfg, ThrowOnCancel thr) { Contour3D t = create_pad_geometry(sup_blueprint, model_blueprint, cfg, thr); out.merge(to_triangle_mesh(std::move(t))); } std::string PadConfig::validate() const { static const double constexpr MIN_BRIM_SIZE_MM = .1; if (brim_size_mm < MIN_BRIM_SIZE_MM || bottom_offset() > brim_size_mm + wing_distance() || get_waffle_offset(*this) <= MIN_BRIM_SIZE_MM) return L("Pad brim size is too small for the current configuration."); return ""; } }} // namespace Slic3r::sla