diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index 95ff990aa..d41b4c13a 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -136,6 +136,7 @@ add_library(libslic3r STATIC ${LIBDIR}/libslic3r/Line.hpp ${LIBDIR}/libslic3r/Model.cpp ${LIBDIR}/libslic3r/Model.hpp + ${LIBDIR}/libslic3r/ModelArrange.hpp ${LIBDIR}/libslic3r/MotionPlanner.cpp ${LIBDIR}/libslic3r/MotionPlanner.hpp ${LIBDIR}/libslic3r/MultiPoint.cpp @@ -729,6 +730,7 @@ set(LIBNEST2D_UNITTESTS ON CACHE BOOL "Force generating unittests for libnest2d" add_subdirectory(${LIBDIR}/libnest2d) target_include_directories(libslic3r PUBLIC BEFORE ${LIBNEST2D_INCLUDES}) +target_include_directories(libslic3r_gui PUBLIC BEFORE ${LIBNEST2D_INCLUDES}) message(STATUS "Libnest2D Libraries: ${LIBNEST2D_LIBRARIES}") target_link_libraries(libslic3r ${LIBNEST2D_LIBRARIES}) diff --git a/xs/src/libnest2d/cmake_modules/DownloadNLopt.cmake b/xs/src/libnest2d/cmake_modules/DownloadNLopt.cmake index 814213b38..0f5392596 100644 --- a/xs/src/libnest2d/cmake_modules/DownloadNLopt.cmake +++ b/xs/src/libnest2d/cmake_modules/DownloadNLopt.cmake @@ -27,5 +27,6 @@ set(NLOPT_LINK_PYTHON OFF CACHE BOOL "" FORCE) add_subdirectory(${nlopt_SOURCE_DIR} ${nlopt_BINARY_DIR}) set(NLopt_LIBS nlopt) -set(NLopt_INCLUDE_DIR ${nlopt_BINARY_DIR}) +set(NLopt_INCLUDE_DIR ${nlopt_BINARY_DIR} + ${nlopt_BINARY_DIR}/src/api) set(SHARED_LIBS_STATE ${SHARED_STATE}) \ No newline at end of file diff --git a/xs/src/libnest2d/examples/main.cpp b/xs/src/libnest2d/examples/main.cpp index a97618578..d6b2ccc34 100644 --- a/xs/src/libnest2d/examples/main.cpp +++ b/xs/src/libnest2d/examples/main.cpp @@ -545,59 +545,128 @@ void arrangeRectangles() { // input.insert(input.end(), crasher.begin(), crasher.end()); Box bin(250*SCALE, 210*SCALE); +// PolygonImpl bin = { +// { +// {25*SCALE, 0}, +// {0, 25*SCALE}, +// {0, 225*SCALE}, +// {25*SCALE, 250*SCALE}, +// {225*SCALE, 250*SCALE}, +// {250*SCALE, 225*SCALE}, +// {250*SCALE, 25*SCALE}, +// {225*SCALE, 0}, +// {25*SCALE, 0} +// }, +// {} +// }; auto min_obj_distance = static_cast(0*SCALE); - using Placer = NfpPlacer; + using Placer = strategies::_NofitPolyPlacer; using Packer = Arranger; Packer arrange(bin, min_obj_distance); Packer::PlacementConfig pconf; pconf.alignment = Placer::Config::Alignment::CENTER; - pconf.starting_point = Placer::Config::Alignment::BOTTOM_LEFT; + pconf.starting_point = Placer::Config::Alignment::CENTER; pconf.rotations = {0.0/*, Pi/2.0, Pi, 3*Pi/2*/}; + pconf.accuracy = 0.5f; - double norm_2 = std::nan(""); - pconf.object_function = [&bin, &norm_2](Placer::Pile pile, const Item& item, - double /*area*/, double norm, double penality) { +// auto bincenter = ShapeLike::boundingBox(bin).center(); +// pconf.object_function = [&bin, bincenter]( +// Placer::Pile pile, const Item& item, +// double /*area*/, double norm, double penality) { - using pl = PointLike; +// using pl = PointLike; - auto bb = ShapeLike::boundingBox(pile); - auto ibb = item.boundingBox(); - auto minc = ibb.minCorner(); - auto maxc = ibb.maxCorner(); +// static const double BIG_ITEM_TRESHOLD = 0.2; +// static const double GRAVITY_RATIO = 0.5; +// static const double DENSITY_RATIO = 1.0 - GRAVITY_RATIO; - if(std::isnan(norm_2)) norm_2 = pow(norm, 2); +// // We will treat big items (compared to the print bed) differently +// NfpPlacer::Pile bigs; +// bigs.reserve(pile.size()); +// for(auto& p : pile) { +// auto pbb = ShapeLike::boundingBox(p); +// auto na = std::sqrt(pbb.width()*pbb.height())/norm; +// if(na > BIG_ITEM_TRESHOLD) bigs.emplace_back(p); +// } - // We get the distance of the reference point from the center of the - // heat bed - auto cc = bb.center(); - auto top_left = PointImpl{getX(minc), getY(maxc)}; - auto bottom_right = PointImpl{getX(maxc), getY(minc)}; +// // Candidate item bounding box +// auto ibb = item.boundingBox(); - auto a = pl::distance(ibb.maxCorner(), cc); - auto b = pl::distance(ibb.minCorner(), cc); - auto c = pl::distance(ibb.center(), cc); - auto d = pl::distance(top_left, cc); - auto e = pl::distance(bottom_right, cc); +// // Calculate the full bounding box of the pile with the candidate item +// pile.emplace_back(item.transformedShape()); +// auto fullbb = ShapeLike::boundingBox(pile); +// pile.pop_back(); - auto area = bb.width() * bb.height() / norm_2; +// // The bounding box of the big items (they will accumulate in the center +// // of the pile +// auto bigbb = bigs.empty()? fullbb : ShapeLike::boundingBox(bigs); - auto min_dist = std::min({a, b, c, d, e}) / norm; +// // The size indicator of the candidate item. This is not the area, +// // but almost... +// auto itemnormarea = std::sqrt(ibb.width()*ibb.height())/norm; - // The score will be the normalized distance which will be minimized, - // effectively creating a circle shaped pile of items - double score = 0.8*min_dist + 0.2*area; +// // Will hold the resulting score +// double score = 0; - // If it does not fit into the print bed we will beat it - // with a large penality. If we would not do this, there would be only - // one big pile that doesn't care whether it fits onto the print bed. - if(!NfpPlacer::wouldFit(bb, bin)) score = 2*penality - score; +// if(itemnormarea > BIG_ITEM_TRESHOLD) { +// // This branch is for the bigger items.. +// // Here we will use the closest point of the item bounding box to +// // the already arranged pile. So not the bb center nor the a choosen +// // corner but whichever is the closest to the center. This will +// // prevent unwanted strange arrangements. - return score; - }; +// auto minc = ibb.minCorner(); // bottom left corner +// auto maxc = ibb.maxCorner(); // top right corner + +// // top left and bottom right corners +// auto top_left = PointImpl{getX(minc), getY(maxc)}; +// auto bottom_right = PointImpl{getX(maxc), getY(minc)}; + +// auto cc = fullbb.center(); // The gravity center + +// // Now the distnce of the gravity center will be calculated to the +// // five anchor points and the smallest will be chosen. +// std::array dists; +// dists[0] = pl::distance(minc, cc); +// dists[1] = pl::distance(maxc, cc); +// dists[2] = pl::distance(ibb.center(), cc); +// dists[3] = pl::distance(top_left, cc); +// dists[4] = pl::distance(bottom_right, cc); + +// auto dist = *(std::min_element(dists.begin(), dists.end())) / norm; + +// // Density is the pack density: how big is the arranged pile +// auto density = std::sqrt(fullbb.width()*fullbb.height()) / norm; + +// // The score is a weighted sum of the distance from pile center +// // and the pile size +// score = GRAVITY_RATIO * dist + DENSITY_RATIO * density; + +// } else if(itemnormarea < BIG_ITEM_TRESHOLD && bigs.empty()) { +// // If there are no big items, only small, we should consider the +// // density here as well to not get silly results +// auto bindist = pl::distance(ibb.center(), bincenter) / norm; +// auto density = std::sqrt(fullbb.width()*fullbb.height()) / norm; +// score = GRAVITY_RATIO * bindist + DENSITY_RATIO * density; +// } else { +// // Here there are the small items that should be placed around the +// // already processed bigger items. +// // No need to play around with the anchor points, the center will be +// // just fine for small items +// score = pl::distance(ibb.center(), bigbb.center()) / norm; +// } + +// // If it does not fit into the print bed we will beat it +// // with a large penality. If we would not do this, there would be only +// // one big pile that doesn't care whether it fits onto the print bed. +// if(!NfpPlacer::wouldFit(fullbb, bin)) score = 2*penality - score; + +// return score; +// }; Packer::SelectionConfig sconf; // sconf.allow_parallel = false; @@ -638,7 +707,7 @@ void arrangeRectangles() { std::vector eff; eff.reserve(result.size()); - auto bin_area = double(bin.height()*bin.width()); + auto bin_area = ShapeLike::area(bin); for(auto& r : result) { double a = 0; std::for_each(r.begin(), r.end(), [&a] (Item& e ){ a += e.area(); }); @@ -673,7 +742,7 @@ void arrangeRectangles() { SVGWriter::Config conf; conf.mm_in_coord_units = SCALE; SVGWriter svgw(conf); - svgw.setSize(bin); + svgw.setSize(Box(250*SCALE, 210*SCALE)); svgw.writePackGroup(result); // std::for_each(input.begin(), input.end(), [&svgw](Item& item){ svgw.writeItem(item);}); svgw.save("out"); diff --git a/xs/src/libnest2d/libnest2d.h b/xs/src/libnest2d/libnest2d.h index e0ad05c41..c9e21ecfb 100644 --- a/xs/src/libnest2d/libnest2d.h +++ b/xs/src/libnest2d/libnest2d.h @@ -6,7 +6,7 @@ #include // We include the stock optimizers for local and global optimization -#include // Local simplex for NfpPlacer +#include // Local subplex for NfpPlacer #include // Genetic for min. bounding box #include diff --git a/xs/src/libnest2d/libnest2d/boost_alg.hpp b/xs/src/libnest2d/libnest2d/boost_alg.hpp index 422616d20..67e19fcbd 100644 --- a/xs/src/libnest2d/libnest2d/boost_alg.hpp +++ b/xs/src/libnest2d/libnest2d/boost_alg.hpp @@ -358,7 +358,7 @@ inline double ShapeLike::area(const PolygonImpl& shape) #endif template<> -inline bool ShapeLike::isInside(const PointImpl& point, +inline bool ShapeLike::isInside(const PointImpl& point, const PolygonImpl& shape) { return boost::geometry::within(point, shape); diff --git a/xs/src/libnest2d/libnest2d/geometry_traits.hpp b/xs/src/libnest2d/libnest2d/geometry_traits.hpp index 568c0a766..058c47cd4 100644 --- a/xs/src/libnest2d/libnest2d/geometry_traits.hpp +++ b/xs/src/libnest2d/libnest2d/geometry_traits.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -85,6 +86,31 @@ public: inline TCoord height() const BP2D_NOEXCEPT; inline RawPoint center() const BP2D_NOEXCEPT; + + inline double area() const BP2D_NOEXCEPT { + return double(width()*height()); + } +}; + +template +class _Circle { + RawPoint center_; + double radius_ = 0; +public: + + _Circle() = default; + + _Circle(const RawPoint& center, double r): center_(center), radius_(r) {} + + inline const RawPoint& center() const BP2D_NOEXCEPT { return center_; } + inline const void center(const RawPoint& c) { center_ = c; } + + inline double radius() const BP2D_NOEXCEPT { return radius_; } + inline void radius(double r) { radius_ = r; } + + inline double area() const BP2D_NOEXCEPT { + return 2.0*Pi*radius_; + } }; /** @@ -614,12 +640,34 @@ struct ShapeLike { return box; } + template + static inline _Box> boundingBox( + const _Circle>& circ) + { + using Coord = TCoord>; + TPoint pmin = { + static_cast(getX(circ.center()) - circ.radius()), + static_cast(getY(circ.center()) - circ.radius()) }; + + TPoint pmax = { + static_cast(getX(circ.center()) + circ.radius()), + static_cast(getY(circ.center()) + circ.radius()) }; + + return {pmin, pmax}; + } + template static inline double area(const _Box>& box) { return static_cast(box.width() * box.height()); } + template + static inline double area(const _Circle>& circ) + { + return circ.area(); + } + template static inline double area(const Shapes& shapes) { @@ -629,6 +677,62 @@ struct ShapeLike { }); } + template + static bool isInside(const TPoint& point, + const _Circle>& circ) + { + return PointLike::distance(point, circ.center()) < circ.radius(); + } + + template + static bool isInside(const TPoint& point, + const _Box>& box) + { + auto px = getX(point); + auto py = getY(point); + auto minx = getX(box.minCorner()); + auto miny = getY(box.minCorner()); + auto maxx = getX(box.maxCorner()); + auto maxy = getY(box.maxCorner()); + + return px > minx && px < maxx && py > miny && py < maxy; + } + + template + static bool isInside(const RawShape& sh, + const _Circle>& circ) + { + return std::all_of(cbegin(sh), cend(sh), + [&circ](const TPoint& p){ + return isInside(p, circ); + }); + } + + template + static bool isInside(const _Box>& box, + const _Circle>& circ) + { + return isInside(box.minCorner(), circ) && + isInside(box.maxCorner(), circ); + } + + template + static bool isInside(const _Box>& ibb, + const _Box>& box) + { + auto iminX = getX(ibb.minCorner()); + auto imaxX = getX(ibb.maxCorner()); + auto iminY = getY(ibb.minCorner()); + auto imaxY = getY(ibb.maxCorner()); + + auto minX = getX(box.minCorner()); + auto maxX = getX(box.maxCorner()); + auto minY = getY(box.minCorner()); + auto maxY = getY(box.maxCorner()); + + return iminX > minX && imaxX < maxX && iminY > minY && imaxY < maxY; + } + template // Potential O(1) implementation may exist static inline TPoint& vertex(RawShape& sh, unsigned long idx) { diff --git a/xs/src/libnest2d/libnest2d/libnest2d.hpp b/xs/src/libnest2d/libnest2d/libnest2d.hpp index 37b5fea95..7f23de358 100644 --- a/xs/src/libnest2d/libnest2d/libnest2d.hpp +++ b/xs/src/libnest2d/libnest2d/libnest2d.hpp @@ -53,8 +53,8 @@ class _Item { enum class Convexity: char { UNCHECKED, - TRUE, - FALSE + C_TRUE, + C_FALSE }; mutable Convexity convexity_ = Convexity::UNCHECKED; @@ -213,10 +213,10 @@ public: switch(convexity_) { case Convexity::UNCHECKED: ret = sl::isConvex(sl::getContour(transformedShape())); - convexity_ = ret? Convexity::TRUE : Convexity::FALSE; + convexity_ = ret? Convexity::C_TRUE : Convexity::C_FALSE; break; - case Convexity::TRUE: ret = true; break; - case Convexity::FALSE:; + case Convexity::C_TRUE: ret = true; break; + case Convexity::C_FALSE:; } return ret; @@ -254,7 +254,13 @@ public: return sl::isInside(transformedShape(), sh.transformedShape()); } + inline bool isInside(const RawShape& sh) const + { + return sl::isInside(transformedShape(), sh); + } + inline bool isInside(const _Box>& box) const; + inline bool isInside(const _Circle>& box) const; inline void translate(const Vertex& d) BP2D_NOEXCEPT { @@ -467,8 +473,12 @@ public: template inline bool _Item::isInside(const _Box>& box) const { - _Rectangle rect(box.width(), box.height()); - return _Item::isInside(rect); + return ShapeLike::isInside(boundingBox(), box); +} + +template inline bool +_Item::isInside(const _Circle>& circ) const { + return ShapeLike::isInside(transformedShape(), circ); } /** diff --git a/xs/src/libnest2d/libnest2d/placers/nfpplacer.hpp b/xs/src/libnest2d/libnest2d/placers/nfpplacer.hpp index 61d923b87..5d09a61fc 100644 --- a/xs/src/libnest2d/libnest2d/placers/nfpplacer.hpp +++ b/xs/src/libnest2d/libnest2d/placers/nfpplacer.hpp @@ -46,14 +46,12 @@ struct NfpPConfig { * function you can e.g. influence the shape of the arranged pile. * * \param shapes The first parameter is a container with all the placed - * polygons including the current candidate. You can calculate a bounding - * box or convex hull on this pile of polygons. + * polygons excluding the current candidate. You can calculate a bounding + * box or convex hull on this pile of polygons without the candidate item + * or push back the candidate item into the container and then calculate + * some features. * - * \param item The second parameter is the candidate item. Note that - * calling transformedShape() on this second argument returns an identical - * shape as calling shapes.back(). These would not be the same objects only - * identical shapes! Using the second parameter is a lot faster due to - * caching some properties of the polygon (area, etc...) + * \param item The second parameter is the candidate item. * * \param occupied_area The third parameter is the sum of areas of the * items in the first parameter so you don't have to iterate through them @@ -127,6 +125,8 @@ template class EdgeCache { std::vector holes_; + double accuracy_ = 1.0; + void createCache(const RawShape& sh) { { // For the contour auto first = ShapeLike::cbegin(sh); @@ -160,11 +160,26 @@ template class EdgeCache { } } + size_t stride(const size_t N) const { + using std::ceil; + using std::round; + using std::pow; + + return static_cast( + std::round(N/std::pow(N, std::pow(accuracy_, 1.0/3.0))) + ); + } + void fetchCorners() const { if(!contour_.corners.empty()) return; - contour_.corners.reserve(contour_.distances.size() / 3 + 1); - for(size_t i = 0; i < contour_.distances.size() - 1; i += 3) { + const auto N = contour_.distances.size(); + const auto S = stride(N); + + contour_.corners.reserve(N / S + 1); + auto N_1 = N-1; + contour_.corners.emplace_back(0.0); + for(size_t i = 0; i < N_1; i += S) { contour_.corners.emplace_back( contour_.distances.at(i) / contour_.full_distance); } @@ -174,8 +189,12 @@ template class EdgeCache { auto& hc = holes_[hidx]; if(!hc.corners.empty()) return; - hc.corners.reserve(hc.distances.size() / 3 + 1); - for(size_t i = 0; i < hc.distances.size() - 1; i += 3) { + const auto N = hc.distances.size(); + const auto S = stride(N); + auto N_1 = N-1; + hc.corners.reserve(N / S + 1); + hc.corners.emplace_back(0.0); + for(size_t i = 0; i < N_1; i += S) { hc.corners.emplace_back( hc.distances.at(i) / hc.full_distance); } @@ -224,6 +243,9 @@ public: createCache(sh); } + /// Resolution of returned corners. The stride is derived from this value. + void accuracy(double a /* within <0.0, 1.0>*/) { accuracy_ = a; } + /** * @brief Get a point on the circumference of a polygon. * @param distance A relative distance from the starting point to the end. @@ -419,12 +441,12 @@ Nfp::Shapes nfp( const Container& polygons, // return nfps; } -template -class _NofitPolyPlacer: public PlacerBoilerplate<_NofitPolyPlacer, - RawShape, _Box>, NfpPConfig> { +template>> +class _NofitPolyPlacer: public PlacerBoilerplate<_NofitPolyPlacer, + RawShape, TBin, NfpPConfig> { - using Base = PlacerBoilerplate<_NofitPolyPlacer, - RawShape, _Box>, NfpPConfig>; + using Base = PlacerBoilerplate<_NofitPolyPlacer, + RawShape, TBin, NfpPConfig>; DECLARE_PLACER(Base) @@ -434,6 +456,7 @@ class _NofitPolyPlacer: public PlacerBoilerplate<_NofitPolyPlacer, const double penality_; using MaxNfpLevel = Nfp::MaxNfpLevel; + using sl = ShapeLike; public: @@ -441,7 +464,7 @@ public: inline explicit _NofitPolyPlacer(const BinType& bin): Base(bin), - norm_(std::sqrt(ShapeLike::area(bin))), + norm_(std::sqrt(sl::area(bin))), penality_(1e6*norm_) {} _NofitPolyPlacer(const _NofitPolyPlacer&) = default; @@ -452,18 +475,26 @@ public: _NofitPolyPlacer& operator=(_NofitPolyPlacer&&) BP2D_NOEXCEPT = default; #endif + bool static inline wouldFit(const Box& bb, const RawShape& bin) { + auto bbin = sl::boundingBox(bin); + auto d = bbin.center() - bb.center(); + _Rectangle rect(bb.width(), bb.height()); + rect.translate(bb.minCorner() + d); + return sl::isInside(rect.transformedShape(), bin); + } + bool static inline wouldFit(const RawShape& chull, const RawShape& bin) { - auto bbch = ShapeLike::boundingBox(chull); - auto bbin = ShapeLike::boundingBox(bin); - auto d = bbin.minCorner() - bbch.minCorner(); + auto bbch = sl::boundingBox(chull); + auto bbin = sl::boundingBox(bin); + auto d = bbch.center() - bbin.center(); auto chullcpy = chull; - ShapeLike::translate(chullcpy, d); - return ShapeLike::isInside(chullcpy, bbin); + sl::translate(chullcpy, d); + return sl::isInside(chullcpy, bin); } bool static inline wouldFit(const RawShape& chull, const Box& bin) { - auto bbch = ShapeLike::boundingBox(chull); + auto bbch = sl::boundingBox(chull); return wouldFit(bbch, bin); } @@ -472,6 +503,17 @@ public: return bb.width() <= bin.width() && bb.height() <= bin.height(); } + bool static inline wouldFit(const Box& bb, const _Circle& bin) + { + return sl::isInside(bb, bin); + } + + bool static inline wouldFit(const RawShape& chull, + const _Circle& bin) + { + return sl::isInside(chull, bin); + } + PackResult trypack(Item& item) { PackResult ret; @@ -510,7 +552,10 @@ public: std::vector> ecache; ecache.reserve(nfps.size()); - for(auto& nfp : nfps ) ecache.emplace_back(nfp); + for(auto& nfp : nfps ) { + ecache.emplace_back(nfp); + ecache.back().accuracy(config_.accuracy); + } struct Optimum { double relpos; @@ -536,18 +581,25 @@ public: pile_area += mitem.area(); } + auto merged_pile = Nfp::merge(pile); + // This is the kernel part of the object function that is // customizable by the library client auto _objfunc = config_.object_function? config_.object_function : - [this](Nfp::Shapes& pile, Item, - double occupied_area, double /*norm*/, - double penality) + [this, &merged_pile]( + Nfp::Shapes& /*pile*/, + const Item& item, + double occupied_area, + double norm, + double /*penality*/) { - auto ch = ShapeLike::convexHull(pile); + merged_pile.emplace_back(item.transformedShape()); + auto ch = sl::convexHull(merged_pile); + merged_pile.pop_back(); // The pack ratio -- how much is the convex hull occupied - double pack_rate = occupied_area/ShapeLike::area(ch); + double pack_rate = occupied_area/sl::area(ch); // ratio of waste double waste = 1.0 - pack_rate; @@ -557,7 +609,7 @@ public: // (larger) values. auto score = std::sqrt(waste); - if(!wouldFit(ch, bin_)) score = 2*penality - score; + if(!wouldFit(ch, bin_)) score += norm; return score; }; @@ -569,23 +621,31 @@ public: d += startpos; item.translation(d); -// pile.emplace_back(item.transformedShape()); - double occupied_area = pile_area + item.area(); double score = _objfunc(pile, item, occupied_area, norm_, penality_); -// pile.pop_back(); - return score; }; + auto boundaryCheck = [&](const Optimum& o) { + auto v = getNfpPoint(o); + auto d = v - iv; + d += startpos; + item.translation(d); + + merged_pile.emplace_back(item.transformedShape()); + auto chull = sl::convexHull(merged_pile); + merged_pile.pop_back(); + + return wouldFit(chull, bin_); + }; + opt::StopCriteria stopcr; - stopcr.max_iterations = 1000; - stopcr.absolute_score_difference = 1e-20*norm_; -// stopcr.relative_score_difference = 1e-20; - opt::TOptimizer solver(stopcr); + stopcr.max_iterations = 100; + stopcr.relative_score_difference = 1e-6; + opt::TOptimizer solver(stopcr); Optimum optimum(0, 0); double best_score = penality_; @@ -604,7 +664,7 @@ public: std::for_each(cache.corners().begin(), cache.corners().end(), [ch, &contour_ofn, &solver, &best_score, - &optimum] (double pos) + &optimum, &boundaryCheck] (double pos) { try { auto result = solver.optimize_min(contour_ofn, @@ -613,22 +673,15 @@ public: ); if(result.score < best_score) { - best_score = result.score; - optimum.relpos = std::get<0>(result.optimum); - optimum.nfpidx = ch; - optimum.hidx = -1; + Optimum o(std::get<0>(result.optimum), ch, -1); + if(boundaryCheck(o)) { + best_score = result.score; + optimum = o; + } } } catch(std::exception& e) { derr() << "ERROR: " << e.what() << "\n"; } - -// auto sc = contour_ofn(pos); -// if(sc < best_score) { -// best_score = sc; -// optimum.relpos = pos; -// optimum.nfpidx = ch; -// optimum.hidx = -1; -// } }); for(unsigned hidx = 0; hidx < cache.holeCount(); ++hidx) { @@ -643,7 +696,7 @@ public: std::for_each(cache.corners(hidx).begin(), cache.corners(hidx).end(), [&hole_ofn, &solver, &best_score, - &optimum, ch, hidx] + &optimum, ch, hidx, &boundaryCheck] (double pos) { try { @@ -653,21 +706,16 @@ public: ); if(result.score < best_score) { - best_score = result.score; Optimum o(std::get<0>(result.optimum), ch, hidx); - optimum = o; + if(boundaryCheck(o)) { + best_score = result.score; + optimum = o; + } } } catch(std::exception& e) { derr() << "ERROR: " << e.what() << "\n"; } -// auto sc = hole_ofn(pos); -// if(sc < best_score) { -// best_score = sc; -// optimum.relpos = pos; -// optimum.nfpidx = ch; -// optimum.hidx = hidx; -// } }); } } @@ -702,34 +750,35 @@ public: m.reserve(items_.size()); for(Item& item : items_) m.emplace_back(item.transformedShape()); - auto&& bb = ShapeLike::boundingBox(m); + auto&& bb = sl::boundingBox(m); Vertex ci, cb; + auto bbin = sl::boundingBox(bin_); switch(config_.alignment) { case Config::Alignment::CENTER: { ci = bb.center(); - cb = bin_.center(); + cb = bbin.center(); break; } case Config::Alignment::BOTTOM_LEFT: { ci = bb.minCorner(); - cb = bin_.minCorner(); + cb = bbin.minCorner(); break; } case Config::Alignment::BOTTOM_RIGHT: { ci = {getX(bb.maxCorner()), getY(bb.minCorner())}; - cb = {getX(bin_.maxCorner()), getY(bin_.minCorner())}; + cb = {getX(bbin.maxCorner()), getY(bbin.minCorner())}; break; } case Config::Alignment::TOP_LEFT: { ci = {getX(bb.minCorner()), getY(bb.maxCorner())}; - cb = {getX(bin_.minCorner()), getY(bin_.maxCorner())}; + cb = {getX(bbin.minCorner()), getY(bbin.maxCorner())}; break; } case Config::Alignment::TOP_RIGHT: { ci = bb.maxCorner(); - cb = bin_.maxCorner(); + cb = bbin.maxCorner(); break; } } @@ -745,31 +794,32 @@ private: void setInitialPosition(Item& item) { Box&& bb = item.boundingBox(); Vertex ci, cb; + auto bbin = sl::boundingBox(bin_); switch(config_.starting_point) { case Config::Alignment::CENTER: { ci = bb.center(); - cb = bin_.center(); + cb = bbin.center(); break; } case Config::Alignment::BOTTOM_LEFT: { ci = bb.minCorner(); - cb = bin_.minCorner(); + cb = bbin.minCorner(); break; } case Config::Alignment::BOTTOM_RIGHT: { ci = {getX(bb.maxCorner()), getY(bb.minCorner())}; - cb = {getX(bin_.maxCorner()), getY(bin_.minCorner())}; + cb = {getX(bbin.maxCorner()), getY(bbin.minCorner())}; break; } case Config::Alignment::TOP_LEFT: { ci = {getX(bb.minCorner()), getY(bb.maxCorner())}; - cb = {getX(bin_.minCorner()), getY(bin_.maxCorner())}; + cb = {getX(bbin.minCorner()), getY(bbin.maxCorner())}; break; } case Config::Alignment::TOP_RIGHT: { ci = bb.maxCorner(); - cb = bin_.maxCorner(); + cb = bbin.maxCorner(); break; } } @@ -780,7 +830,7 @@ private: void placeOutsideOfBin(Item& item) { auto&& bb = item.boundingBox(); - Box binbb = ShapeLike::boundingBox(bin_); + Box binbb = sl::boundingBox(bin_); Vertex v = { getX(bb.maxCorner()), getY(bb.minCorner()) }; diff --git a/xs/src/libnest2d/libnest2d/selections/djd_heuristic.hpp b/xs/src/libnest2d/libnest2d/selections/djd_heuristic.hpp index 17ac1167d..e3ad97c10 100644 --- a/xs/src/libnest2d/libnest2d/selections/djd_heuristic.hpp +++ b/xs/src/libnest2d/libnest2d/selections/djd_heuristic.hpp @@ -535,7 +535,7 @@ public: // then it should be removed from the not_packed list { auto it = store_.begin(); while (it != store_.end()) { - Placer p(bin); + Placer p(bin); p.configure(pconfig); if(!p.pack(*it)) { it = store_.erase(it); } else it++; diff --git a/xs/src/libnest2d/libnest2d/selections/firstfit.hpp b/xs/src/libnest2d/libnest2d/selections/firstfit.hpp index f34961f80..665b9da9f 100644 --- a/xs/src/libnest2d/libnest2d/selections/firstfit.hpp +++ b/xs/src/libnest2d/libnest2d/selections/firstfit.hpp @@ -59,7 +59,7 @@ public: // then it should be removed from the list { auto it = store_.begin(); while (it != store_.end()) { - Placer p(bin); + Placer p(bin); p.configure(pconfig); if(!p.pack(*it)) { it = store_.erase(it); } else it++; diff --git a/xs/src/libslic3r/Model.cpp b/xs/src/libslic3r/Model.cpp index 6e5cadd0d..bceeea258 100644 --- a/xs/src/libslic3r/Model.cpp +++ b/xs/src/libslic3r/Model.cpp @@ -7,11 +7,6 @@ #include "Format/STL.hpp" #include "Format/3mf.hpp" -#include -#include -#include -#include "slic3r/GUI/GUI.hpp" - #include #include @@ -304,435 +299,36 @@ static bool _arrange(const Pointfs &sizes, coordf_t dist, const BoundingBoxf* bb return result; } -namespace arr { - -using namespace libnest2d; - -std::string toString(const Model& model, bool holes = true) { - std::stringstream ss; - - ss << "{\n"; - - for(auto objptr : model.objects) { - if(!objptr) continue; - - auto rmesh = objptr->raw_mesh(); - - for(auto objinst : objptr->instances) { - if(!objinst) continue; - - Slic3r::TriangleMesh tmpmesh = rmesh; - tmpmesh.scale(objinst->scaling_factor); - objinst->transform_mesh(&tmpmesh); - ExPolygons expolys = tmpmesh.horizontal_projection(); - for(auto& expoly_complex : expolys) { - - auto tmp = expoly_complex.simplify(1.0/SCALING_FACTOR); - if(tmp.empty()) continue; - auto expoly = tmp.front(); - expoly.contour.make_clockwise(); - for(auto& h : expoly.holes) h.make_counter_clockwise(); - - ss << "\t{\n"; - ss << "\t\t{\n"; - - for(auto v : expoly.contour.points) ss << "\t\t\t{" - << v.x << ", " - << v.y << "},\n"; - { - auto v = expoly.contour.points.front(); - ss << "\t\t\t{" << v.x << ", " << v.y << "},\n"; - } - ss << "\t\t},\n"; - - // Holes: - ss << "\t\t{\n"; - if(holes) for(auto h : expoly.holes) { - ss << "\t\t\t{\n"; - for(auto v : h.points) ss << "\t\t\t\t{" - << v.x << ", " - << v.y << "},\n"; - { - auto v = h.points.front(); - ss << "\t\t\t\t{" << v.x << ", " << v.y << "},\n"; - } - ss << "\t\t\t},\n"; - } - ss << "\t\t},\n"; - - ss << "\t},\n"; - } - } - } - - ss << "}\n"; - - return ss.str(); -} - -void toSVG(SVG& svg, const Model& model) { - for(auto objptr : model.objects) { - if(!objptr) continue; - - auto rmesh = objptr->raw_mesh(); - - for(auto objinst : objptr->instances) { - if(!objinst) continue; - - Slic3r::TriangleMesh tmpmesh = rmesh; - tmpmesh.scale(objinst->scaling_factor); - objinst->transform_mesh(&tmpmesh); - ExPolygons expolys = tmpmesh.horizontal_projection(); - svg.draw(expolys); - } - } -} - -// A container which stores a pointer to the 3D object and its projected -// 2D shape from top view. -using ShapeData2D = - std::vector>; - -ShapeData2D projectModelFromTop(const Slic3r::Model &model) { - ShapeData2D ret; - - auto s = std::accumulate(model.objects.begin(), model.objects.end(), 0, - [](size_t s, ModelObject* o){ - return s + o->instances.size(); - }); - - ret.reserve(s); - - for(auto objptr : model.objects) { - if(objptr) { - - auto rmesh = objptr->raw_mesh(); - - for(auto objinst : objptr->instances) { - if(objinst) { - Slic3r::TriangleMesh tmpmesh = rmesh; - ClipperLib::PolygonImpl pn; - - tmpmesh.scale(objinst->scaling_factor); - - // TODO export the exact 2D projection - auto p = tmpmesh.convex_hull(); - - p.make_clockwise(); - p.append(p.first_point()); - pn.Contour = Slic3rMultiPoint_to_ClipperPath( p ); - - // Efficient conversion to item. - Item item(std::move(pn)); - - // Invalid geometries would throw exceptions when arranging - if(item.vertexCount() > 3) { - item.rotation(objinst->rotation); - item.translation( { - ClipperLib::cInt(objinst->offset.x/SCALING_FACTOR), - ClipperLib::cInt(objinst->offset.y/SCALING_FACTOR) - }); - ret.emplace_back(objinst, item); - } - } - } - } - } - - return ret; -} - -/** - * \brief Arranges the model objects on the screen. - * - * The arrangement considers multiple bins (aka. print beds) for placing all - * the items provided in the model argument. If the items don't fit on one - * print bed, the remaining will be placed onto newly created print beds. - * The first_bin_only parameter, if set to true, disables this behaviour and - * makes sure that only one print bed is filled and the remaining items will be - * untouched. When set to false, the items which could not fit onto the - * print bed will be placed next to the print bed so the user should see a - * pile of items on the print bed and some other piles outside the print - * area that can be dragged later onto the print bed as a group. - * - * \param model The model object with the 3D content. - * \param dist The minimum distance which is allowed for any pair of items - * on the print bed in any direction. - * \param bb The bounding box of the print bed. It corresponds to the 'bin' - * for bin packing. - * \param first_bin_only This parameter controls whether to place the - * remaining items which do not fit onto the print area next to the print - * bed or leave them untouched (let the user arrange them by hand or remove - * them). - */ -bool arrange(Model &model, coordf_t dist, const Slic3r::BoundingBoxf* bb, - bool first_bin_only, - std::function progressind) -{ - using ArrangeResult = _IndexedPackGroup; - - bool ret = true; - - // Create the arranger config - auto min_obj_distance = static_cast(dist/SCALING_FACTOR); - - // Get the 2D projected shapes with their 3D model instance pointers - auto shapemap = arr::projectModelFromTop(model); - - bool hasbin = bb != nullptr && bb->defined; - double area_max = 0; - - // Copy the references for the shapes only as the arranger expects a - // sequence of objects convertible to Item or ClipperPolygon - std::vector> shapes; - shapes.reserve(shapemap.size()); - std::for_each(shapemap.begin(), shapemap.end(), - [&shapes, min_obj_distance, &area_max, hasbin] - (ShapeData2D::value_type& it) - { - shapes.push_back(std::ref(it.second)); - }); - - Box bin; - - if(hasbin) { - // Scale up the bounding box to clipper scale. - BoundingBoxf bbb = *bb; - bbb.scale(1.0/SCALING_FACTOR); - - bin = Box({ - static_cast(bbb.min.x), - static_cast(bbb.min.y) - }, - { - static_cast(bbb.max.x), - static_cast(bbb.max.y) - }); - } - - // Will use the DJD selection heuristic with the BottomLeft placement - // strategy - using Arranger = Arranger; - using PConf = Arranger::PlacementConfig; - using SConf = Arranger::SelectionConfig; - - PConf pcfg; // Placement configuration - SConf scfg; // Selection configuration - - // Align the arranged pile into the center of the bin - pcfg.alignment = PConf::Alignment::CENTER; - - // Start placing the items from the center of the print bed - pcfg.starting_point = PConf::Alignment::CENTER; - - // TODO cannot use rotations until multiple objects of same geometry can - // handle different rotations - // arranger.useMinimumBoundigBoxRotation(); - pcfg.rotations = { 0.0 }; - - // Magic: we will specify what is the goal of arrangement... In this case - // we override the default object function to make the larger items go into - // the center of the pile and smaller items orbit it so the resulting pile - // has a circle-like shape. This is good for the print bed's heat profile. - // We alse sacrafice a bit of pack efficiency for this to work. As a side - // effect, the arrange procedure is a lot faster (we do not need to - // calculate the convex hulls) - pcfg.object_function = [bin, hasbin]( - NfpPlacer::Pile& pile, // The currently arranged pile - Item item, - double /*area*/, // Sum area of items (not needed) - double norm, // A norming factor for physical dimensions - double penality) // Min penality in case of bad arrangement - { - using pl = PointLike; - - static const double BIG_ITEM_TRESHOLD = 0.2; - static const double GRAVITY_RATIO = 0.5; - static const double DENSITY_RATIO = 1.0 - GRAVITY_RATIO; - - // We will treat big items (compared to the print bed) differently - NfpPlacer::Pile bigs; - bigs.reserve(pile.size()); - for(auto& p : pile) { - auto pbb = ShapeLike::boundingBox(p); - auto na = std::sqrt(pbb.width()*pbb.height())/norm; - if(na > BIG_ITEM_TRESHOLD) bigs.emplace_back(p); - } - - // Candidate item bounding box - auto ibb = item.boundingBox(); - - // Calculate the full bounding box of the pile with the candidate item - pile.emplace_back(item.transformedShape()); - auto fullbb = ShapeLike::boundingBox(pile); - pile.pop_back(); - - // The bounding box of the big items (they will accumulate in the center - // of the pile - auto bigbb = bigs.empty()? fullbb : ShapeLike::boundingBox(bigs); - - // The size indicator of the candidate item. This is not the area, - // but almost... - auto itemnormarea = std::sqrt(ibb.width()*ibb.height())/norm; - - // Will hold the resulting score - double score = 0; - - if(itemnormarea > BIG_ITEM_TRESHOLD) { - // This branch is for the bigger items.. - // Here we will use the closest point of the item bounding box to - // the already arranged pile. So not the bb center nor the a choosen - // corner but whichever is the closest to the center. This will - // prevent unwanted strange arrangements. - - auto minc = ibb.minCorner(); // bottom left corner - auto maxc = ibb.maxCorner(); // top right corner - - // top left and bottom right corners - auto top_left = PointImpl{getX(minc), getY(maxc)}; - auto bottom_right = PointImpl{getX(maxc), getY(minc)}; - - auto cc = fullbb.center(); // The gravity center - - // Now the distnce of the gravity center will be calculated to the - // five anchor points and the smallest will be chosen. - std::array dists; - dists[0] = pl::distance(minc, cc); - dists[1] = pl::distance(maxc, cc); - dists[2] = pl::distance(ibb.center(), cc); - dists[3] = pl::distance(top_left, cc); - dists[4] = pl::distance(bottom_right, cc); - - auto dist = *(std::min_element(dists.begin(), dists.end())) / norm; - - // Density is the pack density: how big is the arranged pile - auto density = std::sqrt(fullbb.width()*fullbb.height()) / norm; - - // The score is a weighted sum of the distance from pile center - // and the pile size - score = GRAVITY_RATIO * dist + DENSITY_RATIO * density; - - } else if(itemnormarea < BIG_ITEM_TRESHOLD && bigs.empty()) { - // If there are no big items, only small, we should consider the - // density here as well to not get silly results - auto bindist = pl::distance(ibb.center(), bin.center()) / norm; - auto density = std::sqrt(fullbb.width()*fullbb.height()) / norm; - score = GRAVITY_RATIO * bindist + DENSITY_RATIO * density; - } else { - // Here there are the small items that should be placed around the - // already processed bigger items. - // No need to play around with the anchor points, the center will be - // just fine for small items - score = pl::distance(ibb.center(), bigbb.center()) / norm; - } - - // If it does not fit into the print bed we will beat it - // with a large penality. If we would not do this, there would be only - // one big pile that doesn't care whether it fits onto the print bed. - if(!NfpPlacer::wouldFit(fullbb, bin)) score = 2*penality - score; - - return score; - }; - - // Create the arranger object - Arranger arranger(bin, min_obj_distance, pcfg, scfg); - - // Set the progress indicator for the arranger. - arranger.progressIndicator(progressind); - - // Arrange and return the items with their respective indices within the - // input sequence. - auto result = arranger.arrangeIndexed(shapes.begin(), shapes.end()); - - auto applyResult = [&shapemap](ArrangeResult::value_type& group, - Coord batch_offset) - { - for(auto& r : group) { - auto idx = r.first; // get the original item index - Item& item = r.second; // get the item itself - - // Get the model instance from the shapemap using the index - ModelInstance *inst_ptr = shapemap[idx].first; - - // Get the tranformation data from the item object and scale it - // appropriately - auto off = item.translation(); - Radians rot = item.rotation(); - Pointf foff(off.X*SCALING_FACTOR + batch_offset, - off.Y*SCALING_FACTOR); - - // write the tranformation data into the model instance - inst_ptr->rotation = rot; - inst_ptr->offset = foff; - } - }; - - if(first_bin_only) { - applyResult(result.front(), 0); - } else { - - const auto STRIDE_PADDING = 1.2; - - Coord stride = static_cast(STRIDE_PADDING* - bin.width()*SCALING_FACTOR); - Coord batch_offset = 0; - - for(auto& group : result) { - applyResult(group, batch_offset); - - // Only the first pack group can be placed onto the print bed. The - // other objects which could not fit will be placed next to the - // print bed - batch_offset += stride; - } - } - - for(auto objptr : model.objects) objptr->invalidate_bounding_box(); - - return ret && result.size() == 1; -} -} - /* arrange objects preserving their instance count but altering their instance positions */ -bool Model::arrange_objects(coordf_t dist, const BoundingBoxf* bb, - std::function progressind) +bool Model::arrange_objects(coordf_t dist, const BoundingBoxf* bb) { - bool ret = false; - if(bb != nullptr && bb->defined) { - // Despite the new arrange is able to run without a specified bin, - // the perl testsuit still fails for this case. For now the safest - // thing to do is to use the new arrange only when a proper bin is - // specified. - ret = arr::arrange(*this, dist, bb, false, progressind); - } else { - // get the (transformed) size of each instance so that we take - // into account their different transformations when packing - Pointfs instance_sizes; - Pointfs instance_centers; - for (const ModelObject *o : this->objects) - for (size_t i = 0; i < o->instances.size(); ++ i) { - // an accurate snug bounding box around the transformed mesh. - BoundingBoxf3 bbox(o->instance_bounding_box(i, true)); - instance_sizes.push_back(bbox.size()); - instance_centers.push_back(bbox.center()); - } - - Pointfs positions; - if (! _arrange(instance_sizes, dist, bb, positions)) - return false; - - size_t idx = 0; - for (ModelObject *o : this->objects) { - for (ModelInstance *i : o->instances) { - i->offset = positions[idx] - instance_centers[idx]; - ++ idx; - } - o->invalidate_bounding_box(); + // get the (transformed) size of each instance so that we take + // into account their different transformations when packing + Pointfs instance_sizes; + Pointfs instance_centers; + for (const ModelObject *o : this->objects) + for (size_t i = 0; i < o->instances.size(); ++ i) { + // an accurate snug bounding box around the transformed mesh. + BoundingBoxf3 bbox(o->instance_bounding_box(i, true)); + instance_sizes.push_back(bbox.size()); + instance_centers.push_back(bbox.center()); } + + Pointfs positions; + if (! _arrange(instance_sizes, dist, bb, positions)) + return false; + + size_t idx = 0; + for (ModelObject *o : this->objects) { + for (ModelInstance *i : o->instances) { + i->offset = positions[idx] - instance_centers[idx]; + ++ idx; + } + o->invalidate_bounding_box(); } - return ret; + return true; } // Duplicate the entire model preserving instance relative positions. diff --git a/xs/src/libslic3r/Model.hpp b/xs/src/libslic3r/Model.hpp index f5e97fb6a..4c650f0de 100644 --- a/xs/src/libslic3r/Model.hpp +++ b/xs/src/libslic3r/Model.hpp @@ -290,8 +290,7 @@ public: void center_instances_around_point(const Pointf &point); void translate(coordf_t x, coordf_t y, coordf_t z) { for (ModelObject *o : this->objects) o->translate(x, y, z); } TriangleMesh mesh() const; - bool arrange_objects(coordf_t dist, const BoundingBoxf* bb = NULL, - std::function progressind = [](unsigned){}); + bool arrange_objects(coordf_t dist, const BoundingBoxf* bb = NULL); // Croaks if the duplicated objects do not fit the print bed. void duplicate(size_t copies_num, coordf_t dist, const BoundingBoxf* bb = NULL); void duplicate_objects(size_t copies_num, coordf_t dist, const BoundingBoxf* bb = NULL); diff --git a/xs/src/libslic3r/ModelArrange.hpp b/xs/src/libslic3r/ModelArrange.hpp new file mode 100644 index 000000000..f2d399ac6 --- /dev/null +++ b/xs/src/libslic3r/ModelArrange.hpp @@ -0,0 +1,597 @@ +#ifndef MODELARRANGE_HPP +#define MODELARRANGE_HPP + +#include "Model.hpp" +#include "SVG.hpp" +#include + +#include +#include + +#include + +namespace Slic3r { +namespace arr { + +using namespace libnest2d; + +std::string toString(const Model& model, bool holes = true) { + std::stringstream ss; + + ss << "{\n"; + + for(auto objptr : model.objects) { + if(!objptr) continue; + + auto rmesh = objptr->raw_mesh(); + + for(auto objinst : objptr->instances) { + if(!objinst) continue; + + Slic3r::TriangleMesh tmpmesh = rmesh; + tmpmesh.scale(objinst->scaling_factor); + objinst->transform_mesh(&tmpmesh); + ExPolygons expolys = tmpmesh.horizontal_projection(); + for(auto& expoly_complex : expolys) { + + auto tmp = expoly_complex.simplify(1.0/SCALING_FACTOR); + if(tmp.empty()) continue; + auto expoly = tmp.front(); + expoly.contour.make_clockwise(); + for(auto& h : expoly.holes) h.make_counter_clockwise(); + + ss << "\t{\n"; + ss << "\t\t{\n"; + + for(auto v : expoly.contour.points) ss << "\t\t\t{" + << v.x << ", " + << v.y << "},\n"; + { + auto v = expoly.contour.points.front(); + ss << "\t\t\t{" << v.x << ", " << v.y << "},\n"; + } + ss << "\t\t},\n"; + + // Holes: + ss << "\t\t{\n"; + if(holes) for(auto h : expoly.holes) { + ss << "\t\t\t{\n"; + for(auto v : h.points) ss << "\t\t\t\t{" + << v.x << ", " + << v.y << "},\n"; + { + auto v = h.points.front(); + ss << "\t\t\t\t{" << v.x << ", " << v.y << "},\n"; + } + ss << "\t\t\t},\n"; + } + ss << "\t\t},\n"; + + ss << "\t},\n"; + } + } + } + + ss << "}\n"; + + return ss.str(); +} + +void toSVG(SVG& svg, const Model& model) { + for(auto objptr : model.objects) { + if(!objptr) continue; + + auto rmesh = objptr->raw_mesh(); + + for(auto objinst : objptr->instances) { + if(!objinst) continue; + + Slic3r::TriangleMesh tmpmesh = rmesh; + tmpmesh.scale(objinst->scaling_factor); + objinst->transform_mesh(&tmpmesh); + ExPolygons expolys = tmpmesh.horizontal_projection(); + svg.draw(expolys); + } + } +} + +namespace bgi = boost::geometry::index; + +using SpatElement = std::pair; +using SpatIndex = bgi::rtree< SpatElement, bgi::rstar<16, 4> >; + +std::tuple +objfunc(const PointImpl& bincenter, + double /*bin_area*/, + ShapeLike::Shapes& pile, // The currently arranged pile + double /*pile_area*/, + const Item &item, + double norm, // A norming factor for physical dimensions + std::vector& areacache, // pile item areas will be cached + // a spatial index to quickly get neighbors of the candidate item + SpatIndex& spatindex + ) +{ + using pl = PointLike; + using sl = ShapeLike; + + static const double BIG_ITEM_TRESHOLD = 0.2; + static const double ROUNDNESS_RATIO = 0.5; + static const double DENSITY_RATIO = 1.0 - ROUNDNESS_RATIO; + + // We will treat big items (compared to the print bed) differently + auto normarea = [norm](double area) { return std::sqrt(area)/norm; }; + + // If a new bin has been created: + if(pile.size() < areacache.size()) { + areacache.clear(); + spatindex.clear(); + } + + // We must fill the caches: + int idx = 0; + for(auto& p : pile) { + if(idx == areacache.size()) { + areacache.emplace_back(sl::area(p)); + if(normarea(areacache[idx]) > BIG_ITEM_TRESHOLD) + spatindex.insert({sl::boundingBox(p), idx}); + } + + idx++; + } + + // Candidate item bounding box + auto ibb = item.boundingBox(); + + // Calculate the full bounding box of the pile with the candidate item + pile.emplace_back(item.transformedShape()); + auto fullbb = ShapeLike::boundingBox(pile); + pile.pop_back(); + + // The bounding box of the big items (they will accumulate in the center + // of the pile + Box bigbb; + if(spatindex.empty()) bigbb = fullbb; + else { + auto boostbb = spatindex.bounds(); + boost::geometry::convert(boostbb, bigbb); + } + + // The size indicator of the candidate item. This is not the area, + // but almost... + double item_normarea = normarea(item.area()); + + // Will hold the resulting score + double score = 0; + + if(item_normarea > BIG_ITEM_TRESHOLD) { + // This branch is for the bigger items.. + // Here we will use the closest point of the item bounding box to + // the already arranged pile. So not the bb center nor the a choosen + // corner but whichever is the closest to the center. This will + // prevent some unwanted strange arrangements. + + auto minc = ibb.minCorner(); // bottom left corner + auto maxc = ibb.maxCorner(); // top right corner + + // top left and bottom right corners + auto top_left = PointImpl{getX(minc), getY(maxc)}; + auto bottom_right = PointImpl{getX(maxc), getY(minc)}; + + // Now the distance of the gravity center will be calculated to the + // five anchor points and the smallest will be chosen. + std::array dists; + auto cc = fullbb.center(); // The gravity center + dists[0] = pl::distance(minc, cc); + dists[1] = pl::distance(maxc, cc); + dists[2] = pl::distance(ibb.center(), cc); + dists[3] = pl::distance(top_left, cc); + dists[4] = pl::distance(bottom_right, cc); + + // The smalles distance from the arranged pile center: + auto dist = *(std::min_element(dists.begin(), dists.end())) / norm; + + // Density is the pack density: how big is the arranged pile + auto density = std::sqrt(fullbb.width()*fullbb.height()) / norm; + + // Prepare a variable for the alignment score. + // This will indicate: how well is the candidate item aligned with + // its neighbors. We will check the aligment with all neighbors and + // return the score for the best alignment. So it is enough for the + // candidate to be aligned with only one item. + auto alignment_score = std::numeric_limits::max(); + + auto& trsh = item.transformedShape(); + + auto querybb = item.boundingBox(); + + // Query the spatial index for the neigbours + std::vector result; + spatindex.query(bgi::intersects(querybb), std::back_inserter(result)); + + for(auto& e : result) { // now get the score for the best alignment + auto idx = e.second; + auto& p = pile[idx]; + auto parea = areacache[idx]; + auto bb = sl::boundingBox(sl::Shapes{p, trsh}); + auto bbarea = bb.area(); + auto ascore = 1.0 - (item.area() + parea)/bbarea; + + if(ascore < alignment_score) alignment_score = ascore; + } + + // The final mix of the score is the balance between the distance + // from the full pile center, the pack density and the + // alignment with the neigbours + auto C = 0.33; + score = C * dist + C * density + C * alignment_score; + + } else if( item_normarea < BIG_ITEM_TRESHOLD && spatindex.empty()) { + // If there are no big items, only small, we should consider the + // density here as well to not get silly results + auto bindist = pl::distance(ibb.center(), bincenter) / norm; + auto density = std::sqrt(fullbb.width()*fullbb.height()) / norm; + score = ROUNDNESS_RATIO * bindist + DENSITY_RATIO * density; + } else { + // Here there are the small items that should be placed around the + // already processed bigger items. + // No need to play around with the anchor points, the center will be + // just fine for small items + score = pl::distance(ibb.center(), bigbb.center()) / norm; + } + + return std::make_tuple(score, fullbb); +} + +template +void fillConfig(PConf& pcfg) { + + // Align the arranged pile into the center of the bin + pcfg.alignment = PConf::Alignment::CENTER; + + // Start placing the items from the center of the print bed + pcfg.starting_point = PConf::Alignment::CENTER; + + // TODO cannot use rotations until multiple objects of same geometry can + // handle different rotations + // arranger.useMinimumBoundigBoxRotation(); + pcfg.rotations = { 0.0 }; + + // The accuracy of optimization. + // Goes from 0.0 to 1.0 and scales performance as well + pcfg.accuracy = 0.6f; +} + +template +class AutoArranger {}; + +template +class _ArrBase { +protected: + using Placer = strategies::_NofitPolyPlacer; + using Selector = FirstFitSelection; + using Packer = Arranger; + using PConfig = typename Packer::PlacementConfig; + using Distance = TCoord; + using Pile = ShapeLike::Shapes; + + Packer pck_; + PConfig pconf_; // Placement configuration + double bin_area_; + std::vector areacache_; + SpatIndex rtree_; +public: + + _ArrBase(const TBin& bin, Distance dist, + std::function progressind): + pck_(bin, dist), bin_area_(ShapeLike::area(bin)) + { + fillConfig(pconf_); + pck_.progressIndicator(progressind); + } + + template inline IndexedPackGroup operator()(Args&&...args) { + areacache_.clear(); + return pck_.arrangeIndexed(std::forward(args)...); + } +}; + +template<> +class AutoArranger: public _ArrBase { +public: + + AutoArranger(const Box& bin, Distance dist, + std::function progressind): + _ArrBase(bin, dist, progressind) + { + pconf_.object_function = [this, bin] ( + Pile& pile, + const Item &item, + double pile_area, + double norm, + double /*penality*/) { + + auto result = objfunc(bin.center(), bin_area_, pile, + pile_area, item, norm, areacache_, rtree_); + double score = std::get<0>(result); + auto& fullbb = std::get<1>(result); + + auto wdiff = fullbb.width() - bin.width(); + auto hdiff = fullbb.height() - bin.height(); + if(wdiff > 0) score += std::pow(wdiff, 2) / norm; + if(hdiff > 0) score += std::pow(hdiff, 2) / norm; + + return score; + }; + + pck_.configure(pconf_); + } +}; + +template<> +class AutoArranger: public _ArrBase { +public: + AutoArranger(const PolygonImpl& bin, Distance dist, + std::function progressind): + _ArrBase(bin, dist, progressind) + { + pconf_.object_function = [this, &bin] ( + Pile& pile, + const Item &item, + double pile_area, + double norm, + double /*penality*/) { + + auto binbb = ShapeLike::boundingBox(bin); + auto result = objfunc(binbb.center(), bin_area_, pile, + pile_area, item, norm, areacache_, rtree_); + double score = std::get<0>(result); + + pile.emplace_back(item.transformedShape()); + auto chull = ShapeLike::convexHull(pile); + pile.pop_back(); + + // If it does not fit into the print bed we will beat it with a + // large penality. If we would not do this, there would be only one + // big pile that doesn't care whether it fits onto the print bed. + if(!Placer::wouldFit(chull, bin)) score += norm; + + return score; + }; + + pck_.configure(pconf_); + } +}; + +template<> // Specialization with no bin +class AutoArranger: public _ArrBase { +public: + + AutoArranger(Distance dist, std::function progressind): + _ArrBase(Box(0, 0), dist, progressind) + { + this->pconf_.object_function = [this] ( + Pile& pile, + const Item &item, + double pile_area, + double norm, + double /*penality*/) { + + auto result = objfunc({0, 0}, 0, pile, pile_area, + item, norm, areacache_, rtree_); + return std::get<0>(result); + }; + + this->pck_.configure(pconf_); + } +}; + +// A container which stores a pointer to the 3D object and its projected +// 2D shape from top view. +using ShapeData2D = + std::vector>; + +ShapeData2D projectModelFromTop(const Slic3r::Model &model) { + ShapeData2D ret; + + auto s = std::accumulate(model.objects.begin(), model.objects.end(), 0, + [](size_t s, ModelObject* o){ + return s + o->instances.size(); + }); + + ret.reserve(s); + + for(auto objptr : model.objects) { + if(objptr) { + + auto rmesh = objptr->raw_mesh(); + + for(auto objinst : objptr->instances) { + if(objinst) { + Slic3r::TriangleMesh tmpmesh = rmesh; + ClipperLib::PolygonImpl pn; + + tmpmesh.scale(objinst->scaling_factor); + + // TODO export the exact 2D projection + auto p = tmpmesh.convex_hull(); + + p.make_clockwise(); + p.append(p.first_point()); + pn.Contour = Slic3rMultiPoint_to_ClipperPath( p ); + + // Efficient conversion to item. + Item item(std::move(pn)); + + // Invalid geometries would throw exceptions when arranging + if(item.vertexCount() > 3) { + item.rotation(objinst->rotation); + item.translation( { + ClipperLib::cInt(objinst->offset.x/SCALING_FACTOR), + ClipperLib::cInt(objinst->offset.y/SCALING_FACTOR) + }); + ret.emplace_back(objinst, item); + } + } + } + } + } + + return ret; +} + +enum BedShapeHint { + BOX, + CIRCLE, + IRREGULAR, + WHO_KNOWS +}; + +BedShapeHint bedShape(const Slic3r::Polyline& /*bed*/) { + // Determine the bed shape by hand + return BOX; +} + +void applyResult( + IndexedPackGroup::value_type& group, + Coord batch_offset, + ShapeData2D& shapemap) +{ + for(auto& r : group) { + auto idx = r.first; // get the original item index + Item& item = r.second; // get the item itself + + // Get the model instance from the shapemap using the index + ModelInstance *inst_ptr = shapemap[idx].first; + + // Get the tranformation data from the item object and scale it + // appropriately + auto off = item.translation(); + Radians rot = item.rotation(); + Pointf foff(off.X*SCALING_FACTOR + batch_offset, + off.Y*SCALING_FACTOR); + + // write the tranformation data into the model instance + inst_ptr->rotation = rot; + inst_ptr->offset = foff; + } +} + + +/** + * \brief Arranges the model objects on the screen. + * + * The arrangement considers multiple bins (aka. print beds) for placing all + * the items provided in the model argument. If the items don't fit on one + * print bed, the remaining will be placed onto newly created print beds. + * The first_bin_only parameter, if set to true, disables this behaviour and + * makes sure that only one print bed is filled and the remaining items will be + * untouched. When set to false, the items which could not fit onto the + * print bed will be placed next to the print bed so the user should see a + * pile of items on the print bed and some other piles outside the print + * area that can be dragged later onto the print bed as a group. + * + * \param model The model object with the 3D content. + * \param dist The minimum distance which is allowed for any pair of items + * on the print bed in any direction. + * \param bb The bounding box of the print bed. It corresponds to the 'bin' + * for bin packing. + * \param first_bin_only This parameter controls whether to place the + * remaining items which do not fit onto the print area next to the print + * bed or leave them untouched (let the user arrange them by hand or remove + * them). + */ +bool arrange(Model &model, coordf_t min_obj_distance, + const Slic3r::Polyline& bed, + BedShapeHint bedhint, + bool first_bin_only, + std::function progressind) +{ + using ArrangeResult = _IndexedPackGroup; + + bool ret = true; + + // Get the 2D projected shapes with their 3D model instance pointers + auto shapemap = arr::projectModelFromTop(model); + + // Copy the references for the shapes only as the arranger expects a + // sequence of objects convertible to Item or ClipperPolygon + std::vector> shapes; + shapes.reserve(shapemap.size()); + std::for_each(shapemap.begin(), shapemap.end(), + [&shapes] (ShapeData2D::value_type& it) + { + shapes.push_back(std::ref(it.second)); + }); + + IndexedPackGroup result; + BoundingBox bbb(bed.points); + + auto binbb = Box({ + static_cast(bbb.min.x), + static_cast(bbb.min.y) + }, + { + static_cast(bbb.max.x), + static_cast(bbb.max.y) + }); + + switch(bedhint) { + case BOX: { + + // Create the arranger for the box shaped bed + AutoArranger arrange(binbb, min_obj_distance, progressind); + + // Arrange and return the items with their respective indices within the + // input sequence. + result = arrange(shapes.begin(), shapes.end()); + break; + } + case CIRCLE: + break; + case IRREGULAR: + case WHO_KNOWS: { + using P = libnest2d::PolygonImpl; + + auto ctour = Slic3rMultiPoint_to_ClipperPath(bed); + P irrbed = ShapeLike::create(std::move(ctour)); + +// std::cout << ShapeLike::toString(irrbed) << std::endl; + + AutoArranger

arrange(irrbed, min_obj_distance, progressind); + + // Arrange and return the items with their respective indices within the + // input sequence. + result = arrange(shapes.begin(), shapes.end()); + break; + } + }; + + if(first_bin_only) { + applyResult(result.front(), 0, shapemap); + } else { + + const auto STRIDE_PADDING = 1.2; + + Coord stride = static_cast(STRIDE_PADDING* + binbb.width()*SCALING_FACTOR); + Coord batch_offset = 0; + + for(auto& group : result) { + applyResult(group, batch_offset, shapemap); + + // Only the first pack group can be placed onto the print bed. The + // other objects which could not fit will be placed next to the + // print bed + batch_offset += stride; + } + } + + for(auto objptr : model.objects) objptr->invalidate_bounding_box(); + + return ret && result.size() == 1; +} + +} +} +#endif // MODELARRANGE_HPP diff --git a/xs/src/slic3r/AppController.cpp b/xs/src/slic3r/AppController.cpp index 151b7f880..58858f5fc 100644 --- a/xs/src/slic3r/AppController.cpp +++ b/xs/src/slic3r/AppController.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -293,6 +294,8 @@ void AppController::arrange_model() supports_asynch()? std::launch::async : std::launch::deferred, [this]() { + using Coord = libnest2d::TCoord; + unsigned count = 0; for(auto obj : model_->objects) count += obj->instances.size(); @@ -310,13 +313,25 @@ void AppController::arrange_model() auto dist = print_ctl()->config().min_object_distance(); - BoundingBoxf bb(print_ctl()->config().bed_shape.values); + // Create the arranger config + auto min_obj_distance = static_cast(dist/SCALING_FACTOR); + + auto& bedpoints = print_ctl()->config().bed_shape.values; + Polyline bed; bed.points.reserve(bedpoints.size()); + for(auto& v : bedpoints) + bed.append(Point::new_scale(v.x, v.y)); if(pind) pind->update(0, _(L("Arranging objects..."))); try { - model_->arrange_objects(dist, &bb, [pind, count](unsigned rem){ - if(pind) pind->update(count - rem, _(L("Arranging objects..."))); + arr::arrange(*model_, + min_obj_distance, + bed, + arr::BOX, + false, // create many piles not just one pile + [pind, count](unsigned rem) { + if(pind) + pind->update(count - rem, _(L("Arranging objects..."))); }); } catch(std::exception& e) { std::cerr << e.what() << std::endl;