diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 9aab3a0eb..63bd0c801 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -271,8 +271,6 @@ ConfigOptionDef* ConfigDef::add_nullable(const t_config_option_key &opt_key, Con return def; } -std::string ConfigOptionDef::nocli = "~~~noCLI"; - std::ostream& ConfigDef::print_cli_help(std::ostream& out, bool show_defaults, std::function filter) const { // prepare a function for wrapping text diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 334593ab5..463f6ef2d 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -1444,7 +1444,7 @@ public: std::vector cli_args(const std::string &key) const; // Assign this key to cli to disable CLI for this option. - static std::string nocli; + static const constexpr char *nocli = "~~~noCLI"; }; // Map from a config option name to its definition. diff --git a/src/libslic3r/SLA/SLAPad.cpp b/src/libslic3r/SLA/SLAPad.cpp index f102f33ff..71f8b1c7f 100644 --- a/src/libslic3r/SLA/SLAPad.cpp +++ b/src/libslic3r/SLA/SLAPad.cpp @@ -216,22 +216,22 @@ ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths, 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; + + Paths result; offs.Execute(result, static_cast(delta)); - + return result; } @@ -249,25 +249,25 @@ void breakstick_holes(Points& pts, { 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) { @@ -277,16 +277,16 @@ void breakstick_holes(Points& pts, 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(); @@ -294,38 +294,31 @@ void breakstick_holes(Points& pts, 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); } -void breakstick_holes(Points &pts, const PadConfig::EmbedObject &c) +template +ExPolygons breakstick_holes(const ExPolygons &input, Args...args) { - breakstick_holes(pts, c.object_gap_mm, c.stick_stride_mm, - c.stick_width_mm, c.stick_penetration_mm); -} - -ExPolygons breakstick_holes(const ExPolygons &input, - const PadConfig::EmbedObject &cfg) -{ - ExPolygons ret = offset_ex(input, scaled(cfg.object_gap_mm), ClipperLib::jtMiter, 1); - + ExPolygons ret = input; for (ExPolygon &p : ret) { - breakstick_holes(p.contour.points, cfg); - for (auto &h : p.holes) breakstick_holes(h.points, cfg); + breakstick_holes(p.contour.points, args...); + for (auto &h : p.holes) breakstick_holes(h.points, args...); } - + return ret; } @@ -335,7 +328,7 @@ ExPolygons breakstick_holes(const ExPolygons &input, /// centroids (a star is created...) class ConcaveHull { Polygons m_polys; - + Point centroid(const Points& pp) const { Point c; @@ -347,7 +340,7 @@ class ConcaveHull { 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); @@ -359,7 +352,7 @@ class ConcaveHull { break; } } - + return c; } @@ -372,10 +365,10 @@ class ConcaveHull { 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, @@ -385,78 +378,78 @@ class ConcaveHull { 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.)); - + 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(); - + + 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()); @@ -470,12 +463,12 @@ public: 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() + c.wall_thickness_mm); + 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; @@ -508,26 +501,26 @@ 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) { if (child->IsHole()) { poly.holes.emplace_back( ClipperPath_to_Slic3rPolygon(child->Contour)); - + traverse_pt_unordered(child->Childs, &ret.inner); } else traverse_pt_unordered(child, &ret.inner); } - + ret.outer.emplace_back(poly); } - + return ret; } @@ -536,32 +529,32 @@ PadSkeleton divide_blueprint(const ExPolygons &bp) 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 @@ -577,7 +570,7 @@ 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, @@ -589,33 +582,41 @@ public: // 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. + // 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); + ConcaveHull fullcvh = - wafflized_concave_hull(support_blueprint, model_blueprint, cfg, thr); - + wafflized_concave_hull(support_blueprint, model_bp_offs, cfg, thr); + auto model_bp_sticks = - breakstick_holes(model_blueprint, cfg.embed_object); - + 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.to_expolygons(), model_bp_sticks); - + remove_redundant_parts(fullpad); - + PadSkeleton divided = divide_blueprint(fullpad); 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. ConcaveHull wafflized_concave_hull(const ExPolygons &supp_bp, @@ -624,16 +625,16 @@ private: 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 ret{allin, ConcaveHull::get_merge_distance(cfg), thr}; ret.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg)); - + return ret; } - + // To remove parts of the pad skeleton which do not host any supports void remove_redundant_parts(ExPolygons &parts) { @@ -641,7 +642,7 @@ private: [this](const ExPolygon &p) { return !m_intersector.intersects(p); }); - + parts.erase(endit, parts.end()); } }; @@ -661,9 +662,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}; - + ochull.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg)); outer = ochull.to_expolygons(); } @@ -673,17 +674,17 @@ public: template ExPolygon offset_contour_only(const ExPolygon &poly, coord_t delta, Args...args) { - ExPolygons tmp = offset_ex(poly.contour, delta, 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(); } @@ -691,17 +692,17 @@ 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(); @@ -721,28 +722,28 @@ Contour3D create_outer_pad_geometry(const ExPolygons & skeleton, 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; - + double z_min = -cfg.height, z_max = 0; ret.merge(walls(top_poly.contour, bottom_poly.contour, z_max, z_min, cfg.bottom_offset(), thr)); - - if (cfg.wing_height > 0. && add_cavity(ret, top_poly, cfg, thr)) + + 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, thr)); - + ret.merge(triangulate_expolygon_3d(bottom_poly, z_min, NORMALS_DOWN)); - ret.merge(triangulate_expolygon_3d(top_poly, NORMALS_UP)); + ret.merge(triangulate_expolygon_3d(top_poly, NORMALS_UP)); } - + return ret; } @@ -751,18 +752,18 @@ Contour3D create_inner_pad_geometry(const ExPolygons & skeleton, ThrowOnCancel thr) { Contour3D ret; - + double z_max = 0., z_min = -cfg.height; for (const ExPolygon &pad_part : skeleton) { ret.merge(straight_walls(pad_part.contour, z_max, z_min,thr)); - + for (auto &h : pad_part.holes) ret.merge(straight_walls(h, z_max, z_min, thr)); - + ret.merge(triangulate_expolygon_3d(pad_part, z_min, NORMALS_DOWN)); ret.merge(triangulate_expolygon_3d(pad_part, z_max, NORMALS_UP)); } - + return ret; } @@ -776,7 +777,7 @@ Contour3D create_pad_geometry(const PadSkeleton &skelet, 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)); @@ -809,13 +810,13 @@ void pad_blueprint(const TriangleMesh & mesh, { if (mesh.empty()) return; TriangleMeshSlicer slicer(&mesh); - + auto out = reserve_vector(heights.size()); slicer.slice(heights, 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) @@ -823,9 +824,9 @@ void pad_blueprint(const TriangleMesh & mesh, 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()); @@ -839,7 +840,7 @@ void pad_blueprint(const TriangleMesh &mesh, ThrowOnCancel thrfn) { float gnd = float(mesh.bounding_box().min(Z)); - + std::vector slicegrid = grid(gnd, gnd + h, layerh); pad_blueprint(mesh, output, slicegrid, thrfn); } @@ -855,10 +856,14 @@ void create_pad(const ExPolygons &sup_blueprint, } std::string PadConfig::validate() const -{ - if (bottom_offset() > brim_size_mm + wing_distance()) - return L("Pad brim size is too low for the current slope."); - +{ + static const double constexpr MIN_BRIM_SIZE_MM = .1; + + if (brim_size_mm < MIN_BRIM_SIZE_MM || + bottom_offset() > brim_size_mm + wing_distance() || + ConcaveHull::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/SLAPad.hpp b/src/libslic3r/SLA/SLAPad.hpp index 96b5834c2..4abcdd281 100644 --- a/src/libslic3r/SLA/SLAPad.hpp +++ b/src/libslic3r/SLA/SLAPad.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Slic3r { @@ -26,8 +27,8 @@ void pad_blueprint( ThrowOnCancel thrfn = [] {}); // Function that throws if cancel was requested void pad_blueprint( - const TriangleMesh &mesh, - ExPolygons & output, + const TriangleMesh &mesh, + ExPolygons & output, float samplingheight = 0.1f, // The height range to sample float layerheight = 0.05f, // The sampling height ThrowOnCancel thrfn = [] {}); @@ -38,7 +39,7 @@ struct PadConfig { double max_merge_dist_mm = 50; double wall_slope = std::atan(1.0); // Universal constant for Pi/4 double brim_size_mm = 1.6; - + struct EmbedObject { double object_gap_mm = 1.; double stick_stride_mm = 10.; @@ -48,7 +49,7 @@ struct PadConfig { bool everywhere = false; operator bool() const { return enabled; } } embed_object; - + inline PadConfig() = default; inline PadConfig(double thickness, double height, @@ -74,10 +75,10 @@ struct PadConfig { { return wall_height_mm + wall_thickness_mm; } - - /// Returns the elevation needed for compensating the pad. + + /// Returns the elevation needed for compensating the pad. inline double required_elevation() const { return wall_thickness_mm; } - + std::string validate() const; }; diff --git a/src/libslic3r/SLA/SLARaster.hpp b/src/libslic3r/SLA/SLARaster.hpp index 23b06c469..e8d6ec747 100644 --- a/src/libslic3r/SLA/SLARaster.hpp +++ b/src/libslic3r/SLA/SLARaster.hpp @@ -15,8 +15,6 @@ namespace ClipperLib { struct Polygon; } namespace Slic3r { namespace sla { -class Raster; - /** * @brief Raster captures an anti-aliased monochrome canvas where vectorial * polygons can be rasterized. Fill color is always white and the background is diff --git a/src/libslic3r/SLA/SLASupportTree.cpp b/src/libslic3r/SLA/SLASupportTree.cpp index 1faf20f34..fea8bf731 100644 --- a/src/libslic3r/SLA/SLASupportTree.cpp +++ b/src/libslic3r/SLA/SLASupportTree.cpp @@ -70,7 +70,7 @@ std::vector SupportTree::slice( auto bb = pad_mesh.bounding_box(); auto maxzit = std::upper_bound(grid.begin(), grid.end(), bb.max.z()); - long cap = grid.end() - maxzit; + auto cap = grid.end() - maxzit; auto padgrid = reserve_vector(size_t(cap > 0 ? cap : 0)); std::copy(grid.begin(), maxzit, std::back_inserter(padgrid)); diff --git a/src/libslic3r/SLA/SLASupportTreeBuilder.hpp b/src/libslic3r/SLA/SLASupportTreeBuilder.hpp index 88ee3ffac..7f38cf280 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuilder.hpp +++ b/src/libslic3r/SLA/SLASupportTreeBuilder.hpp @@ -96,6 +96,8 @@ struct Head { // If there is a pillar connecting to this head, then the id will be set. long pillar_id = ID_UNSET; + long bridge_id = ID_UNSET; + inline void invalidate() { id = ID_UNSET; } inline bool is_valid() const { return id >= 0; } @@ -331,10 +333,16 @@ public: std::lock_guard lk(m_mutex); assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); - if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()) + if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()) m_pillars[size_t(pillar.id)].links++; } + unsigned bridgecount(const Pillar &pillar) const { + std::lock_guard lk(m_mutex); + assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); + return pillar.bridges; + } + template Pillar& add_pillar(Args&&...args) { std::lock_guard lk(m_mutex); diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp index 2404b2cdf..72673babf 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp @@ -345,7 +345,7 @@ EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect( } bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, - const Pillar &nextpillar) + const Pillar &nextpillar) { // We need to get the starting point of the zig-zag pattern. We have to // be aware that the two head junctions are at different heights. We @@ -438,13 +438,14 @@ bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, } bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head, - long nearpillar_id) + long nearpillar_id) { auto nearpillar = [this, nearpillar_id]() { return m_builder.pillar(nearpillar_id); }; - if (nearpillar().bridges > m_cfg.max_bridges_on_pillar) return false; + if (m_builder.bridgecount(nearpillar()) > m_cfg.max_bridges_on_pillar) + return false; Vec3d headjp = head.junction_point(); Vec3d nearjp_u = nearpillar().startpoint(); @@ -502,14 +503,21 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head, // Cannot insert the bridge. (further search might not worth the hassle) if(t < distance(bridgestart, bridgeend)) return false; - // A partial pillar is needed under the starting head. - if(zdiff > 0) { - m_builder.add_pillar(unsigned(head.id), bridgestart, r); - m_builder.add_junction(bridgestart, r); - } + std::lock_guard lk(m_bridge_mutex); - m_builder.add_bridge(bridgestart, bridgeend, r); - m_builder.increment_bridges(nearpillar()); + if (m_builder.bridgecount(nearpillar()) < m_cfg.max_bridges_on_pillar) { + // A partial pillar is needed under the starting head. + if(zdiff > 0) { + m_builder.add_pillar(unsigned(head.id), bridgestart, r); + m_builder.add_junction(bridgestart, r); + } + + auto &br = m_builder.add_bridge(bridgestart, bridgeend, r); + m_builder.increment_bridges(nearpillar()); + if (head.pillar_id == ID_UNSET) + m_builder.head(unsigned(head.id)).bridge_id = br.id; + + } else return false; return true; } @@ -550,9 +558,9 @@ bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &head) } void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, - const Vec3d &sourcedir, - double radius, - long head_id) + const Vec3d &sourcedir, + double radius, + long head_id) { // People were killed for this number (seriously) static const double SQR2 = std::sqrt(2.0); @@ -646,10 +654,8 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, if (normal_mode) { Pillar &plr = head_id >= 0 - ? m_builder.add_pillar(unsigned(head_id), - endp, - radius) - : m_builder.add_pillar(jp, endp, radius); + ? m_builder.add_pillar(unsigned(head_id), endp, radius) + : m_builder.add_pillar(jp, endp, radius); if (can_add_base) plr.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm); @@ -963,7 +969,7 @@ void SupportTreeBuildsteps::routing_to_model() m_builder.add_bridge(hjp, endp, head.r_back_mm); m_builder.add_junction(endp, head.r_back_mm); - this->create_ground_pillar(endp, dir, head.r_back_mm); + this->create_ground_pillar(endp, dir, head.r_back_mm, head.id); }; std::vector modelpillars; @@ -1240,12 +1246,15 @@ void SupportTreeBuildsteps::interconnect_pillars() needpillars = 3; else if (pillar().links < 2 && pillar().height > H2) { // Not enough neighbors to support this pillar - needpillars = 2 - pillar().links; + needpillars = 2; } else if (pillar().links < 1 && pillar().height > H1) { // No neighbors could be found and the pillar is too long. needpillars = 1; } + needpillars = std::max(pillar().links, needpillars) - pillar().links; + if (needpillars == 0) continue; + // Search for new pillar locations: bool found = false; @@ -1318,6 +1327,7 @@ void SupportTreeBuildsteps::interconnect_pillars() newpills.emplace_back(pp.id); m_builder.increment_links(pillar()); + m_builder.increment_links(pp); } } diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp index b9f8db79d..b92e44dbd 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp @@ -32,7 +32,7 @@ long cluster_centroid(const ClusterEl& clust, case 2: /* if two elements, there is no center */ return 0; default: ; } - + // The function works by calculating for each point the average distance // from all the other points in the cluster. We create a selector bitmask of // the same size as the cluster. The bitmask will have two true bits and @@ -40,30 +40,30 @@ long cluster_centroid(const ClusterEl& clust, // permutations of the bitmask (combinations of two points). Get the // distance for the two points and add the distance to the averages. // The point with the smallest average than wins. - + // The complexity should be O(n^2) but we will mostly apply this function // for small clusters only (cca 3 elements) - + std::vector sel(clust.size(), false); // create full zero bitmask std::fill(sel.end() - 2, sel.end(), true); // insert the two ones std::vector avgs(clust.size(), 0.0); // store the average distances - + do { std::array idx; for(size_t i = 0, j = 0; i < clust.size(); i++) if(sel[i]) idx[j++] = i; - + double d = df(pointfn(clust[idx[0]]), pointfn(clust[idx[1]])); - + // add the distance to the sums for both associated points for(auto i : idx) avgs[i] += d; - + // now continue with the next permutation of the bitmask with two 1s } while(std::next_permutation(sel.begin(), sel.end())); - + // Divide by point size in the cluster to get the average (may be redundant) for(auto& a : avgs) a /= clust.size(); - + // get the lowest average distance and return the index auto minit = std::min_element(avgs.begin(), avgs.end()); return long(minit - avgs.begin()); @@ -77,45 +77,45 @@ class PillarIndex { PointIndex m_index; using Mutex = ccr::BlockingMutex; mutable Mutex m_mutex; - + public: - + template inline void guarded_insert(Args&&...args) { std::lock_guard lck(m_mutex); m_index.insert(std::forward(args)...); } - + template inline std::vector guarded_query(Args&&...args) const { std::lock_guard lck(m_mutex); return m_index.query(std::forward(args)...); } - + template inline void insert(Args&&...args) { m_index.insert(std::forward(args)...); } - + template inline std::vector query(Args&&...args) const { return m_index.query(std::forward(args)...); } - + template inline void foreach(Fn fn) { m_index.foreach(fn); } template inline void guarded_foreach(Fn fn) { std::lock_guard lck(m_mutex); m_index.foreach(fn); } - + PointIndex guarded_clone() { std::lock_guard lck(m_mutex); return m_index; - } + } }; // Helper function for pillar interconnection where pairs of already connected @@ -132,13 +132,13 @@ IntegerOnly pairhash(I a, I b) static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; - + I g = min(a, b), l = max(a, b); - + // Assume the hash will fit into the output variable - assert((g ? (ceil(log2(g))) : 0) < shift); - assert((l ? (ceil(log2(l))) : 0) < shift); - + assert((g ? (ceil(log2(g))) : 0) <= shift); + assert((l ? (ceil(log2(l))) : 0) <= shift); + return (DoubleI(g) << shift) + l; } @@ -146,42 +146,45 @@ class SupportTreeBuildsteps { const SupportConfig& m_cfg; const EigenMesh3D& m_mesh; const std::vector& m_support_pts; - + using PtIndices = std::vector; - + PtIndices m_iheads; // support points with pinhead PtIndices m_iheadless; // headless support points - + // supp. pts. connecting to model: point index and the ray hit data std::vector> m_iheads_onmodel; - + // normals for support points from model faces. PointSet m_support_nmls; - + // Clusters of points which can reach the ground directly and can be // bridged to one central pillar std::vector m_pillar_clusters; - + // This algorithm uses the SupportTreeBuilder class to fill gradually // the support elements (heads, pillars, bridges, ...) SupportTreeBuilder& m_builder; - + // support points in Eigen/IGL format PointSet m_points; - + // throw if canceled: It will be called many times so a shorthand will // come in handy. ThrowOnCancel m_thr; - + // 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; + inline double ray_mesh_intersect(const Vec3d& s, const Vec3d& dir) { return m_mesh.query_ray_hit(s, dir).distance(); } - + // This function will test if a future pinhead would not collide with the // model geometry. It does not take a 'Head' object because those are // created after this test. Parameters: s: The touching point on the model @@ -199,7 +202,7 @@ class SupportTreeBuildsteps { double r_pin, double r_back, double width); - + // Checking bridge (pillar and stick as well) intersection with the model. // If the function is used for headless sticks, the ins_check parameter // have to be true as the beginning of the stick might be inside the model @@ -213,37 +216,37 @@ class SupportTreeBuildsteps { const Vec3d& dir, double r, bool ins_check = false); - + // Helper function for interconnecting two pillars with zig-zag bridges. bool interconnect(const Pillar& pillar, const Pillar& nextpillar); - + // For connecting a head to a nearby pillar. bool connect_to_nearpillar(const Head& head, long nearpillar_id); - + bool search_pillar_and_connect(const Head& head); - + // This is a proxy function for pillar creation which will mind the gap // between the pad and the model bottom in zero elevation mode. void create_ground_pillar(const Vec3d &jp, const Vec3d &sourcedir, double radius, - long head_id = ID_UNSET); + long head_id = ID_UNSET); public: SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm); - + // Now let's define the individual steps of the support generation algorithm - + // Filtering step: here we will discard inappropriate support points // and decide the future of the appropriate ones. We will check if a // pinhead is applicable and adjust its angle at each support point. We // will also merge the support points that are just too close and can // be considered as one. void filter(); - + // Pinhead creation: based on the filtering results, the Head objects // will be constructed (together with their triangle meshes). void add_pinheads(); - + // Further classification of the support points with pinheads. If the // ground is directly reachable through a vertical line parallel to the // Z axis we consider a support point as pillar candidate. If touches @@ -253,28 +256,28 @@ public: // these groups may or may not be interconnected. Here we only run the // clustering algorithm. void classify(); - + // Step: Routing the ground connected pinheads, and interconnecting // them with additional (angled) bridges. Not all of these pinheads // will be a full pillar (ground connected). Some will connect to a // nearby pillar using a bridge. The max number of such side-heads for // a central pillar is limited to avoid bad weight distribution. void routing_to_ground(); - + // Step: routing the pinheads that would connect to the model surface // along the Z axis downwards. For now these will actually be connected with // the model surface with a flipped pinhead. In the future here we could use // some smart algorithms to search for a safe path to the ground or to a // nearby pillar that can hold the supported weight. void routing_to_model(); - + void interconnect_pillars(); - + // Step: process the support points where there is not enough space for a // full pinhead. In this case we will use a rounded sphere as a touching // point and use a thinner bridge (let's call it a stick). void routing_headless (); - + inline void merge_result() { m_builder.merged_mesh(); } static bool execute(SupportTreeBuilder & builder, const SupportableMesh &sm); diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 5d847573d..9af2adcc6 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "libslic3r.h" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 06733d054..fcd70d636 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,7 +7,10 @@ set(TEST_DATA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data) file(TO_NATIVE_PATH "${TEST_DATA_DIR}" TEST_DATA_DIR) add_library(test_common INTERFACE) -target_compile_definitions(test_common INTERFACE TEST_DATA_DIR="${TEST_DATA_DIR}") +target_compile_definitions(test_common INTERFACE TEST_DATA_DIR=R"\(${TEST_DATA_DIR}\)") target_link_libraries(test_common INTERFACE GTest::GTest GTest::Main) +if (APPLE) + target_link_libraries(test_common INTERFACE "-liconv -framework IOKit" "-framework CoreFoundation" -lc++) +endif() add_subdirectory(sla_print) diff --git a/tests/sla_print/sla_print_tests_main.cpp b/tests/sla_print/sla_print_tests_main.cpp index 8e78322ce..f1ac8753b 100644 --- a/tests/sla_print/sla_print_tests_main.cpp +++ b/tests/sla_print/sla_print_tests_main.cpp @@ -1,4 +1,6 @@ -#include +#include +#include +#include // Debug #include @@ -18,10 +20,10 @@ #include "libslic3r/SVG.hpp" -#if defined(WIN32) || defined(_WIN32) -#define PATH_SEPARATOR "\\" -#else -#define PATH_SEPARATOR "/" +#if defined(WIN32) || defined(_WIN32) +#define PATH_SEPARATOR R"(\)" +#else +#define PATH_SEPARATOR R"(/)" #endif namespace { @@ -30,7 +32,7 @@ using namespace Slic3r; TriangleMesh load_model(const std::string &obj_filename) { TriangleMesh mesh; - auto fpath = std::string(TEST_DATA_DIR PATH_SEPARATOR) + obj_filename; + auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename; load_obj(fpath.c_str(), &mesh); return mesh; } @@ -46,21 +48,21 @@ void check_validity(const TriangleMesh &input_mesh, ASSUME_NO_REPAIR) { TriangleMesh mesh{input_mesh}; - + if (flags & ASSUME_NO_EMPTY) { ASSERT_FALSE(mesh.empty()); - } else if (mesh.empty()) + } else if (mesh.empty()) return; // If it can be empty and it is, there is nothing left to do. - + ASSERT_TRUE(stl_validate(&mesh.stl)); - + bool do_update_shared_vertices = false; mesh.repair(do_update_shared_vertices); - + if (flags & ASSUME_NO_REPAIR) { ASSERT_FALSE(mesh.needed_repair()); } - + if (flags & ASSUME_MANIFOLD) { mesh.require_shared_vertices(); if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj"); @@ -80,21 +82,21 @@ void test_pad(const std::string & obj_filename, PadByproducts & out) { ASSERT_TRUE(padcfg.validate().empty()); - + TriangleMesh mesh = load_model(obj_filename); - + ASSERT_FALSE(mesh.empty()); - + // Create pad skeleton only from the model Slic3r::sla::pad_blueprint(mesh, out.model_contours); - + ASSERT_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(); ASSERT_DOUBLE_EQ(bb.max.z() - bb.min.z(), padcfg.full_height()); @@ -122,19 +124,25 @@ void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, 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()) { + ASSERT_TRUE(!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) ASSERT_GE(pillar.links, 1); - else if(h > H2) { ASSERT_GE(pillar.links, 2); } + else if(h > H2) { ASSERT_GE(pillar.links, 2); } } - + ASSERT_LE(pillar.links, cfg.pillar_cascade_neighbors); ASSERT_LE(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; @@ -144,15 +152,15 @@ void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, double z = n.z(); double polar = std::acos(z / d); double slope = -polar + PI / 2.; - ASSERT_TRUE(slope >= cfg.bridge_slope || slope <= -cfg.bridge_slope); + ASSERT_GE(std::abs(slope), cfg.bridge_slope - EPSILON); }; - + for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen); ASSERT_LE(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); ASSERT_LE(max_bridgelen, md); } @@ -163,9 +171,9 @@ void test_supports(const std::string & obj_filename, { using namespace Slic3r; TriangleMesh mesh = load_model(obj_filename); - + ASSERT_FALSE(mesh.empty()); - + TriangleMeshSlicer slicer{&mesh}; auto bb = mesh.bounding_box(); @@ -176,50 +184,50 @@ void test_supports(const std::string & obj_filename, 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::SLAAutoSupports::Config autogencfg; autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); sla::SLAAutoSupports 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 (supportcfg.object_elevation_mm < EPSILON) { sla::remove_bottom_points(support_points, zmin, - supportcfg.base_height_mm); + supportcfg.base_height_mm); } else { // Should be support points at least on the bottom of the model ASSERT_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(); ASSERT_DOUBLE_EQ(obb.min.z(), zmin - supportcfg.object_elevation_mm); ASSERT_LE(obb.max.z(), zmax); - + // Move out the support tree into the byproducts, we can examine it further // in various tests. out.supporttree = std::move(treebuilder); @@ -237,31 +245,31 @@ void test_support_model_collision( 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.1; - + 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 ASSERT_EQ(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(); } - + ASSERT_TRUE(notouch); } @@ -283,25 +291,52 @@ const char *const SUPPORT_TEST_MODELS[] = { } // namespace +// Test pair hash for 'nums' random number pairs. template void test_pairhash() { - std::map > ints; - for (I i = 0; i < 1000; ++i) - for (I j = 0; j < 1000; ++j) { - if (j != i) { - II hash_ij = sla::pairhash(i, j); - II hash_ji = sla::pairhash(j, i); - ASSERT_EQ(hash_ij, hash_ji); - - auto it = ints.find(hash_ij); - - if (it != ints.end()) { - ASSERT_TRUE( - (it->second.first == i && it->second.second == j) || - (it->second.first == j && it->second.second == i)); - } else ints[hash_ij] = std::make_pair(i, j); - } - } + const constexpr size_t nums = 1000; + I A[nums] = {0}, B[nums] = {0}; + std::unordered_set CH; + std::unordered_map> ints; + + std::random_device rd; + std::mt19937 gen(rd()); + + const I Ibits = int(sizeof(I) * CHAR_BIT); + const II IIbits = int(sizeof(II) * CHAR_BIT); + const int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; + + const I Imax = I(std::pow(2., bits) - 1); + std::uniform_int_distribution dis(0, Imax); + + for (size_t i = 0; i < nums;) { + I a = dis(gen); + if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } + } + + for (size_t i = 0; i < nums;) { + I b = dis(gen); + if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } + } + + for (size_t i = 0; i < nums; ++i) { + I a = A[i], b = B[i]; + + ASSERT_TRUE(a != b); + + II hash_ab = sla::pairhash(a, b); + II hash_ba = sla::pairhash(b, a); + ASSERT_EQ(hash_ab, hash_ba); + + auto it = ints.find(hash_ab); + + if (it != ints.end()) { + ASSERT_TRUE( + (it->second.first == a && it->second.second == b) || + (it->second.first == b && it->second.second == a)); + } else + ints[hash_ab] = std::make_pair(a, b); + } } TEST(SLASupportGeneration, PillarPairHashShouldBeUnique) { @@ -312,61 +347,61 @@ TEST(SLASupportGeneration, PillarPairHashShouldBeUnique) { TEST(SLASupportGeneration, FlatPadGeometryIsValid) { sla::PadConfig padcfg; - + // Disable wings padcfg.wall_height_mm = .0; - + for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST(SLASupportGeneration, WingedPadGeometryIsValid) { sla::PadConfig padcfg; - + // Add some wings to the pad to test the cavity padcfg.wall_height_mm = 1.; - + for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST(SLASupportGeneration, FlatPadAroundObjectIsValid) { sla::PadConfig padcfg; - + // Add some wings to the pad to test the cavity padcfg.wall_height_mm = 0.; // padcfg.embed_object.stick_stride_mm = 0.; padcfg.embed_object.enabled = true; padcfg.embed_object.everywhere = true; - + for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST(SLASupportGeneration, WingedPadAroundObjectIsValid) { sla::PadConfig padcfg; - + // Add some wings to the pad to test the cavity padcfg.wall_height_mm = 1.; padcfg.embed_object.enabled = true; padcfg.embed_object.everywhere = true; - + for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST(SLASupportGeneration, ElevatedSupportGeometryIsValid) { sla::SupportConfig supportcfg; supportcfg.object_elevation_mm = 5.; - + for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname); } TEST(SLASupportGeneration, FloorSupportGeometryIsValid) { sla::SupportConfig supportcfg; supportcfg.object_elevation_mm = 0; - + for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg); } TEST(SLASupportGeneration, SupportsDoNotPierceModel) { - + sla::SupportConfig supportcfg; for (auto fname : SUPPORT_TEST_MODELS) @@ -382,7 +417,7 @@ TEST(SLARasterOutput, InitializedRasterShouldBeNONEmpty) { // Default Prusa SL1 display parameters sla::Raster::Resolution res{2560, 1440}; sla::Raster::PixelDim pixdim{120. / res.width_px, 68. / res.height_px}; - + sla::Raster raster; raster.reset(res, pixdim); ASSERT_FALSE(raster.empty()); @@ -404,54 +439,54 @@ static void check_raster_transformations(sla::Raster::Orientation o, double disp_w = 120., disp_h = 68.; sla::Raster::Resolution res{2560, 1440}; sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px}; - + auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)}); sla::Raster::Trafo trafo{o, mirroring}; trafo.origin_x = bb.center().x(); trafo.origin_y = bb.center().y(); - + sla::Raster raster{res, pixdim, trafo}; - + // create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors) coord_t pw = 32 * coord_t(std::ceil(scaled(pixdim.w_mm))); coord_t ph = 32 * coord_t(std::ceil(scaled(pixdim.h_mm))); ExPolygon box; box.contour.points = {{-pw, -ph}, {pw, -ph}, {pw, ph}, {-pw, ph}}; - + double tr_x = scaled(20.), tr_y = tr_x; - + box.translate(tr_x, tr_y); ExPolygon expected_box = box; - + // Now calculate the position of the translated box according to output // trafo. if (o == sla::Raster::Orientation::roPortrait) expected_box.rotate(PI / 2.); - + if (mirroring[X]) for (auto &p : expected_box.contour.points) p.x() = -p.x(); - + if (mirroring[Y]) for (auto &p : expected_box.contour.points) p.y() = -p.y(); - + raster.draw(box); - + Point expected_coords = expected_box.contour.bounding_box().center(); double rx = unscaled(expected_coords.x() + bb.center().x()) / pixdim.w_mm; double ry = unscaled(expected_coords.y() + bb.center().y()) / pixdim.h_mm; auto w = size_t(std::floor(rx)); auto h = res.height_px - size_t(std::floor(ry)); - + ASSERT_TRUE(w < res.width_px && h < res.height_px); - + auto px = raster.read_pixel(w, h); - + if (px != FullWhite) { sla::PNGImage img; std::fstream outf("out.png", std::ios::out); - - outf << img.serialize(raster); + + outf << img.serialize(raster); } - + ASSERT_EQ(px, FullWhite); } @@ -472,7 +507,7 @@ static ExPolygon square_with_hole(double v) { ExPolygon poly; coord_t V = scaled(v / 2.); - + poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}}; poly.holes.emplace_back(); V = V / 2; @@ -488,16 +523,16 @@ static double pixel_area(TPixel px, const sla::Raster::PixelDim &pxdim) static double raster_white_area(const sla::Raster &raster) { if (raster.empty()) return std::nan(""); - + auto res = raster.resolution(); double a = 0; - + for (size_t x = 0; x < res.width_px; ++x) for (size_t y = 0; y < res.height_px; ++y) { auto px = raster.read_pixel(x, y); a += pixel_area(px, raster.pixel_dimensions()); } - + return a; } @@ -505,15 +540,15 @@ static double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd) { auto lines = p.lines(); double pix_err = pixel_area(FullWhite, pd) / 2.; - + // Worst case is when a line is parallel to the shorter axis of one pixel, // when the line will be composed of the max number of pixels double pix_l = std::min(pd.h_mm, pd.w_mm); - + double error = 0.; for (auto &l : lines) error += (unscaled(l.length()) / pix_l) * pix_err; - + return error; } @@ -524,26 +559,26 @@ TEST(SLARasterOutput, RasterizedPolygonAreaShouldMatch) { sla::Raster raster{res, pixdim}; auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)}); - + ExPolygon poly = square_with_hole(10.); poly.translate(bb.center().x(), bb.center().y()); raster.draw(poly); - + double a = poly.area() / (scaled(1.) * scaled(1.)); double ra = raster_white_area(raster); double diff = std::abs(a - ra); - + ASSERT_LE(diff, predict_error(poly, pixdim)); - + raster.clear(); poly = square_with_hole(60.); poly.translate(bb.center().x(), bb.center().y()); raster.draw(poly); - + a = poly.area() / (scaled(1.) * scaled(1.)); ra = raster_white_area(raster); diff = std::abs(a - ra); - + ASSERT_LE(diff, predict_error(poly, pixdim)); }