diff --git a/CMakeLists.txt b/CMakeLists.txt index 90e7bf0..63ff7f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,6 @@ option(SLIC3R_GUI "Compile QIDISlicer with GUI components (OpenGL, wxWidge option(SLIC3R_FHS "Assume QIDISlicer is to be installed in a FHS directory structure" 0) option(SLIC3R_PCH "Use precompiled headers" 1) option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1) -option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1) option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0) option(SLIC3R_UBSAN "Enable UBSan on Clang and GCC" 0) option(SLIC3R_ENABLE_FORMAT_STEP "Enable compilation of STEP file support" ON) @@ -236,7 +235,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) - find_package(DBus REQUIRED) + find_package(DBus1 REQUIRED) endif() if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX) @@ -360,7 +359,7 @@ endif() # set(Boost_COMPILER "-mgw81") # boost::process was introduced first in version 1.64.0, # boost::beast::detail::base64 was introduced first in version 1.66.0 -set(MINIMUM_BOOST_VERSION "1.66.0") +set(MINIMUM_BOOST_VERSION "1.83.0") set(_boost_components "system;filesystem;thread;log;locale;regex;chrono;atomic;date_time;iostreams;nowide") find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS ${_boost_components}) diff --git a/src/slic3r-arrange-wrapper/.vscode/settings.json b/src/slic3r-arrange-wrapper/.vscode/settings.json new file mode 100644 index 0000000..a478c1f --- /dev/null +++ b/src/slic3r-arrange-wrapper/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "string_view": "cpp" + } +} \ No newline at end of file diff --git a/src/slic3r-arrange-wrapper/CMakeLists.txt b/src/slic3r-arrange-wrapper/CMakeLists.txt new file mode 100644 index 0000000..3f723e5 --- /dev/null +++ b/src/slic3r-arrange-wrapper/CMakeLists.txt @@ -0,0 +1,35 @@ +project(slic3r-arrange-wrapper) +cmake_minimum_required(VERSION 3.13) + +add_library(slic3r-arrange-wrapper + include/arrange-wrapper/Arrange.hpp + include/arrange-wrapper/ArrangeSettingsDb_AppCfg.hpp + include/arrange-wrapper/ArrangeSettingsView.hpp + include/arrange-wrapper/Items/ArbitraryDataStore.hpp + include/arrange-wrapper/Items/ArrangeItem.hpp + include/arrange-wrapper/Items/MutableItemTraits.hpp + include/arrange-wrapper/Items/SimpleArrangeItem.hpp + include/arrange-wrapper/Items/TrafoOnlyArrangeItem.hpp + include/arrange-wrapper/Scene.hpp + include/arrange-wrapper/SceneBuilder.hpp + include/arrange-wrapper/SegmentedRectangleBed.hpp + include/arrange-wrapper/Tasks/ArrangeTask.hpp + include/arrange-wrapper/Tasks/FillBedTask.hpp + include/arrange-wrapper/Tasks/MultiplySelectionTask.hpp + include/arrange-wrapper/ModelArrange.hpp + + src/ArrangeImpl.hpp + src/ArrangeSettingsDb_AppCfg.cpp + src/Items/SimpleArrangeItem.cpp + src/SceneBuilder.cpp + src/Scene.cpp + src/Items/ArrangeItem.cpp + src/ModelArrange.cpp + src/Tasks/ArrangeTaskImpl.hpp + src/Tasks/FillBedTaskImpl.hpp + src/Tasks/MultiplySelectionTaskImpl.hpp +) + +target_include_directories(slic3r-arrange-wrapper PRIVATE src) +target_include_directories(slic3r-arrange-wrapper PUBLIC include) +target_link_libraries(slic3r-arrange-wrapper PUBLIC slic3r-arrange) diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Arrange.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Arrange.hpp new file mode 100644 index 0000000..6684e88 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Arrange.hpp @@ -0,0 +1,268 @@ +#ifndef ARRANGE2_HPP +#define ARRANGE2_HPP + +#include +#include + +#include "Scene.hpp" +#include "Items/MutableItemTraits.hpp" + +namespace Slic3r { namespace arr2 { + +template class Arranger +{ +public: + class Ctl : public ArrangeTaskCtl { + public: + virtual void on_packed(ArrItem &item) {}; + }; + + virtual ~Arranger() = default; + + virtual void arrange(std::vector &items, + const std::vector &fixed, + const ExtendedBed &bed, + Ctl &ctl) = 0; + + void arrange(std::vector &items, + const std::vector &fixed, + const ExtendedBed &bed, + ArrangeTaskCtl &ctl); + + void arrange(std::vector &items, + const std::vector &fixed, + const ExtendedBed &bed, + Ctl &&ctl) + { + arrange(items, fixed, bed, ctl); + } + + void arrange(std::vector &items, + const std::vector &fixed, + const ExtendedBed &bed, + ArrangeTaskCtl &&ctl) + { + arrange(items, fixed, bed, ctl); + } + + static std::unique_ptr create(const ArrangeSettingsView &settings); +}; + +template using ArrangerCtl = typename Arranger::Ctl; + +template +class DefaultArrangerCtl : public Arranger::Ctl { + ArrangeTaskCtl *taskctl = nullptr; + +public: + DefaultArrangerCtl() = default; + + explicit DefaultArrangerCtl(ArrangeTaskCtl &ctl) : taskctl{&ctl} {} + + void update_status(int st) override + { + if (taskctl) + taskctl->update_status(st); + } + + bool was_canceled() const override + { + if (taskctl) + return taskctl->was_canceled(); + + return false; + } +}; + +template +void Arranger::arrange(std::vector &items, + const std::vector &fixed, + const ExtendedBed &bed, + ArrangeTaskCtl &ctl) +{ + arrange(items, fixed, bed, DefaultArrangerCtl{ctl}); +} + +class EmptyItemOutlineError: public std::exception { + static constexpr const char *Msg = "No outline can be derived for object"; + +public: + const char* what() const noexcept override { return Msg; } +}; + +template class ArrangeableToItemConverter +{ +public: + virtual ~ArrangeableToItemConverter() = default; + + // May throw EmptyItemOutlineError + virtual ArrItem convert(const Arrangeable &arrbl, coord_t offs = 0) const = 0; + + // Returns the extent of simplification that the converter utilizes when + // creating arrange items. Zero shall mean no simplification at all. + virtual coord_t simplification_tolerance() const { return 0; } + + static std::unique_ptr create( + ArrangeSettingsView::GeometryHandling geometry_handling, + coord_t safety_d); + + static std::unique_ptr create( + const Scene &sc) + { + return create(sc.settings().get_geometry_handling(), + scaled(sc.settings().get_distance_from_objects())); + } +}; + +template> +class AnyWritableDataStore: public AnyWritable +{ + DStore &dstore; + +public: + AnyWritableDataStore(DStore &store): dstore{store} {} + + void write(std::string_view key, std::any d) override + { + set_data(dstore, std::string{key}, std::move(d)); + } +}; + +template +class BasicItemConverter : public ArrangeableToItemConverter +{ + coord_t m_safety_d; + coord_t m_simplify_tol; + +public: + BasicItemConverter(coord_t safety_d = 0, coord_t simpl_tol = 0) + : m_safety_d{safety_d}, m_simplify_tol{simpl_tol} + {} + + coord_t safety_dist() const noexcept { return m_safety_d; } + + coord_t simplification_tolerance() const override + { + return m_simplify_tol; + } +}; + +template +class ConvexItemConverter : public BasicItemConverter +{ +public: + using BasicItemConverter::BasicItemConverter; + + ArrItem convert(const Arrangeable &arrbl, coord_t offs) const override; +}; + +template +class AdvancedItemConverter : public BasicItemConverter +{ +protected: + virtual ArrItem get_arritem(const Arrangeable &arrbl, coord_t eps) const; + +public: + using BasicItemConverter::BasicItemConverter; + + ArrItem convert(const Arrangeable &arrbl, coord_t offs) const override; +}; + +template +class BalancedItemConverter : public AdvancedItemConverter +{ +protected: + ArrItem get_arritem(const Arrangeable &arrbl, coord_t offs) const override; + +public: + using AdvancedItemConverter::AdvancedItemConverter; +}; + +template struct ImbueableItemTraits_ +{ + static constexpr const char *Key = "object_id"; + + static void imbue_id(ArrItem &itm, const ObjectID &id) + { + set_arbitrary_data(itm, Key, id); + } + + static std::optional retrieve_id(const ArrItem &itm) + { + std::optional ret; + auto idptr = get_data(itm, Key); + if (idptr) + ret = *idptr; + + return ret; + } +}; + +template +using ImbueableItemTraits = ImbueableItemTraits_>; + +template +void imbue_id(ArrItem &itm, const ObjectID &id) +{ + ImbueableItemTraits::imbue_id(itm, id); +} + +template +std::optional retrieve_id(const ArrItem &itm) +{ + return ImbueableItemTraits::retrieve_id(itm); +} + +template +bool apply_arrangeitem(const ArrItem &itm, ArrangeableModel &mdl) +{ + bool ret = false; + + if (auto id = retrieve_id(itm)) { + mdl.visit_arrangeable(*id, [&itm, &ret](Arrangeable &arrbl) { + if ((ret = arrbl.assign_bed(get_bed_index(itm)))) + arrbl.transform(unscaled(get_translation(itm)), get_rotation(itm)); + }); + } + + return ret; +} + +template +double get_min_area_bounding_box_rotation(const ArrItem &itm) +{ + return MinAreaBoundigBox{envelope_convex_hull(itm), + MinAreaBoundigBox::pcConvex} + .angle_to_X(); +} + +template +double get_fit_into_bed_rotation(const ArrItem &itm, const RectangleBed &bed) +{ + double ret = 0.; + + auto bbsz = envelope_bounding_box(itm).size(); + auto binbb = bounding_box(bed); + auto binbbsz = binbb.size(); + + if (bbsz.x() >= binbbsz.x() || bbsz.y() >= binbbsz.y()) + ret = fit_into_box_rotation(envelope_convex_hull(itm), binbb); + + return ret; +} + +template +auto get_corrected_bed(const ExtendedBed &bed, + const ArrangeableToItemConverter &converter) +{ + auto bedcpy = bed; + visit_bed([tol = -converter.simplification_tolerance()](auto &rawbed) { + rawbed = offset(rawbed, tol); + }, bedcpy); + + return bedcpy; +} + +}} // namespace Slic3r::arr2 + +#endif // ARRANGE2_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/ArrangeSettingsDb_AppCfg.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/ArrangeSettingsDb_AppCfg.hpp new file mode 100644 index 0000000..c14bc7b --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/ArrangeSettingsDb_AppCfg.hpp @@ -0,0 +1,96 @@ +#ifndef ARRANGESETTINGSDB_APPCFG_HPP +#define ARRANGESETTINGSDB_APPCFG_HPP + +#include + +#include "ArrangeSettingsView.hpp" +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r { +class AppConfig; + +class ArrangeSettingsDb_AppCfg: public arr2::ArrangeSettingsDb +{ +public: + enum Slots { slotFFF, slotFFFSeqPrint, slotSLA }; + +private: + AppConfig *m_appcfg; + Slots m_current_slot = slotFFF; + + struct FloatRange { float minval = 0.f, maxval = 100.f; }; + struct Slot + { + Values vals; + Values defaults; + FloatRange dobj_range, dbed_range; + std::string postfix; + }; + + // Settings and their defaults are stored separately for fff, + // sla and fff sequential mode + Slot m_settings_fff, m_settings_fff_seq, m_settings_sla; + + template + static auto & get_slot(Self *self, Slots slot) { + switch(slot) { + case slotFFF: return self->m_settings_fff; + case slotFFFSeqPrint: return self->m_settings_fff_seq; + case slotSLA: return self->m_settings_sla; + } + + return self->m_settings_fff; + } + + template static auto &get_slot(Self *self) + { + return get_slot(self, self->m_current_slot); + } + + template + static auto& get_ref(Self *self) { return get_slot(self).vals; } + +public: + explicit ArrangeSettingsDb_AppCfg(AppConfig *appcfg); + + void sync(); + + float get_distance_from_objects() const override { return get_ref(this).d_obj; } + float get_distance_from_bed() const override { return get_ref(this).d_bed; } + bool is_rotation_enabled() const override { return get_ref(this).rotations; } + + XLPivots get_xl_alignment() const override { return m_settings_fff.vals.xl_align; } + GeometryHandling get_geometry_handling() const override { return m_settings_fff.vals.geom_handling; } + ArrangeStrategy get_arrange_strategy() const override { return m_settings_fff.vals.arr_strategy; } + + void distance_from_obj_range(float &min, float &max) const override; + void distance_from_bed_range(float &min, float &max) const override; + + ArrangeSettingsDb& set_distance_from_objects(float v) override; + ArrangeSettingsDb& set_distance_from_bed(float v) override; + ArrangeSettingsDb& set_rotation_enabled(bool v) override; + + ArrangeSettingsDb& set_xl_alignment(XLPivots v) override; + ArrangeSettingsDb& set_geometry_handling(GeometryHandling v) override; + ArrangeSettingsDb& set_arrange_strategy(ArrangeStrategy v) override; + + Values get_defaults() const override { return get_slot(this).defaults; } + + void set_active_slot(Slots slot) noexcept { m_current_slot = slot; } + void set_distance_from_obj_range(Slots slot, float min, float max) + { + get_slot(this, slot).dobj_range = FloatRange{min, max}; + } + + void set_distance_from_bed_range(Slots slot, float min, float max) + { + get_slot(this, slot).dbed_range = FloatRange{min, max}; + } + + Values &get_defaults(Slots slot) { return get_slot(this, slot).defaults; } +}; + +} // namespace Slic3r + +#endif // ARRANGESETTINGSDB_APPCFG_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/ArrangeSettingsView.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/ArrangeSettingsView.hpp new file mode 100644 index 0000000..eecc059 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/ArrangeSettingsView.hpp @@ -0,0 +1,234 @@ +#ifndef ARRANGESETTINGSVIEW_HPP +#define ARRANGESETTINGSVIEW_HPP + +#include +#include + +#include "libslic3r/StaticMap.hpp" + +namespace Slic3r { namespace arr2 { + +using namespace std::string_view_literals; + +class ArrangeSettingsView +{ +public: + enum GeometryHandling { ghConvex, ghBalanced, ghAdvanced, ghCount }; + enum ArrangeStrategy { asAuto, asPullToCenter, asCount }; + enum XLPivots { + xlpCenter, + xlpRearLeft, + xlpFrontLeft, + xlpFrontRight, + xlpRearRight, + xlpRandom, + xlpCount + }; + + virtual ~ArrangeSettingsView() = default; + + virtual float get_distance_from_objects() const = 0; + virtual float get_distance_from_bed() const = 0; + virtual bool is_rotation_enabled() const = 0; + + virtual XLPivots get_xl_alignment() const = 0; + virtual GeometryHandling get_geometry_handling() const = 0; + virtual ArrangeStrategy get_arrange_strategy() const = 0; + + static constexpr std::string_view get_label(GeometryHandling v) + { + constexpr auto STR = std::array{ + "0"sv, // convex + "1"sv, // balanced + "2"sv, // advanced + "-1"sv, // undefined + }; + + return STR[v]; + } + + static constexpr std::string_view get_label(ArrangeStrategy v) + { + constexpr auto STR = std::array{ + "0"sv, // auto + "1"sv, // pulltocenter + "-1"sv, // undefined + }; + + return STR[v]; + } + + static constexpr std::string_view get_label(XLPivots v) + { + constexpr auto STR = std::array{ + "0"sv, // center + "1"sv, // rearleft + "2"sv, // frontleft + "3"sv, // frontright + "4"sv, // rearright + "5"sv, // random + "-1"sv, // undefined + }; + + return STR[v]; + } + +private: + + template + using EnumMap = StaticMap; + + template + static constexpr std::optional get_enumval(std::string_view str, + const EnumMap &emap) + { + std::optional ret; + + if (auto v = query(emap, str); v.has_value()) { + ret = *v; + } + + return ret; + } + +public: + + static constexpr std::optional to_geometry_handling(std::string_view str) + { + return get_enumval(str, GeometryHandlingLabels); + } + + static constexpr std::optional to_arrange_strategy(std::string_view str) + { + return get_enumval(str, ArrangeStrategyLabels); + } + + static constexpr std::optional to_xl_pivots(std::string_view str) + { + return get_enumval(str, XLPivotsLabels); + } + +private: + + static constexpr const auto GeometryHandlingLabels = make_staticmap({ + {"convex"sv, ghConvex}, + {"balanced"sv, ghBalanced}, + {"advanced"sv, ghAdvanced}, + + {"0"sv, ghConvex}, + {"1"sv, ghBalanced}, + {"2"sv, ghAdvanced}, + }); + + static constexpr const auto ArrangeStrategyLabels = make_staticmap({ + {"auto"sv, asAuto}, + {"pulltocenter"sv, asPullToCenter}, + + {"0"sv, asAuto}, + {"1"sv, asPullToCenter} + }); + + static constexpr const auto XLPivotsLabels = make_staticmap({ + {"center"sv, xlpCenter }, + {"rearleft"sv, xlpRearLeft }, + {"frontleft"sv, xlpFrontLeft }, + {"frontright"sv, xlpFrontRight }, + {"rearright"sv, xlpRearRight }, + {"random"sv, xlpRandom }, + + {"0"sv, xlpCenter }, + {"1"sv, xlpRearLeft }, + {"2"sv, xlpFrontLeft }, + {"3"sv, xlpFrontRight }, + {"4"sv, xlpRearRight }, + {"5"sv, xlpRandom } + }); +}; + +class ArrangeSettingsDb: public ArrangeSettingsView +{ +public: + + virtual void distance_from_obj_range(float &min, float &max) const = 0; + virtual void distance_from_bed_range(float &min, float &max) const = 0; + + virtual ArrangeSettingsDb& set_distance_from_objects(float v) = 0; + virtual ArrangeSettingsDb& set_distance_from_bed(float v) = 0; + virtual ArrangeSettingsDb& set_rotation_enabled(bool v) = 0; + + virtual ArrangeSettingsDb& set_xl_alignment(XLPivots v) = 0; + virtual ArrangeSettingsDb& set_geometry_handling(GeometryHandling v) = 0; + virtual ArrangeSettingsDb& set_arrange_strategy(ArrangeStrategy v) = 0; + + struct Values { + float d_obj = 6.f, d_bed = 0.f; + bool rotations = false; + XLPivots xl_align = XLPivots::xlpFrontLeft; + GeometryHandling geom_handling = GeometryHandling::ghConvex; + ArrangeStrategy arr_strategy = ArrangeStrategy::asAuto; + + Values() = default; + Values(const ArrangeSettingsView &sv) + { + d_bed = sv.get_distance_from_bed(); + d_obj = sv.get_distance_from_objects(); + arr_strategy = sv.get_arrange_strategy(); + geom_handling = sv.get_geometry_handling(); + rotations = sv.is_rotation_enabled(); + xl_align = sv.get_xl_alignment(); + } + }; + + virtual Values get_defaults() const { return {}; } + + ArrangeSettingsDb& set_from(const ArrangeSettingsView &sv) + { + set_distance_from_bed(sv.get_distance_from_bed()); + set_distance_from_objects(sv.get_distance_from_objects()); + set_arrange_strategy(sv.get_arrange_strategy()); + set_geometry_handling(sv.get_geometry_handling()); + set_rotation_enabled(sv.is_rotation_enabled()); + set_xl_alignment(sv.get_xl_alignment()); + + return *this; + } +}; + +class ArrangeSettings: public Slic3r::arr2::ArrangeSettingsDb +{ + ArrangeSettingsDb::Values m_v = {}; + +public: + explicit ArrangeSettings( + const ArrangeSettingsDb::Values &v = {}) + : m_v{v} + {} + + explicit ArrangeSettings(const ArrangeSettingsView &v) + : m_v{v} + {} + + float get_distance_from_objects() const override { return m_v.d_obj; } + float get_distance_from_bed() const override { return m_v.d_bed; } + bool is_rotation_enabled() const override { return m_v.rotations; } + XLPivots get_xl_alignment() const override { return m_v.xl_align; } + GeometryHandling get_geometry_handling() const override { return m_v.geom_handling; } + ArrangeStrategy get_arrange_strategy() const override { return m_v.arr_strategy; } + + void distance_from_obj_range(float &min, float &max) const override { min = 0.f; max = 100.f; } + void distance_from_bed_range(float &min, float &max) const override { min = 0.f; max = 100.f; } + + ArrangeSettings& set_distance_from_objects(float v) override { m_v.d_obj = v; return *this; } + ArrangeSettings& set_distance_from_bed(float v) override { m_v.d_bed = v; return *this; } + ArrangeSettings& set_rotation_enabled(bool v) override { m_v.rotations = v; return *this; } + ArrangeSettings& set_xl_alignment(XLPivots v) override { m_v.xl_align = v; return *this; } + ArrangeSettings& set_geometry_handling(GeometryHandling v) override { m_v.geom_handling = v; return *this; } + ArrangeSettings& set_arrange_strategy(ArrangeStrategy v) override { m_v.arr_strategy = v; return *this; } + + auto & values() const { return m_v; } + auto & values() { return m_v; } +}; + +}} // namespace Slic3r::arr2 + +#endif // ARRANGESETTINGSVIEW_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/ArbitraryDataStore.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/ArbitraryDataStore.hpp new file mode 100644 index 0000000..683683d --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/ArbitraryDataStore.hpp @@ -0,0 +1,91 @@ +#ifndef ARBITRARYDATASTORE_HPP +#define ARBITRARYDATASTORE_HPP + +#include +#include +#include + +#include + +namespace Slic3r { namespace arr2 { + +// An associative container able to store and retrieve any data type. +// Based on std::any +class ArbitraryDataStore { + std::map m_data; + +public: + template void add(const std::string &key, T &&data) + { + m_data[key] = std::any{std::forward(data)}; + } + + void add(const std::string &key, std::any &&data) + { + m_data[key] = std::move(data); + } + + // Return nullptr if the key does not exist or the stored data has a + // type other then T. Otherwise returns a pointer to the stored data. + template const T *get(const std::string &key) const + { + auto it = m_data.find(key); + return it != m_data.end() ? std::any_cast(&(it->second)) : + nullptr; + } + + // Same as above just not const. + template T *get(const std::string &key) + { + auto it = m_data.find(key); + return it != m_data.end() ? std::any_cast(&(it->second)) : nullptr; + } + + bool has_key(const std::string &key) const + { + auto it = m_data.find(key); + return it != m_data.end(); + } +}; + +// Some items can be containers of arbitrary data stored under string keys. +template<> struct DataStoreTraits_ +{ + static constexpr bool Implemented = true; + + template + static const T *get(const ArbitraryDataStore &s, const std::string &key) + { + return s.get(key); + } + + // Same as above just not const. + template + static T *get(ArbitraryDataStore &s, const std::string &key) + { + return s.get(key); + } + + template + static bool has_key(ArbitraryDataStore &s, const std::string &key) + { + return s.has_key(key); + } +}; + +template<> struct WritableDataStoreTraits_ +{ + static constexpr bool Implemented = true; + + template + static void set(ArbitraryDataStore &store, + const std::string &key, + T &&data) + { + store.add(key, std::forward(data)); + } +}; + +}} // namespace Slic3r::arr2 + +#endif // ARBITRARYDATASTORE_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/ArrangeItem.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/ArrangeItem.hpp new file mode 100644 index 0000000..a26c45b --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/ArrangeItem.hpp @@ -0,0 +1,509 @@ +#ifndef ARRANGEITEM_HPP +#define ARRANGEITEM_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace arr2 { +struct InfiniteBed; + +inline bool check_polygons_are_convex(const Polygons &pp) { + return std::all_of(pp.begin(), pp.end(), [](const Polygon &p) { + return polygon_is_convex(p); + }); +} + +// A class that stores a set of polygons that are garanteed to be all convex. +// They collectively represent a decomposition of a more complex shape into +// its convex part. Note that this class only stores the result of the decomp, +// does not do the job itself. In debug mode, an explicit check is done for +// each component to be convex. +// +// Additionally class stores a translation vector and a rotation angle for the +// stored polygon, plus additional privitives that are all cached cached after +// appying a the transformations. The caching is not thread safe! +class DecomposedShape +{ + Polygons m_shape; + + Vec2crd m_translation{0, 0}; // The translation of the poly + double m_rotation{0.0}; // The rotation of the poly in radians + + mutable Polygons m_transformed_outline; + mutable bool m_transformed_outline_valid = false; + + mutable Point m_reference_vertex; + mutable std::vector m_refs; + mutable std::vector m_mins; + mutable bool m_reference_vertex_valid = false; + + mutable Point m_centroid; + mutable bool m_centroid_valid = false; + + mutable Polygon m_convex_hull; + mutable BoundingBox m_bounding_box; + mutable double m_area = 0; + +public: + DecomposedShape() = default; + + explicit DecomposedShape(Polygon sh) + { + m_shape.emplace_back(std::move(sh)); + assert(check_polygons_are_convex(m_shape)); + } + + explicit DecomposedShape(std::initializer_list pts) + : DecomposedShape(Polygon{pts}) + {} + + explicit DecomposedShape(Polygons sh) : m_shape{std::move(sh)} + { + assert(check_polygons_are_convex(m_shape)); + } + + const Polygons &contours() const { return m_shape; } + + const Vec2crd &translation() const { return m_translation; } + double rotation() const { return m_rotation; } + + void translation(const Vec2crd &v) + { + m_translation = v; + m_transformed_outline_valid = false; + m_reference_vertex_valid = false; + m_centroid_valid = false; + } + + void rotation(double v) + { + m_rotation = v; + m_transformed_outline_valid = false; + m_reference_vertex_valid = false; + m_centroid_valid = false; + } + + const Polygons &transformed_outline() const; + const Polygon &convex_hull() const; + const BoundingBox &bounding_box() const; + + // The cached reference vertex in the context of NFP creation. Always + // refers to the leftmost upper vertex. + const Vec2crd &reference_vertex() const; + const Vec2crd &reference_vertex(size_t idx) const; + + // Also for NFP calculations, the rightmost lowest vertex of the shape. + const Vec2crd &min_vertex(size_t idx) const; + + double area_unscaled() const + { + // update cache + transformed_outline(); + + return m_area; + } + + Vec2crd centroid() const; +}; + +DecomposedShape decompose(const ExPolygons &polys); +DecomposedShape decompose(const Polygon &p); + +class ArrangeItem +{ +private: + DecomposedShape m_shape; // Shape of item when it's not moving + AnyPtr m_envelope; // Possibly different shape when packed + + ArbitraryDataStore m_datastore; + + int m_bed_idx{Unarranged}; // To which logical bed does this item belong + int m_priority{0}; // For sorting + std::optional m_bed_constraint; + +public: + ArrangeItem() = default; + + explicit ArrangeItem(DecomposedShape shape) + : m_shape(std::move(shape)), m_envelope{&m_shape} + {} + + explicit ArrangeItem(DecomposedShape shape, DecomposedShape envelope) + : m_shape(std::move(shape)) + , m_envelope{std::make_unique(std::move(envelope))} + {} + + explicit ArrangeItem(const ExPolygons &shape); + explicit ArrangeItem(Polygon shape); + explicit ArrangeItem(std::initializer_list pts) + : ArrangeItem(Polygon{pts}) + {} + + ArrangeItem(const ArrangeItem &); + ArrangeItem(ArrangeItem &&) noexcept; + ArrangeItem & operator=(const ArrangeItem &); + ArrangeItem & operator=(ArrangeItem &&) noexcept; + + int bed_idx() const { return m_bed_idx; } + int priority() const { return m_priority; } + std::optional bed_constraint() const { return m_bed_constraint; }; + + void bed_idx(int v) { m_bed_idx = v; } + void priority(int v) { m_priority = v; } + void bed_constraint(std::optional v) { m_bed_constraint = v; } + + const ArbitraryDataStore &datastore() const { return m_datastore; } + ArbitraryDataStore &datastore() { return m_datastore; } + + const DecomposedShape & shape() const { return m_shape; } + void set_shape(DecomposedShape shape); + + const DecomposedShape & envelope() const { return *m_envelope; } + void set_envelope(DecomposedShape envelope); + + const Vec2crd &translation() const { return m_shape.translation(); } + double rotation() const { return m_shape.rotation(); } + + void translation(const Vec2crd &v) + { + m_shape.translation(v); + m_envelope->translation(v); + } + + void rotation(double v) + { + m_shape.rotation(v); + m_envelope->rotation(v); + } + + void update_caches() const + { + m_shape.reference_vertex(); + m_envelope->reference_vertex(); + m_shape.centroid(); + m_envelope->centroid(); + } +}; + +template<> struct ArrangeItemTraits_ +{ + static const Vec2crd &get_translation(const ArrangeItem &itm) + { + return itm.translation(); + } + + static double get_rotation(const ArrangeItem &itm) + { + return itm.rotation(); + } + + static int get_bed_index(const ArrangeItem &itm) + { + return itm.bed_idx(); + } + + static int get_priority(const ArrangeItem &itm) + { + return itm.priority(); + } + + static std::optional get_bed_constraint(const ArrangeItem &itm) + { + return itm.bed_constraint(); + } + + // Setters: + + static void set_translation(ArrangeItem &itm, const Vec2crd &v) + { + itm.translation(v); + } + + static void set_rotation(ArrangeItem &itm, double v) + { + itm.rotation(v); + } + + static void set_bed_index(ArrangeItem &itm, int v) + { + itm.bed_idx(v); + } + + static void set_bed_constraint(ArrangeItem &itm, std::optional v) + { + itm.bed_constraint(v); + } +}; + +// Some items can be containers of arbitrary data stored under string keys. +template<> struct DataStoreTraits_ +{ + static constexpr bool Implemented = true; + + template + static const T *get(const ArrangeItem &itm, const std::string &key) + { + return itm.datastore().get(key); + } + + // Same as above just not const. + template + static T *get(ArrangeItem &itm, const std::string &key) + { + return itm.datastore().get(key); + } + + static bool has_key(const ArrangeItem &itm, const std::string &key) + { + return itm.datastore().has_key(key); + } +}; + +template<> struct WritableDataStoreTraits_ +{ + static constexpr bool Implemented = true; + + template + static void set(ArrangeItem &itm, + const std::string &key, + T &&data) + { + itm.datastore().add(key, std::forward(data)); + } +}; + +template +static Polygons calculate_nfp_unnormalized(const ArrangeItem &item, + const Range &fixed_items, + StopCond &&stop_cond = {}) +{ + size_t cap = 0; + + for (const ArrangeItem &fixitem : fixed_items) { + const Polygons &outlines = fixitem.shape().transformed_outline(); + cap += outlines.size(); + } + + const Polygons &item_outlines = item.envelope().transformed_outline(); + + auto nfps = reserve_polygons(cap * item_outlines.size()); + + Vec2crd ref_whole = item.envelope().reference_vertex(); + Polygon subnfp; + + for (const ArrangeItem &fixed : fixed_items) { + // fixed_polys should already be a set of strictly convex polygons, + // as ArrangeItem stores convex-decomposed polygons + const Polygons & fixed_polys = fixed.shape().transformed_outline(); + + for (const Polygon &fixed_poly : fixed_polys) { + Point max_fixed = Slic3r::reference_vertex(fixed_poly); + for (size_t mi = 0; mi < item_outlines.size(); ++mi) { + const Polygon &movable = item_outlines[mi]; + const Vec2crd &mref = item.envelope().reference_vertex(mi); + subnfp = nfp_convex_convex_legacy(fixed_poly, movable); + + Vec2crd min_movable = item.envelope().min_vertex(mi); + + Vec2crd dtouch = max_fixed - min_movable; + Vec2crd top_other = mref + dtouch; + Vec2crd max_nfp = Slic3r::reference_vertex(subnfp); + auto dnfp = top_other - max_nfp; + + auto d = ref_whole - mref + dnfp; + subnfp.translate(d); + nfps.emplace_back(subnfp); + } + + if (stop_cond()) + break; + + nfps = union_(nfps); + } + + if (stop_cond()) { + nfps.clear(); + break; + } + } + + return nfps; +} + +template<> struct NFPArrangeItemTraits_ { + template + static ExPolygons calculate_nfp(const ArrangeItem &item, + const Context &packing_context, + const Bed &bed, + StopCond &&stopcond) + { + auto static_items = all_items_range(packing_context); + Polygons nfps = arr2::calculate_nfp_unnormalized(item, static_items, stopcond); + + ExPolygons nfp_ex; + + if (!stopcond()) { + if constexpr (!std::is_convertible_v) { + ExPolygons ifpbed = ifp_convex(bed, item.envelope().convex_hull()); + nfp_ex = diff_ex(ifpbed, nfps); + } else { + nfp_ex = union_ex(nfps); + } + } + + item.update_caches(); + + return nfp_ex; + } + + static const Vec2crd& reference_vertex(const ArrangeItem &item) + { + return item.envelope().reference_vertex(); + } + + static BoundingBox envelope_bounding_box(const ArrangeItem &itm) + { + return itm.envelope().bounding_box(); + } + + static BoundingBox fixed_bounding_box(const ArrangeItem &itm) + { + return itm.shape().bounding_box(); + } + + static double envelope_area(const ArrangeItem &itm) + { + return itm.envelope().area_unscaled() * scaled(1.) * + scaled(1.); + } + + static double fixed_area(const ArrangeItem &itm) + { + return itm.shape().area_unscaled() * scaled(1.) * + scaled(1.); + } + + static const Polygons & envelope_outline(const ArrangeItem &itm) + { + return itm.envelope().transformed_outline(); + } + + static const Polygons & fixed_outline(const ArrangeItem &itm) + { + return itm.shape().transformed_outline(); + } + + static const Polygon & envelope_convex_hull(const ArrangeItem &itm) + { + return itm.envelope().convex_hull(); + } + + static const Polygon & fixed_convex_hull(const ArrangeItem &itm) + { + return itm.shape().convex_hull(); + } + + static const std::vector& allowed_rotations(const ArrangeItem &itm) + { + static const std::vector ret_zero = {0.}; + + const std::vector * ret_ptr = &ret_zero; + + auto rots = get_data>(itm, "rotations"); + if (rots) { + ret_ptr = rots; + } + + return *ret_ptr; + } + + static Vec2crd fixed_centroid(const ArrangeItem &itm) + { + return itm.shape().centroid(); + } + + static Vec2crd envelope_centroid(const ArrangeItem &itm) + { + return itm.envelope().centroid(); + } +}; + +template<> struct IsMutableItem_: public std::true_type {}; + +template<> +struct MutableItemTraits_ { + + static void set_priority(ArrangeItem &itm, int p) { itm.priority(p); } + static void set_convex_shape(ArrangeItem &itm, const Polygon &shape) + { + itm.set_shape(DecomposedShape{shape}); + } + static void set_shape(ArrangeItem &itm, const ExPolygons &shape) + { + itm.set_shape(decompose(shape)); + } + static void set_convex_envelope(ArrangeItem &itm, const Polygon &envelope) + { + itm.set_envelope(DecomposedShape{envelope}); + } + static void set_envelope(ArrangeItem &itm, const ExPolygons &envelope) + { + itm.set_envelope(decompose(envelope)); + } + + template + static void set_arbitrary_data(ArrangeItem &itm, const std::string &key, T &&data) + { + set_data(itm, key, std::forward(data)); + } + + static void set_allowed_rotations(ArrangeItem &itm, const std::vector &rotations) + { + set_data(itm, "rotations", rotations); + } +}; + +extern template struct ImbueableItemTraits_; +extern template class ArrangeableToItemConverter; +extern template struct ArrangeTask; +extern template struct FillBedTask; +extern template struct MultiplySelectionTask; +extern template class Arranger; + +}} // namespace Slic3r::arr2 + +#endif // ARRANGEITEM_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/MutableItemTraits.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/MutableItemTraits.hpp new file mode 100644 index 0000000..e95b437 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/MutableItemTraits.hpp @@ -0,0 +1,136 @@ +#ifndef MutableItemTraits_HPP +#define MutableItemTraits_HPP + +#include + +#include +#include + +namespace Slic3r { namespace arr2 { + +template struct IsMutableItem_ : public std::false_type +{}; + +// Using this interface to set up any arrange item. Provides default +// implementation but it needs to be explicitly switched on with +// IsMutableItem_ or completely reimplement a specialization. +template struct MutableItemTraits_ +{ + static_assert(IsMutableItem_::value, "Not a Writable item type!"); + + static void set_priority(Itm &itm, int p) { itm.set_priority(p); } + + static void set_convex_shape(Itm &itm, const Polygon &shape) + { + itm.set_convex_shape(shape); + } + + static void set_shape(Itm &itm, const ExPolygons &shape) + { + itm.set_shape(shape); + } + + static void set_convex_envelope(Itm &itm, const Polygon &envelope) + { + itm.set_convex_envelope(envelope); + } + + static void set_envelope(Itm &itm, const ExPolygons &envelope) + { + itm.set_envelope(envelope); + } + + template + static void set_arbitrary_data(Itm &itm, const std::string &key, T &&data) + { + if constexpr (IsWritableDataStore) + set_data(itm, key, std::forward(data)); + } + + static void set_allowed_rotations(Itm &itm, + const std::vector &rotations) + { + itm.set_allowed_rotations(rotations); + } +}; + +template +using MutableItemTraits = MutableItemTraits_>; + +template constexpr bool IsMutableItem = IsMutableItem_::value; +template +using MutableItemOnly = std::enable_if_t, TT>; + +template void set_priority(Itm &itm, int p) +{ + MutableItemTraits::set_priority(itm, p); +} + +template void set_convex_shape(Itm &itm, const Polygon &shape) +{ + MutableItemTraits::set_convex_shape(itm, shape); +} + +template void set_shape(Itm &itm, const ExPolygons &shape) +{ + MutableItemTraits::set_shape(itm, shape); +} + +template +void set_convex_envelope(Itm &itm, const Polygon &envelope) +{ + MutableItemTraits::set_convex_envelope(itm, envelope); +} + +template void set_envelope(Itm &itm, const ExPolygons &envelope) +{ + MutableItemTraits::set_envelope(itm, envelope); +} + +template +void set_arbitrary_data(Itm &itm, const std::string &key, T &&data) +{ + MutableItemTraits::set_arbitrary_data(itm, key, std::forward(data)); +} + +template +void set_allowed_rotations(Itm &itm, const std::vector &rotations) +{ + MutableItemTraits::set_allowed_rotations(itm, rotations); +} + +template int raise_priority(ArrItem &itm) +{ + int ret = get_priority(itm) + 1; + set_priority(itm, ret); + + return ret; +} + +template int reduce_priority(ArrItem &itm) +{ + int ret = get_priority(itm) - 1; + set_priority(itm, ret); + + return ret; +} + +template int lowest_priority(const Range &item_range) +{ + auto minp_it = std::min_element(item_range.begin(), + item_range.end(), + [](auto &itm1, auto &itm2) { + return get_priority(itm1) < + get_priority(itm2); + }); + + int min_priority = 0; + if (minp_it != item_range.end()) + min_priority = get_priority(*minp_it); + + return min_priority; +} + +}} // namespace Slic3r::arr2 + +#endif // MutableItemTraits_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/SimpleArrangeItem.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/SimpleArrangeItem.hpp new file mode 100644 index 0000000..ae04f00 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/SimpleArrangeItem.hpp @@ -0,0 +1,233 @@ +#ifndef SIMPLEARRANGEITEM_HPP +#define SIMPLEARRANGEITEM_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + + +namespace Slic3r { namespace arr2 { +struct InfiniteBed; + +class SimpleArrangeItem { + Polygon m_shape; + + Vec2crd m_translation = Vec2crd::Zero(); + double m_rotation = 0.; + int m_priority = 0; + int m_bed_idx = Unarranged; + std::optional m_bed_constraint; + + std::vector m_allowed_rotations = {0.}; + ObjectID m_obj_id; + +public: + explicit SimpleArrangeItem(Polygon chull = {}): m_shape{std::move(chull)} {} + + void set_shape(Polygon chull) { m_shape = std::move(chull); } + + const Vec2crd& get_translation() const noexcept { return m_translation; } + double get_rotation() const noexcept { return m_rotation; } + int get_priority() const noexcept { return m_priority; } + int get_bed_index() const noexcept { return m_bed_idx; } + std::optional get_bed_constraint() const noexcept { + return m_bed_constraint; + } + + void set_translation(const Vec2crd &v) { m_translation = v; } + void set_rotation(double v) noexcept { m_rotation = v; } + void set_priority(int v) noexcept { m_priority = v; } + void set_bed_index(int v) noexcept { m_bed_idx = v; } + void set_bed_constraint(std::optional v) noexcept { m_bed_constraint = v; } + + const Polygon &shape() const { return m_shape; } + Polygon outline() const; + + const auto &allowed_rotations() const noexcept + { + return m_allowed_rotations; + } + + void set_allowed_rotations(std::vector rots) + { + m_allowed_rotations = std::move(rots); + } + + void set_object_id(const ObjectID &id) noexcept { m_obj_id = id; } + const ObjectID & get_object_id() const noexcept { return m_obj_id; } +}; + +template<> struct NFPArrangeItemTraits_ +{ + template + static ExPolygons calculate_nfp(const SimpleArrangeItem &item, + const Context &packing_context, + const Bed &bed, + StopCond &&stop_cond) + { + auto fixed_items = all_items_range(packing_context); + auto nfps = reserve_polygons(fixed_items.size()); + for (const SimpleArrangeItem &fixed_part : fixed_items) { + Polygon subnfp = nfp_convex_convex_legacy(fixed_part.outline(), + item.outline()); + nfps.emplace_back(subnfp); + + + if (stop_cond()) { + nfps.clear(); + break; + } + } + + ExPolygons nfp_ex; + if (!stop_cond()) { + if constexpr (!std::is_convertible_v) { + ExPolygons ifpbed = ifp_convex(bed, item.outline()); + nfp_ex = diff_ex(ifpbed, nfps); + } else { + nfp_ex = union_ex(nfps); + } + } + + return nfp_ex; + } + + static Vec2crd reference_vertex(const SimpleArrangeItem &item) + { + return Slic3r::reference_vertex(item.outline()); + } + + static BoundingBox envelope_bounding_box(const SimpleArrangeItem &itm) + { + return get_extents(itm.outline()); + } + + static BoundingBox fixed_bounding_box(const SimpleArrangeItem &itm) + { + return get_extents(itm.outline()); + } + + static Polygons envelope_outline(const SimpleArrangeItem &itm) + { + return {itm.outline()}; + } + + static Polygons fixed_outline(const SimpleArrangeItem &itm) + { + return {itm.outline()}; + } + + static Polygon envelope_convex_hull(const SimpleArrangeItem &itm) + { + return Geometry::convex_hull(itm.outline()); + } + + static Polygon fixed_convex_hull(const SimpleArrangeItem &itm) + { + return Geometry::convex_hull(itm.outline()); + } + + static double envelope_area(const SimpleArrangeItem &itm) + { + return itm.shape().area(); + } + + static double fixed_area(const SimpleArrangeItem &itm) + { + return itm.shape().area(); + } + + static const auto& allowed_rotations(const SimpleArrangeItem &itm) noexcept + { + return itm.allowed_rotations(); + } + + static Vec2crd fixed_centroid(const SimpleArrangeItem &itm) noexcept + { + return itm.outline().centroid(); + } + + static Vec2crd envelope_centroid(const SimpleArrangeItem &itm) noexcept + { + return itm.outline().centroid(); + } +}; + +template<> struct IsMutableItem_: public std::true_type {}; + +template<> +struct MutableItemTraits_ { + + static void set_priority(SimpleArrangeItem &itm, int p) { itm.set_priority(p); } + static void set_convex_shape(SimpleArrangeItem &itm, const Polygon &shape) + { + itm.set_shape(shape); + } + static void set_shape(SimpleArrangeItem &itm, const ExPolygons &shape) + { + itm.set_shape(Geometry::convex_hull(shape)); + } + static void set_convex_envelope(SimpleArrangeItem &itm, const Polygon &envelope) + { + itm.set_shape(envelope); + } + static void set_envelope(SimpleArrangeItem &itm, const ExPolygons &envelope) + { + itm.set_shape(Geometry::convex_hull(envelope)); + } + + template + static void set_data(SimpleArrangeItem &itm, const std::string &key, T &&data) + {} + + static void set_allowed_rotations(SimpleArrangeItem &itm, const std::vector &rotations) + { + itm.set_allowed_rotations(rotations); + } +}; + +template<> struct ImbueableItemTraits_ +{ + static void imbue_id(SimpleArrangeItem &itm, const ObjectID &id) + { + itm.set_object_id(id); + } + + static std::optional retrieve_id(const SimpleArrangeItem &itm) + { + std::optional ret; + if (itm.get_object_id().valid()) + ret = itm.get_object_id(); + + return ret; + } +}; + +extern template class ArrangeableToItemConverter; +extern template struct ArrangeTask; +extern template struct FillBedTask; +extern template struct MultiplySelectionTask; +extern template class Arranger; + +}} // namespace Slic3r::arr2 + +#endif // SIMPLEARRANGEITEM_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/TrafoOnlyArrangeItem.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/TrafoOnlyArrangeItem.hpp new file mode 100644 index 0000000..02ba27d --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Items/TrafoOnlyArrangeItem.hpp @@ -0,0 +1,82 @@ +#ifndef TRAFOONLYARRANGEITEM_HPP +#define TRAFOONLYARRANGEITEM_HPP + +#include + +#include "ArbitraryDataStore.hpp" +#include "MutableItemTraits.hpp" + +namespace Slic3r { namespace arr2 { + +class TrafoOnlyArrangeItem { + int m_bed_idx = Unarranged; + int m_priority = 0; + Vec2crd m_translation = Vec2crd::Zero(); + double m_rotation = 0.; + std::optional m_bed_constraint; + + ArbitraryDataStore m_datastore; + +public: + TrafoOnlyArrangeItem() = default; + + template + explicit TrafoOnlyArrangeItem(const ArrItm &other) + : m_bed_idx{arr2::get_bed_index(other)}, + m_priority{arr2::get_priority(other)}, + m_translation(arr2::get_translation(other)), + m_rotation{arr2::get_rotation(other)}, + m_bed_constraint{arr2::get_bed_constraint(other)} + {} + + const Vec2crd& get_translation() const noexcept { return m_translation; } + double get_rotation() const noexcept { return m_rotation; } + int get_bed_index() const noexcept { return m_bed_idx; } + int get_priority() const noexcept { return m_priority; } + std::optional get_bed_constraint() const noexcept { return m_bed_constraint; } + + const ArbitraryDataStore &datastore() const noexcept { return m_datastore; } + ArbitraryDataStore &datastore() { return m_datastore; } +}; + +template<> struct DataStoreTraits_ +{ + static constexpr bool Implemented = true; + + template + static const T *get(const TrafoOnlyArrangeItem &itm, const std::string &key) + { + return itm.datastore().get(key); + } + + template + static T *get(TrafoOnlyArrangeItem &itm, const std::string &key) + { + return itm.datastore().get(key); + } + + static bool has_key(const TrafoOnlyArrangeItem &itm, const std::string &key) + { + return itm.datastore().has_key(key); + } +}; + +template<> struct IsMutableItem_: public std::true_type {}; + +template<> struct WritableDataStoreTraits_ +{ + static constexpr bool Implemented = true; + + template + static void set(TrafoOnlyArrangeItem &itm, + const std::string &key, + T &&data) + { + set_data(itm.datastore(), key, std::forward(data)); + } +}; + +} // namespace arr2 +} // namespace Slic3r + +#endif // TRAFOONLYARRANGEITEM_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/ModelArrange.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/ModelArrange.hpp new file mode 100644 index 0000000..a3a85b7 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/ModelArrange.hpp @@ -0,0 +1,41 @@ +#ifndef MODELARRANGE_HPP +#define MODELARRANGE_HPP + +#include +#include +#include + +#include +#include "Scene.hpp" + +namespace Slic3r { + +class Model; +class ModelInstance; + +namespace arr2 { +class ArrangeSettingsView; +} // namespace arr2 + +using ModelInstancePtrs = std::vector; + +//void duplicate(Model &model, ArrangePolygons &copies, VirtualBedFn); +void duplicate_objects(Model &model, size_t copies_num); + +bool arrange_objects(Model &model, + const arr2::ArrangeBed &bed, + const arr2::ArrangeSettingsView &settings); + +void duplicate_objects(Model & model, + size_t copies_num, + const arr2::ArrangeBed &bed, + const arr2::ArrangeSettingsView &settings); + +void duplicate(Model & model, + size_t copies_num, + const arr2::ArrangeBed &bed, + const arr2::ArrangeSettingsView &settings); + +} // namespace Slic3r + +#endif // MODELARRANGE_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Scene.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Scene.hpp new file mode 100644 index 0000000..e6e52cd --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Scene.hpp @@ -0,0 +1,440 @@ +#ifndef ARR2_SCENE_HPP +#define ARR2_SCENE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ArrangeSettingsView.hpp" +#include "SegmentedRectangleBed.hpp" + +namespace Slic3r { namespace arr2 { + +// This module contains all the necessary high level interfaces for +// arrangement. No dependency on the rest of libslic3r is intoduced here. (No +// Model, ModelObject, etc...) except for ObjectID. + + +// An interface that allows to store arbitrary data (std::any) under a specific +// key in an object implementing the interface. This is later used to pass +// arbitrary parameters from any arrangeable object down to the arrangement core. +class AnyWritable +{ +public: + virtual ~AnyWritable() = default; + + virtual void write(std::string_view key, std::any d) = 0; +}; + +// The interface that captures the objects which are actually moved around. +// Implementations must provide means to extract the 2D outline that is used +// by the arrangement core. +class Arrangeable +{ +public: + virtual ~Arrangeable() = default; + + // ID is implementation specific, must uniquely identify an Arrangeable + // object. + virtual ObjectID id() const = 0; + + // This is different than id(), and identifies an underlying group into + // which the Arrangeable belongs. Can be used to group arrangeables sharing + // the same outline. + virtual ObjectID geometry_id() const = 0; + + // Outline extraction can be a demanding operation, so there is a separate + // method the extract the full outline of an object and the convex hull only + // It will depend on the arrangement config to choose which one is called. + // convex_outline might be considerably faster than calling full_outline() + // and then calculating the convex hull from that. + virtual ExPolygons full_outline() const = 0; + virtual Polygon convex_outline() const = 0; + + // Envelope is the boundary that an arrangeble object might have which + // is used when the object is being placed or moved around. Once it is + // placed, the outline (convex or full) will be used to determine the + // boundaries instead of the envelope. This concept can be used to + // implement arranging objects with support structures that can overlap, + // but never touch the actual object. In this case, full envelope would + // return the silhouette of the object with supports (pad, brim, etc...) and + // outline would be the actual object boundary. + virtual ExPolygons full_envelope() const { return {}; } + virtual Polygon convex_envelope() const { return {}; } + + // Write the transformations determined by the arrangement into the object + virtual void transform(const Vec2d &transl, double rot) = 0; + + // An arrangeable can be printable or unprintable, they should not be on + // the same bed. (See arrange tasks) + virtual bool is_printable() const { return true; } + + // An arrangeable can be selected or not, this will determine if treated + // as static objects or movable ones. + virtual bool is_selected() const { return true; } + + // Determines the order in which the objects are arranged. Higher priority + // objects are arranged first. + virtual int priority() const { return 0; } + + virtual std::optional bed_constraint() const { return std::nullopt; } + + // Any implementation specific properties can be passed to the arrangement + // core by overriding this method. This implies that the specific Arranger + // will be able to interpret these properties. An example usage is to mark + // special objects (like a wipe tower) + virtual void imbue_data(AnyWritable &datastore) const {} + + // for convinience to pass an AnyWritable created in the same expression + // as the method call + void imbue_data(AnyWritable &&datastore) const { imbue_data(datastore); } + + // An Arrangeable might reside on a logical bed instead of the real one + // in case that the arrangement can not fit it onto the real bed. Handling + // of logical beds is also implementation specific and are specified with + // the next two methods: + + // Returns the bed index on which the given Arrangeable is sitting. + virtual int get_bed_index() const = 0; + + // Assign the Arrangeable to the given bed index. Note that this + // method can return false, indicating that the given bed is not available + // to be occupied. + virtual bool assign_bed(int bed_idx) = 0; +}; + +// Arrangeable objects are provided by an ArrangeableModel which is also able to +// create new arrangeables given a prototype id to copy. +class ArrangeableModel +{ +public: + virtual ~ArrangeableModel() = default; + + // Visit all arrangeable in this model and call the provided visitor + virtual void for_each_arrangeable(std::function) = 0; + virtual void for_each_arrangeable(std::function) const = 0; + + // Visit a specific arrangeable identified by it's id + virtual void visit_arrangeable(const ObjectID &id, std::function) const = 0; + virtual void visit_arrangeable(const ObjectID &id, std::function) = 0; + + // Add a new arrangeable which is a copy of the one matching prototype_id + // Return the new object id or an invalid id if the new object was not + // created. + virtual ObjectID add_arrangeable(const ObjectID &prototype_id) = 0; + + size_t arrangeable_count() const + { + size_t cnt = 0; + for_each_arrangeable([&cnt](auto &) { ++cnt; }); + + return cnt; + } +}; + +// The special bed type used by XL printers +using XLBed = SegmentedRectangleBed, + std::integral_constant>; + +// ExtendedBed is a variant type holding all bed types supported by the +// arrange core and the additional XLBed + +template struct ExtendedBed_ +{ + using Type = + boost::variant; +}; + +template struct ExtendedBed_> +{ + using Type = boost::variant; +}; + +using ExtendedBed = typename ExtendedBed_::Type; + +template void visit_bed(BedFn &&fn, const ExtendedBed &bed) +{ + boost::apply_visitor(fn, bed); +} + +template void visit_bed(BedFn &&fn, ExtendedBed &bed) +{ + boost::apply_visitor(fn, bed); +} + +inline BoundingBox bounding_box(const ExtendedBed &bed) +{ + BoundingBox bedbb; + visit_bed([&bedbb](auto &rawbed) { bedbb = bounding_box(rawbed); }, bed); + + return bedbb; +} + +inline Vec2crd bed_gap(const ExtendedBed &bed) +{ + Vec2crd gap; + visit_bed([&gap](auto &rawbed) { gap = bed_gap(rawbed); }, bed); + + return gap; +} + +class Scene; + +// SceneBuilderBase is intended for Scene construction. A simple constructor +// is not enough here to capture all the possible ways of constructing a Scene. +// Subclasses of SceneBuilderBase can add more domain specific methods and +// overloads. An rvalue object of this class is handed over to the Scene +// constructor which can then establish itself using the provided builder. + +// A little CRTP is used to implement fluent interface returning Subclass +// references. +template +class SceneBuilderBase +{ +protected: + AnyPtr m_arrangeable_model; + + AnyPtr m_settings; + + ExtendedBed m_bed = arr2::InfiniteBed{}; + + coord_t m_brims_offs = 0; + coord_t m_skirt_offs = 0; + +public: + + virtual ~SceneBuilderBase() = default; + + SceneBuilderBase() = default; + SceneBuilderBase(const SceneBuilderBase &) = delete; + SceneBuilderBase& operator=(const SceneBuilderBase &) = delete; + SceneBuilderBase(SceneBuilderBase &&) = default; + SceneBuilderBase& operator=(SceneBuilderBase &&) = default; + + // All setters return an rvalue reference so that at the end, the + // build_scene method can be called fluently + + Subclass &&set_arrange_settings(AnyPtr settings) + { + m_settings = std::move(settings); + return std::move(static_cast(*this)); + } + + Subclass &&set_arrange_settings(const ArrangeSettingsView &settings) + { + m_settings = std::make_unique(settings); + return std::move(static_cast(*this)); + } + + Subclass &&set_bed(const Points &pts, const Vec2crd &gap) + { + m_bed = arr2::to_arrange_bed(pts, gap); + return std::move(static_cast(*this)); + } + + Subclass && set_bed(const arr2::ArrangeBed &bed) + { + m_bed = bed; + return std::move(static_cast(*this)); + } + + Subclass &&set_bed(const XLBed &bed) + { + m_bed = bed; + return std::move(static_cast(*this)); + } + + Subclass &&set_arrangeable_model(AnyPtr model) + { + m_arrangeable_model = std::move(model); + return std::move(static_cast(*this)); + } + + // Can only be called on an rvalue instance (hence the && at the end), + // the method will potentially move its content into sc + virtual void build_scene(Scene &sc) &&; +}; + +class BasicSceneBuilder: public SceneBuilderBase {}; + +// The Scene class captures all data needed to do an arrangement. +class Scene +{ + template friend class SceneBuilderBase; + + // These fields always need to be initialized to valid objects after + // construction of Scene which is ensured by the SceneBuilder + AnyPtr m_amodel; + AnyPtr m_settings; + ExtendedBed m_bed; + +public: + // Scene can only be built from an rvalue SceneBuilder whose content will + // potentially be moved to the constructed Scene object. + template + explicit Scene(SceneBuilderBase &&bld) + { + std::move(bld).build_scene(*this); + } + + const ArrangeableModel &model() const noexcept { return *m_amodel; } + ArrangeableModel &model() noexcept { return *m_amodel; } + + const ArrangeSettingsView &settings() const noexcept { return *m_settings; } + + template void visit_bed(BedFn &&fn) const + { + arr2::visit_bed(fn, m_bed); + } + + const ExtendedBed & bed() const { return m_bed; } + + std::vector selected_ids() const; +}; + +// Get all the ObjectIDs of Arrangeables which are in selected state +std::set selected_geometry_ids(const Scene &sc); + +// A dummy, empty ArrangeableModel for testing and as placeholder to avoiod using nullptr +class EmptyArrangeableModel: public ArrangeableModel +{ +public: + void for_each_arrangeable(std::function) override {} + void for_each_arrangeable(std::function) const override {} + void visit_arrangeable(const ObjectID &id, std::function) const override {} + void visit_arrangeable(const ObjectID &id, std::function) override {} + ObjectID add_arrangeable(const ObjectID &prototype_id) override { return {}; } +}; + +template +void SceneBuilderBase::build_scene(Scene &sc) && +{ + if (!m_arrangeable_model) + m_arrangeable_model = std::make_unique(); + + if (!m_settings) + m_settings = std::make_unique(); + + // Apply the bed minimum distance by making the original bed smaller + // and arranging on this smaller bed. + coord_t inset = std::max(scaled(m_settings->get_distance_from_bed()), + m_skirt_offs + m_brims_offs); + + // Objects have also a minimum distance from each other implemented + // as inflation applied to object outlines. This object distance + // does not apply to the bed, so the bed is inflated by this amount + // to compensate. + coord_t md = scaled(m_settings->get_distance_from_objects()); + md = md / 2 - inset; + + // Applying the final bed with the corrected dimensions to account + // for safety distances + visit_bed([md](auto &rawbed) { rawbed = offset(rawbed, md); }, m_bed); + + sc.m_settings = std::move(m_settings); + sc.m_amodel = std::move(m_arrangeable_model); + sc.m_bed = std::move(m_bed); +} + +// Arrange tasks produce an object implementing this interface. The arrange +// result can be applied to an ArrangeableModel which may or may not succeed. +// The ArrangeableModel could be in a different state (it's objects may have +// changed or removed) than it was at the time of arranging. +class ArrangeResult +{ +public: + virtual ~ArrangeResult() = default; + + virtual bool apply_on(ArrangeableModel &mdlwt) = 0; +}; + +enum class Tasks { Arrange, FillBed }; + +class ArrangeTaskCtl +{ +public: + virtual ~ArrangeTaskCtl() = default; + + virtual void update_status(int st) = 0; + + virtual bool was_canceled() const = 0; +}; + +class DummyCtl : public ArrangeTaskCtl +{ +public: + void update_status(int) override {} + bool was_canceled() const override { return false; } +}; + +class ArrangeTaskBase +{ +public: + using Ctl = ArrangeTaskCtl; + + virtual ~ArrangeTaskBase() = default; + + [[nodiscard]] virtual std::unique_ptr process(Ctl &ctl) = 0; + + [[nodiscard]] virtual int item_count_to_process() const = 0; + + [[nodiscard]] static std::unique_ptr create( + Tasks task_type, const Scene &sc); + + [[nodiscard]] std::unique_ptr process(Ctl &&ctl) + { + return process(ctl); + } + + [[nodiscard]] std::unique_ptr process() + { + return process(DummyCtl{}); + } +}; + +bool arrange(Scene &scene, ArrangeTaskCtl &ctl); +inline bool arrange(Scene &scene, ArrangeTaskCtl &&ctl = DummyCtl{}) +{ + return arrange(scene, ctl); +} + +inline bool arrange(Scene &&scene, ArrangeTaskCtl &ctl) +{ + return arrange(scene, ctl); +} + +inline bool arrange(Scene &&scene, ArrangeTaskCtl &&ctl = DummyCtl{}) +{ + return arrange(scene, ctl); +} + +template +bool arrange(SceneBuilderBase &&builder, Ctl &&ctl = {}) +{ + return arrange(Scene{std::move(builder)}, ctl); +} + +} // namespace arr2 +} // namespace Slic3r + +#endif // ARR2_SCENE_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/SceneBuilder.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/SceneBuilder.hpp new file mode 100644 index 0000000..600d030 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/SceneBuilder.hpp @@ -0,0 +1,722 @@ +#ifndef SCENEBUILDER_HPP +#define SCENEBUILDER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "Scene.hpp" + +namespace Slic3r { + +class Model; +class ModelInstance; +class ModelWipeTower; +class Print; +class SLAPrint; +class SLAPrintObject; +class PrintObject; +class DynamicPrintConfig; + +namespace arr2 { + +using SelectionPredicate = std::function; + +// Objects implementing this interface should know how to present the wipe tower +// as an Arrangeable. If the wipe tower is not present, the overloads of visit() shouldn't do +// anything. (See MissingWipeTowerHandler) +class WipeTowerHandler +{ +public: + virtual ~WipeTowerHandler() = default; + + virtual void visit(std::function) = 0; + virtual void visit(std::function) const = 0; + virtual void set_selection_predicate(SelectionPredicate pred) = 0; + virtual ObjectID get_id() const = 0; +}; + +// Something that has a bounding box and can be displaced by arbitrary 2D offset and rotated +// by arbitrary rotation. Used as targets to place on virtual beds. Normally this would correspond +// to ModelInstances but the same functionality was needed in more contexts. +class VBedPlaceable { +public: + virtual ~VBedPlaceable() = default; + + virtual BoundingBoxf bounding_box() const = 0; + virtual void displace(const Vec2d &transl, double rot) = 0; +}; + +// An interface to handle virtual beds for VBedPlaceable objects. A VBedPlaceable +// may be assigned to a logical bed identified by an integer index value (zero +// is the actual physical bed). The VBedPlaceable may still be outside of it's +// bed, regardless of being assigned to it. The handler object should provide +// means to read the assigned bed index of a VBedPlaceable, to assign a +// different bed index and to provide a trafo that maps it to the physical bed +// given a logical bed index. The reason is that the arrangement expects items +// to be in the coordinate system of the physical bed. +class VirtualBedHandler +{ +public: + virtual ~VirtualBedHandler() = default; + + // Returns the bed index on which the given VBedPlaceable is sitting. + virtual int get_bed_index(const VBedPlaceable &obj) const = 0; + + // The returned trafo can be used to displace the VBedPlaceable + // to the coordinate system of the physical bed, should that differ from + // the coordinate space of a logical bed. + virtual Transform3d get_physical_bed_trafo(int bed_index) const = 0; + + // Assign the VBedPlaceable to the given bed index. Note that this + // method can return false, indicating that the given bed is not available + // to be occupied (e.g. the handler has a limited amount of logical bed) + virtual bool assign_bed(VBedPlaceable &obj, int bed_idx) = 0; + + bool assign_bed(VBedPlaceable &&obj, int bed_idx) + { + return assign_bed(obj, bed_idx); + } + + static std::unique_ptr create(const ExtendedBed &bed); +}; + +// Holds the info about which object (ID) is selected/unselected +class SelectionMask +{ +public: + virtual ~SelectionMask() = default; + + virtual std::vector selected_objects() const = 0; + virtual std::vector selected_instances(int obj_id) const = 0; + virtual bool is_wipe_tower_selected(int wipe_tower_index) const = 0; +}; + +class FixedSelection : public Slic3r::arr2::SelectionMask +{ + std::vector> m_seldata; + bool m_wp = false; + +public: + FixedSelection() = default; + + explicit FixedSelection(std::initializer_list> seld, + bool wp = false) + : m_seldata{std::move(seld)}, m_wp{wp} + {} + + explicit FixedSelection(const Model &m); + + explicit FixedSelection(const SelectionMask &other); + + std::vector selected_objects() const override; + + std::vector selected_instances(int obj_id) const override + { + return obj_id < int(m_seldata.size()) ? m_seldata[obj_id] : + std::vector{}; + } + + bool is_wipe_tower_selected(int) const override { return m_wp; } +}; + +// Common part of any Arrangeable which is a wipe tower +struct ArrangeableWipeTowerBase: public Arrangeable +{ + ObjectID oid; + + Polygon poly; + SelectionPredicate selection_pred; + int bed_index{0}; + + ArrangeableWipeTowerBase( + const ObjectID &objid, + Polygon shape, + int bed_index, + SelectionPredicate selection_predicate = [](int){ return false; }) + : oid{objid}, + poly{std::move(shape)}, + bed_index{bed_index}, + selection_pred{std::move(selection_predicate)} + {} + + ObjectID id() const override { return oid; } + ObjectID geometry_id() const override { return {}; } + + ExPolygons full_outline() const override + { + auto cpy = poly; + return {ExPolygon{std::move(cpy)}}; + } + + Polygon convex_outline() const override + { + return poly; + } + + bool is_selected() const override + { + return selection_pred(bed_index); + } + + int get_bed_index() const override; + bool assign_bed(int /*bed_idx*/) override; + + int priority() const override { return 1; } + + std::optional bed_constraint() const override { + return this->bed_index; + } + + void transform(const Vec2d &transl, double rot) override {} + + void imbue_data(AnyWritable &datastore) const override + { + datastore.write("is_wipe_tower", {}); + } +}; + +class SceneBuilder; + +struct InstPos { size_t obj_idx = 0, inst_idx = 0; }; + +using BedConstraints = std::map; + +// Implementing ArrangeableModel interface for QIDISlicer's Model, ModelObject, ModelInstance data +// hierarchy +class ArrangeableSlicerModel: public ArrangeableModel +{ +protected: + AnyPtr m_model; + std::vector> m_wths; // Determines how wipe tower is handled + AnyPtr m_vbed_handler; // Determines how virtual beds are handled + AnyPtr m_selmask; // Determines which objects are selected/unselected + BedConstraints m_bed_constraints; + std::optional> m_considered_instances; + +private: + friend class SceneBuilder; + + template + static void for_each_arrangeable_(Self &&self, Fn &&fn); + + template + static void visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn); + +public: + explicit ArrangeableSlicerModel(SceneBuilder &builder); + ~ArrangeableSlicerModel(); + + void for_each_arrangeable(std::function) override; + void for_each_arrangeable(std::function) const override; + + void visit_arrangeable(const ObjectID &id, std::function) const override; + void visit_arrangeable(const ObjectID &id, std::function) override; + + ObjectID add_arrangeable(const ObjectID &prototype_id) override; + + Model & get_model() { return *m_model; } + const Model &get_model() const { return *m_model; } +}; + +// SceneBuilder implementation for QIDISlicer API. +class SceneBuilder: public SceneBuilderBase +{ +protected: + AnyPtr m_model; + std::vector> m_wipetower_handlers; + BedConstraints m_bed_constraints; + std::optional> m_considered_instances; + AnyPtr m_vbed_handler; + AnyPtr m_selection; + + AnyPtr m_sla_print; + AnyPtr m_fff_print; + bool m_xl_printer = false; + + void set_brim_and_skirt(); + +public: + SceneBuilder(); + ~SceneBuilder(); + SceneBuilder(SceneBuilder&&); + SceneBuilder& operator=(SceneBuilder&&); + + SceneBuilder && set_model(AnyPtr mdl); + + SceneBuilder && set_model(Model &mdl); + + SceneBuilder && set_fff_print(AnyPtr fffprint); + SceneBuilder && set_sla_print(AnyPtr mdl_print); + + using SceneBuilderBase::set_bed; + + SceneBuilder &&set_bed(const DynamicPrintConfig &cfg, const Vec2crd &gap); + SceneBuilder &&set_bed(const Print &print, const Vec2crd &gap); + + SceneBuilder && set_wipe_tower_handlers(std::vector> &&handlers) + { + m_wipetower_handlers = std::move(handlers); + return std::move(*this); + } + + SceneBuilder && set_bed_constraints(BedConstraints &&bed_constraints) + { + m_bed_constraints = std::move(bed_constraints); + return std::move(*this); + } + + SceneBuilder && set_considered_instances(std::set &&considered_instances) + { + m_considered_instances = std::move(considered_instances); + return std::move(*this); + } + + SceneBuilder && set_virtual_bed_handler(AnyPtr vbedh) + { + m_vbed_handler = std::move(vbedh); + return std::move(*this); + } + + SceneBuilder && set_sla_print(const SLAPrint *slaprint); + + SceneBuilder && set_selection(AnyPtr sel) + { + m_selection = std::move(sel); + return std::move(*this); + } + + // Can only be called on an rvalue instance (hence the && at the end), + // the method will potentially move its content into sc + void build_scene(Scene &sc) && override; + + void build_arrangeable_slicer_model(ArrangeableSlicerModel &amodel); +}; + +// Only a physical bed, non-zero bed index values are discarded. +class PhysicalOnlyVBedHandler final : public VirtualBedHandler +{ +public: + using VirtualBedHandler::assign_bed; + + int get_bed_index(const VBedPlaceable &obj) const override { return 0; } + + Transform3d get_physical_bed_trafo(int bed_index) const override + { + return Transform3d::Identity(); + } + + bool assign_bed(VBedPlaceable &inst, int bed_idx) override; +}; + +// A virtual bed handler implementation, that defines logical beds to be created +// on the right side of the physical bed along the X axis in a row +class XStriderVBedHandler final : public VirtualBedHandler +{ + coord_t m_stride_scaled; + coord_t m_start; + +public: + explicit XStriderVBedHandler(const BoundingBox &bedbb, coord_t xgap) + : m_stride_scaled{bedbb.size().x() + 2 * std::max(0, xgap)}, + m_start{bedbb.min.x() - std::max(0, xgap)} + { + } + + coord_t stride_scaled() const { return m_stride_scaled; } + + // Can return negative indices when the instance is to the left of the + // physical bed + int get_bed_index(const VBedPlaceable &obj) const override; + + // Only positive beds are accepted + bool assign_bed(VBedPlaceable &inst, int bed_idx) override; + + using VirtualBedHandler::assign_bed; + + Transform3d get_physical_bed_trafo(int bed_index) const override; +}; + +// Same as XStriderVBedHandler only that it lays out vbeds on the Y axis +class YStriderVBedHandler final : public VirtualBedHandler +{ + coord_t m_stride_scaled; + coord_t m_start; + +public: + coord_t stride_scaled() const { return m_stride_scaled; } + + explicit YStriderVBedHandler(const BoundingBox &bedbb, coord_t ygap) + : m_stride_scaled{bedbb.size().y() + 2 * std::max(0, ygap)} + , m_start{bedbb.min.y() - std::max(0, ygap)} + {} + + int get_bed_index(const VBedPlaceable &obj) const override; + bool assign_bed(VBedPlaceable &inst, int bed_idx) override; + + Transform3d get_physical_bed_trafo(int bed_index) const override; +}; + +class GridStriderVBedHandler: public VirtualBedHandler +{ + XStriderVBedHandler m_xstrider; + YStriderVBedHandler m_ystrider; + +public: + GridStriderVBedHandler(const BoundingBox &bedbb, const Vec2crd &gap) + : m_xstrider{bedbb, gap.x()} + , m_ystrider{bedbb, gap.y()} + {} + + int get_bed_index(const VBedPlaceable &obj) const override; + bool assign_bed(VBedPlaceable &inst, int bed_idx) override; + + Transform3d get_physical_bed_trafo(int bed_index) const override; +}; + +std::vector selected_object_indices(const SelectionMask &sm); +std::vector selected_instance_indices(int obj_idx, const SelectionMask &sm); + +coord_t get_skirt_inset(const Print &fffprint); + +coord_t brim_offset(const PrintObject &po); + +// unscaled coords are necessary to be able to handle bigger coordinate range +// than what is available with scaled coords. This is useful when working with +// virtual beds. +void transform_instance(ModelInstance &mi, + const Vec2d &transl_unscaled, + double rot, + const Transform3d &physical_tr = Transform3d::Identity()); + +BoundingBoxf3 instance_bounding_box(const ModelInstance &mi, + bool dont_translate = false); + +BoundingBoxf3 instance_bounding_box(const ModelInstance &mi, + const Transform3d &tr, + bool dont_translate = false); + +constexpr double UnscaledCoordLimit = 1000.; + +ExPolygons extract_full_outline(const ModelInstance &inst, + const Transform3d &tr = Transform3d::Identity()); + +Polygon extract_convex_outline(const ModelInstance &inst, + const Transform3d &tr = Transform3d::Identity()); + +size_t model_instance_count (const Model &m); + +class VBedPlaceableMI : public VBedPlaceable +{ + ModelInstance *m_mi; + +public: + explicit VBedPlaceableMI(ModelInstance &mi) : m_mi{&mi} {} + + BoundingBoxf bounding_box() const override { return to_2d(instance_bounding_box(*m_mi)); } + void displace(const Vec2d &transl, double rot) override + { + transform_instance(*m_mi, transl, rot); + } +}; + +// Arrangeable interface implementation for ModelInstances +template +class ArrangeableModelInstance : public Arrangeable, VBedPlaceable +{ + InstPtr *m_mi; + VBedHPtr *m_vbedh; + const SelectionMask *m_selmask; + InstPos m_pos_within_model; + std::optional m_bed_constraint; + +public: + explicit ArrangeableModelInstance(InstPtr *mi, + VBedHPtr *vbedh, + const SelectionMask *selmask, + const InstPos &pos, + const std::optional bed_constraint) + : m_mi{mi}, m_vbedh{vbedh}, m_selmask{selmask}, m_pos_within_model{pos}, m_bed_constraint(bed_constraint) + { + assert(m_mi != nullptr && m_vbedh != nullptr); + } + + // Arrangeable: + ObjectID id() const override { return m_mi->id(); } + ObjectID geometry_id() const override { return m_mi->get_object()->id(); } + ExPolygons full_outline() const override; + Polygon convex_outline() const override; + bool is_printable() const override { return m_mi->printable; } + bool is_selected() const override; + void transform(const Vec2d &tr, double rot) override; + + int get_bed_index() const override { return m_vbedh->get_bed_index(*this); } + bool assign_bed(int bed_idx) override; + + std::optional bed_constraint() const override { return m_bed_constraint; } + + // VBedPlaceable: + BoundingBoxf bounding_box() const override { return to_2d(instance_bounding_box(*m_mi)); } + void displace(const Vec2d &transl, double rot) override + { + if constexpr (!std::is_const_v) + transform_instance(*m_mi, transl, rot); + } +}; + +extern template class ArrangeableModelInstance; +extern template class ArrangeableModelInstance; + +// Arrangeable implementation for an SLAPrintObject to be able to arrange with the supports and pad +class ArrangeableSLAPrintObject : public Arrangeable +{ + const SLAPrintObject *m_po; + Arrangeable *m_arrbl; + Transform3d m_inst_trafo; + std::optional m_bed_constraint; + +public: + ArrangeableSLAPrintObject(const SLAPrintObject *po, + Arrangeable *arrbl, + const std::optional bed_constraint, + const Transform3d &inst_tr = Transform3d::Identity()) + : m_po{po}, m_arrbl{arrbl}, m_inst_trafo{inst_tr}, m_bed_constraint(bed_constraint) + {} + + ObjectID id() const override { return m_arrbl->id(); } + ObjectID geometry_id() const override { return m_arrbl->geometry_id(); } + + ExPolygons full_outline() const override; + ExPolygons full_envelope() const override; + + Polygon convex_outline() const override; + Polygon convex_envelope() const override; + + void transform(const Vec2d &transl, double rot) override + { + m_arrbl->transform(transl, rot); + } + int get_bed_index() const override { return m_arrbl->get_bed_index(); } + bool assign_bed(int bedidx) override + { + return m_arrbl->assign_bed(bedidx); + } + + std::optional bed_constraint() const override { return m_bed_constraint; } + + bool is_printable() const override { return m_arrbl->is_printable(); } + bool is_selected() const override { return m_arrbl->is_selected(); } + int priority() const override { return m_arrbl->priority(); } +}; + +// Extension of ArrangeableSlicerModel for SLA +class ArrangeableSLAPrint : public ArrangeableSlicerModel { + const SLAPrint *m_slaprint; + + friend class SceneBuilder; + + template + static void for_each_arrangeable_(Self &&self, Fn &&fn); + + template + static void visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn); + +public: + explicit ArrangeableSLAPrint(const SLAPrint *slaprint, SceneBuilder &builder) + : m_slaprint{slaprint} + , ArrangeableSlicerModel{builder} + { + assert(slaprint != nullptr); + } + + void for_each_arrangeable(std::function) override; + + void for_each_arrangeable( + std::function) const override; + + void visit_arrangeable( + const ObjectID &id, + std::function) const override; + + void visit_arrangeable(const ObjectID &id, + std::function) override; +}; + +template +auto find_instance_by_id(Mdl &&model, const ObjectID &id) +{ + std::remove_reference_t< + decltype(std::declval().objects[0]->instances[0])> + ret = nullptr; + + InstPos pos; + + for (auto * obj : model.objects) { + for (auto *inst : obj->instances) { + if (inst->id() == id) { + ret = inst; + break; + } + ++pos.inst_idx; + } + + if (ret) + break; + + ++pos.obj_idx; + pos.inst_idx = 0; + } + + return std::make_pair(ret, pos); +} + +struct ModelDuplicate +{ + ObjectID id; + Vec2d tr = Vec2d::Zero(); + double rot = 0.; + int bed_idx = Unarranged; +}; + +// Implementing the Arrangeable interface with the whole Model being one outline +// with all its objects and instances. +template +class ArrangeableFullModel: public Arrangeable, VBedPlaceable +{ + Mdl *m_mdl; + Dup *m_dup; + VBH *m_vbh; + +public: + explicit ArrangeableFullModel(Mdl *mdl, + Dup *md, + VBH *vbh) + : m_mdl{mdl}, m_dup{md}, m_vbh{vbh} + { + assert(m_mdl != nullptr); + } + + ObjectID id() const override { return m_dup->id.id + 1; } + ObjectID geometry_id() const override; + + ExPolygons full_outline() const override; + + Polygon convex_outline() const override; + + bool is_printable() const override { return true; } + bool is_selected() const override { return m_dup->id == 0; } + + int get_bed_index() const override + { + return m_vbh->get_bed_index(*this); + } + + void transform(const Vec2d &tr, double rot) override + { + if constexpr (!std::is_const_v && !std::is_const_v) { + m_dup->tr += tr; + m_dup->rot += rot; + } + } + + bool assign_bed(int bed_idx) override + { + bool ret = false; + + if constexpr (!std::is_const_v && !std::is_const_v) { + if ((ret = m_vbh->assign_bed(*this, bed_idx))) + m_dup->bed_idx = bed_idx; + } + + return ret; + } + + BoundingBoxf bounding_box() const override { return unscaled(get_extents(convex_outline())); } + void displace(const Vec2d &transl, double rot) override + { + transform(transl, rot); + } +}; + +extern template class ArrangeableFullModel; +extern template class ArrangeableFullModel; + +// An implementation of the ArrangeableModel to be used for the full model 'duplicate' feature +// accessible from CLI +class DuplicableModel: public ArrangeableModel { + AnyPtr m_model; + AnyPtr m_vbh; + std::vector m_duplicates; + BoundingBox m_bedbb; + + template + static void visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn) + { + if (id.valid()) { + size_t idx = id.id - 1; + if (idx < self.m_duplicates.size()) { + auto &md = self.m_duplicates[idx]; + ArrangeableFullModel arrbl{self.m_model.get(), &md, self.m_vbh.get()}; + fn(arrbl); + } + } + } + +public: + explicit DuplicableModel(AnyPtr mdl, + AnyPtr vbh, + const BoundingBox &bedbb); + ~DuplicableModel(); + + void for_each_arrangeable(std::function fn) override + { + for (ModelDuplicate &md : m_duplicates) { + ArrangeableFullModel arrbl{m_model.get(), &md, m_vbh.get()}; + fn(arrbl); + } + } + void for_each_arrangeable(std::function fn) const override + { + for (const ModelDuplicate &md : m_duplicates) { + ArrangeableFullModel arrbl{m_model.get(), &md, m_vbh.get()}; + fn(arrbl); + } + } + void visit_arrangeable(const ObjectID &id, std::function fn) const override + { + visit_arrangeable_(*this, id, fn); + } + void visit_arrangeable(const ObjectID &id, std::function fn) override + { + visit_arrangeable_(*this, id, fn); + } + + ObjectID add_arrangeable(const ObjectID &prototype_id) override; + + void apply_duplicates(); +}; + +} // namespace arr2 +} // namespace Slic3r + +#endif // SCENEBUILDER_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/SegmentedRectangleBed.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/SegmentedRectangleBed.hpp new file mode 100644 index 0000000..09e2249 --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/SegmentedRectangleBed.hpp @@ -0,0 +1,121 @@ +#ifndef SEGMENTEDRECTANGLEBED_HPP +#define SEGMENTEDRECTANGLEBED_HPP + +#include + +namespace Slic3r { namespace arr2 { + +enum class RectPivots { + Center, BottomLeft, BottomRight, TopLeft, TopRight +}; + +template struct IsSegmentedBed_ : public std::false_type {}; +template constexpr bool IsSegmentedBed = IsSegmentedBed_>::value; + +template +struct SegmentedRectangleBed { + Vec<2, size_t> segments = Vec<2, size_t>::Ones(); + BoundingBox bb; + Vec2crd gap; + RectPivots pivot = RectPivots::Center; + + SegmentedRectangleBed() = default; + SegmentedRectangleBed(const BoundingBox &bb, + size_t segments_x, + size_t segments_y, + const Vec2crd &gap, + const RectPivots pivot = RectPivots::Center) + : segments{segments_x, segments_y}, bb{bb}, gap{gap}, pivot{pivot} + {} + + size_t segments_x() const noexcept { return segments.x(); } + size_t segments_y() const noexcept { return segments.y(); } + + auto alignment() const noexcept { return pivot; } +}; + +template +struct SegmentedRectangleBed, + std::integral_constant> +{ + BoundingBox bb; + Vec2crd gap; + RectPivots pivot = RectPivots::Center; + + SegmentedRectangleBed() = default; + + explicit SegmentedRectangleBed(const BoundingBox &b, + const Vec2crd &gap, + const RectPivots pivot = RectPivots::Center) + : bb{b}, + gap{gap} + {} + + size_t segments_x() const noexcept { return SegX; } + size_t segments_y() const noexcept { return SegY; } + + auto alignment() const noexcept { return pivot; } +}; + +template +struct SegmentedRectangleBed, + std::integral_constant, + std::integral_constant> +{ + BoundingBox bb; + Vec2crd gap; + + SegmentedRectangleBed() = default; + + explicit SegmentedRectangleBed(const BoundingBox &b, const Vec2crd &gap) : bb{b}, gap{gap} {} + + size_t segments_x() const noexcept { return SegX; } + size_t segments_y() const noexcept { return SegY; } + + auto alignment() const noexcept { return pivot; } +}; + +template +struct IsSegmentedBed_> + : public std::true_type {}; + +template +auto offset(const SegmentedRectangleBed &bed, coord_t val_scaled) +{ + auto cpy = bed; + cpy.bb.offset(val_scaled); + + return cpy; +} + +template +auto bounding_box(const SegmentedRectangleBed &bed) +{ + return bed.bb; +} + +template +auto bed_gap(const SegmentedRectangleBed &bed) +{ + return bed.gap; +} + +template +auto area(const SegmentedRectangleBed &bed) +{ + return arr2::area(bed.bb); +} + +template +ExPolygons to_expolygons(const SegmentedRectangleBed &bed) +{ + return to_expolygons(RectangleBed{bed.bb}); +} + +template +struct IsRectangular_, void>> : public std::true_type +{}; + +}} // namespace Slic3r::arr2 + +#endif // SEGMENTEDRECTANGLEBED_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/ArrangeTask.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/ArrangeTask.hpp new file mode 100644 index 0000000..a11eb6e --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/ArrangeTask.hpp @@ -0,0 +1,81 @@ +#ifndef ARRANGETASK_HPP +#define ARRANGETASK_HPP + +#include +#include + +namespace Slic3r { namespace arr2 { + +struct ArrangeTaskResult : public ArrangeResult +{ + std::vector items; + + bool apply_on(ArrangeableModel &mdl) override + { + bool ret = true; + for (auto &itm : items) { + if (is_arranged(itm)) + ret = ret && apply_arrangeitem(itm, mdl); + } + + return ret; + } + + template + void add_item(const ArrItem &itm) + { + items.emplace_back(itm); + if (auto id = retrieve_id(itm)) + imbue_id(items.back(), *id); + } + + template + void add_items(const Range &items_range) + { + for (auto &itm : items_range) + add_item(itm); + } +}; + +template struct ArrangeTask : public ArrangeTaskBase +{ + struct ArrangeSet + { + std::vector selected, unselected; + } printable, unprintable; + + ExtendedBed bed; + ArrangeSettings settings; + + static std::unique_ptr create( + const Scene &sc, + const ArrangeableToItemConverter &converter); + + static std::unique_ptr create(const Scene &sc) + { + auto conv = ArrangeableToItemConverter::create(sc); + return create(sc, *conv); + } + + std::unique_ptr process(Ctl &ctl) override + { + return process_native(ctl); + } + + std::unique_ptr process_native(Ctl &ctl); + std::unique_ptr process_native(Ctl &&ctl) + { + return process_native(ctl); + } + + int item_count_to_process() const override + { + return static_cast(printable.selected.size() + + unprintable.selected.size()); + } +}; + +} // namespace arr2 +} // namespace Slic3r + +#endif // ARRANGETASK_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/FillBedTask.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/FillBedTask.hpp new file mode 100644 index 0000000..b9b3c4f --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/FillBedTask.hpp @@ -0,0 +1,57 @@ +#ifndef FILLBEDTASK_HPP +#define FILLBEDTASK_HPP + +#include + +#include "MultiplySelectionTask.hpp" + +namespace Slic3r { namespace arr2 { + +struct FillBedTaskResult: public MultiplySelectionTaskResult {}; + +template +struct FillBedTask: public ArrangeTaskBase +{ + std::optional prototype_item; + + std::vector selected, unselected; + + // For workaround regarding "holes" when filling the bed with the same + // item's copies + std::vector selected_fillers; + + ArrangeSettings settings; + ExtendedBed bed; + size_t selected_existing_count = 0; + + std::unique_ptr process_native(Ctl &ctl); + std::unique_ptr process_native(Ctl &&ctl) + { + return process_native(ctl); + } + + std::unique_ptr process(Ctl &ctl) override + { + return process_native(ctl); + } + + int item_count_to_process() const override + { + return selected.size(); + } + + static std::unique_ptr create( + const Scene &sc, + const ArrangeableToItemConverter &converter); + + static std::unique_ptr create(const Scene &sc) + { + auto conv = ArrangeableToItemConverter::create(sc); + return create(sc, *conv); + } +}; + +} // namespace arr2 +} // namespace Slic3r + +#endif // FILLBEDTASK_HPP diff --git a/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/MultiplySelectionTask.hpp b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/MultiplySelectionTask.hpp new file mode 100644 index 0000000..a897cdd --- /dev/null +++ b/src/slic3r-arrange-wrapper/include/arrange-wrapper/Tasks/MultiplySelectionTask.hpp @@ -0,0 +1,108 @@ +#ifndef MULTIPLYSELECTIONTASK_HPP +#define MULTIPLYSELECTIONTASK_HPP + +#include +#include + +namespace Slic3r { namespace arr2 { + +struct MultiplySelectionTaskResult: public ArrangeResult { + ObjectID prototype_id; + + std::vector arranged_items; + std::vector to_add; + + bool apply_on(ArrangeableModel &mdl) override + { + bool ret = prototype_id.valid(); + + if (!ret) + return ret; + + for (auto &itm : to_add) { + auto id = mdl.add_arrangeable(prototype_id); + imbue_id(itm, id); + ret = ret && apply_arrangeitem(itm, mdl); + } + + for (auto &itm : arranged_items) { + if (is_arranged(itm)) + ret = ret && apply_arrangeitem(itm, mdl); + } + + return ret; + } + + template + void add_arranged_item(const ArrItem &itm) + { + arranged_items.emplace_back(itm); + if (auto id = retrieve_id(itm)) + imbue_id(arranged_items.back(), *id); + } + + template + void add_arranged_items(const Range &items_range) + { + arranged_items.reserve(items_range.size()); + for (auto &itm : items_range) + add_arranged_item(itm); + } + + template void add_new_item(const ArrItem &itm) + { + to_add.emplace_back(itm); + } + + template void add_new_items(const Range &items_range) + { + to_add.reserve(items_range.size()); + for (auto &itm : items_range) { + to_add.emplace_back(itm); + } + } +}; + +template +struct MultiplySelectionTask: public ArrangeTaskBase +{ + std::optional prototype_item; + + std::vector selected, unselected; + + ArrangeSettings settings; + ExtendedBed bed; + size_t selected_existing_count = 0; + + std::unique_ptr process_native(Ctl &ctl); + std::unique_ptr process_native(Ctl &&ctl) + { + return process_native(ctl); + } + + std::unique_ptr process(Ctl &ctl) override + { + return process_native(ctl); + } + + int item_count_to_process() const override + { + return selected.size(); + } + + static std::unique_ptr create( + const Scene &sc, + size_t multiply_count, + const ArrangeableToItemConverter &converter); + + static std::unique_ptr create(const Scene &sc, + size_t multiply_count) + { + auto conv = ArrangeableToItemConverter::create(sc); + return create(sc, multiply_count, *conv); + } +}; + +}} // namespace Slic3r::arr2 + +#endif // MULTIPLYSELECTIONTASK_HPP diff --git a/src/slic3r-arrange-wrapper/src/ArrangeImpl.hpp b/src/slic3r-arrange-wrapper/src/ArrangeImpl.hpp new file mode 100644 index 0000000..ea017b1 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/ArrangeImpl.hpp @@ -0,0 +1,498 @@ +#ifndef ARRANGEIMPL_HPP +#define ARRANGEIMPL_HPP + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +#ifndef NDEBUG +#include +#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, bed.gap}, 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 QIDISlicer 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 && + IsRectangular) { + 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()); + set_bed_constraint(ret, arrbl.bed_constraint()); + + 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()); + set_bed_constraint(ret, arrbl.bed_constraint()); + 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 diff --git a/src/slic3r-arrange-wrapper/src/ArrangeSettingsDb_AppCfg.cpp b/src/slic3r-arrange-wrapper/src/ArrangeSettingsDb_AppCfg.cpp new file mode 100644 index 0000000..94cb2aa --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/ArrangeSettingsDb_AppCfg.cpp @@ -0,0 +1,190 @@ +#include + +#include +#include + +#include + +namespace Slic3r { + +ArrangeSettingsDb_AppCfg::ArrangeSettingsDb_AppCfg(AppConfig *appcfg) : m_appcfg{appcfg} +{ + sync(); +} + +void ArrangeSettingsDb_AppCfg::sync() +{ + m_settings_fff.postfix = "_fff"; + m_settings_fff_seq.postfix = "_fff_seq_print"; + m_settings_sla.postfix = "_sla"; + + std::string dist_fff_str = + m_appcfg->get("arrange", "min_object_distance_fff"); + + std::string dist_bed_fff_str = + m_appcfg->get("arrange", "min_bed_distance_fff"); + + std::string dist_fff_seq_print_str = + m_appcfg->get("arrange", "min_object_distance_fff_seq_print"); + + std::string dist_bed_fff_seq_print_str = + m_appcfg->get("arrange", "min_bed_distance_fff_seq_print"); + + std::string dist_sla_str = + m_appcfg->get("arrange", "min_object_distance_sla"); + + std::string dist_bed_sla_str = + m_appcfg->get("arrange", "min_bed_distance_sla"); + + std::string en_rot_fff_str = + m_appcfg->get("arrange", "enable_rotation_fff"); + + std::string en_rot_fff_seqp_str = + m_appcfg->get("arrange", "enable_rotation_fff_seq_print"); + + std::string en_rot_sla_str = + m_appcfg->get("arrange", "enable_rotation_sla"); + + std::string alignment_xl_str = + m_appcfg->get("arrange", "alignment_xl"); + + std::string geom_handling_str = + m_appcfg->get("arrange", "geometry_handling"); + + std::string strategy_str = + m_appcfg->get("arrange", "arrange_strategy"); + + if (!dist_fff_str.empty()) + m_settings_fff.vals.d_obj = string_to_float_decimal_point(dist_fff_str); + else + m_settings_fff.vals.d_obj = m_settings_fff.defaults.d_obj; + + if (!dist_bed_fff_str.empty()) + m_settings_fff.vals.d_bed = string_to_float_decimal_point(dist_bed_fff_str); + else + m_settings_fff.vals.d_bed = m_settings_fff.defaults.d_bed; + + if (!dist_fff_seq_print_str.empty()) + m_settings_fff_seq.vals.d_obj = string_to_float_decimal_point(dist_fff_seq_print_str); + else + m_settings_fff_seq.vals.d_obj = m_settings_fff_seq.defaults.d_obj; + + if (!dist_bed_fff_seq_print_str.empty()) + m_settings_fff_seq.vals.d_bed = string_to_float_decimal_point(dist_bed_fff_seq_print_str); + else + m_settings_fff_seq.vals.d_bed = m_settings_fff_seq.defaults.d_bed; + + if (!dist_sla_str.empty()) + m_settings_sla.vals.d_obj = string_to_float_decimal_point(dist_sla_str); + else + m_settings_sla.vals.d_obj = m_settings_sla.defaults.d_obj; + + if (!dist_bed_sla_str.empty()) + m_settings_sla.vals.d_bed = string_to_float_decimal_point(dist_bed_sla_str); + else + m_settings_sla.vals.d_bed = m_settings_sla.defaults.d_bed; + + if (!en_rot_fff_str.empty()) + m_settings_fff.vals.rotations = (en_rot_fff_str == "1" || en_rot_fff_str == "yes"); + + if (!en_rot_fff_seqp_str.empty()) + m_settings_fff_seq.vals.rotations = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes"); + else + m_settings_fff_seq.vals.rotations = m_settings_fff_seq.defaults.rotations; + + if (!en_rot_sla_str.empty()) + m_settings_sla.vals.rotations = (en_rot_sla_str == "1" || en_rot_sla_str == "yes"); + else + m_settings_sla.vals.rotations = m_settings_sla.defaults.rotations; + + // Override default alignment and save/load it to a temporary slot "alignment_xl" + auto arr_alignment = ArrangeSettingsView::to_xl_pivots(alignment_xl_str) + .value_or(m_settings_fff.defaults.xl_align); + + m_settings_sla.vals.xl_align = arr_alignment ; + m_settings_fff.vals.xl_align = arr_alignment ; + m_settings_fff_seq.vals.xl_align = arr_alignment ; + + auto geom_handl = ArrangeSettingsView::to_geometry_handling(geom_handling_str) + .value_or(m_settings_fff.defaults.geom_handling); + + m_settings_sla.vals.geom_handling = geom_handl; + m_settings_fff.vals.geom_handling = geom_handl; + m_settings_fff_seq.vals.geom_handling = geom_handl; + + auto arr_strategy = ArrangeSettingsView::to_arrange_strategy(strategy_str) + .value_or(m_settings_fff.defaults.arr_strategy); + + m_settings_sla.vals.arr_strategy = arr_strategy; + m_settings_fff.vals.arr_strategy = arr_strategy; + m_settings_fff_seq.vals.arr_strategy = arr_strategy; +} + +void ArrangeSettingsDb_AppCfg::distance_from_obj_range(float &min, + float &max) const +{ + min = get_slot(this).dobj_range.minval; + max = get_slot(this).dobj_range.maxval; +} + +void ArrangeSettingsDb_AppCfg::distance_from_bed_range(float &min, + float &max) const +{ + min = get_slot(this).dbed_range.minval; + max = get_slot(this).dbed_range.maxval; +} + +arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_distance_from_objects(float v) +{ + Slot &slot = get_slot(this); + slot.vals.d_obj = v; + m_appcfg->set("arrange", "min_object_distance" + slot.postfix, + float_to_string_decimal_point(v)); + + return *this; +} + +arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_distance_from_bed(float v) +{ + Slot &slot = get_slot(this); + slot.vals.d_bed = v; + m_appcfg->set("arrange", "min_bed_distance" + slot.postfix, + float_to_string_decimal_point(v)); + + return *this; +} + +arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_rotation_enabled(bool v) +{ + Slot &slot = get_slot(this); + slot.vals.rotations = v; + m_appcfg->set("arrange", "enable_rotation" + slot.postfix, v ? "1" : "0"); + + return *this; +} + +arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_xl_alignment(XLPivots v) +{ + m_settings_fff.vals.xl_align = v; + m_appcfg->set("arrange", "alignment_xl", std::string{get_label(v)}); + + return *this; +} + +arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_geometry_handling(GeometryHandling v) +{ + m_settings_fff.vals.geom_handling = v; + m_appcfg->set("arrange", "geometry_handling", std::string{get_label(v)}); + + return *this; +} + +arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_arrange_strategy(ArrangeStrategy v) +{ + m_settings_fff.vals.arr_strategy = v; + m_appcfg->set("arrange", "arrange_strategy", std::string{get_label(v)}); + + return *this; +} + +} // namespace Slic3r diff --git a/src/slic3r-arrange-wrapper/src/Items/ArrangeItem.cpp b/src/slic3r-arrange-wrapper/src/Items/ArrangeItem.cpp new file mode 100644 index 0000000..89d6243 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/Items/ArrangeItem.cpp @@ -0,0 +1,207 @@ +#include + +#include +#include + +#include +#include "ArrangeImpl.hpp" // IWYU pragma: keep +#include "Tasks/ArrangeTaskImpl.hpp" // IWYU pragma: keep +#include "Tasks/FillBedTaskImpl.hpp" // IWYU pragma: keep +#include "Tasks/MultiplySelectionTaskImpl.hpp" // IWYU pragma: keep + +namespace Slic3r { namespace arr2 { + +const Polygons &DecomposedShape::transformed_outline() const +{ + constexpr auto sc = scaled(1.) * scaled(1.); + + if (!m_transformed_outline_valid) { + m_transformed_outline = contours(); + for (Polygon &poly : m_transformed_outline) { + poly.rotate(rotation()); + poly.translate(translation()); + } + + m_area = std::accumulate(m_transformed_outline.begin(), + m_transformed_outline.end(), 0., + [sc](double s, const auto &p) { + return s + p.area() / sc; + }); + + m_convex_hull = Geometry::convex_hull(m_transformed_outline); + m_bounding_box = get_extents(m_convex_hull); + + m_transformed_outline_valid = true; + } + + return m_transformed_outline; +} + +const Polygon &DecomposedShape::convex_hull() const +{ + if (!m_transformed_outline_valid) + transformed_outline(); + + return m_convex_hull; +} + +const BoundingBox &DecomposedShape::bounding_box() const +{ + if (!m_transformed_outline_valid) + transformed_outline(); + + return m_bounding_box; +} + +const Vec2crd &DecomposedShape::reference_vertex() const +{ + if (!m_reference_vertex_valid) { + m_reference_vertex = Slic3r::reference_vertex(transformed_outline()); + m_refs.clear(); + m_mins.clear(); + m_refs.reserve(m_transformed_outline.size()); + m_mins.reserve(m_transformed_outline.size()); + for (auto &poly : m_transformed_outline) { + m_refs.emplace_back(Slic3r::reference_vertex(poly)); + m_mins.emplace_back(Slic3r::min_vertex(poly)); + } + m_reference_vertex_valid = true; + } + + return m_reference_vertex; +} + +const Vec2crd &DecomposedShape::reference_vertex(size_t i) const +{ + if (!m_reference_vertex_valid) { + reference_vertex(); + } + + return m_refs[i]; +} + +const Vec2crd &DecomposedShape::min_vertex(size_t idx) const +{ + if (!m_reference_vertex_valid) { + reference_vertex(); + } + + return m_mins[idx]; +} + +Vec2crd DecomposedShape::centroid() const +{ + constexpr double area_sc = scaled(1.) * scaled(1.); + + if (!m_centroid_valid) { + double total_area = 0.0; + Vec2d cntr = Vec2d::Zero(); + + for (const Polygon& poly : transformed_outline()) { + double parea = poly.area() / area_sc; + Vec2d pcntr = unscaled(poly.centroid()); + total_area += parea; + cntr += pcntr * parea; + } + + cntr /= total_area; + m_centroid = scaled(cntr); + m_centroid_valid = true; + } + + return m_centroid; +} + +DecomposedShape decompose(const ExPolygons &shape) +{ + return DecomposedShape{convex_decomposition_tess(shape)}; +} + +DecomposedShape decompose(const Polygon &shape) +{ + Polygons convex_shapes; + + bool is_convex = polygon_is_convex(shape); + if (is_convex) { + convex_shapes.emplace_back(shape); + } else { + convex_shapes = convex_decomposition_tess(shape); + } + + return DecomposedShape{std::move(convex_shapes)}; +} + +ArrangeItem::ArrangeItem(const ExPolygons &shape) + : m_shape{decompose(shape)}, m_envelope{&m_shape} +{} + +ArrangeItem::ArrangeItem(Polygon shape) + : m_shape{decompose(shape)}, m_envelope{&m_shape} +{} + +ArrangeItem::ArrangeItem(const ArrangeItem &other) +{ + this->operator= (other); +} + +ArrangeItem::ArrangeItem(ArrangeItem &&other) noexcept +{ + this->operator=(std::move(other)); +} + +ArrangeItem &ArrangeItem::operator=(const ArrangeItem &other) +{ + m_shape = other.m_shape; + m_datastore = other.m_datastore; + m_bed_idx = other.m_bed_idx; + m_priority = other.m_priority; + m_bed_constraint = other.m_bed_constraint; + + if (other.m_envelope.get() == &other.m_shape) + m_envelope = &m_shape; + else + m_envelope = std::make_unique(other.envelope()); + + return *this; +} + +void ArrangeItem::set_shape(DecomposedShape shape) +{ + m_shape = std::move(shape); + m_envelope = &m_shape; +} + +void ArrangeItem::set_envelope(DecomposedShape envelope) +{ + m_envelope = std::make_unique(std::move(envelope)); + + // Initial synch of transformations of envelope and shape. + // They need to be in synch all the time + m_envelope->translation(m_shape.translation()); + m_envelope->rotation(m_shape.rotation()); +} + +ArrangeItem &ArrangeItem::operator=(ArrangeItem &&other) noexcept +{ + m_shape = std::move(other.m_shape); + m_datastore = std::move(other.m_datastore); + m_bed_idx = other.m_bed_idx; + m_priority = other.m_priority; + m_bed_constraint = other.m_bed_constraint; + + if (other.m_envelope.get() == &other.m_shape) + m_envelope = &m_shape; + else + m_envelope = std::move(other.m_envelope); + + return *this; +} + +template struct ImbueableItemTraits_; +template class ArrangeableToItemConverter; +template struct ArrangeTask; +template struct FillBedTask; +template struct MultiplySelectionTask; +template class Arranger; + +}} // namespace Slic3r::arr2 diff --git a/src/slic3r-arrange-wrapper/src/Items/SimpleArrangeItem.cpp b/src/slic3r-arrange-wrapper/src/Items/SimpleArrangeItem.cpp new file mode 100644 index 0000000..0c03d0e --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/Items/SimpleArrangeItem.cpp @@ -0,0 +1,24 @@ +#include +#include "ArrangeImpl.hpp" // IWYU pragma: keep +#include "Tasks/ArrangeTaskImpl.hpp" // IWYU pragma: keep +#include "Tasks/FillBedTaskImpl.hpp" // IWYU pragma: keep +#include "Tasks/MultiplySelectionTaskImpl.hpp" // IWYU pragma: keep + +namespace Slic3r { namespace arr2 { + +Polygon SimpleArrangeItem::outline() const +{ + Polygon ret = shape(); + ret.rotate(m_rotation); + ret.translate(m_translation); + + return ret; +} + +template class ArrangeableToItemConverter; +template struct ArrangeTask; +template struct FillBedTask; +template struct MultiplySelectionTask; +template class Arranger; + +}} // namespace Slic3r::arr2 diff --git a/src/slic3r-arrange-wrapper/src/ModelArrange.cpp b/src/slic3r-arrange-wrapper/src/ModelArrange.cpp new file mode 100644 index 0000000..8acf6d7 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/ModelArrange.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace Slic3r { + +void duplicate_objects(Model &model, size_t copies_num) +{ + for (ModelObject *o : model.objects) { + // make a copy of the pointers in order to avoid recursion when appending their copies + ModelInstancePtrs instances = o->instances; + for (const ModelInstance *i : instances) + for (size_t k = 2; k <= copies_num; ++ k) + o->add_instance(*i); + } +} + +bool arrange_objects(Model &model, + const arr2::ArrangeBed &bed, + const arr2::ArrangeSettingsView &settings) +{ + return arrange(arr2::SceneBuilder{} + .set_bed(bed) + .set_arrange_settings(settings) + .set_model(model)); +} + +void duplicate_objects(Model &model, + size_t copies_num, + const arr2::ArrangeBed &bed, + const arr2::ArrangeSettingsView &settings) +{ + duplicate_objects(model, copies_num); + arrange_objects(model, bed, settings); +} + +void duplicate(Model &model, + size_t copies_num, + const arr2::ArrangeBed &bed, + const arr2::ArrangeSettingsView &settings) +{ + auto vbh = arr2::VirtualBedHandler::create(bed); + arr2::DuplicableModel dup_model{&model, std::move(vbh), bounding_box(bed)}; + + arr2::Scene scene{arr2::BasicSceneBuilder{} + .set_arrangeable_model(&dup_model) + .set_arrange_settings(&settings) + .set_bed(bed)}; + + if (copies_num >= 1) + copies_num -= 1; + + auto task = arr2::MultiplySelectionTask::create(scene, copies_num); + auto result = task->process_native(arr2::DummyCtl{}); + if (result->apply_on(scene.model())) + dup_model.apply_duplicates(); +} + +} // namespace Slic3r diff --git a/src/slic3r-arrange-wrapper/src/Scene.cpp b/src/slic3r-arrange-wrapper/src/Scene.cpp new file mode 100644 index 0000000..a7e84b7 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/Scene.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +namespace Slic3r { namespace arr2 { + +std::vector Scene::selected_ids() const +{ + auto items = reserve_vector(model().arrangeable_count()); + + model().for_each_arrangeable([ &items](auto &arrbl) mutable { + if (arrbl.is_selected()) + items.emplace_back(arrbl.id()); + }); + + return items; +} + +using DefaultArrangeItem = ArrangeItem; + +std::unique_ptr ArrangeTaskBase::create(Tasks task_type, const Scene &sc) +{ + std::unique_ptr ret; + switch(task_type) { + case Tasks::Arrange: + ret = ArrangeTask::create(sc); + break; + case Tasks::FillBed: + ret = FillBedTask::create(sc); + break; + default: + ; + } + + return ret; +} + +std::set selected_geometry_ids(const Scene &sc) +{ + std::set result; + + std::vector selected_ids = sc.selected_ids(); + for (const ObjectID &id : selected_ids) { + sc.model().visit_arrangeable(id, [&result](const Arrangeable &arrbl) { + auto id = arrbl.geometry_id(); + if (id.valid()) + result.insert(arrbl.geometry_id()); + }); + } + + return result; +} + +bool arrange(Scene &scene, ArrangeTaskCtl &ctl) +{ + auto task = ArrangeTaskBase::create(Tasks::Arrange, scene); + auto result = task->process(ctl); + return result->apply_on(scene.model()); +} + +}} // namespace Slic3r::arr2 diff --git a/src/slic3r-arrange-wrapper/src/SceneBuilder.cpp b/src/slic3r-arrange-wrapper/src/SceneBuilder.cpp new file mode 100644 index 0000000..9a3cd45 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/SceneBuilder.cpp @@ -0,0 +1,986 @@ +#ifndef SCENEBUILDER_CPP +#define SCENEBUILDER_CPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace Slic3r { namespace arr2 { + +coord_t get_skirt_inset(const Print &fffprint) +{ + float skirt_inset = 0.f; + + if (fffprint.has_skirt()) { + float skirtflow = fffprint.objects().empty() + ? 0 + : fffprint.skirt_flow().width(); + skirt_inset = fffprint.config().skirts.value * skirtflow + + fffprint.config().skirt_distance.value; + } + + return scaled(skirt_inset); +} + +coord_t brim_offset(const PrintObject &po) +{ + const BrimType brim_type = po.config().brim_type.value; + const float brim_separation = po.config().brim_separation.getFloat(); + const float brim_width = po.config().brim_width.getFloat(); + const bool has_outer_brim = brim_type == BrimType::btOuterOnly || + brim_type == BrimType::btOuterAndInner; + + // How wide is the brim? (in scaled units) + return has_outer_brim ? scaled(brim_width + brim_separation) : 0; +} + +size_t model_instance_count (const Model &m) +{ + return std::accumulate(m.objects.begin(), + m.objects.end(), + size_t(0), + [](size_t s, const Slic3r::ModelObject *mo) { + return s + mo->instances.size(); + }); +} + +void transform_instance(ModelInstance &mi, + const Vec2d &transl_unscaled, + double rot, + const Transform3d &physical_tr) +{ + auto trafo = mi.get_transformation().get_matrix(); + auto tr = Transform3d::Identity(); + tr.translate(to_3d(transl_unscaled, 0.)); + trafo = physical_tr.inverse() * tr * Eigen::AngleAxisd(rot, Vec3d::UnitZ()) * physical_tr * trafo; + + mi.set_transformation(Geometry::Transformation{trafo}); + + mi.invalidate_object_bounding_box(); +} + +BoundingBoxf3 instance_bounding_box(const ModelInstance &mi, + const Transform3d &tr, + bool dont_translate) +{ + BoundingBoxf3 bb; + const Transform3d inst_matrix + = dont_translate ? mi.get_transformation().get_matrix_no_offset() + : mi.get_transformation().get_matrix(); + + for (ModelVolume *v : mi.get_object()->volumes) { + if (v->is_model_part()) { + bb.merge(v->mesh().transformed_bounding_box(tr * inst_matrix + * v->get_matrix())); + } + } + + return bb; +} + +BoundingBoxf3 instance_bounding_box(const ModelInstance &mi, bool dont_translate) +{ + return instance_bounding_box(mi, Transform3d::Identity(), dont_translate); +} + +bool check_coord_bounds(const BoundingBoxf &bb) +{ + return std::abs(bb.min.x()) < UnscaledCoordLimit && + std::abs(bb.min.y()) < UnscaledCoordLimit && + std::abs(bb.max.x()) < UnscaledCoordLimit && + std::abs(bb.max.y()) < UnscaledCoordLimit; +} + +ExPolygons extract_full_outline(const ModelInstance &inst, const Transform3d &tr) +{ + ExPolygons outline; + + if (check_coord_bounds(to_2d(instance_bounding_box(inst, tr)))) { + for (const ModelVolume *v : inst.get_object()->volumes) { + Polygons vol_outline; + + vol_outline = project_mesh(v->mesh().its, + tr * inst.get_matrix() * v->get_matrix(), + [] {}); + switch (v->type()) { + case ModelVolumeType::MODEL_PART: + outline = union_ex(outline, vol_outline); + break; + case ModelVolumeType::NEGATIVE_VOLUME: + outline = diff_ex(outline, vol_outline); + break; + default:; + } + } + } + + return outline; +} + +Polygon extract_convex_outline(const ModelInstance &inst, const Transform3d &tr) +{ + auto bb = to_2d(instance_bounding_box(inst, tr)); + Polygon ret; + + if (check_coord_bounds(bb)) { + ret = inst.get_object()->convex_hull_2d(tr * inst.get_matrix()); + } + + return ret; +} + +inline static bool is_infinite_bed(const ExtendedBed &ebed) noexcept +{ + bool ret = false; + visit_bed( + [&ret](auto &rawbed) { + ret = std::is_convertible_v; + }, + ebed); + + return ret; +} + +void SceneBuilder::set_brim_and_skirt() +{ + if (!m_fff_print) + return; + + m_brims_offs = 0; + + for (const PrintObject *po : m_fff_print->objects()) { + if (po) { + m_brims_offs = std::max(m_brims_offs, brim_offset(*po)); + } + } + + m_skirt_offs = get_skirt_inset(*m_fff_print); +} + +void SceneBuilder::build_scene(Scene &sc) && +{ + if (m_sla_print && !m_fff_print) { + m_arrangeable_model = std::make_unique(m_sla_print.get(), *this); + } else { + m_arrangeable_model = std::make_unique(*this); + } + + if (m_fff_print && !m_sla_print) { + if (is_infinite_bed(m_bed)) { + set_bed(*m_fff_print, Vec2crd::Zero()); + } else { + set_brim_and_skirt(); + } + } + + // Call the parent class implementation of build_scene to finish constructing of the scene + std::move(*this).SceneBuilderBase::build_scene(sc); +} + +void SceneBuilder::build_arrangeable_slicer_model(ArrangeableSlicerModel &amodel) +{ + if (!m_model) + m_model = std::make_unique(); + + if (!m_selection) + m_selection = std::make_unique(*m_model); + + if (!m_vbed_handler) { + m_vbed_handler = VirtualBedHandler::create(m_bed); + } + + if (m_fff_print && !m_xl_printer) + m_xl_printer = is_XL_printer(m_fff_print->config()); + + const bool has_wipe_tower{std::any_of( + m_wipetower_handlers.begin(), + m_wipetower_handlers.end(), + [](const AnyPtr &handler){ + bool is_on_current_bed{false}; + handler->visit([&](const Arrangeable &arrangeable){ + is_on_current_bed = arrangeable.get_bed_index() == s_multiple_beds.get_active_bed(); + }); + return is_on_current_bed; + } + )}; + + if (m_xl_printer && !has_wipe_tower) { + m_bed = XLBed{bounding_box(m_bed), bed_gap(m_bed)}; + } + + amodel.m_vbed_handler = std::move(m_vbed_handler); + amodel.m_model = std::move(m_model); + amodel.m_selmask = std::move(m_selection); + amodel.m_wths = std::move(m_wipetower_handlers); + amodel.m_bed_constraints = std::move(m_bed_constraints); + amodel.m_considered_instances = std::move(m_considered_instances); + + for (auto &wth : amodel.m_wths) { + wth->set_selection_predicate( + [&amodel](int wipe_tower_index){ + return amodel.m_selmask->is_wipe_tower_selected(wipe_tower_index); + } + ); + } +} + +int XStriderVBedHandler::get_bed_index(const VBedPlaceable &obj) const +{ + int bedidx = 0; + auto stride_s = stride_scaled(); + if (stride_s > 0) { + double bedx = unscaled(m_start); + auto instance_bb = obj.bounding_box(); + auto reference_pos_x = (instance_bb.min.x() - bedx); + auto stride = unscaled(stride_s); + + auto bedidx_d = std::floor(reference_pos_x / stride); + + if (bedidx_d < std::numeric_limits::min()) + bedidx = std::numeric_limits::min(); + else if (bedidx_d > std::numeric_limits::max()) + bedidx = std::numeric_limits::max(); + else + bedidx = static_cast(bedidx_d); + } + + return bedidx; +} + +bool XStriderVBedHandler::assign_bed(VBedPlaceable &obj, int bed_index) +{ + bool ret = false; + auto stride_s = stride_scaled(); + if (bed_index == 0 || (bed_index > 0 && stride_s > 0)) { + auto current_bed_index = get_bed_index(obj); + auto stride = unscaled(stride_s); + auto transl = Vec2d{(bed_index - current_bed_index) * stride, 0.}; + obj.displace(transl, 0.); + + ret = true; + } + + return ret; +} + +Transform3d XStriderVBedHandler::get_physical_bed_trafo(int bed_index) const +{ + auto stride_s = stride_scaled(); + auto tr = Transform3d::Identity(); + tr.translate(Vec3d{-bed_index * unscaled(stride_s), 0., 0.}); + + return tr; +} + +int YStriderVBedHandler::get_bed_index(const VBedPlaceable &obj) const +{ + int bedidx = 0; + auto stride_s = stride_scaled(); + if (stride_s > 0) { + double ystart = unscaled(m_start); + auto instance_bb = obj.bounding_box(); + auto reference_pos_y = (instance_bb.min.y() - ystart); + auto stride = unscaled(stride_s); + + auto bedidx_d = std::floor(reference_pos_y / stride); + + if (bedidx_d < std::numeric_limits::min()) + bedidx = std::numeric_limits::min(); + else if (bedidx_d > std::numeric_limits::max()) + bedidx = std::numeric_limits::max(); + else + bedidx = static_cast(bedidx_d); + } + + return bedidx; +} + +bool YStriderVBedHandler::assign_bed(VBedPlaceable &obj, int bed_index) +{ + bool ret = false; + auto stride_s = stride_scaled(); + if (bed_index == 0 || (bed_index > 0 && stride_s > 0)) { + auto current_bed_index = get_bed_index(obj); + auto stride = unscaled(stride_s); + auto transl = Vec2d{0., (bed_index - current_bed_index) * stride}; + obj.displace(transl, 0.); + + ret = true; + } + + return ret; +} + +Transform3d YStriderVBedHandler::get_physical_bed_trafo(int bed_index) const +{ + auto stride_s = stride_scaled(); + auto tr = Transform3d::Identity(); + tr.translate(Vec3d{0., -bed_index * unscaled(stride_s), 0.}); + + return tr; +} + +int GridStriderVBedHandler::get_bed_index(const VBedPlaceable &obj) const +{ + Vec2i crd = {m_xstrider.get_bed_index(obj), m_ystrider.get_bed_index(obj)}; + + return BedsGrid::grid_coords2index(crd); +} + +bool GridStriderVBedHandler::assign_bed(VBedPlaceable &inst, int bed_idx) +{ + if (bed_idx < 0) { + return false; + } + Vec2i crd = BedsGrid::index2grid_coords(bed_idx); + + bool retx = m_xstrider.assign_bed(inst, crd.x()); + bool rety = m_ystrider.assign_bed(inst, crd.y()); + + return retx && rety; +} + +Transform3d GridStriderVBedHandler::get_physical_bed_trafo(int bed_idx) const +{ + Vec2i crd = BedsGrid::index2grid_coords(bed_idx); + + Transform3d ret = m_xstrider.get_physical_bed_trafo(crd.x()) * + m_ystrider.get_physical_bed_trafo(crd.y()); + + return ret; +} + +FixedSelection::FixedSelection(const Model &m) : m_wp{true} +{ + m_seldata.resize(m.objects.size()); + for (size_t i = 0; i < m.objects.size(); ++i) { + m_seldata[i].resize(m.objects[i]->instances.size(), true); + } +} + +FixedSelection::FixedSelection(const SelectionMask &other) +{ + auto obj_sel = other.selected_objects(); + m_seldata.reserve(obj_sel.size()); + for (int oidx = 0; oidx < static_cast(obj_sel.size()); ++oidx) + m_seldata.emplace_back(other.selected_instances(oidx)); +} + +std::vector FixedSelection::selected_objects() const +{ + auto ret = Slic3r::reserve_vector(m_seldata.size()); + std::transform(m_seldata.begin(), + m_seldata.end(), + std::back_inserter(ret), + [](auto &a) { + return std::any_of(a.begin(), a.end(), [](bool b) { + return b; + }); + }); + return ret; +} + +static std::vector find_true_indices(const std::vector &v) +{ + auto ret = reserve_vector(v.size()); + + for (size_t i = 0; i < v.size(); ++i) + if (v[i]) + ret.emplace_back(i); + + return ret; +} + +std::vector selected_object_indices(const SelectionMask &sm) +{ + auto sel = sm.selected_objects(); + return find_true_indices(sel); +} + +std::vector selected_instance_indices(int obj_idx, const SelectionMask &sm) +{ + auto sel = sm.selected_instances(obj_idx); + return find_true_indices(sel); +} + +SceneBuilder::SceneBuilder() = default; +SceneBuilder::~SceneBuilder() = default; +SceneBuilder::SceneBuilder(SceneBuilder &&) = default; +SceneBuilder& SceneBuilder::operator=(SceneBuilder&&) = default; + +SceneBuilder &&SceneBuilder::set_model(AnyPtr mdl) +{ + m_model = std::move(mdl); + return std::move(*this); +} + +SceneBuilder &&SceneBuilder::set_model(Model &mdl) +{ + m_model = &mdl; + return std::move(*this); +} + +SceneBuilder &&SceneBuilder::set_fff_print(AnyPtr mdl_print) +{ + m_fff_print = std::move(mdl_print); + return std::move(*this); +} + +SceneBuilder &&SceneBuilder::set_sla_print(AnyPtr mdl_print) +{ + m_sla_print = std::move(mdl_print); + + return std::move(*this); +} + +SceneBuilder &&SceneBuilder::set_bed(const DynamicPrintConfig &cfg, const Vec2crd &gap) +{ + Points bedpts = get_bed_shape(cfg); + + if (is_XL_printer(cfg)) { + m_xl_printer = true; + } + + m_bed = arr2::to_arrange_bed(bedpts, gap); + + return std::move(*this); +} + +SceneBuilder &&SceneBuilder::set_bed(const Print &print, const Vec2crd &gap) +{ + Points bedpts = get_bed_shape(print.config()); + + if (is_XL_printer(print.config())) { + m_bed = XLBed{get_extents(bedpts), gap}; + } else { + m_bed = arr2::to_arrange_bed(bedpts, gap); + } + + set_brim_and_skirt(); + + return std::move(*this); +} + +SceneBuilder &&SceneBuilder::set_sla_print(const SLAPrint *slaprint) +{ + m_sla_print = slaprint; + return std::move(*this); +} + +int ArrangeableWipeTowerBase::get_bed_index() const { + return this->bed_index; +} + +bool ArrangeableWipeTowerBase::assign_bed(int bed_idx) +{ + return bed_idx == this->bed_index; +} + +bool PhysicalOnlyVBedHandler::assign_bed(VBedPlaceable &inst, int bed_idx) +{ + return bed_idx == PhysicalBedId; +} + +ArrangeableSlicerModel::ArrangeableSlicerModel(SceneBuilder &builder) +{ + builder.build_arrangeable_slicer_model(*this); +} + +ArrangeableSlicerModel::~ArrangeableSlicerModel() = default; + +void ArrangeableSlicerModel::for_each_arrangeable( + std::function fn) +{ + for_each_arrangeable_(*this, fn); + + for (auto &wth : m_wths) { + wth->visit(fn); + } +} + +void ArrangeableSlicerModel::for_each_arrangeable( + std::function fn) const +{ + for_each_arrangeable_(*this, fn); + + for (auto &wth : m_wths) { + wth->visit(fn); + } +} + +ObjectID ArrangeableSlicerModel::add_arrangeable(const ObjectID &prototype_id) +{ + ObjectID ret; + + auto [inst, pos] = find_instance_by_id(*m_model, prototype_id); + if (inst) { + auto new_inst = inst->get_object()->add_instance(*inst); + if (new_inst) { + ret = new_inst->id(); + } + } + + return ret; +} + +std::optional get_bed_constraint( + const ObjectID &id, + const BedConstraints &bed_constraints +) { + const auto found_constraint{bed_constraints.find(id)}; + if (found_constraint == bed_constraints.end()) { + return std::nullopt; + } + return found_constraint->second; +} + +bool should_include_instance( + const ObjectID &instance_id, + const std::set &considered_instances +) { + if (considered_instances.find(instance_id) == considered_instances.end()) { + return false; + } + return true; +} + +template +void ArrangeableSlicerModel::for_each_arrangeable_(Self &&self, Fn &&fn) +{ + InstPos pos; + for (auto *obj : self.m_model->objects) { + for (auto *inst : obj->instances) { + if (!self.m_considered_instances || should_include_instance(inst->id(), *self.m_considered_instances)) { + ArrangeableModelInstance ainst{ + inst, + self.m_vbed_handler.get(), + self.m_selmask.get(), + pos, + get_bed_constraint(inst->id(), self.m_bed_constraints) + }; + fn(ainst); + } + ++pos.inst_idx; + } + pos.inst_idx = 0; + ++pos.obj_idx; + } +} + +template +void ArrangeableSlicerModel::visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn) +{ + for (auto &wth : self.m_wths) { + if (id == wth->get_id()) { + wth->visit(fn); + return; + } + } + + auto [inst, pos] = find_instance_by_id(*self.m_model, id); + + if (inst) { + ArrangeableModelInstance ainst{ + inst, + self.m_vbed_handler.get(), + self.m_selmask.get(), + pos, + get_bed_constraint(id, self.m_bed_constraints) + }; + fn(ainst); + } +} + +void ArrangeableSlicerModel::visit_arrangeable( + const ObjectID &id, std::function fn) const +{ + visit_arrangeable_(*this, id, fn); +} + +void ArrangeableSlicerModel::visit_arrangeable( + const ObjectID &id, std::function fn) +{ + visit_arrangeable_(*this, id, fn); +} + +template +void ArrangeableSLAPrint::for_each_arrangeable_(Self &&self, Fn &&fn) +{ + InstPos pos; + for (auto *obj : self.m_model->objects) { + for (auto *inst : obj->instances) { + if (!self.m_considered_instances || should_include_instance(inst->id(), *self.m_considered_instances)) { + ArrangeableModelInstance ainst{inst, self.m_vbed_handler.get(), + self.m_selmask.get(), pos, get_bed_constraint(inst->id(), self.m_bed_constraints)}; + + auto obj_id = inst->get_object()->id(); + const SLAPrintObject *po = + self.m_slaprint->get_print_object_by_model_object_id(obj_id); + + if (po) { + auto &vbh = self.m_vbed_handler; + auto phtr = vbh->get_physical_bed_trafo(vbh->get_bed_index(VBedPlaceableMI{*inst})); + ArrangeableSLAPrintObject ainst_po{ + po, + &ainst, + get_bed_constraint(inst->id(), self.m_bed_constraints), + phtr * inst->get_matrix() + }; + fn(ainst_po); + } else { + fn(ainst); + } + } + ++pos.inst_idx; + } + pos.inst_idx = 0; + ++pos.obj_idx; + } +} + +void ArrangeableSLAPrint::for_each_arrangeable( + std::function fn) +{ + for_each_arrangeable_(*this, fn); + + for (auto &wth : m_wths) { + wth->visit(fn); + } +} + +void ArrangeableSLAPrint::for_each_arrangeable( + std::function fn) const +{ + for_each_arrangeable_(*this, fn); + + for (auto &wth : m_wths) { + wth->visit(fn); + } +} + +template +void ArrangeableSLAPrint::visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn) +{ + auto [inst, pos] = find_instance_by_id(*self.m_model, id); + + if (inst) { + ArrangeableModelInstance ainst{inst, self.m_vbed_handler.get(), + self.m_selmask.get(), pos, std::nullopt}; + + auto obj_id = inst->get_object()->id(); + const SLAPrintObject *po = + self.m_slaprint->get_print_object_by_model_object_id(obj_id); + + if (po) { + auto &vbh = self.m_vbed_handler; + auto phtr = vbh->get_physical_bed_trafo(vbh->get_bed_index(VBedPlaceableMI{*inst})); + ArrangeableSLAPrintObject ainst_po{ + po, + &ainst, + get_bed_constraint(inst->id(), self.m_bed_constraints), + phtr * inst->get_matrix() + }; + fn(ainst_po); + } else { + fn(ainst); + } + } +} + +void ArrangeableSLAPrint::visit_arrangeable( + const ObjectID &id, std::function fn) const +{ + visit_arrangeable_(*this, id, fn); +} + +void ArrangeableSLAPrint::visit_arrangeable( + const ObjectID &id, std::function fn) +{ + visit_arrangeable_(*this, id, fn); +} + +template +ExPolygons ArrangeableModelInstance::full_outline() const +{ + int bedidx = m_vbedh->get_bed_index(*this); + auto tr = m_vbedh->get_physical_bed_trafo(bedidx); + + return extract_full_outline(*m_mi, tr); +} + +template +Polygon ArrangeableModelInstance::convex_outline() const +{ + int bedidx = m_vbedh->get_bed_index(*this); + auto tr = m_vbedh->get_physical_bed_trafo(bedidx); + + return extract_convex_outline(*m_mi, tr); +} + +template +bool ArrangeableModelInstance::is_selected() const +{ + bool ret = false; + + if (m_selmask) { + auto sel = m_selmask->selected_instances(m_pos_within_model.obj_idx); + if (m_pos_within_model.inst_idx < sel.size() && + sel[m_pos_within_model.inst_idx]) + ret = true; + } + + return ret; +} + +template +void ArrangeableModelInstance::transform(const Vec2d &transl, double rot) +{ + if constexpr (!std::is_const_v && !std::is_const_v) { + int bedidx = m_vbedh->get_bed_index(*this); + auto physical_trafo = m_vbedh->get_physical_bed_trafo(bedidx); + + transform_instance(*m_mi, transl, rot, physical_trafo); + } +} + +template +bool ArrangeableModelInstance::assign_bed(int bed_idx) +{ + bool ret = false; + + if constexpr (!std::is_const_v && !std::is_const_v) + ret = m_vbedh->assign_bed(*this, bed_idx); + + return ret; +} + +template class ArrangeableModelInstance; +template class ArrangeableModelInstance; + +ExPolygons ArrangeableSLAPrintObject::full_outline() const +{ + ExPolygons ret; + + auto laststep = m_po->last_completed_step(); + if (laststep < slaposCount && laststep > slaposSupportTree) { + Polygons polys; + auto omesh = m_po->get_mesh_to_print(); + auto &smesh = m_po->support_mesh(); + + Transform3d trafo_instance = m_inst_trafo * m_po->trafo().inverse(); + + if (omesh) { + Polygons ptmp = project_mesh(*omesh, trafo_instance, [] {}); + std::move(ptmp.begin(), ptmp.end(), std::back_inserter(polys)); + } + + Polygons ptmp = project_mesh(smesh.its, trafo_instance, [] {}); + std::move(ptmp.begin(), ptmp.end(), std::back_inserter(polys)); + ret = union_ex(polys); + } else { + ret = m_arrbl->full_outline(); + } + + return ret; +} + +ExPolygons ArrangeableSLAPrintObject::full_envelope() const +{ + ExPolygons ret = full_outline(); + + auto laststep = m_po->last_completed_step(); + if (laststep < slaposCount && laststep > slaposSupportTree) { + auto &pmesh = m_po->pad_mesh(); + if (!pmesh.empty()) { + + Transform3d trafo_instance = m_inst_trafo * m_po->trafo().inverse(); + + Polygons ptmp = project_mesh(pmesh.its, trafo_instance, [] {}); + ret = union_ex(ret, ptmp); + } + } + + return ret; +} + +Polygon ArrangeableSLAPrintObject::convex_outline() const +{ + Polygons polys; + + polys.emplace_back(m_arrbl->convex_outline()); + + auto laststep = m_po->last_completed_step(); + if (laststep < slaposCount && laststep > slaposSupportTree) { + auto omesh = m_po->get_mesh_to_print(); + auto &smesh = m_po->support_mesh(); + + Transform3f trafo_instance = m_inst_trafo.cast(); + trafo_instance = trafo_instance * m_po->trafo().cast().inverse(); + + Polygons polys; + polys.reserve(3); + auto zlvl = -m_po->get_elevation(); + + if (omesh) { + polys.emplace_back( + its_convex_hull_2d_above(*omesh, trafo_instance, zlvl)); + } + + polys.emplace_back( + its_convex_hull_2d_above(smesh.its, trafo_instance, zlvl)); + } + + return Geometry::convex_hull(polys); +} + +Polygon ArrangeableSLAPrintObject::convex_envelope() const +{ + Polygons polys; + + polys.emplace_back(convex_outline()); + + auto laststep = m_po->last_completed_step(); + if (laststep < slaposCount && laststep > slaposSupportTree) { + auto &pmesh = m_po->pad_mesh(); + if (!pmesh.empty()) { + + Transform3f trafo_instance = m_inst_trafo.cast(); + trafo_instance = trafo_instance * m_po->trafo().cast().inverse(); + auto zlvl = -m_po->get_elevation(); + + polys.emplace_back( + its_convex_hull_2d_above(pmesh.its, trafo_instance, zlvl)); + } + } + + return Geometry::convex_hull(polys); +} + +DuplicableModel::DuplicableModel(AnyPtr mdl, AnyPtr vbh, const BoundingBox &bedbb) + : m_model{std::move(mdl)}, m_vbh{std::move(vbh)}, m_duplicates(1), m_bedbb{bedbb} +{ +} + +DuplicableModel::~DuplicableModel() = default; + +ObjectID DuplicableModel::add_arrangeable(const ObjectID &prototype_id) +{ + ObjectID ret; + if (prototype_id.valid()) { + size_t idx = prototype_id.id - 1; + if (idx < m_duplicates.size()) { + ModelDuplicate md = m_duplicates[idx]; + md.id = m_duplicates.size(); + ret = md.id.id + 1; + m_duplicates.emplace_back(std::move(md)); + } + } + + return ret; +} + +void DuplicableModel::apply_duplicates() +{ + for (ModelObject *o : m_model->objects) { + // make a copy of the pointers in order to avoid recursion + // when appending their copies + ModelInstancePtrs instances = o->instances; + o->instances.clear(); + for (const ModelInstance *i : instances) { + for (const ModelDuplicate &md : m_duplicates) { + ModelInstance *instance = o->add_instance(*i); + arr2::transform_instance(*instance, md.tr, md.rot); + } + } + for (auto *i : instances) + delete i; + + instances.clear(); + + o->invalidate_bounding_box(); + } +} + +template +ObjectID ArrangeableFullModel::geometry_id() const { return m_mdl->id(); } + +template +ExPolygons ArrangeableFullModel::full_outline() const +{ + auto ret = reserve_vector(arr2::model_instance_count(*m_mdl)); + + auto transl = Transform3d::Identity(); + transl.translate(to_3d(m_dup->tr, 0.)); + Transform3d trafo = transl* Eigen::AngleAxisd(m_dup->rot, Vec3d::UnitZ()); + + for (auto *mo : m_mdl->objects) { + for (auto *mi : mo->instances) { + auto expolys = arr2::extract_full_outline(*mi, trafo); + std::move(expolys.begin(), expolys.end(), std::back_inserter(ret)); + } + } + + return ret; +} + +template +Polygon ArrangeableFullModel::convex_outline() const +{ + auto ret = reserve_polygons(arr2::model_instance_count(*m_mdl)); + + auto transl = Transform3d::Identity(); + transl.translate(to_3d(m_dup->tr, 0.)); + Transform3d trafo = transl* Eigen::AngleAxisd(m_dup->rot, Vec3d::UnitZ()); + + for (auto *mo : m_mdl->objects) { + for (auto *mi : mo->instances) { + ret.emplace_back(arr2::extract_convex_outline(*mi, trafo)); + } + } + + return Geometry::convex_hull(ret); +} + +template class ArrangeableFullModel; +template class ArrangeableFullModel; + +std::unique_ptr VirtualBedHandler::create(const ExtendedBed &bed) +{ + std::unique_ptr ret; + if (is_infinite_bed(bed)) { + ret = std::make_unique(); + } else { + Vec2crd gap; + visit_bed([&gap](auto &rawbed) { gap = bed_gap(rawbed); }, bed); + BoundingBox bedbb; + visit_bed([&bedbb](auto &rawbed) { bedbb = bounding_box(rawbed); }, bed); + + ret = std::make_unique(bedbb, gap); + } + + return ret; +} + +}} // namespace Slic3r::arr2 + +#endif // SCENEBUILDER_CPP diff --git a/src/slic3r-arrange-wrapper/src/Tasks/ArrangeTaskImpl.hpp b/src/slic3r-arrange-wrapper/src/Tasks/ArrangeTaskImpl.hpp new file mode 100644 index 0000000..281356c --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/Tasks/ArrangeTaskImpl.hpp @@ -0,0 +1,147 @@ +#ifndef ARRANGETASK_IMPL_HPP +#define ARRANGETASK_IMPL_HPP + +#include + +#include + +#include + +#include +#include + +namespace Slic3r { namespace arr2 { + +// Prepare the selected and unselected items separately. If nothing is +// selected, behaves as if everything would be selected. +template +void extract_selected(ArrangeTask &task, + const ArrangeableModel &mdl, + const ArrangeableToItemConverter &itm_conv) +{ + // Go through the objects and check if inside the selection + mdl.for_each_arrangeable( + [&task, &itm_conv](const Arrangeable &arrbl) { + bool selected = arrbl.is_selected(); + bool printable = arrbl.is_printable(); + + try { + auto itm = itm_conv.convert(arrbl, selected ? 0 : -SCALED_EPSILON); + + auto &container_parent = printable ? task.printable : + task.unprintable; + + auto &container = selected ? + container_parent.selected : + container_parent.unselected; + + container.emplace_back(std::move(itm)); + } catch (const EmptyItemOutlineError &ex) { + BOOST_LOG_TRIVIAL(error) + << "ObjectID " << std::to_string(arrbl.id().id) << ": " << ex.what(); + } + }); +} + +template +std::unique_ptr> ArrangeTask::create( + const Scene &sc, const ArrangeableToItemConverter &converter) +{ + auto task = std::make_unique>(); + + task->settings.set_from(sc.settings()); + + task->bed = get_corrected_bed(sc.bed(), converter); + + extract_selected(*task, sc.model(), converter); + + return task; +} + +// Remove all items on the physical bed (not occupyable for unprintable items) +// and shift all items to the next lower bed index, so that arrange will think +// that logical bed no. 1 is the physical one +template +void prepare_fixed_unselected(ItemCont &items, int shift) +{ + for (auto &itm : items) + set_bed_index(itm, get_bed_index(itm) - shift); + + items.erase(std::remove_if(items.begin(), items.end(), + [](auto &itm) { return !is_arranged(itm); }), + items.end()); +} + +inline int find_first_empty_bed(const std::vector& bed_indices, + int starting_from = 0) { + int ret = starting_from; + + for (int idx : bed_indices) { + if (idx == ret) { + ret++; + } else if (idx > ret) { + break; + } + } + + return ret; +} + +template +std::unique_ptr +ArrangeTask::process_native(Ctl &ctl) +{ + auto result = std::make_unique(); + + auto arranger = Arranger::create(settings); + + class TwoStepArrangeCtl: public Ctl + { + Ctl &parent; + ArrangeTask &self; + public: + TwoStepArrangeCtl(Ctl &p, ArrangeTask &slf) : parent{p}, self{slf} {} + + void update_status(int remaining) override + { + parent.update_status(remaining + self.unprintable.selected.size()); + } + + bool was_canceled() const override { return parent.was_canceled(); } + + } subctl{ctl, *this}; + + arranger->arrange(printable.selected, printable.unselected, bed, subctl); + + std::vector printable_bed_indices = + get_bed_indices(crange(printable.selected), crange(printable.unselected)); + + // If there are no printables, leave the physical bed empty + static constexpr int SearchFrom = 1; + + // Unprintable items should go to the first logical (!) bed not containing + // any printable items + int first_empty_bed = find_first_empty_bed(printable_bed_indices, SearchFrom); + + prepare_fixed_unselected(unprintable.unselected, first_empty_bed); + + arranger->arrange(unprintable.selected, unprintable.unselected, bed, ctl); + + result->add_items(crange(printable.selected)); + + for (auto &itm : unprintable.selected) { + if (is_arranged(itm)) { + int bedidx = get_bed_index(itm) + first_empty_bed; + arr2::set_bed_index(itm, bedidx); + } + + result->add_item(itm); + } + + return result; +} + +} // namespace arr2 +} // namespace Slic3r + +#endif //ARRANGETASK_IMPL_HPP diff --git a/src/slic3r-arrange-wrapper/src/Tasks/FillBedTaskImpl.hpp b/src/slic3r-arrange-wrapper/src/Tasks/FillBedTaskImpl.hpp new file mode 100644 index 0000000..cbe8974 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/Tasks/FillBedTaskImpl.hpp @@ -0,0 +1,215 @@ +#ifndef FILLBEDTASKIMPL_HPP +#define FILLBEDTASKIMPL_HPP + +#include + +#include + +#include + +namespace Slic3r { namespace arr2 { + +template +int calculate_items_needed_to_fill_bed(const ExtendedBed &bed, + const ArrItem &prototype_item, + size_t prototype_count, + const std::vector &fixed) +{ + double poly_area = fixed_area(prototype_item); + + auto area_sum_fn = [&](double s, const auto &itm) { + return s + (get_bed_index(itm) == get_bed_constraint(prototype_item)) * fixed_area(itm); + }; + + double unsel_area = std::accumulate(fixed.begin(), + fixed.end(), + 0., + area_sum_fn); + + double fixed_area = unsel_area + prototype_count * poly_area; + double bed_area = 0.; + + visit_bed([&bed_area] (auto &realbed) { bed_area = area(realbed); }, bed); + + // This is the maximum number of items, + // the real number will always be close but less. + auto needed_items = static_cast( + std::ceil((bed_area - fixed_area) / poly_area)); + + return needed_items; +} + +template +void extract(FillBedTask &task, + const Scene &scene, + const ArrangeableToItemConverter &itm_conv) +{ + task.prototype_item = {}; + + auto selected_ids = scene.selected_ids(); + + if (selected_ids.empty()) + return; + + std::set selected_objects = selected_geometry_ids(scene); + + if (selected_objects.size() != 1) + return; + + ObjectID prototype_geometry_id = *(selected_objects.begin()); + + auto set_prototype_item = [&task, &itm_conv](const Arrangeable &arrbl) { + if (arrbl.is_printable()) + task.prototype_item = itm_conv.convert(arrbl); + }; + + scene.model().visit_arrangeable(selected_ids.front(), set_prototype_item); + + if (!task.prototype_item) + return; + + // Workaround for missing items when arranging the same geometry only: + // Injecting a number of items but with slightly shrinked shape, so that + // they can fill the emerging holes. + ArrItem prototype_item_shrinked; + scene.model().visit_arrangeable(selected_ids.front(), + [&prototype_item_shrinked, &itm_conv](const Arrangeable &arrbl) { + if (arrbl.is_printable()) + prototype_item_shrinked = itm_conv.convert(arrbl, -SCALED_EPSILON); + }); + + const int bed_constraint{*get_bed_constraint(*task.prototype_item)}; + if (bed_constraint != get_bed_index(*task.prototype_item)) { + return; + } + + set_bed_index(*task.prototype_item, Unarranged); + + auto collect_task_items = [&prototype_geometry_id, &task, + &itm_conv, &bed_constraint](const Arrangeable &arrbl) { + try { + if (arrbl.bed_constraint() == bed_constraint) { + if (arrbl.geometry_id() == prototype_geometry_id) { + if (arrbl.is_printable()) { + auto itm = itm_conv.convert(arrbl); + raise_priority(itm); + task.selected.emplace_back(std::move(itm)); + } + } else { + auto itm = itm_conv.convert(arrbl, -SCALED_EPSILON); + task.unselected.emplace_back(std::move(itm)); + } + } + } catch (const EmptyItemOutlineError &ex) { + BOOST_LOG_TRIVIAL(error) + << "ObjectID " << std::to_string(arrbl.id().id) << ": " << ex.what(); + } + }; + + scene.model().for_each_arrangeable(collect_task_items); + + int needed_items = calculate_items_needed_to_fill_bed(task.bed, + *task.prototype_item, + task.selected.size(), + task.unselected); + + task.selected_existing_count = task.selected.size(); + task.selected.reserve(task.selected.size() + needed_items); + std::fill_n(std::back_inserter(task.selected), needed_items, + *task.prototype_item); + + // Add as many filler items as there are needed items. Most of them will + // be discarded anyways. + std::fill_n(std::back_inserter(task.selected_fillers), needed_items, + prototype_item_shrinked); +} + + +template +std::unique_ptr> FillBedTask::create( + const Scene &sc, const ArrangeableToItemConverter &converter) +{ + auto task = std::make_unique>(); + + task->settings.set_from(sc.settings()); + + task->bed = get_corrected_bed(sc.bed(), converter); + + extract(*task, sc, converter); + + return task; +} + +template +std::unique_ptr FillBedTask::process_native( + Ctl &ctl) +{ + auto result = std::make_unique(); + + if (!prototype_item) + return result; + + result->prototype_id = retrieve_id(*prototype_item).value_or(ObjectID{}); + + class FillBedCtl: public ArrangerCtl + { + ArrangeTaskCtl &parent; + FillBedTask &self; + bool do_stop = false; + + public: + FillBedCtl(ArrangeTaskCtl &p, FillBedTask &slf) : parent{p}, self{slf} {} + + void update_status(int remaining) override + { + parent.update_status(remaining); + } + + bool was_canceled() const override + { + return parent.was_canceled() || do_stop; + } + + void on_packed(ArrItem &itm) override + { + // Stop at the first filler that is not on the physical bed + do_stop = get_bed_index(itm) == -1 && get_priority(itm) == 0; + } + + } subctl(ctl, *this); + + auto arranger = Arranger::create(settings); + + arranger->arrange(selected, unselected, bed, subctl); + + auto unsel_cpy = unselected; + for (const auto &itm : selected) { + unsel_cpy.emplace_back(itm); + } + + arranger->arrange(selected_fillers, unsel_cpy, bed, FillBedCtl{ctl, *this}); + + auto arranged_range = Range{selected.begin(), + selected.begin() + selected_existing_count}; + + result->add_arranged_items(arranged_range); + + auto to_add_range = Range{selected.begin() + selected_existing_count, + selected.end()}; + + for (auto &itm : to_add_range) { + if (get_bed_index(itm) == get_bed_constraint(itm)) + result->add_new_item(itm); + } + + for (auto &itm : selected_fillers) + if (get_bed_index(itm) == get_bed_constraint(itm)) + result->add_new_item(itm); + + return result; +} + +} // namespace arr2 +} // namespace Slic3r + +#endif // FILLBEDTASKIMPL_HPP diff --git a/src/slic3r-arrange-wrapper/src/Tasks/MultiplySelectionTaskImpl.hpp b/src/slic3r-arrange-wrapper/src/Tasks/MultiplySelectionTaskImpl.hpp new file mode 100644 index 0000000..fb9ff15 --- /dev/null +++ b/src/slic3r-arrange-wrapper/src/Tasks/MultiplySelectionTaskImpl.hpp @@ -0,0 +1,127 @@ +#ifndef MULTIPLYSELECTIONTASKIMPL_HPP +#define MULTIPLYSELECTIONTASKIMPL_HPP + +#include + +#include + +namespace Slic3r { namespace arr2 { + +template +std::unique_ptr> MultiplySelectionTask::create( + const Scene &scene, size_t count, const ArrangeableToItemConverter &itm_conv) +{ + auto task_ptr = std::make_unique>(); + + auto &task = *task_ptr; + + task.settings.set_from(scene.settings()); + + task.bed = get_corrected_bed(scene.bed(), itm_conv); + + task.prototype_item = {}; + + auto selected_ids = scene.selected_ids(); + + if (selected_ids.empty()) + return task_ptr; + + std::set selected_objects = selected_geometry_ids(scene); + + if (selected_objects.size() != 1) + return task_ptr; + + ObjectID prototype_geometry_id = *(selected_objects.begin()); + + auto set_prototype_item = [&task, &itm_conv](const Arrangeable &arrbl) { + if (arrbl.is_printable()) + task.prototype_item = itm_conv.convert(arrbl); + }; + + scene.model().visit_arrangeable(selected_ids.front(), set_prototype_item); + + if (!task.prototype_item) + return task_ptr; + + set_bed_index(*task.prototype_item, Unarranged); + + auto collect_task_items = [&prototype_geometry_id, &task, + &itm_conv](const Arrangeable &arrbl) { + try { + if (arrbl.geometry_id() == prototype_geometry_id) { + if (arrbl.is_printable()) { + auto itm = itm_conv.convert(arrbl); + raise_priority(itm); + task.selected.emplace_back(std::move(itm)); + } + } else { + auto itm = itm_conv.convert(arrbl, -SCALED_EPSILON); + task.unselected.emplace_back(std::move(itm)); + } + } catch (const EmptyItemOutlineError &ex) { + BOOST_LOG_TRIVIAL(error) + << "ObjectID " << std::to_string(arrbl.id().id) << ": " << ex.what(); + } + }; + + scene.model().for_each_arrangeable(collect_task_items); + + task.selected_existing_count = task.selected.size(); + task.selected.reserve(task.selected.size() + count); + std::fill_n(std::back_inserter(task.selected), count, *task.prototype_item); + + return task_ptr; +} + +template +std::unique_ptr +MultiplySelectionTask::process_native(Ctl &ctl) +{ + auto result = std::make_unique(); + + if (!prototype_item) + return result; + + result->prototype_id = retrieve_id(*prototype_item).value_or(ObjectID{}); + + class MultiplySelectionCtl: public ArrangerCtl + { + ArrangeTaskCtl &parent; + MultiplySelectionTask &self; + + public: + MultiplySelectionCtl(ArrangeTaskCtl &p, MultiplySelectionTask &slf) + : parent{p}, self{slf} {} + + void update_status(int remaining) override + { + parent.update_status(remaining); + } + + bool was_canceled() const override + { + return parent.was_canceled(); + } + + } subctl(ctl, *this); + + auto arranger = Arranger::create(settings); + + arranger->arrange(selected, unselected, bed, subctl); + + auto arranged_range = Range{selected.begin(), + selected.begin() + selected_existing_count}; + + result->add_arranged_items(arranged_range); + + auto to_add_range = Range{selected.begin() + selected_existing_count, + selected.end()}; + + result->add_new_items(to_add_range); + + return result; +} + +}} // namespace Slic3r::arr2 + +#endif // MULTIPLYSELECTIONTASKIMPL_HPP diff --git a/src/slic3r-arrange/.vscode/settings.json b/src/slic3r-arrange/.vscode/settings.json new file mode 100644 index 0000000..a478c1f --- /dev/null +++ b/src/slic3r-arrange/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "string_view": "cpp" + } +} \ No newline at end of file diff --git a/src/slic3r-arrange/CMakeLists.txt b/src/slic3r-arrange/CMakeLists.txt new file mode 100644 index 0000000..98ab247 --- /dev/null +++ b/src/slic3r-arrange/CMakeLists.txt @@ -0,0 +1,34 @@ +project(slic3r-arrange) +cmake_minimum_required(VERSION 3.13) + +add_library(slic3r-arrange + include/arrange/Beds.hpp + include/arrange/ArrangeItemTraits.hpp + include/arrange/PackingContext.hpp + include/arrange/NFP/NFPArrangeItemTraits.hpp + include/arrange/NFP/NFP.hpp + include/arrange/ArrangeBase.hpp + include/arrange/DataStoreTraits.hpp + include/arrange/ArrangeFirstFit.hpp + include/arrange/NFP/PackStrategyNFP.hpp + include/arrange/NFP/Kernels/TMArrangeKernel.hpp + include/arrange/NFP/Kernels/GravityKernel.hpp + include/arrange/NFP/RectangleOverfitPackingStrategy.hpp + include/arrange/NFP/EdgeCache.hpp + include/arrange/NFP/Kernels/KernelTraits.hpp + include/arrange/NFP/NFPConcave_Tesselate.hpp + include/arrange/NFP/Kernels/KernelUtils.hpp + include/arrange/NFP/Kernels/CompactifyKernel.hpp + include/arrange/NFP/Kernels/RectangleOverfitKernelWrapper.hpp + include/arrange/NFP/Kernels/SVGDebugOutputKernelWrapper.hpp + + src/Beds.cpp + src/NFP/NFP.cpp + src/NFP/NFPConcave_Tesselate.cpp + src/NFP/EdgeCache.cpp + src/NFP/CircularEdgeIterator.hpp +) + +target_include_directories(slic3r-arrange PRIVATE src) +target_include_directories(slic3r-arrange PUBLIC include) +target_link_libraries(slic3r-arrange PUBLIC libslic3r) diff --git a/src/slic3r-arrange/include/arrange/ArrangeBase.hpp b/src/slic3r-arrange/include/arrange/ArrangeBase.hpp new file mode 100644 index 0000000..b46d9d1 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/ArrangeBase.hpp @@ -0,0 +1,294 @@ +#ifndef ARRANGEBASE_HPP +#define ARRANGEBASE_HPP + +#include +#include + +#include + +#include +#include + +namespace Slic3r { namespace arr2 { + +namespace detail_is_const_it { + +template +struct IsConstIt_ { static constexpr bool value = false; }; + +template +using iterator_category_t = typename std::iterator_traits::iterator_category; + +template +using iterator_reference_t = typename std::iterator_traits::reference; + +template +struct IsConstIt_ >> > +{ + static constexpr bool value = + std::is_const_v>>; +}; + +} // namespace detail_is_const_it + +template +static constexpr bool IsConstIterator = detail_is_const_it::IsConstIt_::value; + +template +constexpr bool is_const_iterator(const It &it) noexcept { return IsConstIterator; } + +// The pack() function will use tag dispatching, based on the given strategy +// object that is used as its first argument. + +// This tag is derived for a packing strategy as default, and will be used +// to cast a compile error. +struct UnimplementedPacking {}; + +// PackStrategyTag_ needs to be specialized for any valid packing strategy class +template struct PackStrategyTag_ { + using Tag = UnimplementedPacking; +}; + +// Helper metafunc to derive packing strategy tag from a strategy object. +template +using PackStrategyTag = + typename PackStrategyTag_>::Tag; + + +template struct PackStrategyTraits_ { + template using Context = DefaultPackingContext; + + template + static Context create_context(PackStrategy &ps, + const Bed &bed, + int bed_index) + { + return {}; + } +}; + +template using PackStrategyTraits = PackStrategyTraits_>; + +template +using PackStrategyContext = + typename PackStrategyTraits::template Context>; + +template +PackStrategyContext create_context(PackStrategy &&ps, + const Bed &bed, + int bed_index) +{ + return PackStrategyTraits::template create_context< + StripCVRef>(ps, bed, bed_index); +} + +// Function to pack one item into a bed. +// strategy parameter holds clue to what packing strategy to use. This function +// needs to be overloaded for the strategy tag belonging to the given +// strategy. +// 'bed' parameter is the type of bed into which the new item should be packed. +// See beds.hpp for valid bed classes. +// 'item' parameter is the item to be packed. After succesful arrangement +// (see return value) the item will have it's translation and rotation +// set correctly. If the function returns false, the translation and +// rotation of the input item might be changed to arbitrary values. +// 'fixed_items' paramter holds a range of ArrItem type objects that are already +// on the bed and need to be avoided by the newly packed item. +// 'remaining_items' is a range of ArrItem type objects that are intended to be +// packed in the future. This information can be leveradged by +// the packing strategy to make more intelligent placement +// decisions for the input item. +template +bool pack(Strategy &&strategy, + const Bed &bed, + ArrItem &item, + const PackStrategyContext &context, + const Range &remaining_items) +{ + static_assert(IsConstIterator, "Remaining item iterator is not const!"); + + // Dispatch: + return pack(std::forward(strategy), bed, item, context, + remaining_items, PackStrategyTag{}); +} + +// Overload without fixed items: +template +bool pack(Strategy &&strategy, const Bed &bed, ArrItem &item) +{ + std::vector dummy; + auto context = create_context(strategy, bed, PhysicalBedId); + return pack(std::forward(strategy), bed, item, context, + crange(dummy)); +} + +// Overload when strategy is unkown, yields compile error: +template +bool pack(Strategy &&strategy, + const Bed &bed, + ArrItem &item, + const PackStrategyContext &context, + const Range &remaining_items, + const UnimplementedPacking &) +{ + static_assert(always_false::value, + "Packing unimplemented for this placement strategy"); + + return false; +} + +// Helper function to remove unpackable items from the input container. +template +void remove_unpackable_items(PackStrategy &&ps, + Container &c, + const Bed &bed, + const StopCond &stopcond) +{ + // Safety test: try to pack each item into an empty bed. If it fails + // then it should be removed from the list + auto it = c.begin(); + while (it != c.end() && !stopcond()) { + StripCVRef &itm = *it; + auto cpy{itm}; + + if (!pack(ps, bed, cpy)) { + set_bed_index(itm, Unarranged); + it = c.erase(it); + } else + it++; + } +} + +// arrange() function will use tag dispatching based on the selection strategy +// given as its first argument. + +// This tag is derived for a selection strategy as default, and will be used +// to cast a compile error. +struct UnimplementedSelection {}; + +// SelStrategyTag_ needs to be specialized for any valid selection strategy class +template struct SelStrategyTag_ { + using Tag = UnimplementedSelection; +}; + +// Helper metafunc to derive the selection strategy tag from a strategy object. +template +using SelStrategyTag = typename SelStrategyTag_>::Tag; + +// Main function to start the arrangement. Takes a selection and a packing +// strategy object as the first two parameters. An implementation +// (function overload) must exist for this function that takes the coresponding +// selection strategy tag belonging to the given selstrategy argument. +// +// items parameter is a range of arrange items to arrange. +// fixed parameter is a range of arrange items that have fixed position and will +// not move during the arrangement but need to be avoided by the +// moving items. +// bed parameter is the type of bed into which the items need to fit. +template +void arrange(SelectionStrategy &&selstrategy, + PackStrategy &&packingstrategy, + const Range &items, + const Range &fixed, + const TBed &bed) +{ + static_assert(IsConstIterator, "Fixed item iterator is not const!"); + + // Dispatch: + arrange(std::forward(selstrategy), + std::forward(packingstrategy), items, fixed, bed, + SelStrategyTag{}); +} + +template +void arrange(SelectionStrategy &&selstrategy, + PackStrategy &&packingstrategy, + const Range &items, + const TBed &bed) +{ + std::vector::value_type> dummy; + arrange(std::forward(selstrategy), + std::forward(packingstrategy), items, crange(dummy), + bed); +} + +// Overload for unimplemented selection strategy, yields compile error: +template +void arrange(SelectionStrategy &&selstrategy, + PackStrategy &&packingstrategy, + const Range &items, + const Range &fixed, + const TBed &bed, + const UnimplementedSelection &) +{ + static_assert(always_false::value, + "Arrange unimplemented for this selection strategy"); +} + +template +std::vector get_bed_indices(const Range &items) +{ + auto bed_indices = reserve_vector(items.size()); + + for (auto &itm : items) + bed_indices.emplace_back(get_bed_index(itm)); + + std::sort(bed_indices.begin(), bed_indices.end()); + auto endit = std::unique(bed_indices.begin(), bed_indices.end()); + + bed_indices.erase(endit, bed_indices.end()); + + return bed_indices; +} + +template +std::vector get_bed_indices(const Range &items, const Range &fixed) +{ + std::vector ret; + + auto iitems = get_bed_indices(items); + auto ifixed = get_bed_indices(fixed); + ret.reserve(std::max(iitems.size(), ifixed.size())); + std::set_union(iitems.begin(), iitems.end(), + ifixed.begin(), ifixed.end(), + std::back_inserter(ret)); + + return ret; +} + +template +size_t get_bed_count(const Range &items) +{ + return get_bed_indices(items).size(); +} + +template int get_max_bed_index(const Range &items) +{ + auto it = std::max_element(items.begin(), + items.end(), + [](auto &i1, auto &i2) { + return get_bed_index(i1) < get_bed_index(i2); + }); + + int ret = Unarranged; + if (it != items.end()) + ret = get_bed_index(*it); + + return ret; +} + +struct DefaultStopCondition { + constexpr bool operator()() const noexcept { return false; } +}; + +}} // namespace Slic3r::arr2 + +#endif // ARRANGEBASE_HPP diff --git a/src/slic3r-arrange/include/arrange/ArrangeFirstFit.hpp b/src/slic3r-arrange/include/arrange/ArrangeFirstFit.hpp new file mode 100644 index 0000000..969ab20 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/ArrangeFirstFit.hpp @@ -0,0 +1,176 @@ +#ifndef ARRANGEFIRSTFIT_HPP +#define ARRANGEFIRSTFIT_HPP + +#include +#include + +#include + +namespace Slic3r { namespace arr2 { namespace firstfit { + +struct SelectionTag {}; + +// Can be specialized by Items +template +struct ItemArrangedVisitor { + template + static void on_arranged(ArrItem &itm, + const Bed &bed, + const Range &packed_items, + const Range &remaining_items) + {} +}; + +// Use the the visitor baked into the ArrItem type by default +struct DefaultOnArrangedFn { + template + void operator()(ArrItem &itm, + const Bed &bed, + const Range &packed, + const Range &remaining) + { + ItemArrangedVisitor>::on_arranged(itm, bed, packed, + remaining); + } +}; + +struct DefaultItemCompareFn { + template + bool operator() (const ArrItem &ia, const ArrItem &ib) + { + return get_priority(ia) > get_priority(ib); + } +}; + +template +struct SelectionStrategy +{ + CompareFn cmpfn; + OnArrangedFn on_arranged_fn; + StopCondition cancel_fn; + + SelectionStrategy(CompareFn cmp = {}, + OnArrangedFn on_arranged = {}, + StopCondition stopcond = {}) + : cmpfn{cmp}, + on_arranged_fn{std::move(on_arranged)}, + cancel_fn{std::move(stopcond)} + {} +}; + +} // namespace firstfit + +template struct SelStrategyTag_> { + using Tag = firstfit::SelectionTag; +}; + +template +void arrange( + SelStrategy &&sel, + PackStrategy &&ps, + const Range &items, + const Range &fixed, + const TBed &bed, + const firstfit::SelectionTag &) +{ + using ArrItem = typename std::iterator_traits::value_type; + using ArrItemRef = std::reference_wrapper; + + auto sorted_items = reserve_vector(items.size()); + + for (auto &itm : items) { + set_bed_index(itm, Unarranged); + sorted_items.emplace_back(itm); + } + + using Context = PackStrategyContext; + + std::map bed_contexts; + auto get_or_init_context = [&ps, &bed, &bed_contexts](int bedidx) -> Context& { + auto ctx_it = bed_contexts.find(bedidx); + if (ctx_it == bed_contexts.end()) { + auto res = bed_contexts.emplace( + bedidx, create_context(ps, bed, bedidx)); + + assert(res.second); + + ctx_it = res.first; + } + + return ctx_it->second; + }; + + for (auto &itm : fixed) { + auto bedidx = get_bed_index(itm); + if (bedidx >= 0) { + Context &ctx = get_or_init_context(bedidx); + add_fixed_item(ctx, itm); + } + } + + if constexpr (!std::is_null_pointer_v) { + std::stable_sort(sorted_items.begin(), sorted_items.end(), sel.cmpfn); + } + + auto is_cancelled = [&sel]() { + return sel.cancel_fn(); + }; + + remove_unpackable_items(ps, sorted_items, bed, [&is_cancelled]() { + return is_cancelled(); + }); + + auto it = sorted_items.begin(); + + using SConstIt = typename std::vector::const_iterator; + + while (it != sorted_items.end() && !is_cancelled()) { + bool was_packed = false; + int bedidx = 0; + while (!was_packed && !is_cancelled()) { + for (; !was_packed && !is_cancelled(); bedidx++) { + const std::optional bed_constraint{get_bed_constraint(*it)}; + if (bed_constraint && bedidx != *bed_constraint) { + continue; + } + set_bed_index(*it, bedidx); + + auto remaining = Range{std::next(static_cast(it)), + sorted_items.cend()}; + + Context &ctx = get_or_init_context(bedidx); + + was_packed = pack(ps, bed, *it, ctx, remaining); + + if(was_packed) { + add_packed_item(ctx, *it); + + auto packed_range = Range{sorted_items.cbegin(), + static_cast(it)}; + + sel.on_arranged_fn(*it, bed, packed_range, remaining); + } else { + set_bed_index(*it, Unarranged); + if (bed_constraint && bedidx == *bed_constraint) { + // Leave the item as is as it does not fit on the enforced bed. + auto packed_range = Range{sorted_items.cbegin(), + static_cast(it)}; + was_packed = true; + sel.on_arranged_fn(*it, bed, packed_range, remaining); + } + } + } + } + ++it; + } +} + +}} // namespace Slic3r::arr2 + +#endif // ARRANGEFIRSTFIT_HPP diff --git a/src/slic3r-arrange/include/arrange/ArrangeItemTraits.hpp b/src/slic3r-arrange/include/arrange/ArrangeItemTraits.hpp new file mode 100644 index 0000000..af677b4 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/ArrangeItemTraits.hpp @@ -0,0 +1,133 @@ +#ifndef ARRANGE_ITEM_TRAITS_HPP +#define ARRANGE_ITEM_TRAITS_HPP + +#include + +namespace Slic3r { namespace arr2 { + +// A logical bed representing an object not being arranged. Either the arrange +// has not yet successfully run on this ArrangePolygon or it could not fit the +// object due to overly large size or invalid geometry. +const constexpr int Unarranged = -1; + +const constexpr int PhysicalBedId = 0; + +// Basic interface of an arrange item. This struct can be specialized for any +// type that is arrangeable. +template struct ArrangeItemTraits_ { + static Vec2crd get_translation(const ArrItem &ap) + { + return ap.get_translation(); + } + + static double get_rotation(const ArrItem &ap) + { + return ap.get_rotation(); + } + + static int get_bed_index(const ArrItem &ap) { return ap.get_bed_index(); } + + static std::optional get_bed_constraint(const ArrItem &ap) + { + return ap.get_bed_constraint(); + } + + static int get_priority(const ArrItem &ap) { return ap.get_priority(); } + + // Setters: + + static void set_translation(ArrItem &ap, const Vec2crd &v) + { + ap.set_translation(v); + } + + static void set_rotation(ArrItem &ap, double v) { ap.set_rotation(v); } + + static void set_bed_index(ArrItem &ap, int v) { ap.set_bed_index(v); } + + static void set_bed_constraint(ArrItem &ap, std::optional v) + { + ap.set_bed_constraint(v); + } +}; + +template using ArrangeItemTraits = ArrangeItemTraits_>; + +// Getters: + +template Vec2crd get_translation(const T &itm) +{ + return ArrangeItemTraits::get_translation(itm); +} + +template double get_rotation(const T &itm) +{ + return ArrangeItemTraits::get_rotation(itm); +} + +template int get_bed_index(const T &itm) +{ + return ArrangeItemTraits::get_bed_index(itm); +} + +template int get_priority(const T &itm) +{ + return ArrangeItemTraits::get_priority(itm); +} + +template std::optional get_bed_constraint(const T &itm) +{ + return ArrangeItemTraits::get_bed_constraint(itm); +} + +// Setters: + +template void set_translation(T &itm, const Vec2crd &v) +{ + ArrangeItemTraits::set_translation(itm, v); +} + +template void set_rotation(T &itm, double v) +{ + ArrangeItemTraits::set_rotation(itm, v); +} + +template void set_bed_index(T &itm, int v) +{ + ArrangeItemTraits::set_bed_index(itm, v); +} + +template void set_bed_constraint(T &itm, std::optional v) +{ + ArrangeItemTraits::set_bed_constraint(itm, v); +} + +// Helper functions for arrange items +template bool is_arranged(const ArrItem &ap) +{ + return get_bed_index(ap) > Unarranged; +} + +template bool is_fixed(const ArrItem &ap) +{ + return get_bed_index(ap) >= PhysicalBedId; +} + +template bool is_on_physical_bed(const ArrItem &ap) +{ + return get_bed_index(ap) == PhysicalBedId; +} + +template void translate(ArrItem &ap, const Vec2crd &t) +{ + set_translation(ap, get_translation(ap) + t); +} + +template void rotate(ArrItem &ap, double rads) +{ + set_rotation(ap, get_rotation(ap) + rads); +} + +}} // namespace Slic3r::arr2 + +#endif // ARRANGE_ITEM_HPP diff --git a/src/slic3r-arrange/include/arrange/Beds.hpp b/src/slic3r-arrange/include/arrange/Beds.hpp new file mode 100644 index 0000000..13a20b8 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/Beds.hpp @@ -0,0 +1,230 @@ +#ifndef BEDS_HPP +#define BEDS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Polygon.hpp" +#include "libslic3r/libslic3r.h" + +namespace Slic3r { namespace arr2 { + +// Bed types to be used with arrangement. Most generic bed is a simple polygon +// with holes, but other special bed types are also valid, like a bed without +// boundaries, or a special case of a rectangular or circular bed which leaves +// a lot of room for optimizations. + +// Representing an unbounded bed. +struct InfiniteBed { + Point center; + explicit InfiniteBed(const Point &p = {0, 0}): center{p} {} +}; + +BoundingBox bounding_box(const InfiniteBed &bed); + +inline InfiniteBed offset(const InfiniteBed &bed, coord_t) { return bed; } +inline Vec2crd bed_gap(const InfiniteBed &) +{ + return Vec2crd::Zero(); +} + +struct RectangleBed { + BoundingBox bb; + Vec2crd gap; + + explicit RectangleBed(const BoundingBox &bedbb, const Vec2crd &gap) : bb{bedbb}, gap{gap} {} + explicit RectangleBed(coord_t w, coord_t h, const Vec2crd &gap = Vec2crd::Zero(), Point c = {0, 0}): + bb{{c.x() - w / 2, c.y() - h / 2}, {c.x() + w / 2, c.y() + h / 2}}, gap{gap} + {} + + coord_t width() const { return bb.size().x(); } + coord_t height() const { return bb.size().y(); } +}; + +inline BoundingBox bounding_box(const RectangleBed &bed) { return bed.bb; } +inline RectangleBed offset(RectangleBed bed, coord_t v) +{ + bed.bb.offset(v); + return bed; +} +inline Vec2crd bed_gap(const RectangleBed &bed) { + return bed.gap; +} + +Polygon to_rectangle(const BoundingBox &bb); + +inline Polygon to_rectangle(const RectangleBed &bed) +{ + return to_rectangle(bed.bb); +} + +class CircleBed { + Point m_center; + double m_radius; + Vec2crd m_gap; + +public: + CircleBed(): m_center(0, 0), m_radius(NaNd), m_gap(Vec2crd::Zero()) {} + explicit CircleBed(const Point& c, double r, const Vec2crd &g) + : m_center(c) + , m_radius(r) + , m_gap(g) + {} + + double radius() const { return m_radius; } + const Point& center() const { return m_center; } + const Vec2crd &gap() const { return m_gap; } +}; + +// Function to approximate a circle with a convex polygon +Polygon approximate_circle_with_polygon(const CircleBed &bed, int nedges = 24); + +inline BoundingBox bounding_box(const CircleBed &bed) +{ + auto r = static_cast(std::round(bed.radius())); + Point R{r, r}; + + return {bed.center() - R, bed.center() + R}; +} +inline CircleBed offset(const CircleBed &bed, coord_t v) +{ + return CircleBed{bed.center(), bed.radius() + v, bed.gap()}; +} +inline Vec2crd bed_gap(const CircleBed &bed) +{ + return bed.gap(); +} + +struct IrregularBed { ExPolygons poly; Vec2crd gap; }; +inline BoundingBox bounding_box(const IrregularBed &bed) +{ + return get_extents(bed.poly); +} + +inline IrregularBed offset(IrregularBed bed, coord_t v) +{ + bed.poly = offset_ex(bed.poly, v); + return bed; +} +inline Vec2crd bed_gap(const IrregularBed &bed) +{ + return bed.gap; +} + +using ArrangeBed = + boost::variant; + +inline BoundingBox bounding_box(const ArrangeBed &bed) +{ + BoundingBox ret; + auto visitor = [&ret](const auto &b) { ret = bounding_box(b); }; + boost::apply_visitor(visitor, bed); + + return ret; +} + +inline ArrangeBed offset(ArrangeBed bed, coord_t v) +{ + auto visitor = [v](auto &b) { b = offset(b, v); }; + boost::apply_visitor(visitor, bed); + + return bed; +} + +inline Vec2crd bed_gap(const ArrangeBed &bed) +{ + Vec2crd ret; + auto visitor = [&ret](const auto &b) { ret = bed_gap(b); }; + boost::apply_visitor(visitor, bed); + + return ret; +} + +inline double area(const BoundingBox &bb) +{ + auto bbsz = bb.size(); + return double(bbsz.x()) * bbsz.y(); +} + +inline double area(const RectangleBed &bed) +{ + auto bbsz = bed.bb.size(); + return double(bbsz.x()) * bbsz.y(); +} + +inline double area(const InfiniteBed &bed) +{ + return std::numeric_limits::infinity(); +} + +inline double area(const IrregularBed &bed) +{ + return std::accumulate(bed.poly.begin(), bed.poly.end(), 0., + [](double s, auto &p) { return s + p.area(); }); +} + +inline double area(const CircleBed &bed) +{ + return bed.radius() * bed.radius() * PI; +} + +inline double area(const ArrangeBed &bed) +{ + double ret = 0.; + auto visitor = [&ret](auto &b) { ret = area(b); }; + boost::apply_visitor(visitor, bed); + + return ret; +} + +inline ExPolygons to_expolygons(const InfiniteBed &bed) +{ + return {ExPolygon{to_rectangle(RectangleBed{scaled(1000.), scaled(1000.)})}}; +} + +inline ExPolygons to_expolygons(const RectangleBed &bed) +{ + return {ExPolygon{to_rectangle(bed)}}; +} + +inline ExPolygons to_expolygons(const CircleBed &bed) +{ + return {ExPolygon{approximate_circle_with_polygon(bed)}}; +} + +inline ExPolygons to_expolygons(const IrregularBed &bed) { return bed.poly; } + +inline ExPolygons to_expolygons(const ArrangeBed &bed) +{ + ExPolygons ret; + auto visitor = [&ret](const auto &b) { ret = to_expolygons(b); }; + boost::apply_visitor(visitor, bed); + + return ret; +} + +ArrangeBed to_arrange_bed(const Points &bedpts, const Vec2crd &gap); + +template struct IsRectangular_ : public std::false_type {}; +template<> struct IsRectangular_: public std::true_type {}; +template<> struct IsRectangular_: public std::true_type {}; + +template static constexpr bool IsRectangular = IsRectangular_::value; + +} // namespace arr2 + +inline BoundingBox &bounding_box(BoundingBox &bb) { return bb; } +inline const BoundingBox &bounding_box(const BoundingBox &bb) { return bb; } +inline BoundingBox bounding_box(const Polygon &p) { return get_extents(p); } + +} // namespace Slic3r + +#endif // BEDS_HPP diff --git a/src/slic3r-arrange/include/arrange/DataStoreTraits.hpp b/src/slic3r-arrange/include/arrange/DataStoreTraits.hpp new file mode 100644 index 0000000..4aca486 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/DataStoreTraits.hpp @@ -0,0 +1,78 @@ +#ifndef DATASTORETRAITS_HPP +#define DATASTORETRAITS_HPP + +#include + +#include "libslic3r/libslic3r.h" + +namespace Slic3r { namespace arr2 { + +// Some items can be containers of arbitrary data stored under string keys. +template struct DataStoreTraits_ +{ + static constexpr bool Implemented = false; + + template static const T *get(const ArrItem &, const std::string &key) + { + return nullptr; + } + + // Same as above just not const. + template static T *get(ArrItem &, const std::string &key) + { + return nullptr; + } + + static bool has_key(const ArrItem &itm, const std::string &key) + { + return false; + } +}; + +template struct WritableDataStoreTraits_ +{ + static constexpr bool Implemented = false; + + template static void set(ArrItem &, const std::string &key, T &&data) + { + } +}; + +template using DataStoreTraits = DataStoreTraits_>; +template constexpr bool IsDataStore = DataStoreTraits>::Implemented; +template using DataStoreOnly = std::enable_if_t, TT>; + +template +const T *get_data(const ArrItem &itm, const std::string &key) +{ + return DataStoreTraits::template get(itm, key); +} + +template +bool has_key(const ArrItem &itm, const std::string &key) +{ + return DataStoreTraits::has_key(itm, key); +} + +template +T *get_data(ArrItem &itm, const std::string &key) +{ + return DataStoreTraits::template get(itm, key); +} + +template using WritableDataStoreTraits = WritableDataStoreTraits_>; +template constexpr bool IsWritableDataStore = WritableDataStoreTraits>::Implemented; +template using WritableDataStoreOnly = std::enable_if_t, TT>; + +template +void set_data(ArrItem &itm, const std::string &key, T &&data) +{ + WritableDataStoreTraits::template set(itm, key, std::forward(data)); +} + +template constexpr bool IsReadWritableDataStore = IsDataStore && IsWritableDataStore; +template using ReadWritableDataStoreOnly = std::enable_if_t, TT>; + +}} // namespace Slic3r::arr2 + +#endif // DATASTORETRAITS_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/EdgeCache.hpp b/src/slic3r-arrange/include/arrange/NFP/EdgeCache.hpp new file mode 100644 index 0000000..24650cc --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/EdgeCache.hpp @@ -0,0 +1,80 @@ +#ifndef EDGECACHE_HPP +#define EDGECACHE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/libslic3r.h" + +namespace Slic3r { namespace arr2 { + +// Position on the circumference of an ExPolygon. +// countour_id: 0th is contour, 1..N are holes +// dist: position given as a floating point number within <0., 1.> +struct ContourLocation { size_t contour_id; double dist; }; + +void fill_distances(const Polygon &poly, std::vector &distances); + +Vec2crd coords(const Polygon &poly, const std::vector& distances, double distance); + +// A class for getting a point on the circumference of the polygon (in log time) +// +// This is a transformation of the provided polygon to be able to pinpoint +// locations on the circumference. The optimizer will pass a floating point +// value e.g. within <0,1> and we have to transform this value quickly into a +// coordinate on the circumference. By definition 0 should yield the first +// vertex and 1.0 would be the last (which should coincide with first). +// +// We also have to make this work for the holes of the captured polygon. +class EdgeCache { + struct ContourCache { + const Polygon *poly; + std::vector distances; + } m_contour; + + std::vector m_holes; + + void create_cache(const ExPolygon& sh); + + Vec2crd coords(const ContourCache& cache, double distance) const; + +public: + + explicit EdgeCache(const ExPolygon *sh) + { + create_cache(*sh); + } + + // Given coeff for accuracy <0., 1.>, return the number of vertices to skip + // when fetching corners. + static inline size_t stride(const size_t N, double accuracy) + { + size_t n = std::max(size_t{1}, N); + return static_cast( + std::round(N / std::pow(n, std::pow(accuracy, 1./3.))) + ); + } + + void sample_contour(double accuracy, std::vector &samples); + + Vec2crd coords(const ContourLocation &loc) const + { + assert(loc.contour_id <= m_holes.size()); + + return loc.contour_id > 0 ? + coords(m_holes[loc.contour_id - 1], loc.dist) : + coords(m_contour, loc.dist); + } +}; + +}} // namespace Slic3r::arr2 + +#endif // EDGECACHE_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/CompactifyKernel.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/CompactifyKernel.hpp new file mode 100644 index 0000000..c476774 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/CompactifyKernel.hpp @@ -0,0 +1,61 @@ +#ifndef COMPACTIFYKERNEL_HPP +#define COMPACTIFYKERNEL_HPP + +#include + +#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp" +#include "libslic3r/Arrange/Core/Beds.hpp" + +#include +#include + +#include "KernelUtils.hpp" + +namespace Slic3r { namespace arr2 { + +struct CompactifyKernel { + ExPolygons merged_pile; + + template + double placement_fitness(const ArrItem &itm, const Vec2crd &transl) const + { + auto pile = merged_pile; + + ExPolygons itm_tr = to_expolygons(envelope_outline(itm)); + for (auto &p : itm_tr) + p.translate(transl); + + append(pile, std::move(itm_tr)); + + pile = union_ex(pile); + + Polygon chull = Geometry::convex_hull(pile); + + return -(chull.area()); + } + + template + bool on_start_packing(ArrItem &itm, + const Bed &bed, + const Context &packing_context, + const Range & /*remaining_items*/) + { + bool ret = find_initial_position(itm, bounding_box(bed).center(), bed, + packing_context); + + merged_pile.clear(); + for (const auto &gitm : all_items_range(packing_context)) { + append(merged_pile, to_expolygons(fixed_outline(gitm))); + } + merged_pile = union_ex(merged_pile); + + return ret; + } + + template + bool on_item_packed(ArrItem &itm) { return true; } +}; + +}} // namespace Slic3r::arr2 + +#endif // COMPACTIFYKERNEL_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/GravityKernel.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/GravityKernel.hpp new file mode 100644 index 0000000..8018d43 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/GravityKernel.hpp @@ -0,0 +1,60 @@ +#ifndef GRAVITYKERNEL_HPP +#define GRAVITYKERNEL_HPP + +#include +#include + +#include "KernelUtils.hpp" + +namespace Slic3r { namespace arr2 { + +struct GravityKernel { + std::optional sink; + std::optional item_sink; + Vec2d active_sink; + + GravityKernel(Vec2crd gravity_center) : + sink{gravity_center}, active_sink{unscaled(gravity_center)} {} + + GravityKernel() = default; + + template + double placement_fitness(const ArrItem &itm, const Vec2crd &transl) const + { + Vec2d center = unscaled(envelope_centroid(itm)); + + center += unscaled(transl); + + return - (center - active_sink).squaredNorm(); + } + + template + bool on_start_packing(ArrItem &itm, + const Bed &bed, + const Ctx &packing_context, + const Range & /*remaining_items*/) + { + bool ret = false; + + item_sink = get_gravity_sink(itm); + + if (!sink) { + sink = bounding_box(bed).center(); + } + + if (item_sink) + active_sink = unscaled(*item_sink); + else + active_sink = unscaled(*sink); + + ret = find_initial_position(itm, scaled(active_sink), bed, packing_context); + + return ret; + } + + template bool on_item_packed(ArrItem &itm) { return true; } +}; + +}} // namespace Slic3r::arr2 + +#endif // GRAVITYKERNEL_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/KernelTraits.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/KernelTraits.hpp new file mode 100644 index 0000000..737ada3 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/KernelTraits.hpp @@ -0,0 +1,57 @@ +#ifndef KERNELTRAITS_HPP +#define KERNELTRAITS_HPP + +#include + +namespace Slic3r { namespace arr2 { + +// An arrangement kernel that specifies the object function to the arrangement +// optimizer and additional callback functions to be able to track the state +// of the arranged pile during arrangement. +template struct KernelTraits_ +{ + // Has to return a score value marking the quality of the arrangement. The + // higher this value is, the better a particular placement of the item is. + // parameter transl is the translation needed for the item to be moved to + // the candidate position. + // To discard the item, return NaN as score for every translation. + template + static double placement_fitness(const Kernel &k, + const ArrItem &itm, + const Vec2crd &transl) + { + return k.placement_fitness(itm, transl); + } + + // Called whenever a new item is about to be processed by the optimizer. + // The current state of the arrangement can be saved by the kernel: the + // already placed items and the remaining items that need to fit into a + // particular bed. + // Returns true if the item is can be packed immediately, false if it + // should be processed further. This way, a kernel have the power to + // choose an initial position for the item that is not on the NFP. + template + static bool on_start_packing(Kernel &k, + ArrItem &itm, + const Bed &bed, + const Ctx &packing_context, + const Range &remaining_items) + { + return k.on_start_packing(itm, bed, packing_context, remaining_items); + } + + // Called when an item has been succesfully packed. itm should have the + // final translation and rotation already set. + // Can return false to discard the item after the optimization. + template + static bool on_item_packed(Kernel &k, ArrItem &itm) + { + return k.on_item_packed(itm); + } +}; + +template using KernelTraits = KernelTraits_>; + +}} // namespace Slic3r::arr2 + +#endif // KERNELTRAITS_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/KernelUtils.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/KernelUtils.hpp new file mode 100644 index 0000000..335abe6 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/KernelUtils.hpp @@ -0,0 +1,76 @@ +#ifndef ARRANGEKERNELUTILS_HPP +#define ARRANGEKERNELUTILS_HPP + +#include + +#include +#include +#include + +namespace Slic3r { namespace arr2 { + +template +bool find_initial_position(Itm &itm, + const Vec2crd &sink, + const Bed &bed, + const Context &packing_context) +{ + bool ret = false; + + if constexpr (std::is_convertible_v || + std::is_convertible_v || + std::is_convertible_v) + { + if (all_items_range(packing_context).empty()) { + auto rotations = allowed_rotations(itm); + set_rotation(itm, 0.); + auto chull = envelope_convex_hull(itm); + + for (double rot : rotations) { + auto chullcpy = chull; + chullcpy.rotate(rot); + auto bbitm = bounding_box(chullcpy); + + Vec2crd cb = sink; + Vec2crd ci = bbitm.center(); + + Vec2crd d = cb - ci; + bbitm.translate(d); + + if (bounding_box(bed).contains(bbitm)) { + rotate(itm, rot); + translate(itm, d); + ret = true; + break; + } + } + } + } + + return ret; +} + +template std::optional get_gravity_sink(const ArrItem &itm) +{ + constexpr const char * SinkKey = "sink"; + + std::optional ret; + + auto ptr = get_data(itm, SinkKey); + + if (ptr) + ret = *ptr; + + return ret; +} + +template bool is_wipe_tower(const ArrItem &itm) +{ + constexpr const char * Key = "is_wipe_tower"; + + return has_key(itm, Key); +} + +}} // namespace Slic3r::arr2 + +#endif // ARRANGEKERNELUTILS_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/RectangleOverfitKernelWrapper.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/RectangleOverfitKernelWrapper.hpp new file mode 100644 index 0000000..e0a755e --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/RectangleOverfitKernelWrapper.hpp @@ -0,0 +1,94 @@ +#ifndef RECTANGLEOVERFITKERNELWRAPPER_HPP +#define RECTANGLEOVERFITKERNELWRAPPER_HPP + +#include "KernelTraits.hpp" + +#include +#include + +namespace Slic3r { namespace arr2 { + +// This is a kernel wrapper that will apply a penality to the object function +// if the result cannot fit into the given rectangular bounds. This can be used +// to arrange into rectangular boundaries without calculating the IFP of the +// rectangle bed. Note that after the arrangement, what is garanteed is that +// the resulting pile will fit into the rectangular boundaries, but it will not +// be within the given rectangle. The items need to be moved afterwards manually. +// Use RectangeOverfitPackingStrategy to automate this post process step. +template +struct RectangleOverfitKernelWrapper { + Kernel &k; + BoundingBox binbb; + BoundingBox pilebb; + + RectangleOverfitKernelWrapper(Kernel &kern, const BoundingBox &limits) + : k{kern} + , binbb{limits} + {} + + double overfit(const BoundingBox &itmbb) const + { + auto fullbb = pilebb; + fullbb.merge(itmbb); + auto fullbbsz = fullbb.size(); + auto binbbsz = binbb.size(); + + auto wdiff = fullbbsz.x() - binbbsz.x() - SCALED_EPSILON; + auto hdiff = fullbbsz.y() - binbbsz.y() - SCALED_EPSILON; + double miss = .0; + if (wdiff > 0) + miss += double(wdiff); + if (hdiff > 0) + miss += double(hdiff); + + miss = miss > 0? miss : 0; + + return miss; + } + + template + double placement_fitness(const ArrItem &item, const Vec2crd &transl) const + { + double score = KernelTraits::placement_fitness(k, item, transl); + + auto itmbb = envelope_bounding_box(item); + itmbb.translate(transl); + double miss = overfit(itmbb); + score -= miss * miss; + + return score; + } + + template + bool on_start_packing(ArrItem &itm, + const Bed &bed, + const Ctx &packing_context, + const Range &remaining_items) + { + pilebb = BoundingBox{}; + + for (auto &fitm : all_items_range(packing_context)) + pilebb.merge(fixed_bounding_box(fitm)); + + return KernelTraits::on_start_packing(k, itm, RectangleBed{binbb, Vec2crd::Zero()}, + packing_context, + remaining_items); + } + + template + bool on_item_packed(ArrItem &itm) + { + bool ret = KernelTraits::on_item_packed(k, itm); + + double miss = overfit(envelope_bounding_box(itm)); + + if (miss > 0.) + ret = false; + + return ret; + } +}; + +}} // namespace Slic3r::arr2 + +#endif // RECTANGLEOVERFITKERNELWRAPPER_H diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/SVGDebugOutputKernelWrapper.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/SVGDebugOutputKernelWrapper.hpp new file mode 100644 index 0000000..6404fca --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/SVGDebugOutputKernelWrapper.hpp @@ -0,0 +1,96 @@ +#ifndef SVGDEBUGOUTPUTKERNELWRAPPER_HPP +#define SVGDEBUGOUTPUTKERNELWRAPPER_HPP + +#include + +#include "KernelTraits.hpp" + +#include "arrange/PackingContext.hpp" +#include "arrange/NFP/NFPArrangeItemTraits.hpp" +#include "arrange/Beds.hpp" + +#include + +namespace Slic3r { namespace arr2 { + +template +struct SVGDebugOutputKernelWrapper { + Kernel &k; + std::unique_ptr svg; + BoundingBox drawbounds; + + template + SVGDebugOutputKernelWrapper(const BoundingBox &bounds, Kernel &kern) + : k{kern}, drawbounds{bounds} + {} + + template + bool on_start_packing(ArrItem &itm, + const Bed &bed, + const Context &packing_context, + const Range &rem) + { + using namespace Slic3r; + + bool ret = KernelTraits::on_start_packing(k, itm, bed, + packing_context, + rem); + + if (arr2::get_bed_index(itm) < 0) + return ret; + + svg.reset(); + auto bounds = drawbounds; + auto fixed = all_items_range(packing_context); + svg = std::make_unique(std::string("arrange_bed") + + std::to_string( + arr2::get_bed_index(itm)) + + "_" + std::to_string(fixed.size()) + + ".svg", + bounds, 0, false); + + svg->draw(ExPolygon{arr2::to_rectangle(drawbounds)}, "blue", .2f); + + auto nfp = calculate_nfp(itm, packing_context, bed); + svg->draw_outline(nfp); + svg->draw(nfp, "green", 0.2f); + + for (const auto &fixeditm : fixed) { + ExPolygons fixeditm_outline = to_expolygons(fixed_outline(fixeditm)); + svg->draw_outline(fixeditm_outline); + svg->draw(fixeditm_outline, "yellow", 0.5f); + } + + return ret; + } + + template + double placement_fitness(const ArrItem &item, const Vec2crd &transl) const + { + return KernelTraits::placement_fitness(k, item, transl); + } + + template + bool on_item_packed(ArrItem &itm) + { + using namespace Slic3r; + using namespace Slic3r::arr2; + + bool ret = KernelTraits::on_item_packed(k, itm); + + if (svg) { + ExPolygons itm_outline = to_expolygons(fixed_outline(itm)); + + svg->draw_outline(itm_outline); + svg->draw(itm_outline, "grey"); + + svg->Close(); + } + + return ret; + } +}; + +}} // namespace Slic3r::arr2 + +#endif // SVGDEBUGOUTPUTKERNELWRAPPER_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/Kernels/TMArrangeKernel.hpp b/src/slic3r-arrange/include/arrange/NFP/Kernels/TMArrangeKernel.hpp new file mode 100644 index 0000000..487405f --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/Kernels/TMArrangeKernel.hpp @@ -0,0 +1,244 @@ +#ifndef TMARRANGEKERNEL_HPP +#define TMARRANGEKERNEL_HPP + +#include +#include +#include + +#include +#include + +namespace Slic3r { namespace arr2 { + +// Summon the spatial indexing facilities from boost +namespace bgi = boost::geometry::index; +using SpatElement = std::pair; +using SpatIndex = bgi::rtree >; + +class TMArrangeKernel { + SpatIndex m_rtree; // spatial index for the normal (bigger) objects + SpatIndex m_smallsrtree; // spatial index for only the smaller items + BoundingBox m_pilebb; + double m_bin_area = NaNd; + double m_norm; + size_t m_rem_cnt = 0; + size_t m_item_cnt = 0; + + + struct ItemStats { double area = 0.; BoundingBox bb; }; + std::vector m_itemstats; + + // A coefficient used in separating bigger items and smaller items. + static constexpr double BigItemTreshold = 0.02; + + template ArithmeticOnly norm(T val) const + { + return double(val) / m_norm; + } + + // Treat big items (compared to the print bed) differently + bool is_big(double a) const { return a / m_bin_area > BigItemTreshold; } + +protected: + std::optional sink; + std::optional item_sink; + Point active_sink; + + const BoundingBox & pilebb() const { return m_pilebb; } + +public: + TMArrangeKernel() = default; + TMArrangeKernel(Vec2crd gravity_center, size_t itm_cnt, double bedarea = NaNd) + : m_bin_area(bedarea) + , m_item_cnt{itm_cnt} + , sink{gravity_center} + {} + + TMArrangeKernel(size_t itm_cnt, double bedarea = NaNd) + : m_bin_area(bedarea), m_item_cnt{itm_cnt} + {} + + template + double placement_fitness(const ArrItem &item, const Vec2crd &transl) const + { + // Candidate item bounding box + auto ibb = envelope_bounding_box(item); + ibb.translate(transl); + auto itmcntr = envelope_centroid(item); + itmcntr += transl; + + // Calculate the full bounding box of the pile with the candidate item + auto fullbb = m_pilebb; + fullbb.merge(ibb); + + // The bounding box of the big items (they will accumulate in the center + // of the pile + BoundingBox bigbb; + if(m_rtree.empty()) { + bigbb = fullbb; + } + else { + auto boostbb = m_rtree.bounds(); + boost::geometry::convert(boostbb, bigbb); + } + + // Will hold the resulting score + double score = 0; + + // Distinction of cases for the arrangement scene + enum e_cases { + // This branch is for big items in a mixed (big and small) scene + // OR for all items in a small-only scene. + BIG_ITEM, + + // For small items in a mixed scene. + SMALL_ITEM, + + WIPE_TOWER, + } compute_case; + + bool is_wt = is_wipe_tower(item); + bool bigitems = is_big(envelope_area(item)) || m_rtree.empty(); + if (is_wt) + compute_case = WIPE_TOWER; + else if (bigitems) + compute_case = BIG_ITEM; + else + compute_case = SMALL_ITEM; + + switch (compute_case) { + case WIPE_TOWER: { + score = (unscaled(itmcntr) - unscaled(active_sink)).squaredNorm(); + break; + } + case BIG_ITEM: { + const Point& minc = ibb.min; // bottom left corner + const Point& maxc = ibb.max; // top right corner + + // top left and bottom right corners + Point top_left{minc.x(), maxc.y()}; + Point bottom_right{maxc.x(), minc.y()}; + + // The smallest distance from the arranged pile center: + double dist = norm((itmcntr - m_pilebb.center()).template cast().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 alignment + // 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 = 1.; + + auto query = bgi::intersects(ibb); + auto& index = is_big(envelope_area(item)) ? m_rtree : m_smallsrtree; + + // Query the spatial index for the neighbors + std::vector result; + result.reserve(index.size()); + + index.query(query, std::back_inserter(result)); + + // now get the score for the best alignment + for(auto& e : result) { + auto idx = e.second; + const ItemStats& p = m_itemstats[idx]; + auto parea = p.area; + if(std::abs(1.0 - parea / fixed_area(item)) < 1e-6) { + auto bb = p.bb; + bb.merge(ibb); + auto bbarea = area(bb); + auto ascore = 1.0 - (area(fixed_bounding_box(item)) + area(p.bb)) / bbarea; + + if(ascore < alignment_score) + alignment_score = ascore; + } + } + + double R = double(m_rem_cnt) / (m_item_cnt); + R = std::pow(R, 1./3.); + + // 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 neighbors + + // Let the density matter more when fewer objects remain + score = 0.6 * dist + 0.1 * alignment_score + (1.0 - R) * (0.3 * dist) + R * 0.3 * alignment_score; + + break; + } + case SMALL_ITEM: { + // 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 = norm((itmcntr - bigbb.center()).template cast().norm()); + break; + } + } + + return -score; + } + + template + bool on_start_packing(ArrItem &itm, + const Bed &bed, + const Context &packing_context, + const Range &remaining_items) + { + item_sink = get_gravity_sink(itm); + + if (!sink) { + sink = bounding_box(bed).center(); + } + + if (item_sink) + active_sink = *item_sink; + else + active_sink = *sink; + + auto fixed = all_items_range(packing_context); + + bool ret = find_initial_position(itm, active_sink, bed, packing_context); + + m_rem_cnt = remaining_items.size(); + + if (m_item_cnt == 0) + m_item_cnt = m_rem_cnt + fixed.size() + 1; + + if (std::isnan(m_bin_area)) { + auto sz = bounding_box(bed).size(); + + m_bin_area = scaled(unscaled(sz.x()) * unscaled(sz.y())); + } + + m_norm = std::sqrt(m_bin_area); + + m_itemstats.clear(); + m_itemstats.reserve(fixed.size()); + m_rtree.clear(); + m_smallsrtree.clear(); + m_pilebb = {active_sink, active_sink}; + unsigned idx = 0; + for (auto &fixitem : fixed) { + auto fixitmbb = fixed_bounding_box(fixitem); + m_itemstats.emplace_back(ItemStats{fixed_area(fixitem), fixitmbb}); + m_pilebb.merge(fixitmbb); + + if(is_big(fixed_area(fixitem))) + m_rtree.insert({fixitmbb, idx}); + + m_smallsrtree.insert({fixitmbb, idx}); + idx++; + } + + return ret; + } + + template + bool on_item_packed(ArrItem &itm) { return true; } +}; + +}} // namespace Slic3r::arr2 + +#endif // TMARRANGEKERNEL_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/NFP.hpp b/src/slic3r-arrange/include/arrange/NFP/NFP.hpp new file mode 100644 index 0000000..9ff5df2 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/NFP.hpp @@ -0,0 +1,57 @@ +#ifndef NFP_HPP +#define NFP_HPP + +#include +#include +#include + +#include +#include +#include + +#include + +namespace Slic3r { + +template +Unit dotperp(const Vec<2, T> &a, const Vec<2, T> &b) +{ + return Unit(a.x()) * Unit(b.y()) - Unit(a.y()) * Unit(b.x()); +} + +// Convex-Convex nfp in linear time (fixed.size() + movable.size()), +// no memory allocations (if out param is used). +// FIXME: Currently broken for very sharp triangles. +Polygon nfp_convex_convex(const Polygon &fixed, const Polygon &movable); +void nfp_convex_convex(const Polygon &fixed, const Polygon &movable, Polygon &out); +Polygon nfp_convex_convex_legacy(const Polygon &fixed, const Polygon &movable); + +Polygon ifp_convex_convex(const Polygon &fixed, const Polygon &movable); + +ExPolygons ifp_convex(const arr2::RectangleBed &bed, const Polygon &convexpoly); +ExPolygons ifp_convex(const arr2::CircleBed &bed, const Polygon &convexpoly); +ExPolygons ifp_convex(const arr2::IrregularBed &bed, const Polygon &convexpoly); +inline ExPolygons ifp_convex(const arr2::InfiniteBed &bed, const Polygon &convexpoly) +{ + return {}; +} + +inline ExPolygons ifp_convex(const arr2::ArrangeBed &bed, const Polygon &convexpoly) +{ + ExPolygons ret; + auto visitor = [&ret, &convexpoly](const auto &b) { ret = ifp_convex(b, convexpoly); }; + boost::apply_visitor(visitor, bed); + + return ret; +} + +Vec2crd reference_vertex(const Polygon &outline); +Vec2crd reference_vertex(const ExPolygon &outline); +Vec2crd reference_vertex(const Polygons &outline); +Vec2crd reference_vertex(const ExPolygons &outline); + +Vec2crd min_vertex(const Polygon &outline); + +} // namespace Slic3r + +#endif // NFP_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/NFPArrangeItemTraits.hpp b/src/slic3r-arrange/include/arrange/NFP/NFPArrangeItemTraits.hpp new file mode 100644 index 0000000..549525b --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/NFPArrangeItemTraits.hpp @@ -0,0 +1,197 @@ +#ifndef NFPARRANGEITEMTRAITS_HPP +#define NFPARRANGEITEMTRAITS_HPP + +#include + +#include +#include + +#include + + +namespace Slic3r { namespace arr2 { + +// Additional methods that an ArrangeItem object has to implement in order +// to be usable with PackStrategyNFP. +template struct NFPArrangeItemTraits_ +{ + template + static ExPolygons calculate_nfp(const ArrItem &item, + const Context &packing_context, + const Bed &bed, + StopCond stop_condition = {}) + { + static_assert(always_false::value, + "NFP unimplemented for this item type."); + return {}; + } + + static Vec2crd reference_vertex(const ArrItem &item) + { + return item.reference_vertex(); + } + + static BoundingBox envelope_bounding_box(const ArrItem &itm) + { + return itm.envelope_bounding_box(); + } + + static BoundingBox fixed_bounding_box(const ArrItem &itm) + { + return itm.fixed_bounding_box(); + } + + static const Polygons & envelope_outline(const ArrItem &itm) + { + return itm.envelope_outline(); + } + + static const Polygons & fixed_outline(const ArrItem &itm) + { + return itm.fixed_outline(); + } + + static const Polygon & envelope_convex_hull(const ArrItem &itm) + { + return itm.envelope_convex_hull(); + } + + static const Polygon & fixed_convex_hull(const ArrItem &itm) + { + return itm.fixed_convex_hull(); + } + + static double envelope_area(const ArrItem &itm) + { + return itm.envelope_area(); + } + + static double fixed_area(const ArrItem &itm) + { + return itm.fixed_area(); + } + + static auto allowed_rotations(const ArrItem &) + { + return std::array{0.}; + } + + static Vec2crd fixed_centroid(const ArrItem &itm) + { + return fixed_bounding_box(itm).center(); + } + + static Vec2crd envelope_centroid(const ArrItem &itm) + { + return envelope_bounding_box(itm).center(); + } +}; + +template +using NFPArrangeItemTraits = NFPArrangeItemTraits_>; + +template +ExPolygons calculate_nfp(const ArrItem &itm, + const Context &context, + const Bed &bed, + StopCond stopcond = {}) +{ + return NFPArrangeItemTraits::calculate_nfp(itm, context, bed, + std::move(stopcond)); +} + +template Vec2crd reference_vertex(const ArrItem &itm) +{ + return NFPArrangeItemTraits::reference_vertex(itm); +} + +template BoundingBox envelope_bounding_box(const ArrItem &itm) +{ + return NFPArrangeItemTraits::envelope_bounding_box(itm); +} + +template BoundingBox fixed_bounding_box(const ArrItem &itm) +{ + return NFPArrangeItemTraits::fixed_bounding_box(itm); +} + +template decltype(auto) envelope_convex_hull(const ArrItem &itm) +{ + return NFPArrangeItemTraits::envelope_convex_hull(itm); +} + +template decltype(auto) fixed_convex_hull(const ArrItem &itm) +{ + return NFPArrangeItemTraits::fixed_convex_hull(itm); +} + +template decltype(auto) envelope_outline(const ArrItem &itm) +{ + return NFPArrangeItemTraits::envelope_outline(itm); +} + +template decltype(auto) fixed_outline(const ArrItem &itm) +{ + return NFPArrangeItemTraits::fixed_outline(itm); +} + +template double envelope_area(const ArrItem &itm) +{ + return NFPArrangeItemTraits::envelope_area(itm); +} + +template double fixed_area(const ArrItem &itm) +{ + return NFPArrangeItemTraits::fixed_area(itm); +} + +template Vec2crd fixed_centroid(const ArrItem &itm) +{ + return NFPArrangeItemTraits::fixed_centroid(itm); +} + +template Vec2crd envelope_centroid(const ArrItem &itm) +{ + return NFPArrangeItemTraits::envelope_centroid(itm); +} + +template +auto allowed_rotations(const ArrItem &itm) +{ + return NFPArrangeItemTraits::allowed_rotations(itm); +} + +template +BoundingBox bounding_box(const Range &itms) noexcept +{ + auto pilebb = + std::accumulate(itms.begin(), itms.end(), BoundingBox{}, + [](BoundingBox bb, const auto &itm) { + bb.merge(fixed_bounding_box(itm)); + return bb; + }); + + return pilebb; +} + +template +BoundingBox bounding_box_on_bedidx(const Range &itms, int bed_index) noexcept +{ + auto pilebb = + std::accumulate(itms.begin(), itms.end(), BoundingBox{}, + [bed_index](BoundingBox bb, const auto &itm) { + if (bed_index == get_bed_index(itm)) + bb.merge(fixed_bounding_box(itm)); + + return bb; + }); + + return pilebb; +} + +}} // namespace Slic3r::arr2 + +#endif // ARRANGEITEMTRAITSNFP_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/NFPConcave_Tesselate.hpp b/src/slic3r-arrange/include/arrange/NFP/NFPConcave_Tesselate.hpp new file mode 100644 index 0000000..625a8bb --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/NFPConcave_Tesselate.hpp @@ -0,0 +1,17 @@ +#ifndef NFPCONCAVE_TESSELATE_HPP +#define NFPCONCAVE_TESSELATE_HPP + +#include + +#include "libslic3r/Polygon.hpp" + +namespace Slic3r { + +Polygons convex_decomposition_tess(const Polygon &expoly); +Polygons convex_decomposition_tess(const ExPolygon &expoly); +Polygons convex_decomposition_tess(const ExPolygons &expolys); +ExPolygons nfp_concave_concave_tess(const ExPolygon &fixed, const ExPolygon &movable); + +} // namespace Slic3r + +#endif // NFPCONCAVE_TESSELATE_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/PackStrategyNFP.hpp b/src/slic3r-arrange/include/arrange/NFP/PackStrategyNFP.hpp new file mode 100644 index 0000000..97d4f80 --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/PackStrategyNFP.hpp @@ -0,0 +1,284 @@ +#ifndef PACKSTRATEGYNFP_HPP +#define PACKSTRATEGYNFP_HPP + +#include + +#include +#include +#include + +#include "libslic3r/Optimize/NLoptOptimizer.hpp" +#include "libslic3r/Execution/ExecutionSeq.hpp" + +namespace Slic3r { namespace arr2 { + +struct NFPPackingTag{}; + +struct DummyArrangeKernel +{ + template + double placement_fitness(const ArrItem &itm, const Vec2crd &dest_pos) const + { + return NaNd; + } + + template + bool on_start_packing(ArrItem &itm, + const Bed &bed, + const Context &packing_context, + const Range &remaining_items) + { + return true; + } + + template bool on_item_packed(ArrItem &itm) { return true; } +}; + +template using OptAlg = typename Strategy::OptAlg; + +template +struct PackStrategyNFP { + using OptAlg = OptMethod; + + ArrangeKernel kernel; + ExecPolicy ep; + double accuracy = 1.; + opt::Optimizer solver; + StopCond stop_condition; + + PackStrategyNFP(opt::Optimizer slv, + ArrangeKernel k = {}, + ExecPolicy execpolicy = {}, + double accur = 1., + StopCond stop_cond = {}) + : kernel{std::move(k)}, + ep{std::move(execpolicy)}, + accuracy{accur}, + solver{std::move(slv)}, + stop_condition{std::move(stop_cond)} + {} + + PackStrategyNFP(ArrangeKernel k = {}, + ExecPolicy execpolicy = {}, + double accur = 1., + StopCond stop_cond = {}) + : PackStrategyNFP{opt::Optimizer{}, std::move(k), + std::move(execpolicy), accur, std::move(stop_cond)} + { + // Defaults for AlgNLoptSubplex + auto iters = static_cast(std::floor(1000 * accuracy)); + auto optparams = + opt::StopCriteria{}.max_iterations(iters).rel_score_diff( + 1e-20) /*.abs_score_diff(1e-20)*/; + + solver.set_criteria(optparams); + } +}; + +template +struct PackStrategyTag_> +{ + using Tag = NFPPackingTag; +}; + + +template +double pick_best_spot_on_nfp_verts_only(ArrItem &item, + const ExPolygons &nfp, + const Bed &bed, + const PStrategy &strategy) +{ + using KernelT = KernelTraits; + + auto score = -std::numeric_limits::infinity(); + Vec2crd orig_tr = get_translation(item); + Vec2crd translation{0, 0}; + + auto eval_fitness = [&score, &strategy, &item, &translation, + &orig_tr](const Vec2crd &p) { + set_translation(item, orig_tr); + Vec2crd ref_v = reference_vertex(item); + Vec2crd tr = p - ref_v; + double fitness = KernelT::placement_fitness(strategy.kernel, item, tr); + if (fitness > score) { + score = fitness; + translation = tr; + } + }; + + for (const ExPolygon &expoly : nfp) { + for (const Point &p : expoly.contour) { + eval_fitness(p); + } + + for (const Polygon &h : expoly.holes) + for (const Point &p : h.points) + eval_fitness(p); + } + + set_translation(item, orig_tr + translation); + + return score; +} + +struct CornerResult +{ + size_t contour_id; + opt::Result<1> oresult; +}; + +template +double pick_best_spot_on_nfp(ArrItem &item, + const ExPolygons &nfp, + const Bed &bed, + const PackStrategyNFP &strategy) +{ + auto &ex_policy = strategy.ep; + using KernelT = KernelTraits; + + auto score = -std::numeric_limits::infinity(); + Vec2crd orig_tr = get_translation(item); + Vec2crd translation{0, 0}; + Vec2crd ref_v = reference_vertex(item); + + auto edge_caches = reserve_vector(nfp.size()); + auto sample_sets = reserve_vector>( + nfp.size()); + + for (const ExPolygon &expoly : nfp) { + edge_caches.emplace_back(EdgeCache{&expoly}); + edge_caches.back().sample_contour(strategy.accuracy, + sample_sets.emplace_back()); + } + + auto nthreads = execution::max_concurrency(ex_policy); + + std::vector gresults(edge_caches.size()); + + auto resultcmp = [](auto &a, auto &b) { + return a.oresult.score < b.oresult.score; + }; + + execution::for_each( + ex_policy, size_t(0), edge_caches.size(), + [&](size_t edge_cache_idx) { + auto &ec_contour = edge_caches[edge_cache_idx]; + auto &corners = sample_sets[edge_cache_idx]; + std::vector results(corners.size()); + + auto cornerfn = [&](size_t i) { + ContourLocation cr = corners[i]; + auto objfn = [&](opt::Input<1> &in) { + Vec2crd p = ec_contour.coords(ContourLocation{cr.contour_id, in[0]}); + Vec2crd tr = p - ref_v; + + return KernelT::placement_fitness(strategy.kernel, item, tr); + }; + + // Assuming that solver is a lightweight object + auto solver = strategy.solver; + solver.to_max(); + auto oresult = solver.optimize(objfn, + opt::initvals({cr.dist}), + opt::bounds({{0., 1.}})); + + results[i] = CornerResult{cr.contour_id, oresult}; + }; + + execution::for_each(ex_policy, size_t(0), results.size(), + cornerfn, nthreads); + + auto it = std::max_element(results.begin(), results.end(), + resultcmp); + + if (it != results.end()) + gresults[edge_cache_idx] = *it; + }, + nthreads); + + auto it = std::max_element(gresults.begin(), gresults.end(), resultcmp); + if (it != gresults.end()) { + score = it->oresult.score; + size_t path_id = std::distance(gresults.begin(), it); + size_t contour_id = it->contour_id; + double dist = it->oresult.optimum[0]; + + Vec2crd pos = edge_caches[path_id].coords(ContourLocation{contour_id, dist}); + Vec2crd tr = pos - ref_v; + + set_translation(item, orig_tr + tr); + } + + return score; +} + +template +bool pack(Strategy &strategy, + const Bed &bed, + ArrItem &item, + const PackStrategyContext &packing_context, + const Range &remaining_items, + const NFPPackingTag &) +{ + using KernelT = KernelTraits; + + // The kernel might pack the item immediately + bool packed = KernelT::on_start_packing(strategy.kernel, item, bed, + packing_context, remaining_items); + + double orig_rot = get_rotation(item); + double final_rot = 0.; + double final_score = -std::numeric_limits::infinity(); + Vec2crd orig_tr = get_translation(item); + Vec2crd final_tr = orig_tr; + + bool cancelled = strategy.stop_condition(); + const auto & rotations = allowed_rotations(item); + + // Check all rotations but only if item is not already packed + for (auto rot_it = rotations.begin(); + !cancelled && !packed && rot_it != rotations.end(); ++rot_it) { + + double rot = *rot_it; + + set_rotation(item, orig_rot + rot); + set_translation(item, orig_tr); + + auto nfp = calculate_nfp(item, packing_context, bed, + strategy.stop_condition); + double score = NaNd; + if (!nfp.empty()) { + score = pick_best_spot_on_nfp(item, nfp, bed, strategy); + + cancelled = strategy.stop_condition(); + if (score > final_score) { + final_score = score; + final_rot = rot; + final_tr = get_translation(item); + } + } + } + + // If the score is not valid, and the item is not already packed, or + // the packing was cancelled asynchronously by stop condition, then + // discard the packing + bool is_score_valid = !std::isnan(final_score) && !std::isinf(final_score); + packed = !cancelled && (packed || is_score_valid); + + if (packed) { + set_translation(item, final_tr); + set_rotation(item, orig_rot + final_rot); + + // Finally, consult the kernel if the packing is sane + packed = KernelT::on_item_packed(strategy.kernel, item); + } + + return packed; +} + +}} // namespace Slic3r::arr2 + +#endif // PACKSTRATEGYNFP_HPP diff --git a/src/slic3r-arrange/include/arrange/NFP/RectangleOverfitPackingStrategy.hpp b/src/slic3r-arrange/include/arrange/NFP/RectangleOverfitPackingStrategy.hpp new file mode 100644 index 0000000..63a73cf --- /dev/null +++ b/src/slic3r-arrange/include/arrange/NFP/RectangleOverfitPackingStrategy.hpp @@ -0,0 +1,141 @@ +#ifndef RECTANGLEOVERFITPACKINGSTRATEGY_HPP +#define RECTANGLEOVERFITPACKINGSTRATEGY_HPP + +#include + +#include "Kernels/RectangleOverfitKernelWrapper.hpp" +#include "PackStrategyNFP.hpp" + +namespace Slic3r { namespace arr2 { + +using PostAlignmentFn = std::function; + +struct CenterAlignmentFn { + Vec2crd operator() (const BoundingBox &bedbb, + const BoundingBox &pilebb) + { + return bedbb.center() - pilebb.center(); + } +}; + +template +struct RectangleOverfitPackingContext : public DefaultPackingContext +{ + BoundingBox limits; + int bed_index; + PostAlignmentFn post_alignment_fn; + + explicit RectangleOverfitPackingContext(const BoundingBox limits, + int bedidx, + PostAlignmentFn alignfn = CenterAlignmentFn{}) + : limits{limits}, bed_index{bedidx}, post_alignment_fn{alignfn} + {} + + void align_pile() + { + // Here, the post alignment can be safely done. No throwing + // functions are called! + if (fixed_items_range(*this).empty()) { + auto itms = packed_items_range(*this); + auto pilebb = bounding_box(itms); + + for (auto &itm : itms) { + translate(itm, post_alignment_fn(limits, pilebb)); + } + } + } + + ~RectangleOverfitPackingContext() { align_pile(); } +}; + +// With rectange bed, and no fixed items, an infinite bed with +// RectangleOverfitKernelWrapper can produce better results than a pure +// RectangleBed with inner-fit polygon calculation. +template +struct RectangleOverfitPackingStrategy { + PackStrategyNFP base_strategy; + + PostAlignmentFn post_alignment_fn = CenterAlignmentFn{}; + + template + using Context = RectangleOverfitPackingContext; + + RectangleOverfitPackingStrategy(PackStrategyNFP s, + PostAlignmentFn post_align_fn) + : base_strategy{std::move(s)}, post_alignment_fn{post_align_fn} + {} + + RectangleOverfitPackingStrategy(PackStrategyNFP s) + : base_strategy{std::move(s)} + {} +}; + +struct RectangleOverfitPackingStrategyTag {}; + +template +struct PackStrategyTag_> { + using Tag = RectangleOverfitPackingStrategyTag; +}; + +template +struct PackStrategyTraits_> { + template + using Context = typename RectangleOverfitPackingStrategy< + Args...>::template Context>; + + template + static Context create_context( + RectangleOverfitPackingStrategy &ps, + const Bed &bed, + int bed_index) + { + return Context{bounding_box(bed), bed_index, + ps.post_alignment_fn}; + } +}; + +template +struct PackingContextTraits_> + : public PackingContextTraits_> +{ + static void add_packed_item(RectangleOverfitPackingContext &ctx, ArrItem &itm) + { + ctx.add_packed_item(itm); + + // to prevent coords going out of range + ctx.align_pile(); + } +}; + +template +bool pack(Strategy &strategy, + const Bed &bed, + ArrItem &item, + const PackStrategyContext &packing_context, + const Range &remaining_items, + const RectangleOverfitPackingStrategyTag &) +{ + bool ret = false; + + if (fixed_items_range(packing_context).empty()) { + auto &base = strategy.base_strategy; + PackStrategyNFP modded_strategy{ + base.solver, + RectangleOverfitKernelWrapper{base.kernel, packing_context.limits}, + base.ep, base.accuracy}; + + ret = pack(modded_strategy, + InfiniteBed{packing_context.limits.center()}, item, + packing_context, remaining_items, NFPPackingTag{}); + } else { + ret = pack(strategy.base_strategy, bed, item, packing_context, + remaining_items, NFPPackingTag{}); + } + + return ret; +} + +}} // namespace Slic3r::arr2 + +#endif // RECTANGLEOVERFITPACKINGSTRATEGY_HPP diff --git a/src/slic3r-arrange/include/arrange/PackingContext.hpp b/src/slic3r-arrange/include/arrange/PackingContext.hpp new file mode 100644 index 0000000..77aa87e --- /dev/null +++ b/src/slic3r-arrange/include/arrange/PackingContext.hpp @@ -0,0 +1,124 @@ +#ifndef PACKINGCONTEXT_HPP +#define PACKINGCONTEXT_HPP + +#include "ArrangeItemTraits.hpp" + +namespace Slic3r { namespace arr2 { + +template +struct PackingContextTraits_ { + template + static void add_fixed_item(Ctx &ctx, const ArrItem &itm) + { + ctx.add_fixed_item(itm); + } + + template + static void add_packed_item(Ctx &ctx, ArrItem &itm) + { + ctx.add_packed_item(itm); + } + + // returns a range of all packed items in the context ctx + static auto all_items_range(const Ctx &ctx) + { + return ctx.all_items_range(); + } + + static auto fixed_items_range(const Ctx &ctx) + { + return ctx.fixed_items_range(); + } + + static auto packed_items_range(const Ctx &ctx) + { + return ctx.packed_items_range(); + } + + static auto packed_items_range(Ctx &ctx) + { + return ctx.packed_items_range(); + } +}; + +template +void add_fixed_item(Ctx &ctx, const ArrItem &itm) +{ + PackingContextTraits_>::add_fixed_item(ctx, itm); +} + +template +void add_packed_item(Ctx &ctx, ArrItem &itm) +{ + PackingContextTraits_>::add_packed_item(ctx, itm); +} + +template +auto all_items_range(const Ctx &ctx) +{ + return PackingContextTraits_>::all_items_range(ctx); +} + +template +auto fixed_items_range(const Ctx &ctx) +{ + return PackingContextTraits_>::fixed_items_range(ctx); +} + +template +auto packed_items_range(Ctx &&ctx) +{ + return PackingContextTraits_>::packed_items_range(ctx); +} + +template +class DefaultPackingContext { + using ArrItemRaw = StripCVRef; + std::vector> m_fixed; + std::vector> m_packed; + std::vector> m_items; + +public: + DefaultPackingContext() = default; + + template + explicit DefaultPackingContext(const Range &fixed_items) + { + std::copy(fixed_items.begin(), fixed_items.end(), std::back_inserter(m_fixed)); + std::copy(fixed_items.begin(), fixed_items.end(), std::back_inserter(m_items)); + } + + auto all_items_range() const noexcept { return crange(m_items); } + auto fixed_items_range() const noexcept { return crange(m_fixed); } + auto packed_items_range() const noexcept { return crange(m_packed); } + auto packed_items_range() noexcept { return range(m_packed); } + + void add_fixed_item(const ArrItem &itm) + { + m_fixed.emplace_back(itm); + m_items.emplace_back(itm); + } + + void add_packed_item(ArrItem &itm) + { + m_packed.emplace_back(itm); + m_items.emplace_back(itm); + } +}; + +template +auto default_context(const Range &items) +{ + using ArrItem = StripCVRef::value_type>; + return DefaultPackingContext{items}; +} + +template +auto default_context(const Cont &container) +{ + return DefaultPackingContext{crange(container)}; +} + +}} // namespace Slic3r::arr2 + +#endif // PACKINGCONTEXT_HPP diff --git a/src/slic3r-arrange/src/Beds.cpp b/src/slic3r-arrange/src/Beds.cpp new file mode 100644 index 0000000..1cebd96 --- /dev/null +++ b/src/slic3r-arrange/src/Beds.cpp @@ -0,0 +1,135 @@ +#include + +#include + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Point.hpp" + +namespace Slic3r { namespace arr2 { + +BoundingBox bounding_box(const InfiniteBed &bed) +{ + BoundingBox ret; + using C = coord_t; + + // It is important for Mx and My to be strictly less than half of the + // range of type C. width(), height() and area() will not overflow this way. + C Mx = C((std::numeric_limits::lowest() + 2 * bed.center.x()) / 4.01); + C My = C((std::numeric_limits::lowest() + 2 * bed.center.y()) / 4.01); + + ret.max = bed.center - Point{Mx, My}; + ret.min = bed.center + Point{Mx, My}; + + return ret; +} + +Polygon to_rectangle(const BoundingBox &bb) +{ + Polygon ret; + ret.points = { + bb.min, + Point{bb.max.x(), bb.min.y()}, + bb.max, + Point{bb.min.x(), bb.max.y()} + }; + + return ret; +} + +Polygon approximate_circle_with_polygon(const arr2::CircleBed &bed, int nedges) +{ + Polygon ret; + + double angle_incr = (2 * M_PI) / nedges; // Angle increment for each edge + double angle = 0; // Starting angle + + // Loop to generate vertices for each edge + for (int i = 0; i < nedges; i++) { + // Calculate coordinates of the vertices using trigonometry + auto x = bed.center().x() + static_cast(bed.radius() * std::cos(angle)); + auto y = bed.center().y() + static_cast(bed.radius() * std::sin(angle)); + + // Add vertex to the vector + ret.points.emplace_back(x, y); + + // Update the angle for the next iteration + angle += angle_incr; + } + + return ret; +} + +inline coord_t width(const BoundingBox &box) +{ + return box.max.x() - box.min.x(); +} +inline coord_t height(const BoundingBox &box) +{ + return box.max.y() - box.min.y(); +} +inline double poly_area(const Points &pts) +{ + return std::abs(Polygon::area(pts)); +} +inline double distance_to(const Point &p1, const Point &p2) +{ + double dx = p2.x() - p1.x(); + double dy = p2.y() - p1.y(); + return std::sqrt(dx * dx + dy * dy); +} + +static CircleBed to_circle(const Point ¢er, const Points &points, const Vec2crd &gap) +{ + std::vector vertex_distances; + double avg_dist = 0; + + for (const Point &pt : points) { + double distance = distance_to(center, pt); + vertex_distances.push_back(distance); + avg_dist += distance; + } + + avg_dist /= vertex_distances.size(); + + CircleBed ret(center, avg_dist, gap); + for (auto el : vertex_distances) { + if (std::abs(el - avg_dist) > 10 * SCALED_EPSILON) { + ret = {}; + break; + } + } + + return ret; +} + +template auto call_with_bed(const Points &bed, const Vec2crd &gap, Fn &&fn) +{ + if (bed.empty()) + return fn(InfiniteBed{}); + else if (bed.size() == 1) + return fn(InfiniteBed{bed.front()}); + else { + auto bb = BoundingBox(bed); + CircleBed circ = to_circle(bb.center(), bed, gap); + auto parea = poly_area(bed); + + if ((1.0 - parea / area(bb)) < 1e-3) { + return fn(RectangleBed{bb, gap}); + } else if (!std::isnan(circ.radius()) && (1.0 - parea / area(circ)) < 1e-2) + return fn(circ); + else + return fn(IrregularBed{{ExPolygon(bed)}, gap}); + } +} + +ArrangeBed to_arrange_bed(const Points &bedpts, const Vec2crd &gap) +{ + ArrangeBed ret; + + call_with_bed(bedpts, gap, [&](const auto &bed) { ret = bed; }); + + return ret; +} + +}} // namespace Slic3r::arr2 diff --git a/src/slic3r-arrange/src/NFP/CircularEdgeIterator.hpp b/src/slic3r-arrange/src/NFP/CircularEdgeIterator.hpp new file mode 100644 index 0000000..3370530 --- /dev/null +++ b/src/slic3r-arrange/src/NFP/CircularEdgeIterator.hpp @@ -0,0 +1,110 @@ +#ifndef CIRCULAR_EDGEITERATOR_HPP +#define CIRCULAR_EDGEITERATOR_HPP + +#include +#include + +namespace Slic3r { + +// Circular iterator over a polygon yielding individual edges as Line objects +// if flip_lines is true, the orientation of each line is flipped (not the +// direction of traversal) +template +class CircularEdgeIterator_ { + const Polygon *m_poly = nullptr; + size_t m_i = 0; + size_t m_c = 0; // counting how many times the iterator has circled over + +public: + + // i: vertex position of first line's starting vertex + // poly: target polygon + CircularEdgeIterator_(size_t i, const Polygon &poly) + : m_poly{&poly} + , m_i{!poly.empty() ? i % poly.size() : 0} + , m_c{!poly.empty() ? i / poly.size() : 0} + {} + + explicit CircularEdgeIterator_ (const Polygon &poly) + : CircularEdgeIterator_(0, poly) {} + + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = Line; + using pointer = Line*; + using reference = Line&; + + CircularEdgeIterator_ & operator++() + { + assert (m_poly); + ++m_i; + if (m_i == m_poly->size()) { // faster than modulo (?) + m_i = 0; + ++m_c; + } + + return *this; + } + + CircularEdgeIterator_ operator++(int) + { + auto cpy = *this; ++(*this); return cpy; + } + + Line operator*() const + { + size_t nx = m_i == m_poly->size() - 1 ? 0 : m_i + 1; + Line ret; + if constexpr (flip_lines) + ret = Line((*m_poly)[nx], (*m_poly)[m_i]); + else + ret = Line((*m_poly)[m_i], (*m_poly)[nx]); + + return ret; + } + + Line operator->() const { return *(*this); } + + bool operator==(const CircularEdgeIterator_& other) const + { + return m_i == other.m_i && m_c == other.m_c; + } + + bool operator!=(const CircularEdgeIterator_& other) const + { + return !(*this == other); + } + + CircularEdgeIterator_& operator +=(size_t dist) + { + m_i = (m_i + dist) % m_poly->size(); + m_c = (m_i + (m_c * m_poly->size()) + dist) / m_poly->size(); + + return *this; + } + + CircularEdgeIterator_ operator +(size_t dist) + { + auto cpy = *this; + cpy += dist; + + return cpy; + } +}; + +using CircularEdgeIterator = CircularEdgeIterator_<>; +using CircularReverseEdgeIterator = CircularEdgeIterator_; + +inline Range line_range(const Polygon &poly) +{ + return Range{CircularEdgeIterator{0, poly}, CircularEdgeIterator{poly.size(), poly}}; +} + +inline Range line_range_flp(const Polygon &poly) +{ + return Range{CircularReverseEdgeIterator{0, poly}, CircularReverseEdgeIterator{poly.size(), poly}}; +} + +} // namespace Slic3r + +#endif // CIRCULAR_EDGEITERATOR_HPP diff --git a/src/slic3r-arrange/src/NFP/EdgeCache.cpp b/src/slic3r-arrange/src/NFP/EdgeCache.cpp new file mode 100644 index 0000000..deca5c0 --- /dev/null +++ b/src/slic3r-arrange/src/NFP/EdgeCache.cpp @@ -0,0 +1,104 @@ +#include + +#include + +#include "CircularEdgeIterator.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Line.hpp" + +namespace Slic3r { namespace arr2 { + +void EdgeCache::create_cache(const ExPolygon &sh) +{ + m_contour.distances.reserve(sh.contour.size()); + m_holes.reserve(sh.holes.size()); + + m_contour.poly = &sh.contour; + + fill_distances(sh.contour, m_contour.distances); + + for (const Polygon &hole : sh.holes) { + auto &hc = m_holes.emplace_back(); + hc.poly = &hole; + fill_distances(hole, hc.distances); + } +} + +Vec2crd EdgeCache::coords(const ContourCache &cache, double distance) const +{ + assert(cache.poly); + return arr2::coords(*cache.poly, cache.distances, distance); +} + +void EdgeCache::sample_contour(double accuracy, std::vector &samples) +{ + const auto N = m_contour.distances.size(); + const auto S = stride(N, accuracy); + + if (N == 0 || S == 0) + return; + + samples.reserve(N / S + 1); + for(size_t i = 0; i < N; i += S) { + samples.emplace_back( + ContourLocation{0, m_contour.distances[i] / m_contour.distances.back()}); + } + + for (size_t hidx = 1; hidx <= m_holes.size(); ++hidx) { + auto& hc = m_holes[hidx - 1]; + + const auto NH = hc.distances.size(); + const auto SH = stride(NH, accuracy); + + if (NH == 0 || SH == 0) + continue; + + samples.reserve(samples.size() + NH / SH + 1); + for (size_t i = 0; i < NH; i += SH) { + samples.emplace_back( + ContourLocation{hidx, hc.distances[i] / hc.distances.back()}); + } + } +} + +Vec2crd coords(const Polygon &poly, const std::vector &distances, double distance) +{ + assert(poly.size() > 1 && distance >= .0 && distance <= 1.0); + + // distance is from 0.0 to 1.0, we scale it up to the full length of + // the circumference + double d = distance * distances.back(); + + // Magic: we find the right edge in log time + auto it = std::lower_bound(distances.begin(), distances.end(), d); + + assert(it != distances.end()); + + auto idx = it - distances.begin(); // get the index of the edge + auto &pts = poly.points; + auto edge = idx == long(pts.size() - 1) ? Line(pts.back(), pts.front()) : + Line(pts[idx], pts[idx + 1]); + + // Get the remaining distance on the target edge + auto ed = d - (idx > 0 ? *std::prev(it) : 0 ); + + double t = ed / edge.length(); + Vec2d n {double(edge.b.x()) - edge.a.x(), double(edge.b.y()) - edge.a.y()}; + Vec2crd ret = (edge.a.cast() + t * n).cast(); + + return ret; +} + +void fill_distances(const Polygon &poly, std::vector &distances) +{ + distances.reserve(poly.size()); + + double dist = 0.; + auto lrange = line_range(poly); + for (const Line l : lrange) { + dist += l.length(); + distances.emplace_back(dist); + } +} + +}} // namespace Slic3r::arr2 diff --git a/src/slic3r-arrange/src/NFP/NFP.cpp b/src/slic3r-arrange/src/NFP/NFP.cpp new file mode 100644 index 0000000..400b5b0 --- /dev/null +++ b/src/slic3r-arrange/src/NFP/NFP.cpp @@ -0,0 +1,434 @@ +#ifndef NFP_CPP +#define NFP_CPP + +#include +#include + +#include "CircularEdgeIterator.hpp" +#include +#include +#include +#include + +#include + +#if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__) +namespace Slic3r { using LargeInt = __int128; } +#else +#include + +namespace Slic3r { using LargeInt = boost::multiprecision::int128_t; } +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +static bool line_cmp(const Line& e1, const Line& e2) +{ + using Ratio = boost::rational; + + const Vec<2, int64_t> ax(1, 0); // Unit vector for the X axis + + Vec<2, int64_t> p1 = (e1.b - e1.a).cast(); + Vec<2, int64_t> p2 = (e2.b - e2.a).cast(); + + // Quadrant mapping array. The quadrant of a vector can be determined + // from the dot product of the vector and its perpendicular pair + // with the unit vector X axis. The products will carry the values + // lcos = dot(p, ax) = l * cos(phi) and + // lsin = -dotperp(p, ax) = l * sin(phi) where + // l is the length of vector p. From the signs of these values we can + // construct an index which has the sign of lcos as MSB and the + // sign of lsin as LSB. This index can be used to retrieve the actual + // quadrant where vector p resides using the following map: + // (+ is 0, - is 1) + // cos | sin | decimal | quadrant + // + | + | 0 | 0 + // + | - | 1 | 3 + // - | + | 2 | 1 + // - | - | 3 | 2 + std::array quadrants {0, 3, 1, 2 }; + + std::array q {0, 0}; // Quadrant indices for p1 and p2 + + using TDots = std::array; + TDots lcos { p1.dot(ax), p2.dot(ax) }; + TDots lsin { -dotperp(p1, ax), -dotperp(p2, ax) }; + + // Construct the quadrant indices for p1 and p2 + for(size_t i = 0; i < 2; ++i) { + if (lcos[i] == 0) + q[i] = lsin[i] > 0 ? 1 : 3; + else if (lsin[i] == 0) + q[i] = lcos[i] > 0 ? 0 : 2; + else + q[i] = quadrants[((lcos[i] < 0) << 1) + (lsin[i] < 0)]; + } + + if (q[0] == q[1]) { // only bother if p1 and p2 are in the same quadrant + auto lsq1 = p1.squaredNorm(); // squared magnitudes, avoid sqrt + auto lsq2 = p2.squaredNorm(); // squared magnitudes, avoid sqrt + + // We will actually compare l^2 * cos^2(phi) which saturates the + // cos function. But with the quadrant info we can get the sign back + int sign = q[0] == 1 || q[0] == 2 ? -1 : 1; + + // If Ratio is an actual rational type, there is no precision loss + auto pcos1 = Ratio(lcos[0]) / lsq1 * sign * lcos[0]; + auto pcos2 = Ratio(lcos[1]) / lsq2 * sign * lcos[1]; + + return q[0] < 2 ? pcos1 > pcos2 : pcos1 < pcos2; + } + + // If in different quadrants, compare the quadrant indices only. + return q[0] < q[1]; +} + +static inline bool vsort(const Vec2crd& v1, const Vec2crd& v2) +{ + return v1.y() == v2.y() ? v1.x() < v2.x() : v1.y() < v2.y(); +} + +ExPolygons ifp_convex(const arr2::RectangleBed &obed, const Polygon &convexpoly) +{ + ExPolygon ret; + + auto sbox = bounding_box(convexpoly); + auto sboxsize = sbox.size(); + coord_t sheight = sboxsize.y(); + coord_t swidth = sboxsize.x(); + Point sliding_top = reference_vertex(convexpoly); + auto leftOffset = sliding_top.x() - sbox.min.x(); + auto rightOffset = sliding_top.x() - sbox.max.x(); + coord_t topOffset = 0; + auto bottomOffset = sheight; + + auto bedbb = obed.bb; +// bedbb.offset(1); + auto bedsz = bedbb.size(); + auto boxWidth = bedsz.x(); + auto boxHeight = bedsz.y(); + + auto bedMinx = bedbb.min.x(); + auto bedMiny = bedbb.min.y(); + auto bedMaxx = bedbb.max.x(); + auto bedMaxy = bedbb.max.y(); + + Polygon innerNfp{ Point{bedMinx + leftOffset, bedMaxy + topOffset}, + Point{bedMaxx + rightOffset, bedMaxy + topOffset}, + Point{bedMaxx + rightOffset, bedMiny + bottomOffset}, + Point{bedMinx + leftOffset, bedMiny + bottomOffset}, + Point{bedMinx + leftOffset, bedMaxy + topOffset} }; + + if (sheight <= boxHeight && swidth <= boxWidth) + ret.contour = std::move(innerNfp); + + return {ret}; +} + +Polygon ifp_convex_convex(const Polygon &fixed, const Polygon &movable) +{ + auto subnfps = reserve_polygons(fixed.size()); + + // For each edge of the bed polygon, determine the nfp of convexpoly and + // the zero area polygon formed by the edge. The union of all these sub-nfps + // will contain a hole that is the actual ifp. + auto lrange = line_range(fixed); + for (const Line l : lrange) { // Older mac compilers generate warnging if line_range is called in-place + Polygon fixed = {l.a, l.b}; + subnfps.emplace_back(nfp_convex_convex_legacy(fixed, movable)); + } + + // Do the union and then keep only the holes (should be only one or zero, if + // the convexpoly cannot fit into the bed) + Polygons ifp = union_(subnfps); + Polygon ret; + + // find the first hole + auto it = std::find_if(ifp.begin(), ifp.end(), [](const Polygon &subifp){ + return subifp.is_clockwise(); + }); + + if (it != ifp.end()) { + ret = std::move(*it); + std::reverse(ret.begin(), ret.end()); + } + + return ret; +} + +ExPolygons ifp_convex(const arr2::CircleBed &bed, const Polygon &convexpoly) +{ + Polygon circle = approximate_circle_with_polygon(bed); + + return {ExPolygon{ifp_convex_convex(circle, convexpoly)}}; +} + +ExPolygons ifp_convex(const arr2::IrregularBed &bed, const Polygon &convexpoly) +{ + auto bb = get_extents(bed.poly); + bb.offset(scaled(1.)); + + Polygon rect = arr2::to_rectangle(bb); + + ExPolygons blueprint = diff_ex(rect, bed.poly); + Polygons ifp; + for (const ExPolygon &part : blueprint) { + Polygons triangles = Slic3r::convex_decomposition_tess(part); + for (const Polygon &tr : triangles) { + Polygon subifp = nfp_convex_convex_legacy(tr, convexpoly); + ifp.emplace_back(std::move(subifp)); + } + } + + ifp = union_(ifp); + + Polygons ret; + + std::copy_if(ifp.begin(), ifp.end(), std::back_inserter(ret), + [](const Polygon &p) { return p.is_clockwise(); }); + + for (Polygon &p : ret) + std::reverse(p.begin(), p.end()); + + return to_expolygons(ret); +} + +Vec2crd reference_vertex(const Polygon &poly) +{ + Vec2crd ret{std::numeric_limits::min(), + std::numeric_limits::min()}; + + auto it = std::max_element(poly.points.begin(), poly.points.end(), vsort); + if (it != poly.points.end()) + ret = std::max(ret, static_cast(*it), vsort); + + return ret; +} + +Vec2crd reference_vertex(const ExPolygon &expoly) +{ + return reference_vertex(expoly.contour); +} + +Vec2crd reference_vertex(const Polygons &outline) +{ + Vec2crd ret{std::numeric_limits::min(), + std::numeric_limits::min()}; + + for (const Polygon &poly : outline) + ret = std::max(ret, reference_vertex(poly), vsort); + + return ret; +} + +Vec2crd reference_vertex(const ExPolygons &outline) +{ + Vec2crd ret{std::numeric_limits::min(), + std::numeric_limits::min()}; + + for (const ExPolygon &expoly : outline) + ret = std::max(ret, reference_vertex(expoly), vsort); + + return ret; +} + +Vec2crd min_vertex(const Polygon &poly) +{ + Vec2crd ret{std::numeric_limits::max(), + std::numeric_limits::max()}; + + auto it = std::min_element(poly.points.begin(), poly.points.end(), vsort); + if (it != poly.points.end()) + ret = std::min(ret, static_cast(*it), vsort); + + return ret; +} + +// Find the vertex corresponding to the edge with minimum angle to X axis. +// Only usable with CircularEdgeIterator<> template. +template It find_min_anglex_edge(It from) +{ + bool found = false; + auto it = from; + while (!found ) { + found = !line_cmp(*it, *std::next(it)); + ++it; + } + + return it; +} + +// Only usable if both fixed and movable polygon is convex. In that case, +// their edges are already sorted by angle to X axis, only the starting +// (lowest X axis) edge needs to be found first. +void nfp_convex_convex(const Polygon &fixed, const Polygon &movable, Polygon &poly) +{ + if (fixed.empty() || movable.empty()) + return; + + // Clear poly and adjust its capacity. Nothing happens if poly is + // already sufficiently large and and empty. + poly.clear(); + poly.points.reserve(fixed.size() + movable.size()); + + // Find starting positions on the fixed and moving polygons + auto it_fx = find_min_anglex_edge(CircularEdgeIterator{fixed}); + auto it_mv = find_min_anglex_edge(CircularReverseEdgeIterator{movable}); + + // End positions are at the same vertex after completing one full circle + auto end_fx = it_fx + fixed.size(); + auto end_mv = it_mv + movable.size(); + + // Pos zero is just fine as starting point: + poly.points.emplace_back(0, 0); + + // Output iterator adapter for std::merge + struct OutItAdaptor { + using value_type [[maybe_unused]] = Line; + using difference_type [[maybe_unused]] = std::ptrdiff_t; + using pointer [[maybe_unused]] = Line*; + using reference [[maybe_unused]] = Line& ; + using iterator_category [[maybe_unused]] = std::output_iterator_tag; + + Polygon *outpoly; + OutItAdaptor(Polygon &out) : outpoly{&out} {} + + OutItAdaptor &operator *() { return *this; } + void operator=(const Line &l) + { + // Yielding l.b, offsetted so that l.a touches the last vertex in + // in outpoly + outpoly->points.emplace_back(l.b + outpoly->back() - l.a); + } + + OutItAdaptor& operator++() { return *this; }; + }; + + // Use std algo to merge the edges from the two polygons + std::merge(it_fx, end_fx, it_mv, end_mv, OutItAdaptor{poly}, line_cmp); +} + +Polygon nfp_convex_convex(const Polygon &fixed, const Polygon &movable) +{ + Polygon ret; + nfp_convex_convex(fixed, movable, ret); + + return ret; +} + +static void buildPolygon(const std::vector& edgelist, + Polygon& rpoly, + Point& top_nfp) +{ + auto& rsh = rpoly.points; + + rsh.reserve(2 * edgelist.size()); + + // Add the two vertices from the first edge into the final polygon. + rsh.emplace_back(edgelist.front().a); + rsh.emplace_back(edgelist.front().b); + + // Sorting function for the nfp reference vertex search + + // the reference (rightmost top) vertex so far + top_nfp = *std::max_element(std::cbegin(rsh), std::cend(rsh), vsort); + + auto tmp = std::next(std::begin(rsh)); + + // Construct final nfp by placing each edge to the end of the previous + for(auto eit = std::next(edgelist.begin()); eit != edgelist.end(); ++eit) { + auto d = *tmp - eit->a; + Vec2crd p = eit->b + d; + + rsh.emplace_back(p); + + // Set the new reference vertex + if (vsort(top_nfp, p)) + top_nfp = p; + + tmp = std::next(tmp); + } +} + +Polygon nfp_convex_convex_legacy(const Polygon &fixed, const Polygon &movable) +{ + assert (!fixed.empty()); + assert (!movable.empty()); + + Polygon rsh; // Final nfp placeholder + Point max_nfp; + std::vector edgelist; + + auto cap = fixed.points.size() + movable.points.size(); + + // Reserve the needed memory + edgelist.reserve(cap); + rsh.points.reserve(cap); + + auto add_edge = [&edgelist](const Point &v1, const Point &v2) { + Line e{v1, v2}; + if ((e.b - e.a).cast().squaredNorm() > 0) + edgelist.emplace_back(e); + }; + + Point max_fixed = fixed.points.front(); + { // place all edges from fixed into edgelist + auto first = std::cbegin(fixed); + auto next = std::next(first); + + while(next != std::cend(fixed)) { + add_edge(*(first), *(next)); + max_fixed = std::max(max_fixed, *first, vsort); + + ++first; ++next; + } + + add_edge(*std::crbegin(fixed), *std::cbegin(fixed)); + max_fixed = std::max(max_fixed, *std::crbegin(fixed), vsort); + } + + Point max_movable = movable.points.front(); + Point min_movable = movable.points.front(); + { // place all edges from movable into edgelist + auto first = std::cbegin(movable); + auto next = std::next(first); + + while(next != std::cend(movable)) { + add_edge(*(next), *(first)); + min_movable = std::min(min_movable, *first, vsort); + max_movable = std::max(max_movable, *first, vsort); + + ++first; ++next; + } + + add_edge(*std::cbegin(movable), *std::crbegin(movable)); + min_movable = std::min(min_movable, *std::crbegin(movable), vsort); + max_movable = std::max(max_movable, *std::crbegin(movable), vsort); + } + + std::sort(edgelist.begin(), edgelist.end(), line_cmp); + + buildPolygon(edgelist, rsh, max_nfp); + + auto dtouch = max_fixed - min_movable; + auto top_other = max_movable + dtouch; + auto dnfp = top_other - max_nfp; + rsh.translate(dnfp); + + return rsh; +} + +} // namespace Slic3r + +#endif // NFP_CPP diff --git a/src/slic3r-arrange/src/NFP/NFPConcave_Tesselate.cpp b/src/slic3r-arrange/src/NFP/NFPConcave_Tesselate.cpp new file mode 100644 index 0000000..6ea717f --- /dev/null +++ b/src/slic3r-arrange/src/NFP/NFPConcave_Tesselate.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" + +namespace Slic3r { + +Polygons convex_decomposition_tess(const Polygon &expoly) +{ + return convex_decomposition_tess(ExPolygon{expoly}); +} + +Polygons convex_decomposition_tess(const ExPolygon &expoly) +{ + std::vector tr = Slic3r::triangulate_expolygon_2d(expoly); + + auto ret = Slic3r::reserve_polygons(tr.size() / 3); + for (size_t i = 0; i < tr.size(); i += 3) { + ret.emplace_back( + Polygon{scaled(tr[i]), scaled(tr[i + 1]), scaled(tr[i + 2])}); + } + + return ret; +} + +Polygons convex_decomposition_tess(const ExPolygons &expolys) +{ + constexpr size_t AvgTriangleCountGuess = 50; + + auto ret = reserve_polygons(AvgTriangleCountGuess * expolys.size()); + for (const ExPolygon &expoly : expolys) { + Polygons convparts = convex_decomposition_tess(expoly); + std::move(convparts.begin(), convparts.end(), std::back_inserter(ret)); + } + + return ret; +} + +ExPolygons nfp_concave_concave_tess(const ExPolygon &fixed, + const ExPolygon &movable) +{ + Polygons fixed_decomp = convex_decomposition_tess(fixed); + Polygons movable_decomp = convex_decomposition_tess(movable); + + auto refs_mv = reserve_vector(movable_decomp.size()); + + for (const Polygon &p : movable_decomp) + refs_mv.emplace_back(reference_vertex(p)); + + auto nfps = reserve_polygons(fixed_decomp.size() * movable_decomp.size()); + + Vec2crd ref_whole = reference_vertex(movable); + for (const Polygon &fixed_part : fixed_decomp) { + size_t mvi = 0; + for (const Polygon &movable_part : movable_decomp) { + Polygon subnfp = nfp_convex_convex(fixed_part, movable_part); + const Vec2crd &ref_mp = refs_mv[mvi]; + auto d = ref_whole - ref_mp; + subnfp.translate(d); + nfps.emplace_back(subnfp); + mvi++; + } + } + + return union_ex(nfps); +} + +} // namespace Slic3r diff --git a/tests/arrange/CMakeLists.txt b/tests/arrange/CMakeLists.txt index d71f0b6..6ce3126 100644 --- a/tests/arrange/CMakeLists.txt +++ b/tests/arrange/CMakeLists.txt @@ -5,7 +5,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp ../data/qidiparts.cpp ) -target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) +target_link_libraries(${_TEST_NAME}_tests test_common slic3r-arrange-wrapper) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") if (WIN32) diff --git a/tests/arrange/test_arrange.cpp b/tests/arrange/test_arrange.cpp index cdfcd1d..80b9060 100644 --- a/tests/arrange/test_arrange.cpp +++ b/tests/arrange/test_arrange.cpp @@ -3,21 +3,17 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include -#include -#include - -#include -#include -#include - -#include -#include -#include +#include +#include +#include #include @@ -386,7 +382,7 @@ template<> inline Slic3r::arr2::RectangleBed init_bed inline Slic3r::arr2::CircleBed init_bed() { - return Slic3r::arr2::CircleBed{Slic3r::Point::Zero(), scaled(300.)}; + return Slic3r::arr2::CircleBed{Slic3r::Point::Zero(), scaled(300.), Slic3r::Vec2crd{0, 0}}; } template<> inline Slic3r::arr2::IrregularBed init_bed() @@ -640,6 +636,7 @@ struct RectangleItem { void set_bed_index(int idx) { bed_index = idx; } int get_bed_index() const noexcept { return bed_index; } + std::optional get_bed_constraint() const { return std::nullopt; } void set_translation(const Vec2crd &tr) { translation = tr; } const Vec2crd & get_translation() const noexcept { return translation; } diff --git a/tests/arrange/test_arrange_integration.cpp b/tests/arrange/test_arrange_integration.cpp index 2f1a345..0f91969 100644 --- a/tests/arrange/test_arrange_integration.cpp +++ b/tests/arrange/test_arrange_integration.cpp @@ -1,16 +1,15 @@ #include #include "test_utils.hpp" -#include -#include -#include - -#include +#include +#include +#include +#include +#include #include "libslic3r/Model.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" #include "libslic3r/Format/3mf.hpp" -#include "libslic3r/ModelArrange.hpp" static Slic3r::Model get_example_model_with_20mm_cube() { @@ -135,7 +134,7 @@ TEST_CASE("ModelInstance should be retrievable when imbued into ArrangeItem", arr2::ArrangeItem itm; arr2::PhysicalOnlyVBedHandler vbedh; auto vbedh_ptr = static_cast(&vbedh); - auto arrbl = arr2::ArrangeableModelInstance{mi, vbedh_ptr, nullptr, {0, 0}}; + auto arrbl = arr2::ArrangeableModelInstance{mi, vbedh_ptr, nullptr, {0, 0}, std::nullopt}; arr2::imbue_id(itm, arrbl.id()); std::optional id_returned = arr2::retrieve_id(itm); @@ -330,7 +329,7 @@ auto create_vbed_handler(const Slic3r::Boundi template<> auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap) { - return Slic3r::arr2::GridStriderVBedHandler{bedbb, gap}; + return Slic3r::arr2::GridStriderVBedHandler{bedbb, {gap, gap}}; } TEMPLATE_TEST_CASE("Common virtual bed handlers", @@ -628,7 +627,7 @@ TEMPLATE_TEST_CASE("Bed needs to be completely filled with 1cm cubes", ModelObject* new_object = m.add_object(); new_object->name = "10mm_box"; - new_object->add_instance(); + ModelInstance *instance = new_object->add_instance(); TriangleMesh mesh = make_cube(10., 10., 10.); ModelVolume* new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; @@ -641,11 +640,15 @@ TEMPLATE_TEST_CASE("Bed needs to be completely filled with 1cm cubes", arr2::FixedSelection sel({{true}}); + arr2::BedConstraints constraints; + constraints.insert({instance->id(), 0}); + arr2::Scene scene{arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(settings) .set_selection(&sel) - .set_bed(cfg)}; + .set_bed_constraints(std::move(constraints)) + .set_bed(cfg, Point::new_scale(10, 10))}; auto task = arr2::FillBedTask::create(scene); auto result = task->process_native(arr2::DummyCtl{}); @@ -654,7 +657,7 @@ TEMPLATE_TEST_CASE("Bed needs to be completely filled with 1cm cubes", store_3mf("fillbed_10mm_result.3mf", &m, &cfg, false); Points bedpts = get_bed_shape(cfg); - arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts); + arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts, Point::new_scale(10, 10)); REQUIRE(bed.which() == 1); // Rectangle bed @@ -799,7 +802,7 @@ TEST_CASE("Testing arrangement involving virtual beds", "[arrange2][integration] DynamicPrintConfig cfg; cfg.load_from_ini(std::string(TEST_DATA_DIR PATH_SEPARATOR) + "default_fff.ini", ForwardCompatibilitySubstitutionRule::Enable); - auto bed = arr2::to_arrange_bed(get_bed_shape(cfg)); + auto bed = arr2::to_arrange_bed(get_bed_shape(cfg), Point::new_scale(10, 10)); auto bedbb = bounding_box(bed); auto bedsz = unscaled(bedbb.size()); @@ -815,7 +818,7 @@ TEST_CASE("Testing arrangement involving virtual beds", "[arrange2][integration] arr2::Scene scene{arr2::SceneBuilder{} .set_model(model) .set_arrange_settings(settings) - .set_bed(cfg)}; + .set_bed(cfg, Point::new_scale(10, 10))}; auto itm_conv = arr2::ArrangeableToItemConverter::create(scene); @@ -883,7 +886,7 @@ public: }; class MocWTH : public WipeTowerHandler { - std::function m_sel_pred; + std::function m_sel_pred; ObjectID m_id; public: @@ -891,18 +894,22 @@ public: void visit(std::function fn) override { - MocWT wt{m_id, Polygon{}, m_sel_pred}; + MocWT wt{m_id, Polygon{}, 0, m_sel_pred}; fn(wt); } void visit(std::function fn) const override { - MocWT wt{m_id, Polygon{}, m_sel_pred}; + MocWT wt{m_id, Polygon{}, 0, m_sel_pred}; fn(wt); } - void set_selection_predicate(std::function pred) override + void set_selection_predicate(std::function pred) override { m_sel_pred = std::move(pred); } + + ObjectID get_id() const override { + return m_id; + } }; }} // namespace Slic3r::arr2 @@ -974,7 +981,7 @@ TEST_CASE("Test SceneBuilder", "[arrange2][integration]") WHEN("a scene is built with a bed initialized from this DynamicPrintConfig") { - arr2::Scene scene(arr2::SceneBuilder{}.set_bed(cfg)); + arr2::Scene scene(arr2::SceneBuilder{}.set_bed(cfg, Point::new_scale(10, 10))); auto bedbb = bounding_box(get_bed_shape(cfg)); @@ -1001,7 +1008,10 @@ TEST_CASE("Test SceneBuilder", "[arrange2][integration]") arr2::SceneBuilder bld; Model mdl; bld.set_model(mdl); - bld.set_wipe_tower_handler(std::make_unique(mdl.wipe_tower.id())); + + std::vector> handlers; + handlers.push_back(std::make_unique(wipe_tower_instance_id(0))); + bld.set_wipe_tower_handlers(std::move(handlers)); WHEN("the selection mask is initialized as a fallback default in the created scene") { @@ -1014,7 +1024,7 @@ TEST_CASE("Test SceneBuilder", "[arrange2][integration]") bool wt_selected = false; scene.model() - .visit_arrangeable(mdl.wipe_tower.id(), + .visit_arrangeable(wipe_tower_instance_id(0), [&wt_selected]( const arr2::Arrangeable &arrbl) { wt_selected = arrbl.is_selected(); diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index f50e8c1..880b54a 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(${_TEST_NAME}_tests test_seam_aligned.cpp test_seam_rear.cpp test_seam_random.cpp + test_seam_scarf.cpp benchmark_seams.cpp test_gcodefindreplace.cpp test_gcodewriter.cpp @@ -38,7 +39,7 @@ add_executable(${_TEST_NAME}_tests test_thin_walls.cpp test_trianglemesh.cpp ) -target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) +target_link_libraries(${_TEST_NAME}_tests test_common slic3r-arrange-wrapper) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") target_compile_definitions(${_TEST_NAME}_tests PUBLIC CATCH_CONFIG_ENABLE_BENCHMARKING) diff --git a/tests/fff_print/benchmark_seams.cpp b/tests/fff_print/benchmark_seams.cpp index 41f5242..8eeeb9c 100644 --- a/tests/fff_print/benchmark_seams.cpp +++ b/tests/fff_print/benchmark_seams.cpp @@ -105,13 +105,14 @@ TEST_CASE_METHOD(Slic3r::Test::SeamsFixture, "Seam benchmarks", "[Seams][.Benchm using namespace Slic3r; Placer placer; placer.init(print->objects(), params, [](){}); - std::vector> loops; + std::vector> loops; const PrintObject* object{print->objects().front()}; for (const Layer* layer :object->layers()) { for (const LayerSlice& lslice : layer->lslices_ex) { for (const LayerIsland &island : lslice.islands) { const LayerRegion &layer_region = *layer->get_region(island.perimeters.region()); + const PrintRegion ®ion = print->get_print_region(layer_region.region().print_region_id()); for (uint32_t perimeter_id : island.perimeters) { const auto *entity_collection{static_cast(layer_region.perimeters().entities[perimeter_id])}; if (entity_collection != nullptr) { @@ -120,7 +121,7 @@ TEST_CASE_METHOD(Slic3r::Test::SeamsFixture, "Seam benchmarks", "[Seams][.Benchm if (loop == nullptr) { continue; } - loops.emplace_back(layer, loop); + loops.emplace_back(layer, loop, ®ion); } } } @@ -129,8 +130,8 @@ TEST_CASE_METHOD(Slic3r::Test::SeamsFixture, "Seam benchmarks", "[Seams][.Benchm } BENCHMARK_ADVANCED("Place seam benchy")(Catch::Benchmark::Chronometer meter) { meter.measure([&] { - for (const auto &[layer, loop] : loops) { - placer.place_seam(layer, *loop, {0, 0}); + for (const auto &[layer, loop, region] : loops) { + placer.place_seam(layer, region, *loop, false, {0, 0}); } }); }; diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp index b28196b..9dbd1c7 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_data.cpp @@ -7,6 +7,8 @@ #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/Format/STL.hpp" +#include + #include #include @@ -14,7 +16,6 @@ #include #include #include -#include using namespace std; @@ -254,7 +255,7 @@ void init_print(std::vector &&meshes, Slic3r::Print &print, Slic3r double distance = min_object_distance(config); arr2::ArrangeSettings arrange_settings{}; arrange_settings.set_distance_from_objects(distance); - arr2::ArrangeBed bed{arr2::to_arrange_bed(get_bed_shape(config))}; + arr2::ArrangeBed bed{arr2::to_arrange_bed(get_bed_shape(config), Vec2crd{0, 0})}; if (duplicate_count > 1) { duplicate(model, duplicate_count, bed, arrange_settings); } diff --git a/tests/fff_print/test_data.hpp b/tests/fff_print/test_data.hpp index 939d1a7..9eee9c9 100644 --- a/tests/fff_print/test_data.hpp +++ b/tests/fff_print/test_data.hpp @@ -165,7 +165,8 @@ inline std::unique_ptr process_3mf(const std::filesystem::path &path) { Model model; ConfigSubstitutionContext context{ForwardCompatibilitySubstitutionRule::Disable}; - load_3mf(path.string().c_str(), config, context, &model, false); + boost::optional version; + load_3mf(path.string().c_str(), config, context, &model, false, version); Slic3r::Test::init_print(std::vector{}, *print, model, config); print->process(); diff --git a/tests/fff_print/test_gcode.cpp b/tests/fff_print/test_gcode.cpp index 7061c48..8af6fe4 100644 --- a/tests/fff_print/test_gcode.cpp +++ b/tests/fff_print/test_gcode.cpp @@ -6,7 +6,6 @@ #include "libslic3r/GCode.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" -#include "libslic3r/ModelArrange.hpp" #include "test_data.hpp" using namespace Slic3r; diff --git a/tests/fff_print/test_model.cpp b/tests/fff_print/test_model.cpp index 243cb10..2f61425 100644 --- a/tests/fff_print/test_model.cpp +++ b/tests/fff_print/test_model.cpp @@ -2,7 +2,7 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/Model.hpp" -#include "libslic3r/ModelArrange.hpp" +#include #include #include @@ -43,7 +43,7 @@ SCENARIO("Model construction", "[Model]") { } model_object->add_instance(); arrange_objects(model, - arr2::to_arrange_bed(get_bed_shape(config)), + arr2::to_arrange_bed(get_bed_shape(config), Point::new_scale(10, 10)), arr2::ArrangeSettings{}.set_distance_from_objects( min_object_distance(config))); diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index ca6fbbc..517d144 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -48,6 +48,7 @@ SCENARIO("Perimeter nesting", "[Perimeters]") //B56 ExPolygons fill_expolygons_no_overlap; Flow flow(1., 1., 1.); + PerimeterRegions perimeter_regions; PerimeterGenerator::Parameters perimeter_generator_params( 1., // layer height -1, // layer ID @@ -55,6 +56,7 @@ SCENARIO("Perimeter nesting", "[Perimeters]") static_cast(config), static_cast(config), static_cast(config), + perimeter_regions, false); // spiral_vase Polygons lower_layer_polygons_cache; Polygons upper_layer_polygons_cache; diff --git a/tests/fff_print/test_seam_geometry.cpp b/tests/fff_print/test_seam_geometry.cpp index a12fbdb..67dcc3e 100644 --- a/tests/fff_print/test_seam_geometry.cpp +++ b/tests/fff_print/test_seam_geometry.cpp @@ -143,3 +143,30 @@ TEST_CASE("Calculate overhangs", "[Seams][SeamGeometry]") { 0.0, M_PI / 4.0, M_PI / 4.0, 0.0 })); } + +const Linesf lines{to_unscaled_linesf({ExPolygon{ + scaled(Vec2d{0.0, 0.0}), + scaled(Vec2d{1.0, 0.0}), + scaled(Vec2d{1.0, 1.0}), + scaled(Vec2d{0.0, 1.0}) +}})}; + +TEST_CASE("Offset along loop lines forward", "[Seams][SeamGeometry]") { + const std::optional result{Seams::Geometry::offset_along_lines( + {0.5, 0.0}, 0, lines, 3.9, Seams::Geometry::Direction1D::forward + )}; + REQUIRE(result); + const auto &[point, line_index] = *result; + CHECK((scaled(point) - Point::new_scale(0.4, 0.0)).norm() < scaled(EPSILON)); + CHECK(line_index == 0); +} + +TEST_CASE("Offset along loop lines backward", "[Seams][SeamGeometry]") { + const std::optional result{Seams::Geometry::offset_along_lines( + {1.0, 0.5}, 1, lines, 1.8, Seams::Geometry::Direction1D::backward + )}; + REQUIRE(result); + const auto &[point, line_index] = *result; + CHECK((scaled(point) - Point::new_scale(0.0, 0.3)).norm() < scaled(EPSILON)); + CHECK(line_index == 3); +} diff --git a/tests/fff_print/test_seam_perimeters.cpp b/tests/fff_print/test_seam_perimeters.cpp index 36e4f67..245b3b3 100644 --- a/tests/fff_print/test_seam_perimeters.cpp +++ b/tests/fff_print/test_seam_perimeters.cpp @@ -124,44 +124,48 @@ constexpr const char *to_string(Perimeters::AngleType angle_type) { throw std::runtime_error("Unreachable"); } -void serialize_shell(std::ostream &output, const Shells::Shell &shell) { +void serialize_shells(std::ostream &output, const Shells::Shells<> &shells) { output << "x,y,z,point_type,point_classification,angle_type,layer_index," - "point_index,distance,distance_to_previous,is_degenerate" + "point_index,distance,distance_to_previous,is_degenerate,shell_index" << std::endl; - for (std::size_t perimeter_index{0}; perimeter_index < shell.size(); ++perimeter_index) { - const Shells::Slice<> &slice{shell[perimeter_index]}; - const Perimeters::Perimeter &perimeter{slice.boundary}; - const std::vector &points{perimeter.positions}; + for (std::size_t shell_index{0}; shell_index < shells.size(); ++shell_index) { + const Shells::Shell<> &shell{shells[shell_index]}; + for (std::size_t perimeter_index{0}; perimeter_index < shell.size(); ++perimeter_index) { + const Shells::Slice<> &slice{shell[perimeter_index]}; + const Perimeters::Perimeter &perimeter{slice.boundary}; + const std::vector &points{perimeter.positions}; - double total_distance{0.0}; - for (std::size_t point_index{0}; point_index < perimeter.point_types.size(); ++point_index) { - const Vec3d point{to_3d(points[point_index], perimeter.slice_z)}; - const Perimeters::PointType point_type{perimeter.point_types[point_index]}; - const Perimeters::PointClassification point_classification{ - perimeter.point_classifications[point_index]}; - const Perimeters::AngleType angle_type{perimeter.angle_types[point_index]}; - const std::size_t layer_index{slice.layer_index}; - const std::size_t previous_index{point_index == 0 ? points.size() - 1 : point_index - 1}; - const double distance_to_previous{(points[point_index] - points[previous_index]).norm()}; - total_distance += point_index == 0 ? 0.0 : distance_to_previous; - const double distance{total_distance}; - const bool is_degenerate{perimeter.is_degenerate}; + double total_distance{0.0}; + for (std::size_t point_index{0}; point_index < perimeter.point_types.size(); ++point_index) { + const Vec3d point{to_3d(points[point_index], perimeter.slice_z)}; + const Perimeters::PointType point_type{perimeter.point_types[point_index]}; + const Perimeters::PointClassification point_classification{ + perimeter.point_classifications[point_index]}; + const Perimeters::AngleType angle_type{perimeter.angle_types[point_index]}; + const std::size_t layer_index{slice.layer_index}; + const std::size_t previous_index{point_index == 0 ? points.size() - 1 : point_index - 1}; + const double distance_to_previous{(points[point_index] - points[previous_index]).norm()}; + total_distance += point_index == 0 ? 0.0 : distance_to_previous; + const double distance{total_distance}; + const bool is_degenerate{perimeter.is_degenerate}; - // clang-format off - output - << point.x() << "," - << point.y() << "," - << point.z() << "," - << to_string(point_type) << "," - << to_string(point_classification) << "," - << to_string(angle_type) << "," - << layer_index << "," - << point_index << "," - << distance << "," - << distance_to_previous << "," - << is_degenerate << std::endl; - // clang-format on + // clang-format off + output + << point.x() << "," + << point.y() << "," + << point.z() << "," + << to_string(point_type) << "," + << to_string(point_classification) << "," + << to_string(angle_type) << "," + << layer_index << "," + << point_index << "," + << distance << "," + << distance_to_previous << "," + << is_degenerate << "," + << shell_index << std::endl; + // clang-format on + } } } } @@ -175,6 +179,6 @@ TEST_CASE_METHOD(Test::SeamsFixture, "Create perimeters", "[Seams][SeamPerimeter if constexpr (debug_files) { std::ofstream csv{"perimeters.csv"}; - serialize_shell(csv, shells[0]); + serialize_shells(csv, shells); } } diff --git a/tests/fff_print/test_seam_scarf.cpp b/tests/fff_print/test_seam_scarf.cpp new file mode 100644 index 0000000..2f9b727 --- /dev/null +++ b/tests/fff_print/test_seam_scarf.cpp @@ -0,0 +1,320 @@ +#include +#include +#include + +using namespace Slic3r; +using Seams::Scarf::Impl::PathPoint; + +TEST_CASE("Get path point", "[Seams][Scarf]") { + using Seams::Scarf::Impl::get_path_point; + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(0, 1), + Point::new_scale(0, 2), + Point::new_scale(0, 3), + Point::new_scale(0, 4), + }; + const ExtrusionPaths paths{ + {{points[0], points[1]}, {}}, + {{points[1], points[2]}, {}}, + {{points[2], points[3], points[4]}, {}}, + }; + const std::size_t global_index{5}; // Index if paths are flattened. + const Point point{Point::new_scale(0, 3.5)}; + const PathPoint path_point{get_path_point(paths, point, global_index)}; + + CHECK(path_point.path_index == 2); + CHECK(path_point.previous_point_on_path_index == 1); + CHECK(path_point.point == point); +} + +TEST_CASE("Split path", "[Seams][Scarf]") { + using Seams::Scarf::Impl::split_path; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(1, 0), + Point::new_scale(2, 0), + }; + + const auto split_point{Point::new_scale(1.5, 0)}; + + const ExtrusionPath path{Polyline{points}, {}}; + const auto[path_before, path_after]{split_path( + path, split_point, 1 + )}; + + REQUIRE(path_before.size() == 3); + CHECK(path_before.first_point() == points.front()); + CHECK(path_before.last_point() == split_point); + + REQUIRE(path_after.size() == 2); + CHECK(path_after.first_point() == split_point); + CHECK(path_after.last_point() == points.back()); +} + +TEST_CASE("Split paths", "[Seams][Scarf]") { + using Seams::Scarf::Impl::split_paths; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(0, 1), + Point::new_scale(0, 2), + }; + ExtrusionPaths paths{ + {{points[0], points[1]}, {}}, + {{points[1], points[2]}, {}}, + }; + const auto split_point{Point::new_scale(0, 1.5)}; + PathPoint path_point{}; + path_point.point = split_point; + path_point.previous_point_on_path_index = 0; + path_point.path_index = 1; + const ExtrusionPaths result{split_paths(std::move(paths), path_point)}; + + REQUIRE(result.size() == 3); + CHECK(result[1].last_point() == split_point); + CHECK(result[2].first_point() == split_point); +} + +TEST_CASE("Get length", "[Seams][Scarf]") { + using Seams::Scarf::Impl::get_length; + using Seams::Scarf::Impl::convert_to_smooth; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(0, 1), + Point::new_scale(0, 2.2), + }; + ExtrusionPaths paths{ + {{points[0], points[1]}, {}}, + {{points[1], points[2]}, {}}, + }; + + CHECK(get_length(convert_to_smooth(paths)) == scaled(2.2)); +} + +TEST_CASE("Linspace", "[Seams][Scarf]") { + using Seams::Scarf::Impl::linspace; + + const auto from{Point::new_scale(1, 0)}; + const auto to{Point::new_scale(3, 0)}; + + Points points{linspace(from, to, 3)}; + REQUIRE(points.size() == 3); + CHECK(points[1] == Point::new_scale(2, 0)); + + points = linspace(from, to, 5); + REQUIRE(points.size() == 5); + CHECK(points[1] == Point::new_scale(1.5, 0)); + CHECK(points[2] == Point::new_scale(2.0, 0)); + CHECK(points[3] == Point::new_scale(2.5, 0)); +} + +TEST_CASE("Ensure max distance", "[Seams][Scarf]") { + using Seams::Scarf::Impl::ensure_max_distance; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(0, 1), + }; + + Points result{ensure_max_distance(points, scaled(0.5))}; + REQUIRE(result.size() == 3); + CHECK(result[1] == Point::new_scale(0, 0.5)); + + result = ensure_max_distance(points, scaled(0.49)); + REQUIRE(result.size() == 4); +} + +TEST_CASE("Lineary increase extrusion height", "[Seams][Scarf]") { + using Seams::Scarf::Impl::lineary_increase_extrusion_height; + using GCode::SmoothPath, GCode::SmoothPathElement; + + SmoothPath path{ + {{}, {{Point::new_scale(0, 0)}, {Point::new_scale(1, 0)}}}, + {{}, {{Point::new_scale(1, 0)}, {Point::new_scale(2, 0)}}}, + }; + + SmoothPath result{lineary_increase_extrusion_height(std::move(path), 0.5)}; + + CHECK(result[0].path[0].height_fraction == Approx(0.5)); + CHECK(result[0].path[0].e_fraction == Approx(0.0)); + CHECK(result[0].path[1].height_fraction == Approx(0.75)); + CHECK(result[0].path[1].e_fraction == Approx(0.5)); + CHECK(result[1].path[0].height_fraction == Approx(0.75)); + CHECK(result[1].path[0].e_fraction == Approx(0.5)); + CHECK(result[1].path[1].height_fraction == Approx(1.0)); + CHECK(result[1].path[1].e_fraction == Approx(1.0)); +} + +TEST_CASE("Lineary reduce extrusion amount", "[Seams][Scarf]") { + using Seams::Scarf::Impl::lineary_readuce_extrusion_amount; + using GCode::SmoothPath, GCode::SmoothPathElement; + + SmoothPath path{ + {{}, {{Point::new_scale(0, 0)}, {Point::new_scale(1, 0)}}}, + {{}, {{Point::new_scale(1, 0)}, {Point::new_scale(2, 0)}}}, + }; + + SmoothPath result{lineary_readuce_extrusion_amount(std::move(path))}; + + CHECK(result[0].path[0].e_fraction == Approx(1.0)); + CHECK(result[0].path[1].e_fraction == Approx(0.5)); + CHECK(result[1].path[0].e_fraction == Approx(0.5)); + CHECK(result[1].path[1].e_fraction == Approx(0.0)); +} + +TEST_CASE("Elevate scarf", "[Seams][Scarf]") { + using Seams::Scarf::Impl::elevate_scarf; + using Seams::Scarf::Impl::convert_to_smooth; + + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(1, 0), + Point::new_scale(2, 0), + Point::new_scale(3, 0), + }; + const ExtrusionPaths paths{ + {{points[0], points[1]}, {}}, + {{points[1], points[2]}, {}}, + {{points[2], points[3]}, {}}, + }; + + const GCode::SmoothPath result{elevate_scarf( + paths, + 1, + convert_to_smooth, + 0.5 + )}; + + + REQUIRE(result.size() == 3); + REQUIRE(result[0].path.size() == 2); + CHECK(result[0].path[0].e_fraction == Approx(0.0)); + CHECK(result[0].path[0].height_fraction == Approx(0.5)); + CHECK(result[0].path[1].e_fraction == Approx(1.0)); + CHECK(result[0].path[1].height_fraction == Approx(1.0)); + REQUIRE(result[1].path.size() == 2); + CHECK(result[1].path[0].e_fraction == Approx(1.0)); + CHECK(result[1].path[0].height_fraction == Approx(1.0)); + CHECK(result[1].path[1].e_fraction == Approx(1.0)); + CHECK(result[1].path[1].height_fraction == Approx(1.0)); + REQUIRE(result[2].path.size() == 2); + CHECK(result[2].path[0].e_fraction == Approx(1.0)); + CHECK(result[2].path[0].height_fraction == Approx(1.0)); + CHECK(result[2].path[1].e_fraction == Approx(0.0)); + CHECK(result[2].path[1].height_fraction == Approx(1.0)); +} + +TEST_CASE("Get point offset from the path end", "[Seams][Scarf]") { + using Seams::Scarf::Impl::get_point_offset_from_end; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(1, 0), + Point::new_scale(2, 0), + Point::new_scale(3, 0), + }; + const ExtrusionPaths paths{ + {{points[0], points[1]}, {}}, + {{points[1], points[2]}, {}}, + {{points[2], points[3]}, {}}, + }; + + std::optional result{get_point_offset_from_end(paths, scaled(1.6))}; + + REQUIRE(result); + CHECK(result->point == Point::new_scale(1.4, 0)); + CHECK(result->previous_point_on_path_index == 0); + CHECK(result->path_index == 1); +} + +TEST_CASE("Find point on path from the path end", "[Seams][Scarf]") { + using Seams::Scarf::Impl::get_point_offset_from_end; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(1, 0), + Point::new_scale(2, 0), + Point::new_scale(3, 0), + Point::new_scale(4, 0), + }; + const ExtrusionPaths paths{ + {{points[0], points[1]}, {}}, + {{points[1], points[2]}, {}}, + {{points[2], points[3], points[4]}, {}}, + }; + + const auto point{Point::new_scale(3.4, 0)}; + + std::optional result{Seams::Scarf::Impl::find_path_point_from_end(paths, point, scaled(1e-2))}; + + REQUIRE(result); + CHECK(result->point == point); + CHECK(result->previous_point_on_path_index == 1); + CHECK(result->path_index == 2); +} + +TEST_CASE("Add scarf seam", "[Seams][Scarf]") { + using Seams::Scarf::add_scarf_seam; + using Seams::Scarf::Impl::convert_to_smooth; + using Seams::Scarf::Impl::get_length; + using Seams::Scarf::Scarf; + + const Points points{ + Point::new_scale(0, 0), + Point::new_scale(1, 0), + Point::new_scale(1, 1), + Point::new_scale(0, 1), + Point::new_scale(0, 0), + }; + const ExtrusionPaths paths{ + {Polyline{points}, {}}, + }; + + Scarf scarf{}; + scarf.start_point = Point::new_scale(0.5, 0); + scarf.end_point = Point::new_scale(1, 0.5); + scarf.start_height = 0.2; + scarf.max_segment_length = 0.1; + scarf.end_point_previous_index = 1; + scarf.entire_loop = false; + + const auto [path, wipe_offset]{add_scarf_seam(ExtrusionPaths{paths}, scarf, convert_to_smooth, false)}; + + REQUIRE(path.size() == 4); + CHECK(wipe_offset == 1); + + REQUIRE(path.back().path.size() >= 1.0 / scarf.max_segment_length); + CHECK(path.back().path.back().point == scarf.end_point); + CHECK(path.back().path.front().point == scarf.start_point); + CHECK(path.back().path.back().e_fraction == Approx(0)); + + REQUIRE(path.front().path.size() >= 1.0 / scarf.max_segment_length); + CHECK(path.front().path.back().point == scarf.end_point); + CHECK(path.front().path.front().point == scarf.start_point); + CHECK(path.front().path.front().e_fraction == Approx(0)); + CHECK(path.front().path.front().height_fraction == Approx(scarf.start_height)); + + CHECK(path.front().path[5].point == points[1]); + CHECK(path.front().path[5].e_fraction == Approx(0.5)); + CHECK(path.front().path[5].height_fraction == Approx(0.6)); + CHECK(path.back().path[5].e_fraction == Approx(0.5)); + CHECK(path.back().path[5].height_fraction == Approx(1.0)); + + scarf.entire_loop = true; + const auto [loop_path, _]{add_scarf_seam(ExtrusionPaths{paths}, scarf, convert_to_smooth, false)}; + + CHECK(get_length(loop_path) == scaled(8.0)); + REQUIRE(!loop_path.empty()); + REQUIRE(!loop_path.front().path.empty()); + CHECK(loop_path.front().path.front().point == scarf.end_point); + CHECK(loop_path.front().path.front().e_fraction == Approx(0)); + REQUIRE(!loop_path.back().path.empty()); + CHECK(loop_path.back().path.back().point == scarf.end_point); + + CHECK(loop_path.front().path.at(20).e_fraction == Approx(0.5)); + CHECK(loop_path.front().path.at(20).point == Point::new_scale(0, 0.5)); +} diff --git a/tests/fff_print/test_seam_shells.cpp b/tests/fff_print/test_seam_shells.cpp index 852b17c..cbc0cfa 100644 --- a/tests/fff_print/test_seam_shells.cpp +++ b/tests/fff_print/test_seam_shells.cpp @@ -10,8 +10,6 @@ using namespace Slic3r; using namespace Slic3r::Seams; -constexpr bool debug_files{false}; - struct ProjectionFixture { Polygon extrusion_path{ diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index e490a10..1552e50 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(${_TEST_NAME}_tests test_stl.cpp test_meshboolean.cpp test_marchingsquares.cpp + test_multiple_beds.cpp test_region_expansion.cpp test_timeutils.cpp test_utils.cpp diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 0888ed1..eabf85a 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -15,7 +15,8 @@ SCENARIO("Reading 3mf file", "[3mf]") { std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; DynamicPrintConfig config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; - bool ret = load_3mf(path.c_str(), config, ctxt, &model, false); + boost::optional version; + bool ret = load_3mf(path.c_str(), config, ctxt, &model, false, version); THEN("load should succeed") { REQUIRE(ret); } @@ -59,7 +60,8 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") { DynamicPrintConfig dst_config; { ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; - load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false); + boost::optional version; + load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false, version); } boost::filesystem::remove(test_file); diff --git a/tests/libslic3r/test_arc_welder.cpp b/tests/libslic3r/test_arc_welder.cpp index f0ab8fb..35eda98 100644 --- a/tests/libslic3r/test_arc_welder.cpp +++ b/tests/libslic3r/test_arc_welder.cpp @@ -3,6 +3,9 @@ #include +#include +#include +#include #include #include #include @@ -398,6 +401,122 @@ TEST_CASE("arc wedge test", "[ArcWelder]") { } } +// Distilled a test case for failing assert(p != prev) inside GCodeGenerator::_extrude() that is caused +// by performing simplification of each ExtrusionPath in ExtrusionMultiPath one by one and not +// simplifying ExtrusionMultiPath as a whole. +TEST_CASE("ExtrusionMultiPath simplification", "[ArcWelderMultiPathSimplify][!mayfail]") +{ + using namespace Slic3r::Geometry; + using namespace Slic3r::GCode; + + ExtrusionMultiPath multi_path; + + multi_path.paths.emplace_back(Polyline({Point(3615254, 8843476), Point(5301926, 8703627), Point(5503271, 8717959), + Point(5787717, 8834837), Point(7465587, 10084995), Point(7565376, 10117372)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0626713, 0.449999f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(7565376, 10117372), Point(7751661, 10097239)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0604367, 0.435101f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(7751661, 10097239), Point(11289346, 8638614), Point(11412324, 8600432)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0547566, 0.397234f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(11412324, 8600432), Point(11727623, 8578798)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.059829, 0.43105f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(11727623, 8578798), Point(12042923, 8557165)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0654324, 0.468406f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(12042923, 8557165), Point(12358223, 8535532), Point(12339460, 8545477)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0710358, 0.505762f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(12339460, 8545477), Point(12035789, 8689023)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0701369, 0.499769f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(12035789, 8689023), Point(11732119, 8832569)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0650101, 0.465591f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(11732119, 8832569), Point(11428449, 8976115)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0598834, 0.431413f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(11428449, 8976115), Point(7890375, 10433797)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0547566, 0.397234f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(7890375, 10433797), Point(7890196, 10433871)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0546036, 0.396214f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(7890196, 10433871), Point(7645162, 10520244)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0586375, 0.423107f, 0.15f), false)); + + multi_path.paths.emplace_back(Polyline({Point(7645162, 10520244), Point(7400129, 10606618), Point(6491466, 10980845), + Point(3782930, 8968079)}), + ExtrusionAttributes(ExtrusionRole::SolidInfill, ExtrusionFlow(0.0626713, 0.449999f, 0.15f), false)); + + const double resolution = 8000.; + SmoothPathCache smooth_path_cache; + SmoothPath smooth_path = smooth_path_cache.resolve_or_fit(multi_path, false, resolution); + + double min_segment_length = std::numeric_limits::max(); + for (const SmoothPathElement &el : smooth_path) { + assert(el.path.size() > 1); + Point prev_pt = el.path.front().point; + + for (auto segment_it = std::next(el.path.begin()); segment_it != el.path.end(); ++segment_it) { + if (const double length = (segment_it->point - prev_pt).cast().norm(); length < min_segment_length) { + min_segment_length = length; + } + } + } + + REQUIRE(min_segment_length >= resolution); +} + +TEST_CASE("SmoothPath clipping test", "[ArcWelder]") { + using namespace Slic3r::Geometry; + + const Polyline polyline = { + Point(9237362, -279099), Point(9239309, -204770), Point(9232158, 477899), Point(9153712, 1292530), + Point(9014384, 2036579), Point(8842322, 2697128), Point(8569131, 3468590), Point(8287136, 4090253), + Point(8050736, 4537759), Point(7786167, 4978071), Point(7502123, 5396751), Point(7085512, 5937730), + Point(6536631, 6536722), Point(5937701, 7085536), Point(5336389, 7545178), Point(4766354, 7921046), + Point(4287299, 8181151), Point(3798566, 8424823), Point(3161891, 8687141), Point(2477384, 8903260), + Point(1985727, 9025657), Point(1488659, 9120891), Point(811611, 9208824), Point(229795, 9234222), + Point(-477899, 9232158), Point(-1292541, 9153710), Point(-1963942, 9030487), Point(-2483966, 8901437), + Point(-2967612, 8752145), Point(-3606656, 8511944), Point(-4098726, 8277235), Point(-4583048, 8025111), + Point(-5164553, 7667365), Point(-5602853, 7343037), Point(-6030084, 7003203), Point(-6532687, 6541035), + Point(-7085558, 5937673), Point(-7502041, 5396860), Point(-7802209, 4952884), Point(-8061668, 4518435), + Point(-8375899, 3912214), Point(-8689042, 3156205), Point(-8915304, 2433948), Point(-9073554, 1769674), + Point(-9194504, 960323), Point(-9238723, 227049), Point(-9237360, -279112), Point(-9194498, -960380), + Point(-9073524, -1769810), Point(-8895452, -2505523), Point(-8689032, -3156238), Point(-8375859, -3912298), + Point(-8025112, -4583044), Point(-7667378, -5164532), Point(-7180536, -5822455), Point(-6729193, -6334406), + Point(-6350620, -6713810), Point(-5973693, -7051366), Point(-5438560, -7475505), Point(-4756170, -7927163), + Point(-4110103, -8277232), Point(-3651006, -8489813), Point(-3015355, -8738921), Point(-2492584, -8893770), + Point(-1963947, -9030483), Point(-1286636, -9154696), Point(-590411, -9222659), Point(14602, -9244383), + Point(974789, -9192915), Point(1634833, -9095889), Point(2193590, -8977466), Point(2851102, -8793883), + Point(3612042, -8509372), Point(4098709, -8277242), Point(4583076, -8025095), Point(5164577, -7667349), + Point(5822437, -7180551), Point(6388368, -6677987), Point(6866030, -6190211), Point(7236430, -5740880), + Point(7660739, -5174380), Point(8088357, -4476558), Point(8394013, -3866175), Point(8593000, -3400880), + Point(8768650, -2918284), Point(8915319, -2433894), Point(9073549, -1769711), Point(9194508, -960282), + Point(9237362, -279099) + }; + + const ExtrusionAttributes extrusion_attributes(ExtrusionRole::Perimeter, ExtrusionFlow{1.0, 1.0, 1.0}); + const GCode::SmoothPath smooth_path = {GCode::SmoothPathElement{extrusion_attributes, ArcWelder::fit_path(polyline.points, 32000., 0.05)}}; + const double smooth_path_length = GCode::length(smooth_path); + + const size_t clip_segment_cnt = 20; + for (size_t segment_idx = 1; segment_idx <= clip_segment_cnt; ++segment_idx) { + const double clip_length = static_cast(segment_idx) * (smooth_path_length / (clip_segment_cnt + 1)); + GCode::SmoothPath smooth_path_clipped = smooth_path; + + clip_end(smooth_path_clipped, smooth_path_length - clip_length, scaled(GCode::ExtrusionOrder::min_gcode_segment_length)); + + const double smooth_path_clipped_length = GCode::length(smooth_path_clipped); + const double relative_diff = std::abs(1. - (clip_length / smooth_path_clipped_length)); + REQUIRE(relative_diff <= 0.000001); + } +} + #if 0 // For quantization //#include diff --git a/tests/libslic3r/test_emboss.cpp b/tests/libslic3r/test_emboss.cpp index df5a80d..40f448f 100644 --- a/tests/libslic3r/test_emboss.cpp +++ b/tests/libslic3r/test_emboss.cpp @@ -103,12 +103,13 @@ std::string get_font_filepath() { // Explicit horror include (used to be implicit) - libslic3r "officialy" does not depend on imgui. #include "../../bundled_deps/imgui/imgui/imstb_truetype.h" // stbtt_fontinfo +#include "boost/nowide/cstdio.hpp" TEST_CASE("Read glyph C shape from font, stb library calls ONLY", "[Emboss]") { std::string font_path = get_font_filepath(); char letter = 'C'; // Read font file - FILE *file = fopen(font_path.c_str(), "rb"); + FILE *file = boost::nowide::fopen(font_path.c_str(), "rb"); REQUIRE(file != nullptr); // find size of file REQUIRE(fseek(file, 0L, SEEK_END) == 0); diff --git a/tests/libslic3r/test_multiple_beds.cpp b/tests/libslic3r/test_multiple_beds.cpp new file mode 100644 index 0000000..10640a4 --- /dev/null +++ b/tests/libslic3r/test_multiple_beds.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include + +using namespace Slic3r; +TEST_CASE("Conversion between grid coords and index", "[MultipleBeds]") +{ + std::vector original_indices(10); + std::iota(original_indices.begin(), original_indices.end(), 0); + + // Add indexes covering the whole int positive range. + const int n{100}; + std::generate_n(std::back_inserter(original_indices), n, [i = 1]() mutable { + return std::numeric_limits::max() / n * i++; + }); + + std::vector coords; + std::transform( + original_indices.begin(), + original_indices.end(), + std::back_inserter(coords), + BedsGrid::index2grid_coords + ); + + std::vector indices; + std::transform( + coords.begin(), + coords.end(), + std::back_inserter(indices), + BedsGrid::grid_coords2index + ); + + CHECK(original_indices == indices); +} diff --git a/tests/libslic3r/test_point.cpp b/tests/libslic3r/test_point.cpp index 06d4433..156995f 100644 --- a/tests/libslic3r/test_point.cpp +++ b/tests/libslic3r/test_point.cpp @@ -33,7 +33,7 @@ TEST_CASE("Distance to line", "[Point]") { TEST_CASE("Distance to diagonal line", "[Point]") { const Line line{{50, 50}, {125, -25}}; - CHECK(std::abs(line.distance_to(Point{100, 0})) == Approx(0)); + CHECK_THAT(std::abs(line.distance_to(Point{100, 0})), Catch::Matchers::WithinAbs(0, 1e-6)); } TEST_CASE("Perp distance to line does not overflow", "[Point]") { diff --git a/tests/libslic3r/test_polyline.cpp b/tests/libslic3r/test_polyline.cpp index 7734027..befad4b 100644 --- a/tests/libslic3r/test_polyline.cpp +++ b/tests/libslic3r/test_polyline.cpp @@ -78,14 +78,14 @@ SCENARIO("Simplify polyne, template", "[Polyline]") Points polyline{ {0,0}, {1000,0}, {2000,0}, {2000,1000}, {2000,2000}, {1000,2000}, {0,2000}, {0,1000}, {0,0} }; WHEN("simplified with Douglas-Peucker with back inserter") { Points out; - douglas_peucker(polyline.begin(), polyline.end(), std::back_inserter(out), 10, [](const Point &p) { return p; }); + douglas_peucker(polyline.begin(), polyline.end(), std::back_inserter(out), 10., [](const Point &p) { return p; }); THEN("simplified correctly") { REQUIRE(out == Points{ {0,0}, {2000,0}, {2000,2000}, {0,2000}, {0,0} }); } } WHEN("simplified with Douglas-Peucker in place") { Points out{ polyline }; - out.erase(douglas_peucker(out.begin(), out.end(), out.begin(), 10, [](const Point &p) { return p; }), out.end()); + out.erase(douglas_peucker(out.begin(), out.end(), out.begin(), 10., [](const Point &p) { return p; }), out.end()); THEN("simplified correctly") { REQUIRE(out == Points{ {0,0}, {2000,0}, {2000,2000}, {0,2000}, {0,0} }); } diff --git a/tests/slic3rutils/slic3r_arrangejob_tests.cpp b/tests/slic3rutils/slic3r_arrangejob_tests.cpp index b76861a..1cd35de 100644 --- a/tests/slic3rutils/slic3r_arrangejob_tests.cpp +++ b/tests/slic3rutils/slic3r_arrangejob_tests.cpp @@ -90,14 +90,14 @@ TEST_CASE("Basic arrange with cube", "[arrangejob]") { arr2::ArrangeSettings settings; Points bedpts = get_bed_shape(cfg); - arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts); + arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts, Vec2crd{0, 0}); SECTION("Single cube needs to be centered") { w.push(std::make_unique(arr2::Scene{ arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(&settings) - .set_bed(cfg)})); + .set_bed(cfg, Vec2crd{0, 0})})); w.process_events(); @@ -126,7 +126,7 @@ TEST_CASE("Basic arrange with cube", "[arrangejob]") { arr2::Scene scene{arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(&settings) - .set_bed(cfg) + .set_bed(cfg, Vec2crd{0, 0}) .set_selection(&sel)}; w.push(std::make_unique(std::move(scene))); @@ -160,7 +160,7 @@ TEST_CASE("Basic arrange with cube", "[arrangejob]") { arr2::Scene scene{arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(&settings) - .set_bed(cfg) + .set_bed(cfg, Vec2crd{0, 0}) .set_selection(&sel)}; w.push(std::make_unique(std::move(scene))); @@ -217,7 +217,7 @@ TEST_CASE("Basic arrange with cube", "[arrangejob]") { arr2::Scene scene{arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(&settings) - .set_bed(cfg)}; + .set_bed(cfg, Point::new_scale(10, 10))}; w.push(std::make_unique(std::move(scene))); w.process_events(); @@ -266,7 +266,7 @@ TEST_CASE("Test for modifying model during arrangement", "[arrangejob][fillbedjo new_volume->name = new_object->name; Points bedpts = get_bed_shape(cfg); - arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts); + arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts, Vec2crd{0, 0}); BoostThreadWorker w(std::make_unique()); RandomArrangeSettings settings; @@ -278,7 +278,7 @@ TEST_CASE("Test for modifying model during arrangement", "[arrangejob][fillbedjo arr2::Scene scene{arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(&settings) - .set_bed(cfg)}; + .set_bed(cfg, Vec2crd{0, 0})}; ArrangeJob2::Callbacks cbs; cbs.on_prepared = [&m] (auto &) {