From 49c6ce76d0552112855067822f6a435d8a0b9797 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Tue, 22 Jun 2021 11:23:19 +0200 Subject: [PATCH] Search for suitable rotation when arranging items larger than the bed --- .../include/libnest2d/utils/rotcalipers.hpp | 242 +++++++++++++----- src/libslic3r/Arrange.cpp | 21 +- 2 files changed, 191 insertions(+), 72 deletions(-) diff --git a/src/libnest2d/include/libnest2d/utils/rotcalipers.hpp b/src/libnest2d/include/libnest2d/utils/rotcalipers.hpp index 76f91ba0c..e703a3801 100644 --- a/src/libnest2d/include/libnest2d/utils/rotcalipers.hpp +++ b/src/libnest2d/include/libnest2d/utils/rotcalipers.hpp @@ -90,12 +90,29 @@ inline R rectarea(const Pt& w, const std::array& rect) return rectarea(w, *rect[0], *rect[1], *rect[2], *rect[3]); } +template, class R = TCompute> +inline R rectarea(const Pt& w, // the axis + const Unit& a, + const Unit& b) +{ + R m = R(a) / pl::magnsq(w); + m = m * b; + return m; +}; + +template +inline R rectarea(const RotatedBox &rb) +{ + return rectarea(rb.axis(), rb.bottom_extent(), rb.right_extent()); +}; + // This function is only applicable to counter-clockwise oriented convex // polygons where only two points can be collinear witch each other. -template , - class Ratio = TCompute> -RotatedBox, Unit> minAreaBoundingBox(const RawShape& sh) +template , + class Ratio = TCompute, + class VisitFn> +void rotcalipers(const RawShape& sh, VisitFn &&visitfn) { using Point = TPoint; using Iterator = typename TContour::const_iterator; @@ -104,21 +121,21 @@ RotatedBox, Unit> minAreaBoundingBox(const RawShape& sh) // Get the first and the last vertex iterator auto first = sl::cbegin(sh); auto last = std::prev(sl::cend(sh)); - + // Check conditions and return undefined box if input is not sane. - if(last == first) return {}; + if(last == first) return; if(getX(*first) == getX(*last) && getY(*first) == getY(*last)) --last; - if(last - first < 2) return {}; - + if(last - first < 2) return; + RawShape shcpy; // empty at this point - { + { Point p = *first, q = *std::next(first), r = *last; - + // Determine orientation from first 3 vertex (should be consistent) Unit d = (Unit(getY(q)) - getY(p)) * (Unit(getX(r)) - getX(p)) - (Unit(getX(q)) - getX(p)) * (Unit(getY(r)) - getY(p)); - - if(d > 0) { + + if(d > 0) { // The polygon is clockwise. A flip is needed (for now) sl::reserve(shcpy, last - first); auto it = last; while(it != first) sl::addVertex(shcpy, *it--); @@ -126,69 +143,69 @@ RotatedBox, Unit> minAreaBoundingBox(const RawShape& sh) first = sl::cbegin(shcpy); last = std::prev(sl::cend(shcpy)); } } - + // Cyclic iterator increment auto inc = [&first, &last](Iterator& it) { - if(it == last) it = first; else ++it; + if(it == last) it = first; else ++it; }; - + // Cyclic previous iterator - auto prev = [&first, &last](Iterator it) { - return it == first ? last : std::prev(it); + auto prev = [&first, &last](Iterator it) { + return it == first ? last : std::prev(it); }; - + // Cyclic next iterator auto next = [&first, &last](Iterator it) { - return it == last ? first : std::next(it); + return it == last ? first : std::next(it); }; - - // Establish initial (axis aligned) rectangle support verices by determining + + // Establish initial (axis aligned) rectangle support verices by determining // polygon extremes: - + auto it = first; Iterator minX = it, maxX = it, minY = it, maxY = it; - + do { // Linear walk through the vertices and save the extreme positions - + Point v = *it, d = v - *minX; if(getX(d) < 0 || (getX(d) == 0 && getY(d) < 0)) minX = it; - + d = v - *maxX; if(getX(d) > 0 || (getX(d) == 0 && getY(d) > 0)) maxX = it; - + d = v - *minY; if(getY(d) < 0 || (getY(d) == 0 && getX(d) > 0)) minY = it; - + d = v - *maxY; if(getY(d) > 0 || (getY(d) == 0 && getX(d) < 0)) maxY = it; - + } while(++it != std::next(last)); - + // Update the vertices defining the bounding rectangle. The rectangle with - // the smallest rotation is selected and the supporting vertices are + // the smallest rotation is selected and the supporting vertices are // returned in the 'rect' argument. auto update = [&next, &inc] - (const Point& w, std::array& rect) + (const Point& w, std::array& rect) { Iterator B = rect[0], Bn = next(B); Iterator R = rect[1], Rn = next(R); Iterator T = rect[2], Tn = next(T); Iterator L = rect[3], Ln = next(L); - + Point b = *Bn - *B, r = *Rn - *R, t = *Tn - *T, l = *Ln - *L; Point pw = perp(w); using Pt = Point; - + Unit dotwpb = dot( w, b), dotwpr = dot(-pw, r); Unit dotwpt = dot(-w, t), dotwpl = dot( pw, l); Unit dw = magnsq(w); - + std::array angles; angles[0] = (Ratio(dotwpb) / magnsq(b)) * dotwpb; angles[1] = (Ratio(dotwpr) / magnsq(r)) * dotwpr; angles[2] = (Ratio(dotwpt) / magnsq(t)) * dotwpt; angles[3] = (Ratio(dotwpl) / magnsq(l)) * dotwpl; - + using AngleIndex = std::pair; std::vector A; A.reserve(4); @@ -196,65 +213,84 @@ RotatedBox, Unit> minAreaBoundingBox(const RawShape& sh) if(rect[i] != rect[j] && angles[i] < dw) { auto iv = std::make_pair(angles[i], i); auto it = std::lower_bound(A.begin(), A.end(), iv, - [](const AngleIndex& ai, - const AngleIndex& aj) - { - return ai.first > aj.first; + [](const AngleIndex& ai, + const AngleIndex& aj) + { + return ai.first > aj.first; }); - + A.insert(it, iv); } } - + // The polygon is supposed to be a rectangle. if(A.empty()) return false; - + auto amin = A.front().first; auto imin = A.front().second; for(auto& a : A) if(a.first == amin) inc(rect[a.second]); - + std::rotate(rect.begin(), rect.begin() + imin, rect.end()); - + return true; }; - + Point w(1, 0); - Point w_min = w; - Ratio minarea((Unit(getX(*maxX)) - getX(*minX)) * - (Unit(getY(*maxY)) - getY(*minY))); - std::array rect = {minY, maxX, maxY, minX}; - std::array minrect = rect; - + + { + Unit a = dot(w, *rect[1] - *rect[3]); + Unit b = dot(-perp(w), *rect[2] - *rect[0]); + if (!visitfn(RotatedBox{w, a, b})) + return; + } + // An edge might be examined twice in which case the algorithm terminates. size_t c = 0, count = last - first + 1; std::vector edgemask(count, false); - - while(c++ < count) - { + + while(c++ < count) + { // Update the support vertices, if cannot be updated, break the cycle. if(! update(w, rect)) break; - + size_t eidx = size_t(rect[0] - first); - + if(edgemask[eidx]) break; edgemask[eidx] = true; - + // get the unnormalized direction vector w = *rect[0] - *prev(rect[0]); - - // get the area of the rotated rectangle - Ratio rarea = rectarea(w, rect); - - // Update min area and the direction of the min bounding box; - if(rarea <= minarea) { w_min = w; minarea = rarea; minrect = rect; } + + Unit a = dot(w, *rect[1] - *rect[3]); + Unit b = dot(-perp(w), *rect[2] - *rect[0]); + if (!visitfn(RotatedBox{w, a, b})) + break; } - - Unit a = dot(w_min, *minrect[1] - *minrect[3]); - Unit b = dot(-perp(w_min), *minrect[2] - *minrect[0]); - RotatedBox bb(w_min, a, b); - - return bb; +} + +// This function is only applicable to counter-clockwise oriented convex +// polygons where only two points can be collinear witch each other. +template , + class Ratio = TCompute> +RotatedBox, Unit> minAreaBoundingBox(const S& sh) +{ + RotatedBox, Unit> minbox; + Ratio minarea = std::numeric_limits::max(); + auto minfn = [&minarea, &minbox](const RotatedBox, Unit> &rbox){ + Ratio area = rectarea(rbox); + if (area <= minarea) { + minarea = area; + minbox = rbox; + } + + return true; // continue search + }; + + rotcalipers(sh, minfn); + + return minbox; } template Radians minAreaBoundingBoxRotation(const RawShape& sh) @@ -262,7 +298,75 @@ template Radians minAreaBoundingBoxRotation(const RawShape& sh) return minAreaBoundingBox(sh).angleToX(); } +// Function to find a rotation for a shape that makes it fit into a box. +// +// The method is based on finding a pair of rotations from the rotating calipers +// algorithm such that the aspect ratio is changing from being smaller than +// that of the target to being bigger or vice versa. So that the correct +// AR is somewhere between the obtained pair of angles. Then bisecting that +// interval is sufficient to find the correct angle. +// +// The argument eps is the absolute error limit for the searched angle interval. +template, class Ratio = TCompute> +Radians fitIntoBoxRotation(const S &shape, const _Box> &box, Radians eps = 1e-4) +{ + constexpr auto get_aspect_r = [](const auto &b) -> double { + return double(b.width()) / b.height(); + }; + auto aspect_r = get_aspect_r(box); + + RotatedBox, Unit> prev_rbox; + Radians a_from = 0., a_to = 0.; + auto visitfn = [&](const RotatedBox, Unit> &rbox) { + bool lower_prev = get_aspect_r(prev_rbox) < aspect_r; + bool lower_current = get_aspect_r(rbox) < aspect_r; + + if (lower_prev != lower_current) { + a_from = prev_rbox.angleToX(); + a_to = rbox.angleToX(); + return false; + } + + return true; + }; + + rotcalipers(shape, visitfn); + + auto rot_shape_bb = [&shape](Radians r) { + auto s = shape; + sl::rotate(s, r); + return sl::boundingBox(s); + }; + + auto rot_aspect_r = [&rot_shape_bb, &get_aspect_r](Radians r) { + return get_aspect_r(rot_shape_bb(r)); + }; + + // Lets bisect the retrieved interval where the correct aspect ratio is. + double ar_from = rot_aspect_r(a_from); + auto would_fit = [&box](const _Box> &b) { + return b.width() < box.width() && b.height() < box.height(); + }; + + Radians middle = (a_from + a_to) / 2.; + _Box> box_middle = rot_shape_bb(middle); + while (!would_fit(box_middle) && std::abs(a_to - a_from) > eps) + { + double ar_middle = get_aspect_r(box_middle); + if ((ar_from < aspect_r) != (ar_middle < aspect_r)) + a_to = middle; + else + a_from = middle; + + ar_from = rot_aspect_r(a_from); + middle = (a_from + a_to) / 2.; + box_middle = rot_shape_bb(middle); + } + + return middle; } +} // namespace libnest2d + #endif // ROTCALIPERS_HPP diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index 9602ebdbd..bf2a219d0 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -379,7 +379,7 @@ public: }); if (stopcond) m_pck.stopCondition(stopcond); - + m_pck.configure(m_pconf); } @@ -472,6 +472,12 @@ template Radians min_area_boundingbox_rotation(const S &sh) .angleToX(); } +template +Radians fit_into_box_rotation(const S &sh, const _Box> &box) +{ + return fitIntoBoxRotation, boost::rational>(sh, box); +} + template // Arrange for arbitrary bin type void _arrange( std::vector & shapes, @@ -509,10 +515,19 @@ void _arrange( // Use the minimum bounding box rotation as a starting point. // TODO: This only works for convex hull. If we ever switch to concave // polygon nesting, a convex hull needs to be calculated. - if (params.allow_rotations) - for (auto &itm : shapes) + if (params.allow_rotations) { + for (auto &itm : shapes) { itm.rotation(min_area_boundingbox_rotation(itm.rawShape())); + // If the item is too big, try to find a rotation that makes it fit + if constexpr (std::is_same_v) { + auto bb = itm.boundingBox(); + if (bb.width() >= bin.width() || bb.height() >= bin.height()) + itm.rotate(fit_into_box_rotation(itm.transformedShape(), bin)); + } + } + } + arranger(inp.begin(), inp.end()); for (Item &itm : inp) itm.inflate(-infl); }