///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #ifndef ARRANGEIMPL_HPP #define ARRANGEIMPL_HPP #include #include #include "Arrange.hpp" #include "Core/ArrangeBase.hpp" #include "Core/ArrangeFirstFit.hpp" #include "Core/NFP/PackStrategyNFP.hpp" #include "Core/NFP/Kernels/TMArrangeKernel.hpp" #include "Core/NFP/Kernels/GravityKernel.hpp" #include "Core/NFP/RectangleOverfitPackingStrategy.hpp" #include "Core/Beds.hpp" #include "Items/MutableItemTraits.hpp" #include "SegmentedRectangleBed.hpp" #include "libslic3r/Execution/ExecutionTBB.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" #ifndef NDEBUG #include "Core/NFP/Kernels/SVGDebugOutputKernelWrapper.hpp" #endif namespace Slic3r { namespace arr2 { // arrange overload for SegmentedRectangleBed which is exactly what is used // by XL printers. template void arrange(SelectionStrategy &&selstrategy, PackStrategy &&packingstrategy, const Range &items, const Range &fixed, const SegmentedRectangleBed &bed) { // Dispatch: arrange(std::forward(selstrategy), std::forward(packingstrategy), items, fixed, RectangleBed{bed.bb}, SelStrategyTag{}); std::vector bed_indices = get_bed_indices(items, fixed); std::map pilebb; std::map bed_occupied; for (auto &itm : items) { auto bedidx = get_bed_index(itm); if (bedidx >= 0) { pilebb[bedidx].merge(fixed_bounding_box(itm)); if (is_wipe_tower(itm)) bed_occupied[bedidx] = true; } } for (auto &fxitm : fixed) { auto bedidx = get_bed_index(fxitm); if (bedidx >= 0) bed_occupied[bedidx] = true; } auto bedbb = bounding_box(bed); auto piecesz = unscaled(bedbb).size(); piecesz.x() /= bed.segments_x(); piecesz.y() /= bed.segments_y(); using Pivots = RectPivots; Pivots pivot = bed.alignment(); for (int bedidx : bed_indices) { if (auto occup_it = bed_occupied.find(bedidx); occup_it != bed_occupied.end() && occup_it->second) continue; BoundingBox bb; auto pilesz = unscaled(pilebb[bedidx]).size(); bb.max.x() = scaled(std::ceil(pilesz.x() / piecesz.x()) * piecesz.x()); bb.max.y() = scaled(std::ceil(pilesz.y() / piecesz.y()) * piecesz.y()); switch (pivot) { case Pivots::BottomLeft: bb.translate(bedbb.min - bb.min); break; case Pivots::TopRight: bb.translate(bedbb.max - bb.max); break; case Pivots::BottomRight: { Point bedref{bedbb.max.x(), bedbb.min.y()}; Point bbref {bb.max.x(), bb.min.y()}; bb.translate(bedref - bbref); break; } case Pivots::TopLeft: { Point bedref{bedbb.min.x(), bedbb.max.y()}; Point bbref {bb.min.x(), bb.max.y()}; bb.translate(bedref - bbref); break; } case Pivots::Center: { bb.translate(bedbb.center() - bb.center()); break; } default: ; } Vec2crd d = bb.center() - pilebb[bedidx].center(); auto pilebbx = pilebb[bedidx]; pilebbx.translate(d); Point corr{0, 0}; corr.x() = -std::min(0, pilebbx.min.x() - bedbb.min.x()) -std::max(0, pilebbx.max.x() - bedbb.max.x()); corr.y() = -std::min(0, pilebbx.min.y() - bedbb.min.y()) -std::max(0, pilebbx.max.y() - bedbb.max.y()); d += corr; for (auto &itm : items) if (get_bed_index(itm) == static_cast(bedidx) && !is_wipe_tower(itm)) translate(itm, d); } } using VariantKernel = boost::variant; template<> struct KernelTraits_ { template static double placement_fitness(const VariantKernel &kernel, const ArrItem &itm, const Vec2crd &transl) { double ret = NaNd; boost::apply_visitor( [&](auto &k) { ret = k.placement_fitness(itm, transl); }, kernel); return ret; } template static bool on_start_packing(VariantKernel &kernel, ArrItem &itm, const Bed &bed, const Ctx &packing_context, const Range &remaining_items) { bool ret = false; boost::apply_visitor([&](auto &k) { ret = k.on_start_packing(itm, bed, packing_context, remaining_items); }, kernel); return ret; } template static bool on_item_packed(VariantKernel &kernel, ArrItem &itm) { bool ret = false; boost::apply_visitor([&](auto &k) { ret = k.on_item_packed(itm); }, kernel); return ret; } }; template struct firstfit::ItemArrangedVisitor> { template static void on_arranged(ArrItem &itm, const Bed &bed, const Range &packed, const Range &remaining) { using OnArrangeCb = std::function &)>; auto cb = get_data(itm, "on_arranged"); if (cb) { (*cb)(itm); } } }; inline RectPivots xlpivots_to_rect_pivots(ArrangeSettingsView::XLPivots xlpivot) { if (xlpivot == arr2::ArrangeSettingsView::xlpRandom) { // means it should be random std::random_device rd{}; std::mt19937 rng(rd()); std::uniform_int_distribution dist(0, arr2::ArrangeSettingsView::xlpRandom - 1); xlpivot = static_cast(dist(rng)); } RectPivots rectpivot = RectPivots::Center; switch(xlpivot) { case arr2::ArrangeSettingsView::xlpCenter: rectpivot = RectPivots::Center; break; case arr2::ArrangeSettingsView::xlpFrontLeft: rectpivot = RectPivots::BottomLeft; break; case arr2::ArrangeSettingsView::xlpFrontRight: rectpivot = RectPivots::BottomRight; break; case arr2::ArrangeSettingsView::xlpRearLeft: rectpivot = RectPivots::TopLeft; break; case arr2::ArrangeSettingsView::xlpRearRight: rectpivot = RectPivots::TopRight; break; default: ; } return rectpivot; } template void fill_rotations(const Range &items, const Bed &bed, const ArrangeSettingsView &settings) { if (!settings.is_rotation_enabled()) return; for (auto &itm : items) { if (is_wipe_tower(itm)) // Rotating the wipe tower is currently problematic continue; // Use the minimum bounding box rotation as a starting point. auto minbbr = get_min_area_bounding_box_rotation(itm); std::vector rotations = {minbbr, minbbr + PI / 4., minbbr + PI / 2., minbbr + PI, minbbr + 3 * PI / 4.}; // Add the original rotation of the item if minbbr // is not already the original rotation (zero) if (std::abs(minbbr) > 0.) rotations.emplace_back(0.); // Also try to find the rotation that fits the item // into a rectangular bed, given that it cannot fit, // and there exists a rotation which can fit. if constexpr (std::is_convertible_v) { double fitbrot = get_fit_into_bed_rotation(itm, bed); if (std::abs(fitbrot) > 0.) rotations.emplace_back(fitbrot); } set_allowed_rotations(itm, rotations); } } // An arranger put together to fulfill all the requirements of PrusaSlicer based // on the supplied ArrangeSettings template class DefaultArranger: public Arranger { ArrangeSettings m_settings; static constexpr auto Accuracy = 1.; template void arrange_( const Range &items, const Range &fixed, const Bed &bed, ArrangerCtl &ctl) { auto cmpfn = [](const auto &itm1, const auto &itm2) { int pa = get_priority(itm1); int pb = get_priority(itm2); return pa == pb ? area(envelope_convex_hull(itm1)) > area(envelope_convex_hull(itm2)) : pa > pb; }; auto on_arranged = [&ctl](auto &itm, auto &bed, auto &ctx, auto &rem) { ctl.update_status(rem.size()); ctl.on_packed(itm); firstfit::DefaultOnArrangedFn{}(itm, bed, ctx, rem); }; auto stop_cond = [&ctl] { return ctl.was_canceled(); }; firstfit::SelectionStrategy sel{cmpfn, on_arranged, stop_cond}; constexpr auto ep = ex_tbb; VariantKernel basekernel; switch (m_settings.get_arrange_strategy()) { default: [[fallthrough]]; case ArrangeSettingsView::asAuto: if constexpr (std::is_convertible_v){ basekernel = GravityKernel{}; } else { basekernel = TMArrangeKernel{items.size(), area(bed)}; } break; case ArrangeSettingsView::asPullToCenter: basekernel = GravityKernel{}; break; } #ifndef NDEBUG SVGDebugOutputKernelWrapper kernel{bounding_box(bed), basekernel}; #else auto & kernel = basekernel; #endif fill_rotations(items, bed, m_settings); bool with_wipe_tower = std::any_of(items.begin(), items.end(), [](auto &itm) { return is_wipe_tower(itm); }); // With rectange bed, and no fixed items, let's use an infinite bed // with RectangleOverfitKernelWrapper. It produces better results than // a pure RectangleBed with inner-fit polygon calculation. if (!with_wipe_tower && m_settings.get_arrange_strategy() == ArrangeSettingsView::asAuto && std::is_convertible_v) { PackStrategyNFP base_strategy{std::move(kernel), ep, Accuracy, stop_cond}; RectangleOverfitPackingStrategy final_strategy{std::move(base_strategy)}; arr2::arrange(sel, final_strategy, items, fixed, bed); } else { PackStrategyNFP ps{std::move(kernel), ep, Accuracy, stop_cond}; arr2::arrange(sel, ps, items, fixed, bed); } } public: explicit DefaultArranger(const ArrangeSettingsView &settings) { m_settings.set_from(settings); } void arrange( std::vector &items, const std::vector &fixed, const ExtendedBed &bed, ArrangerCtl &ctl) override { visit_bed([this, &items, &fixed, &ctl](auto rawbed) { if constexpr (IsSegmentedBed) rawbed.pivot = xlpivots_to_rect_pivots( m_settings.get_xl_alignment()); arrange_(range(items), crange(fixed), rawbed, ctl); }, bed); } }; template std::unique_ptr> Arranger::create( const ArrangeSettingsView &settings) { // Currently all that is needed is handled by DefaultArranger return std::make_unique>(settings); } template ArrItem ConvexItemConverter::convert(const Arrangeable &arrbl, coord_t offs) const { auto bed_index = arrbl.get_bed_index(); Polygon outline = arrbl.convex_outline(); if (outline.empty()) throw EmptyItemOutlineError{}; Polygon envelope = arrbl.convex_envelope(); coord_t infl = offs + coord_t(std::ceil(this->safety_dist() / 2.)); if (infl != 0) { outline = Geometry::convex_hull(offset(outline, infl)); if (! envelope.empty()) envelope = Geometry::convex_hull(offset(envelope, infl)); } ArrItem ret; set_convex_shape(ret, outline); if (! envelope.empty()) set_convex_envelope(ret, envelope); set_bed_index(ret, bed_index); set_priority(ret, arrbl.priority()); imbue_id(ret, arrbl.id()); if constexpr (IsWritableDataStore) arrbl.imbue_data(AnyWritableDataStore{ret}); return ret; } template ArrItem AdvancedItemConverter::convert(const Arrangeable &arrbl, coord_t offs) const { auto bed_index = arrbl.get_bed_index(); ArrItem ret = get_arritem(arrbl, offs); set_bed_index(ret, bed_index); set_priority(ret, arrbl.priority()); imbue_id(ret, arrbl.id()); if constexpr (IsWritableDataStore) arrbl.imbue_data(AnyWritableDataStore{ret}); return ret; } template ArrItem AdvancedItemConverter::get_arritem(const Arrangeable &arrbl, coord_t offs) const { coord_t infl = offs + coord_t(std::ceil(this->safety_dist() / 2.)); auto outline = arrbl.full_outline(); if (outline.empty()) throw EmptyItemOutlineError{}; auto envelope = arrbl.full_envelope(); if (infl != 0) { outline = offset_ex(outline, infl); if (! envelope.empty()) envelope = offset_ex(envelope, infl); } auto simpl_tol = static_cast(this->simplification_tolerance()); if (simpl_tol > 0) { outline = expolygons_simplify(outline, simpl_tol); if (!envelope.empty()) envelope = expolygons_simplify(envelope, simpl_tol); } ArrItem ret; set_shape(ret, outline); if (! envelope.empty()) set_envelope(ret, envelope); return ret; } template ArrItem BalancedItemConverter::get_arritem(const Arrangeable &arrbl, coord_t offs) const { ArrItem ret = AdvancedItemConverter::get_arritem(arrbl, offs); set_convex_envelope(ret, envelope_convex_hull(ret)); return ret; } template std::unique_ptr> ArrangeableToItemConverter::create( ArrangeSettingsView::GeometryHandling gh, coord_t safety_d) { std::unique_ptr> ret; constexpr coord_t SimplifyTol = scaled(.2); switch(gh) { case arr2::ArrangeSettingsView::ghConvex: ret = std::make_unique>(safety_d); break; case arr2::ArrangeSettingsView::ghBalanced: ret = std::make_unique>(safety_d, SimplifyTol); break; case arr2::ArrangeSettingsView::ghAdvanced: ret = std::make_unique>(safety_d, SimplifyTol); break; default: ; } return ret; } }} // namespace Slic3r::arr2 #endif // ARRANGEIMPL_HPP